diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c834e6..ebc3520 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,25 +14,12 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" - # Pinned to the pre-commit revisions so CI and local hooks agree. - - run: pip install ruff==0.4.4 + - run: pip install ruff mypy - run: ruff check src tests - run: ruff format --check src tests - type: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - # Pinned to the pre-commit revision (see .pre-commit-config.yaml). - - run: pip install mypy==1.10.0 pydantic numpy - - run: pip install -e . - - run: mypy src/otko/core src/otko/services - - test-headless: - needs: [lint, type] + test: + needs: lint strategy: fail-fast: false matrix: @@ -44,38 +31,18 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - run: pip install -e ".[dev]" - - run: pytest tests/unit tests/services -m "not slow" - - test-gui: - needs: [lint, type] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - name: Install Linux Qt deps + if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y libegl1 libxkbcommon-x11-0 libxcb-icccm4 \ libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \ libxcb-shape0 libxcb-sync1 libxcb-xfixes0 libxcb-xinerama0 \ libxcb-cursor0 libdbus-1-3 libgl1 libxkbcommon0 xvfb - - run: pip install -e ".[gui,dev]" - - run: xvfb-run -a pytest tests/gui -m "not slow" - - test-integration: - needs: [lint, type] - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - run: pip install -e ".[dev]" - - run: pytest tests/integration -m "not slow" \ No newline at end of file + - name: Run tests (Linux, headless) + if: runner.os == 'Linux' + run: xvfb-run -a pytest --cov --cov-report=xml -m "not slow" + - name: Run tests (Win/Mac) + if: runner.os != 'Linux' + run: pytest --cov --cov-report=xml -m "not slow" diff --git a/.gitignore b/.gitignore index eac3bc1..c82640e 100644 --- a/.gitignore +++ b/.gitignore @@ -61,7 +61,3 @@ examples/ozan*.osmodel examples/ozan_results/ /Ex*.csv /Ex*.png - -# BEGIN oh-my-opencode-slim clonedeps -.slim/clonedeps/repos/ -# END oh-my-opencode-slim clonedeps diff --git a/.ignore b/.ignore deleted file mode 100644 index 32dbf4f..0000000 --- a/.ignore +++ /dev/null @@ -1,9 +0,0 @@ -# BEGIN oh-my-opencode-slim clonedeps -!.slim/ -!.slim/clonedeps.json -!.slim/clonedeps/ -!.slim/clonedeps/repos/ -!.slim/clonedeps/repos/** -.slim/clonedeps/repos/**/.git/ -.slim/clonedeps/repos/**/.git/** -# END oh-my-opencode-slim clonedeps diff --git a/.slim/clonedeps.json b/.slim/clonedeps.json deleted file mode 100644 index ddddcab..0000000 --- a/.slim/clonedeps.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "version": "1.0.0", - "updatedAt": "2026-09-16T17:30:00.000Z", - "dependencies": [ - { - "name": "opstool", - "resolvedVersion": "1.0.26", - "repoUrl": "https://github.com/yexiang92/opstool.git", - "ref": "v1.0.26", - "path": ".slim/clonedeps/repos/yexiang92__opstool", - "packagePath": "opstool/vis", - "reason": "Read-only source for evaluating whether to port opstool's PyVista/Plotly visualization settings into otko's canvas backends, or to depend on opstool.vis directly (GPL-3.0, compatible with otko's AGPL-3.0 via GPLv3 section 13)." - } - ] -} diff --git a/AGENTS.md b/AGENTS.md index babac38..5f3ce53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ Pre-alpha SAP2000-style desktop GUI for OpenSeesPy. Python 3.10+; **Windows requ ## Install ```bash -pip install -e ".[gui,dev]" # desktop: Qt + PyVista + plotly.js backend + dev tools +pip install -e ".[gui,dev]" # desktop: Qt + PyVista + dev tools pip install -e . # headless: core + services only, no Qt (scripts, notebooks, web backends) python -m otko # launch GUI (src/otko/app.py:run) ``` @@ -19,13 +19,15 @@ Strict one-way MVVM + services: `views → viewmodels → services → core`. - `core/` (entities: `project.py`, `geometry/`, `materials/`, `sections/`, `loads/`, `analysis/`, `catalog/`): stdlib + numpy + pydantic only. **No Qt, no openseespy. Period.** - `services/` (`opensees_runner.py`, `persistence.py`, `results.py`, ...): may use core + h5py + openseespy. **No Qt.** - `views/`: PySide6/pyvistaqt only. **No direct `import openseespy`** — go through a service. -- `views/canvas3d/` (**PyVista/VTK**, default) and `views/canvas_plotly/` (plotly.js in a `QWebEngineView`) are two backends for the same central 3D view. Both satisfy the `CanvasBackend` protocol in `views/canvas_base.py`, share one `SelectionState` owned by `MainWindow`, and are swapped live via **Options → Canvas Backend** (persisted in `QSettings` under `canvas/backend`). `MainWindow._activate_canvas` keeps both widgets in a `QStackedWidget` — never destroy a canvas mid-session (VTK leaves dangling make-current callbacks). Backend-specific gaps are declared by `CanvasCapabilities` (e.g. Plotly has no force-diagram overlay or video export yet); gate UI on `canvas.capabilities`, never on the backend name. `canvas_plotly/trace_builder.py` is pure (no Qt, no pyvista) and unit-tested headless. `PlotlyCanvas` pushes with `Plotly.react`, which resets scene attributes the layout omits: data-only pushes pass `preserveView` so `html.py` carries the live camera and padded axis ranges forward, and the hover/snap marker is restyled only when its visible state changes (a restyle per hover event redraws per mouse move until the stack blows). Regression tests: `tests/gui/test_plotly_view_preservation.py`, `tests/gui/test_plotly_hover.py`. -- `views/canvas3d/style.py` (`RenderStyle`) is the single source of truth for colours/sizes on **both** backends; renderers must read it rather than hard-coding a colour. The `STYLE_FIELDS` subset is user-editable via **Options → Plot Properties…** (`views/dialogs/plot_properties.py`), previews live through `MainWindow.set_plot_style(..., persist=False)`, and persists as JSON in `QSettings` under `plot/props`. - `viewmodels/` bridges core↔Qt (signals, `QUndoStack`); `commands/` holds `QUndoCommand` subclasses. - Rules: public functions need type hints + docstring; new domain entities go through Pydantic validation; ops >50 ms run off the GUI thread (`AnalysisWorker` in QThread, cancel via `isInterruptionRequested()`, results cross threads as lightweight `ResultsHandle` to HDF5). Runner emits OpenSeesPy commands in fixed order (`docs/architecture.md`): `wipe → model → node → fix → material → section → geomTransf → element → timeSeries → pattern/load → recorder → system/numberer/... → analyze`. Never reorder. +## Branches + +- Do all feature/fix work on `develop` (or short-lived branches off `develop`, merged back into it). `main` is release-only — never commit directly to it. + ## Verify (in this order) ```bash @@ -39,36 +41,8 @@ Focused runs: `pytest tests/unit` (pure logic, ms), `pytest tests/gui -k ` Notes: `tests/conftest.py` auto-`ops.wipe()`s the OpenSees domain between tests (lazy import so core tests stay Qt/OpenSees-free). Coverage omits `views/`. Commit style: Conventional Commits (`feat:`, `fix:`, ...). `pre-commit install` runs ruff + mypy (mypy hook scoped to `core|services|viewmodels`). -## Git & Forgejo - -Remote is a self-hosted Forgejo, SSH-only: - -```bash -git remote add origin ssh://git@smill-home.ddns.net/smill/otko.git -``` - -- Web UI / HTTPS clone: `https://smill-home.ddns.net/forgejo/smill/otko.git` (note the `/forgejo` subpath — SSH URLs don't have it). -- Forgejo SSH runs through the system sshd on **port 22** (`SSH_PORT = 22` in the server's `app.ini`). If the site ever advertises a `:2222` SSH URL again, that config regressed — push/pull with port 22 anyway and flag it. -- Auth is by SSH key (Forgejo → Settings → SSH Keys). Test: `ssh -T git@smill-home.ddns.net` should answer `Hi there, ! ... Forgejo does not provide shell access.` -- Default branch `main`. Work on short-lived branches (`fix/`, `docs/`), open a PR against `main`. Never commit directly to `main` from an agent session. -- Repo root is `otko/` itself. (On the dev laptop there is also an empty, commit-less parent repo at `Sync/coding/` — ignore it; all git work happens inside `otko/`.) - ## Examples & persistence - `examples/*.py` are source of truth; `examples/*.osmodel` are generated artifacts (checked in). Never hand-edit `.osmodel` — change the script and regen: `python examples/cantilever.py` (each script saves, reloads, asserts clean round-trip). - Projects persist as single Pydantic-validated JSON `.osmodel` (diffable); analysis output goes to `*.osresults.h5` (HDF5, one group per case). - Quick smoke: open `examples/cantilever.osmodel` → run `Tip-Load` → M3 peaks 50 kN·m at fixed end. - -## Cloned Dependency Source - -Read-only dependency source repositories are available under -`.slim/clonedeps/repos/` for inspection. Do not edit these clones. The -structured manifest is `.slim/clonedeps.json`. - -- `.slim/clonedeps/repos/yexiang92__opstool/` — `yexiang92/opstool` at `v1.0.26`; the OpenSeesPy - pre/post-processor whose PyVista and Plotly visualization settings - (`opstool/vis/{pyvista,plotly}/plot_utils.py`, `plot_resp_base.py`, `vis_model.py`) are the - reference for otko's canvas look-and-feel. GPL-3.0, and GPLv3 §13 explicitly permits combining - it with otko's AGPL-3.0. Any code actually ported from it must keep the opstool copyright notice - and record that it was modified (GPLv3 §5a/b) — add that entry to `NOTICE` when the port lands. - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab9023e..9f911d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,97 +1,37 @@ # Contributing -Thanks for helping with OTKO — a SAP2000-style desktop GUI for -OpenSeesPy. Early-stage project: the bar is architecture cleanliness, not -feature count. If your change breaks a layering rule below, it won't -merge — no matter how useful the feature. +Early-stage project. The bar is architecture cleanliness, not feature +count. If your change breaks a layering rule below, it won't merge — +no matter how useful the feature. ## Dev setup ```bash python -m venv .venv -source .venv/bin/activate # Linux / macOS -# .venv\Scripts\activate # Windows -pip install -e ".[gui,dev]" +source .venv/bin/activate +pip install -e ".[dev]" pre-commit install ``` -`pip install -e ".[gui,dev]"` pulls the Qt/PyVista desktop stack plus the -dev tools. For a headless checkout (core + services only, no Qt) use -`pip install -e .` instead. Python 3.10+; on Windows use 3.12+. - -Launch the GUI with: - -```bash -python -m otko -``` - -`pre-commit install` wires ruff + mypy into your local git hooks so -obvious issues are caught before a commit. Run it once per clone. - ## Before opening a PR -Run the verify commands in this order and make sure they are all clean: - ```bash ruff check src tests -ruff format src tests # line-length 100, E501 ignored +ruff format src tests mypy src/otko/core src/otko/services -pytest -m "not slow" # CI gate: lint → this, on 3.10/3.11/3.12 × ubuntu/windows/macos +pytest -m "not slow" ``` -Focused runs are faster while iterating: `pytest tests/unit` (pure logic, -milliseconds), `pytest tests/gui -k ` (pytest-qt, needs a display; -Linux GUI tests want `xvfb-run -a pytest ...`), and -`pytest tests/integration -k ` (real OpenSeesPy runs). Single test: -`pytest tests/unit/test_project.py::test_name -q`. Markers: `gui`, `slow`. - ## Architectural rules (enforced in review) -OTKO uses a strict one-way **MVVM + service** architecture. Dependencies -flow outward-in only: - -``` -views → viewmodels → services → core -``` - -`commands` sits alongside the bridge and owns every model mutation. - -1. `core/` is pure Python — stdlib + numpy + pydantic. It may **not** - import Qt or `openseespy`. Period. -2. `services/` may use `core` + `h5py` + `openseespy`, but may **not** - import Qt. -3. `views/` (PySide6/pyvistaqt) may **not** import `openseespy` directly — - go through a service. No business logic in `views`. -4. `viewmodels/` bridges `core` ↔ Qt (signals, `QUndoStack`). -5. `commands/` holds the `QUndoCommand` subclasses; all model mutations - go through `commands`, not ad-hoc edits in `views`. -6. Public functions and methods need type hints and a docstring. -7. New domain entities go through Pydantic validation. -8. Long-running operations (>50 ms) run off the GUI thread (`AnalysisWorker` - in a `QThread`, cancelled via `isInterruptionRequested()`; results cross - threads as a lightweight `ResultsHandle` written to HDF5). - -The full package map and the fixed OpenSeesPy command order the runner -emits live in [`docs/architecture.md`](docs/architecture.md). Never -reorder the runner's commands. - -## Branch model - -`main` is the default and integration branch. Work on short-lived topic -branches cut from `main` — `feat/`, `fix/`, or -`docs/` — and open a pull request against `main`. Do not commit -directly to `main` from an agent session. There is no `develop` branch. +1. `core/` may not import Qt or `openseespy`. Period. +2. `services/` may not import Qt. +3. `views/` may not import `openseespy` directly — go through a service. +4. Public functions and methods need type hints and a docstring. +5. New domain entities go through Pydantic validation. +6. Long-running operations (>50 ms) run off the GUI thread. ## Commit style Conventional Commits — `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, -`chore:`, `ci:`. Keep each commit focused; a `feat:` commit should add a -feature, not mix one in with unrelated refactors. - -## Documentation - -If your change is user-visible, update -[`docs/QUICK_GUIDE.md`](docs/QUICK_GUIDE.md) and the relevant roadmap or -ADR entry. Project model files are `.osmodel` (Pydantic-validated JSON); -regenerate the checked-in `examples/*.osmodel` from their scripts with -`python examples/.py` rather than hand-editing them. +`chore:`, `ci:`. diff --git a/NOTICE b/NOTICE deleted file mode 100644 index 24b2dd1..0000000 --- a/NOTICE +++ /dev/null @@ -1,85 +0,0 @@ -# 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. -``` - -## Ported / adapted code: `opstool` - -The canvas element colour palette in `src/otko/views/canvas3d/style.py` -(per-family element colours and the diverging response colour scale) and the -resulting diagram colouring in `views/canvas3d/diagram_renderer.py` were -adapted from the `opstool` project, which is distributed under the **GNU -General Public License v3.0**. opstool is Copyright © Yexiang Yan and -contributors. - -In accordance with GPLv3 §5(a)/(b) this notice records that the material was -modified and adapted for OTKO. Combining the GPLv3-covered material with -OTKO's AGPL-3.0 code is permitted by GPLv3 §13; the combined work is -conveyed under AGPL-3.0, and the GPLv3 terms continue to apply to the -opstool-derived portions. - -## 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/README.md b/README.md index c291d67..38f1fd9 100644 --- a/README.md +++ b/README.md @@ -82,13 +82,6 @@ views (Qt) → viewmodels → services (OpenSeesRunner, Persistence) → c Long version in [`docs/architecture.md`](docs/architecture.md), including the OpenSeesPy command order the runner emits. -## Documentation - -Practical, task-first walkthroughs live in -[`docs/QUICK_GUIDE.md`](docs/QUICK_GUIDE.md) — a cantilever build, -modal analysis, report/script export, display units, and undo/redo. -The full index is [`docs/README.md`](docs/README.md). - ## Install (development) **Desktop GUI** (Qt, PyVista, pyqtgraph, imageio): diff --git a/docs/QUICK_GUIDE.md b/docs/QUICK_GUIDE.md deleted file mode 100644 index 0bc4b8c..0000000 --- a/docs/QUICK_GUIDE.md +++ /dev/null @@ -1,132 +0,0 @@ -# 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 deleted file mode 100644 index 9ef3bfb..0000000 --- a/docs/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# 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/docs/consistent_units.md b/docs/consistent_units.md new file mode 100644 index 0000000..3bd59f2 --- /dev/null +++ b/docs/consistent_units.md @@ -0,0 +1,249 @@ +# Consistent Units + +Plan for making units explicit and uniform across OTKO: model input, +solver boundary, result display, and persistence. This is a plan, not +an implementation — no code changes ship with this document. + +## 1. Principle + +OpenSees is unit-agnostic: it never converts. The engineer picks one +consistent system and sticks to it. OTKO follows the same rule, one +step further: + +1. **Stored values are always in the project's native system** + (`ProjectMeta.units`, default `SI_M_N`). The solver, persistence, + and undo stack only ever see native values. +2. **Views may relabel or rescale for humans, never for the solver.** + Any display conversion is a pure view-layer factor applied on read; + converted values are never written back. +3. **Every number shown to the user carries its unit**, or is + explicitly dimensionless (strain, drift ratio, damping ratio). + +What this document does NOT propose: auto-conversion on input, unit +migration of existing models, or any change to OpenSeesPy command +emission order/content (`docs/architecture.md` stays authoritative). + +## 2. Where we are + +- `src/otko/core/units.py` — `UnitSystem` enum (4 systems), frozen + `UnitLabels` bundle (length / force / moment / stress / curvature / + rotation, rotation always `rad`), `labels_for()` helper. Display + only; zero conversion factors anywhere in the repo. +- `ProjectMeta.units` (`src/otko/core/project.py:124`) persisted in + every `.osmodel`; `SetUnitsCommand` (`src/otko/commands/project.py`) + is display-only by contract; `Options → Set Display Units` + (`src/otko/views/action_handlers.py`) applies it and refreshes docks. +- Views consume labels via `set_units()` (`src/otko/views/dock_manager.py`): + pushover curve and material tester do; force diagrams, time-history, + results panel, and response-spectrum Sa axis do not. `section_editor` + hardcodes `y (m)` / `z (m)` — wrong for kip-in models. DOF labels + (`views/docks/_labels.py`) are unitless by design. +- ADR-0001 §2.7 deferred per-field `UnitTag` quantity annotations; + generated catalog fields are still `str` placeholders (`'500 MPa'`, + `# TODO: unit-aware type`). The quantity taxonomy in §5 below is + meant to become those `UnitTag` strings verbatim. +- Examples use two systems in practice: `SI_M_N` (cantilever → + M3 peaks 50 kN·m; portal, space frame, concrete04) and `US_IN_KIP` + (ex1a–ex4 families in inches/kips/ksi, `G=386.4 in/s²`). + `SI_MM_N` and `US_FT_KIP` appear only in the enum, tests, and + `assign_hinge` prefills. `basic_truss.py` hand-converts + (`IN_TO_M`, `KIP_TO_N`) to work in SI — the only example that does. +- Roadmap backlog item 3 already asks for "units-aware labels" in the + input-dialog pass; this plan is the spec for that item. + +## 3. Canonical systems + +> **Update (Sept 2026):** collapsed to two stored systems, `METRIC` +> and `IMPERIAL` (§3.1 and §3.4 below, renamed). `SI_MM_N` and +> `US_FT_KIP` are retired — old `.osmodel` files still load via the +> `LEGACY_UNIT_SYSTEMS` map in `core/units.py`, but their stored +> numbers keep the old mm/ft scale (re-enter values natively). +> The status bar offers Metric / Imperial only; switching routes +> through `SetUnitsCommand` and refreshes every units-aware label +> (`_sync_units_everywhere` on `modelMutated`, covering combo, menu, +> and undo/redo). All values stay native, so every label next to a +> value uses the `NATIVE` table (`native_label()`), never the +> `DISPLAY` prefixes — `DISPLAY` remains data-only for a future +> true display-switching phase. +> Each table lists the **stored unit** (what the solver and +> `.osmodel` see) and the **display unit + factor** (view layer only, +> applied on read). + +### 3.1 Metric — `SI_M_N` (m, N, Pa) + +Building/civil scale. Stored = displayed, except where humans expect +scaled prefixes (factor applies at the view, values untouched). + +| Quantity | Stored | Display | Factor | +|---|---|---|---| +| Geometry / length | m | m | 1 | +| Section dims | m | mm | 1000 | +| Displacement | m | mm | 1000 | +| Rotation | rad | rad | 1 (° toggle, §6) | +| Point load | N | kN | 1e-3 | +| Distributed load | N/m | kN/m | 1e-3 | +| Moment | N·m | kN·m | 1e-3 | +| Stress / modulus | Pa | MPa | 1e-6 | +| Area | m² | mm² | 1e6 | +| Inertia | m⁴ | mm⁴ / cm⁴ | 1e12 / 1e8 | +| Mass | kg | kg (t for large) | 1 | +| Time / period | s | s | 1 | + +### 3.2 Metric — `SI_MM_N` (mm, N, MPa) + +Detail/component scale (steel connections, lab specimens). Stored = +displayed everywhere; no factors. + +| Quantity | Stored = Display | +|---|---| +| Geometry, section dims, displacement | mm | +| Rotation | rad | +| Point load | N | +| Distributed load | N/mm | +| Moment | N·mm | +| Stress / modulus | MPa | +| Mass | t | + +### 3.3 Imperial — `US_FT_KIP` (ft, kip, ksf) + +Building scale. The mixed ft/in convention engineers expect +(lengths in feet, displacements in inches) **requires display +factors** — this is the case that forces §4 to exist. + +| Quantity | Stored | Display | Factor | +|---|---|---|---| +| Geometry / length | ft | ft | 1 | +| Section dims | ft | in | 12 | +| Displacement | ft | in | 12 | +| Rotation | rad | rad | 1 (° toggle, §6) | +| Point load | kip | kip | 1 | +| Distributed load | kip/ft | kip/ft (plf alt.) | 1 | +| Moment | kip·ft | kip·ft | 1 | +| Stress / modulus | ksf | ksi (materials) | 1/144 | +| Area | ft² | in² | 144 | +| Inertia | ft⁴ | in⁴ | 20736 | +| Mass | slug | slug | 1 | +| Time / period | s | s | 1 | + +Small-load alternative: `lbf` / `plf` / `lb·ft` are display aliases +(×1000 from kip units), not separate systems. A view showing +`0.004 kip` should render `4.0 lbf`; threshold and format rules are +view concerns (§6). + +### 3.4 Imperial — `US_IN_KIP` (in, kip, ksi) + +Component scale (members, sections, the ex1a–ex4 example families). +Stored = displayed everywhere; no factors. + +| Quantity | Stored = Display | +|---|---| +| Geometry, section dims, displacement | in | +| Rotation | rad | +| Point load | kip (lbf alias ×1000) | +| Distributed load | kip/in | +| Moment | kip·in | +| Stress / modulus | ksi | +| Mass | kip·s²/in | + +Gravity for mass derivation is `386.4 in/s²` in this system +(`32.2 ft/s²` under `US_FT_KIP`, `9.81 m/s²` metric) — document the +constant next to every mass-from-weight computation; never hardcode +it in a system-agnostic path. + +## 4. Display-factor mechanism (view layer only) + +New pure-data table in `core/units.py`, e.g. +`DISPLAY: dict[UnitSystem, dict[str, tuple[str, float]]]` +mapping quantity → (display label, multiply-by-factor). Rules: + +- Factors live in `core/` as **data only** (like `_LABELS` today) — + no Qt, no application logic, trivially unit-testable. +- Factors are **applied in views/viewmodels on read** (axis labels, + table cells, diagram annotations). Converted values never flow + into commands, services, or persistence. +- `rotation` factor is always 1 (`rad`); a degrees toggle is a + formatting option (§6), not a system. +- Dimensionless quantities (strain, drift, damping ratio, mass + participation) never take factors. + +This keeps the current "we don't auto-convert" contract intact: the +solver boundary is untouched; only human-facing strings change. + +## 5. Quantity taxonomy (shared with ADR-0001) + +Each numeric field in `core/` and `catalog/` eventually gets one of +these quantity tags (same strings as the future `UnitTag`, so the +follow-up ADR adopts them unchanged): + +`length, displacement, rotation, force, moment, distributed_load, +stress, area, inertia, mass, time, frequency, temperature` + +Minimum viable step (no ADR needed): use the taxonomy as the key set +for the §4 table and as the vocabulary for dialog hints (§6). Field +annotation (`UnitTag`) stays deferred per ADR-0001 §2.7. + +## 6. UI rollout checklist + +File-by-file, each item independently reviewable: + +- [ ] `views/dialogs/section_editor.py:184-185` — replace hardcoded + `(m)` with `labels_for()` length unit. (Bug fix, do first.) +- [ ] `views/docks/force_diagram.py` — append force/moment display + units to component labels and min/max annotations. +- [ ] `views/docks/time_history.py` — y-axis unit per trace kind + (displacement / velocity / acceleration); x-axis stays `s`. +- [ ] `views/docks/results_panel.py` — displacement/force headers + take display units; modal table keeps `rad²/s²`, `rad/s`, `Hz`, `s`. +- [ ] `views/docks/response_spectrum.py` — Sa axis takes the + acceleration display unit; period stays `s`. +- [ ] Assign dialogs (`assign_load`, `distributed_load`, + `pattern_loads`) — unit hints on field labels, e.g. + `wy (kip/ft)`, driven by project system (roadmap item 3). +- [ ] Table dock headers — `name`-style unit suffixes where numeric + columns carry units; dimensionless columns stay bare. +- [ ] Rotation display — solver and storage stay `rad`; add an + optional ° formatting toggle in post views only (×180/π on read). +- [ ] `lbf`/`plf` small-value aliases — formatting rule in shared + label helper, not per-view logic. + +## 7. Persistence, examples, conventions + +- `meta.units` keeps meaning "native stored system". Display choices + (factors applied, ° toggle, lbf alias) are **never persisted** — + reopening a file always shows native-unit defaults. +- Old files without new fields (if any are added as `Optional` with + defaults) load unchanged; `ProjectMeta extra="forbid"` still + rejects unknown keys — no migration needed for this plan. +- Document each example family's convention at the top of its + script (one comment line: system + key constants). Add one + `SI_MM_N` and one `US_FT_KIP` example so all four systems have + runner-verified coverage; today two systems have none. +- Rule for new examples: no hand-conversion constants. Either model + natively in the declared system or, once §4 exists, use the shared + factor table. `basic_truss.py` is grandfathered until then. + +## 8. Verification + +- Extend `tests/unit/test_unit_labels.py`: factor-table coverage per + system (identity for `US_IN_KIP`/`SI_MM_N`; ft→in ×12, N→kN, + Pa→MPa, ksf→ksi spots), rotation always factor 1, dimensionless + quantities absent from the table. +- Round-trip invariant: regen all examples, `git diff` on + `*.osmodel` must show label/metadata changes only — never numeric + value changes. +- GUI smoke (offscreen/xvfb): open one model per system, flip + display units, assert axis/table labels change and stored values + do not (compare `model_dump()` before/after). + +## 9. Phases + +- **A — label the stored unit everywhere** (§6 minus factors): + no new mechanism, pure label plumbing. Shippable alone. +- **B — display-factor table + apply in post views** (§4, §5 keys): + ft→in, N→kN, Pa→MPa and friends appear; solver untouched. +- **C — input-dialog hints + section_editor fix** (roadmap item 3): + hints only, values still entered in native units. +- **D — optional, needs follow-up ADR**: true display-unit + switching (type in inches, store feet) and `UnitTag` field + annotation per ADR-0001 §2.7. Explicitly out of scope until A–C + ship. diff --git a/examples/_ex4_portal2d_common.py b/examples/_ex4_portal2d_common.py index 4fd7fdc..f669b33 100644 --- a/examples/_ex4_portal2d_common.py +++ b/examples/_ex4_portal2d_common.py @@ -304,7 +304,7 @@ def build_ex4_portal2d_elastic_element() -> Project: name=ELASTIC_VARIANT.name, author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna", description=ELASTIC_VARIANT.description, - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, @@ -333,7 +333,7 @@ def build_ex4_portal2d_inelastic_section() -> Project: name=INELASTIC_SECTION_VARIANT.name, author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna", description=INELASTIC_SECTION_VARIANT.description, - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, @@ -383,7 +383,7 @@ def build_ex4_portal2d_inelastic_fiber_section() -> Project: name=FIBER_VARIANT.name, author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna", description=FIBER_VARIANT.description, - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, diff --git a/examples/basic_truss.osmodel b/examples/basic_truss.osmodel index e646581..f25e508 100644 --- a/examples/basic_truss.osmodel +++ b/examples/basic_truss.osmodel @@ -4,7 +4,7 @@ "name": "Basic Truss", "description": "3-bar asymmetric planar truss, linear static analysis.", "author": "OpenSees Examples Manual - Example 1", - "units": "SI (m, N, kg, s, Pa)" + "units": "Metric (m, N, kg, s, Pa)" }, "ndm": 2, "ndf": 2, @@ -245,6 +245,7 @@ "rho": 0.0 } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -284,6 +285,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 1, "load_factor_increment": 1.0, "system": "BandGeneral", diff --git a/examples/basic_truss.py b/examples/basic_truss.py index e121ce9..390e587 100644 --- a/examples/basic_truss.py +++ b/examples/basic_truss.py @@ -95,7 +95,7 @@ def build_basic_truss() -> Project: name="Basic Truss", author="OpenSees Examples Manual - Example 1", description="3-bar asymmetric planar truss, linear static analysis.", - units=UnitSystem.SI_M_N, + units=UnitSystem.METRIC, ), ndm=2, ndf=2, coord_systems=[ diff --git a/examples/beam_quad_2d.osmodel b/examples/beam_quad_2d.osmodel index 8931807..648551b 100644 --- a/examples/beam_quad_2d.osmodel +++ b/examples/beam_quad_2d.osmodel @@ -4,7 +4,7 @@ "name": "Simply Supported Beam — Quad Elements (OpenSees Ex 6.4)", "description": "16×4 plane-stress quad mesh of a 40×10 deep beam with two midspan point loads, LoadControl 0→10 in 10 steps.", "author": "OpenSees Examples Manual", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 2, @@ -3391,6 +3391,7 @@ "b2": 0.0 } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -3441,6 +3442,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 1.0, "system": "ProfileSPD", @@ -3456,6 +3458,7 @@ "name": "FreeVibration", "type": "Transient", "pattern_ids": [], + "pattern_factors": {}, "dt": 0.5, "n_steps": 1500, "system": "BandGeneral", @@ -3471,6 +3474,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/beam_quad_2d.py b/examples/beam_quad_2d.py index 56d5d59..8cbda82 100644 --- a/examples/beam_quad_2d.py +++ b/examples/beam_quad_2d.py @@ -132,7 +132,7 @@ def build_beam_quad_2d() -> Project: f"{NX}×{NY} plane-stress quad mesh of a {L:.0f}×{H:.0f} deep " "beam with two midspan point loads, LoadControl 0→10 in 10 steps." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=2, nodes=nodes, diff --git a/examples/cantilever.osmodel b/examples/cantilever.osmodel index 67dbb8c..ca632ce 100644 --- a/examples/cantilever.osmodel +++ b/examples/cantilever.osmodel @@ -4,7 +4,7 @@ "name": "Cantilever", "description": "", "author": "Ozan", - "units": "SI (m, N, kg, s, Pa)" + "units": "Metric (m, N, kg, s, Pa)" }, "ndm": 3, "ndf": 6, @@ -198,7 +198,8 @@ "Iz": 0.0002, "Iy": 4.5e-05, "G": 80000000000.0, - "J": 8.5e-07 + "J": 8.5e-07, + "shape": null } ], "elements": [ @@ -212,7 +213,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 2, @@ -224,7 +226,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 3, @@ -236,7 +239,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 4, @@ -248,7 +252,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 5, @@ -260,9 +265,11 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -341,6 +348,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 1, "load_factor_increment": 1.0, "system": "BandGeneral", @@ -358,6 +366,7 @@ "pattern_ids": [ 2 ], + "pattern_factors": {}, "n_steps": 1, "load_factor_increment": 1.0, "system": "BandGeneral", diff --git a/examples/cantilever.py b/examples/cantilever.py index 625f2d6..aa87718 100644 --- a/examples/cantilever.py +++ b/examples/cantilever.py @@ -58,7 +58,7 @@ def build_cantilever() -> Project: ] return Project( - meta=ProjectMeta(name="Cantilever", author="Ozan", units=UnitSystem.SI_M_N), + meta=ProjectMeta(name="Cantilever", author="Ozan", units=UnitSystem.METRIC), ndm=3, ndf=6, nodes=nodes, sections=[ diff --git a/examples/concrete04_cantilever.osmodel b/examples/concrete04_cantilever.osmodel index b42cf46..124c06f 100644 --- a/examples/concrete04_cantilever.osmodel +++ b/examples/concrete04_cantilever.osmodel @@ -4,7 +4,7 @@ "name": "Concrete04 Cantilever", "description": "Single RC column cantilever using Concrete04 (Popovics) fiber section. Demonstrates the Concrete04 material end-to-end: schema → model → runner → results.", "author": "", - "units": "SI (m, N, kg, s, Pa)" + "units": "Metric (m, N, kg, s, Pa)" }, "ndm": 2, "ndf": 3, @@ -237,6 +237,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -254,6 +255,7 @@ "pattern_ids": [ 2 ], + "pattern_factors": {}, "control_node": 2, "control_dof": 1, "target_disp": 0.15000000000000002, diff --git a/examples/concrete04_cantilever.py b/examples/concrete04_cantilever.py index 307334c..ca95380 100644 --- a/examples/concrete04_cantilever.py +++ b/examples/concrete04_cantilever.py @@ -85,7 +85,7 @@ def build_concrete04_cantilever() -> Project: "fiber section. Demonstrates the Concrete04 material " "end-to-end: schema → model → runner → results." ), - units=UnitSystem.SI_M_N, + units=UnitSystem.METRIC, ), ndm=2, ndf=3, diff --git a/examples/eigen_two_storey_one_bay_frame.osmodel b/examples/eigen_two_storey_one_bay_frame.osmodel index ec165eb..ba493bf 100644 --- a/examples/eigen_two_storey_one_bay_frame.osmodel +++ b/examples/eigen_two_storey_one_bay_frame.osmodel @@ -4,7 +4,7 @@ "name": "Eigen - Two-Storey One-Bay Frame", "description": "Two-storey one-bay elastic frame with two-mode eigenvalue analysis.", "author": "OpenSees Wiki / Chopra Example 10.5", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -198,7 +198,8 @@ "Iz": 640.0, "Iy": null, "G": null, - "J": null + "J": null, + "shape": null }, { "id": 2, @@ -209,7 +210,8 @@ "Iz": 320.0, "Iy": null, "G": null, - "J": null + "J": null, + "shape": null } ], "elements": [ @@ -223,7 +225,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 2, @@ -235,7 +238,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 3, @@ -247,7 +251,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 4, @@ -259,7 +264,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 5, @@ -271,7 +277,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 6, @@ -283,7 +290,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], "mp_constraints": [], diff --git a/examples/eigen_two_storey_one_bay_frame.py b/examples/eigen_two_storey_one_bay_frame.py index 8c37230..1496f4d 100644 --- a/examples/eigen_two_storey_one_bay_frame.py +++ b/examples/eigen_two_storey_one_bay_frame.py @@ -56,7 +56,7 @@ def build_eigen_two_storey_one_bay_frame() -> Project: description=( "Two-storey one-bay elastic frame with two-mode eigenvalue analysis." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, diff --git a/examples/eigen_two_storey_shear_frame.osmodel b/examples/eigen_two_storey_shear_frame.osmodel index 7c9f127..d5c22d8 100644 --- a/examples/eigen_two_storey_shear_frame.osmodel +++ b/examples/eigen_two_storey_shear_frame.osmodel @@ -4,7 +4,7 @@ "name": "Eigen - Two-Storey Shear Frame", "description": "Two-storey shear frame with equalDOF floor constraints and two-mode eigenvalue analysis.", "author": "OpenSees Wiki / Chopra Example 10.4", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -198,7 +198,8 @@ "Iz": 640.0, "Iy": null, "G": null, - "J": null + "J": null, + "shape": null }, { "id": 2, @@ -209,7 +210,8 @@ "Iz": 320.0, "Iy": null, "G": null, - "J": null + "J": null, + "shape": null }, { "id": 3, @@ -220,7 +222,8 @@ "Iz": 10000000000000.0, "Iy": null, "G": null, - "J": null + "J": null, + "shape": null } ], "elements": [ @@ -234,7 +237,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 2, @@ -246,7 +250,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 3, @@ -258,7 +263,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 4, @@ -270,7 +276,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 5, @@ -282,7 +289,8 @@ ], "section_id": 3, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 6, @@ -294,7 +302,8 @@ ], "section_id": 3, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], "mp_constraints": [ diff --git a/examples/eigen_two_storey_shear_frame.py b/examples/eigen_two_storey_shear_frame.py index 0d9b506..fe30806 100644 --- a/examples/eigen_two_storey_shear_frame.py +++ b/examples/eigen_two_storey_shear_frame.py @@ -62,7 +62,7 @@ def build_eigen_two_storey_shear_frame() -> Project: "Two-storey shear frame with equalDOF floor constraints and " "two-mode eigenvalue analysis." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, diff --git a/examples/elastic_frame.osmodel b/examples/elastic_frame.osmodel index 3cc3dd2..7b82d49 100644 --- a/examples/elastic_frame.osmodel +++ b/examples/elastic_frame.osmodel @@ -4,7 +4,7 @@ "name": "Elastic Frame (OpenSees Ex 4)", "description": "3-story 3-bay 2D elastic frame, AISC W-shape sections, gravity (distributed) + lateral (point) + 5-mode eigen.", "author": "OpenSees Examples Manual", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -523,7 +523,8 @@ "Iz": 3400.0, "Iy": 3400.0, "G": 11200.0, - "J": 1.0 + "J": 1.0, + "shape": null }, { "id": 2, @@ -534,7 +535,8 @@ "Iz": 4330.0, "Iy": 4330.0, "G": 11200.0, - "J": 1.0 + "J": 1.0, + "shape": null }, { "id": 3, @@ -545,7 +547,8 @@ "Iz": 5900.0, "Iy": 5900.0, "G": 11200.0, - "J": 1.0 + "J": 1.0, + "shape": null }, { "id": 4, @@ -556,7 +559,8 @@ "Iz": 4930.0, "Iy": 4930.0, "G": 11200.0, - "J": 1.0 + "J": 1.0, + "shape": null }, { "id": 5, @@ -567,7 +571,8 @@ "Iz": 1830.0, "Iy": 1830.0, "G": 11200.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -581,7 +586,8 @@ ], "section_id": 1, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 2, @@ -593,7 +599,8 @@ ], "section_id": 2, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 3, @@ -605,7 +612,8 @@ ], "section_id": 2, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 4, @@ -617,7 +625,8 @@ ], "section_id": 1, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 5, @@ -629,7 +638,8 @@ ], "section_id": 1, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 6, @@ -641,7 +651,8 @@ ], "section_id": 2, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 7, @@ -653,7 +664,8 @@ ], "section_id": 2, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 8, @@ -665,7 +677,8 @@ ], "section_id": 1, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 9, @@ -677,7 +690,8 @@ ], "section_id": 1, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 10, @@ -689,7 +703,8 @@ ], "section_id": 2, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 11, @@ -701,7 +716,8 @@ ], "section_id": 2, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 12, @@ -713,7 +729,8 @@ ], "section_id": 1, "geom_transf": "PDelta", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 13, @@ -725,7 +742,8 @@ ], "section_id": 3, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 14, @@ -737,7 +755,8 @@ ], "section_id": 3, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 15, @@ -749,7 +768,8 @@ ], "section_id": 3, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 16, @@ -761,7 +781,8 @@ ], "section_id": 4, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 17, @@ -773,7 +794,8 @@ ], "section_id": 4, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 18, @@ -785,7 +807,8 @@ ], "section_id": 4, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 19, @@ -797,7 +820,8 @@ ], "section_id": 5, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 20, @@ -809,7 +833,8 @@ ], "section_id": 5, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 21, @@ -821,9 +846,11 @@ ], "section_id": 5, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -954,6 +981,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 1, "load_factor_increment": 1.0, "system": "BandGeneral", @@ -972,6 +1000,7 @@ 1, 2 ], + "pattern_factors": {}, "n_steps": 1, "load_factor_increment": 1.0, "system": "BandGeneral", diff --git a/examples/elastic_frame.py b/examples/elastic_frame.py index c4d1974..f901755 100644 --- a/examples/elastic_frame.py +++ b/examples/elastic_frame.py @@ -227,7 +227,7 @@ def build_elastic_frame() -> Project: "3-story 3-bay 2D elastic frame, AISC W-shape sections, " "gravity (distributed) + lateral (point) + 5-mode eigen." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, coord_systems=[ diff --git a/examples/ex1a_canti2d.osmodel b/examples/ex1a_canti2d.osmodel index 3786c7b..6f1f11a 100644 --- a/examples/ex1a_canti2d.osmodel +++ b/examples/ex1a_canti2d.osmodel @@ -4,7 +4,7 @@ "name": "OpenSees Ex 1a - 2D Elastic Cantilever Column", "description": "Original Ex 1a elastic cantilever column with shared gravity preload, static pushover, and BM68elc base-excitation cases.", "author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -98,7 +98,8 @@ "Iz": 1080000.0, "Iy": 1080000.0, "G": 1.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -112,7 +113,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], "mp_constraints": [], @@ -4138,7 +4140,8 @@ 0.0006658006, 0.0006636009 ], - "file_path": "BM68elc.acc" + "file_path": "BM68elc.acc", + "use_last": false } ], "load_patterns": [ @@ -4202,6 +4205,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -4219,6 +4223,7 @@ "pattern_ids": [ 2 ], + "pattern_factors": {}, "control_node": 2, "control_dof": 1, "target_disp": 100.0, @@ -4243,6 +4248,7 @@ "pattern_ids": [ 3 ], + "pattern_factors": {}, "dt": 0.02, "n_steps": 1000, "system": "BandGeneral", @@ -4258,6 +4264,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/ex1a_canti2d.py b/examples/ex1a_canti2d.py index 645fefc..53474a4 100644 --- a/examples/ex1a_canti2d.py +++ b/examples/ex1a_canti2d.py @@ -84,7 +84,7 @@ def build_ex1a_canti2d() -> Project: "Original Ex 1a elastic cantilever column with shared gravity " "preload, static pushover, and BM68elc base-excitation cases." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, @@ -222,7 +222,6 @@ def main() -> None: project = build_ex1a_canti2d() project.validate_references() gm = next(ts for ts in project.time_series if ts.id == 3) - assert isinstance(gm, PathTimeSeries) print(f"Built '{project.meta.name}'") print(f" Gravity + pushover + earthquake cases: {len(project.analyses)}") print(f" Ground motion file: {GROUND_MOTION_FILE.name}") diff --git a/examples/ex1a_canti2d_eq.osmodel b/examples/ex1a_canti2d_eq.osmodel index 36e96bd..81140d5 100644 --- a/examples/ex1a_canti2d_eq.osmodel +++ b/examples/ex1a_canti2d_eq.osmodel @@ -4,7 +4,7 @@ "name": "2D Elastic Cantilever EQ (OpenSees Ex 1a)", "description": "Elastic 2D cantilever with gravity preload + horizontal UniformExcitation time history from the A10000 record.", "author": "OpenSees Wiki / Examples Manual", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -98,7 +98,8 @@ "Iz": 1080000.0, "Iy": 1080000.0, "G": 1.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -112,9 +113,11 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -8121,7 +8124,8 @@ -0.000147416, -0.000147326 ], - "file_path": "A10000.txt" + "file_path": "A10000.txt", + "use_last": false } ], "load_patterns": [ @@ -8165,6 +8169,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -8182,6 +8187,7 @@ "pattern_ids": [ 2 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 3995, "system": "BandGeneral", @@ -8197,6 +8203,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/ex1a_canti2d_eq.py b/examples/ex1a_canti2d_eq.py index 4e616d5..8094d73 100644 --- a/examples/ex1a_canti2d_eq.py +++ b/examples/ex1a_canti2d_eq.py @@ -81,7 +81,7 @@ def build_ex1a_canti2d_eq() -> Project: "Elastic 2D cantilever with gravity preload + horizontal " "UniformExcitation time history from the A10000 record." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, @@ -190,7 +190,6 @@ def main() -> None: project = build_ex1a_canti2d_eq() project.validate_references() gm = next(ts for ts in project.time_series if ts.id == 2) - assert isinstance(gm, PathTimeSeries) print(f"Built '{project.meta.name}'") print(f" Ground motion file: {GROUND_MOTION_FILE.name}") print(f" Reference Tcl: {REFERENCE_TCL.name}") diff --git a/examples/ex1b_portal2d.osmodel b/examples/ex1b_portal2d.osmodel index deb27fc..e072f3a 100644 --- a/examples/ex1b_portal2d.osmodel +++ b/examples/ex1b_portal2d.osmodel @@ -4,7 +4,7 @@ "name": "OpenSees Ex 1b - Elastic Portal Frame", "description": "Original Ex 1b elastic portal frame with shared gravity preload, static pushover, and BM68elc earthquake cases.", "author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -148,7 +148,8 @@ "Iz": 1080000.0, "Iy": 1080000.0, "G": 1.0, - "J": 1.0 + "J": 1.0, + "shape": null }, { "id": 2, @@ -159,7 +160,8 @@ "Iz": 4423680.0, "Iy": 4423680.0, "G": 1.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -173,7 +175,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 2, @@ -185,7 +188,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 3, @@ -197,7 +201,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], "mp_constraints": [], @@ -4223,7 +4228,8 @@ 0.0006658006, 0.0006636009 ], - "file_path": "BM68elc.acc" + "file_path": "BM68elc.acc", + "use_last": false } ], "load_patterns": [ @@ -4293,6 +4299,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -4310,6 +4317,7 @@ "pattern_ids": [ 2 ], + "pattern_factors": {}, "control_node": 3, "control_dof": 1, "target_disp": 10.0, @@ -4335,6 +4343,7 @@ "pattern_ids": [ 3 ], + "pattern_factors": {}, "dt": 0.02, "n_steps": 1000, "system": "BandGeneral", @@ -4350,6 +4359,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/ex1b_portal2d.py b/examples/ex1b_portal2d.py index c8e80e3..87f5400 100644 --- a/examples/ex1b_portal2d.py +++ b/examples/ex1b_portal2d.py @@ -88,7 +88,7 @@ def build_ex1b_portal2d() -> Project: "Original Ex 1b elastic portal frame with shared gravity " "preload, static pushover, and BM68elc earthquake cases." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, @@ -209,7 +209,6 @@ def main() -> None: project = build_ex1b_portal2d() project.validate_references() gm = next(ts for ts in project.time_series if ts.id == 3) - assert isinstance(gm, PathTimeSeries) print(f"Built '{project.meta.name}'") print(f" Gravity + pushover + earthquake cases: {len(project.analyses)}") print(f" Ground motion file: {GROUND_MOTION_FILE.name}") diff --git a/examples/ex2a_canti2d_elastic_element.osmodel b/examples/ex2a_canti2d_elastic_element.osmodel index c246716..21f2c9c 100644 --- a/examples/ex2a_canti2d_elastic_element.osmodel +++ b/examples/ex2a_canti2d_elastic_element.osmodel @@ -4,7 +4,7 @@ "name": "OpenSees Ex 2a - Elastic Cantilever Column with Variables", "description": "Variable-driven elastic cantilever model with derived mass, section, pushover, and earthquake analysis parameters.", "author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -98,7 +98,8 @@ "Iz": 1080000.0, "Iy": 1080000.0, "G": 1.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -112,7 +113,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], "mp_constraints": [], @@ -4138,7 +4140,8 @@ 0.0006658006, 0.0006636009 ], - "file_path": "BM68elc.acc" + "file_path": "BM68elc.acc", + "use_last": false } ], "load_patterns": [ @@ -4202,6 +4205,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -4219,6 +4223,7 @@ "pattern_ids": [ 200 ], + "pattern_factors": {}, "control_node": 2, "control_dof": 1, "target_disp": 4.32, @@ -4243,6 +4248,7 @@ "pattern_ids": [ 400 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 1000, "system": "SparseGeneral", @@ -4258,6 +4264,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/ex2a_canti2d_elastic_element.py b/examples/ex2a_canti2d_elastic_element.py index f39f791..93f1767 100644 --- a/examples/ex2a_canti2d_elastic_element.py +++ b/examples/ex2a_canti2d_elastic_element.py @@ -99,7 +99,7 @@ def build_ex2a_canti2d_elastic_element() -> Project: "Variable-driven elastic cantilever model with derived mass, " "section, pushover, and earthquake analysis parameters." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, @@ -231,7 +231,6 @@ def main() -> None: project = build_ex2a_canti2d_elastic_element() project.validate_references() gm = next(ts for ts in project.time_series if ts.id == 3) - assert isinstance(gm, PathTimeSeries) print(f"Built '{project.meta.name}'") print(f" LCol={L_COL}, Weight={WEIGHT}, ACol={A_COL:.1f}, Ec={E_C:.3f}") print(f" Gravity + pushover + earthquake cases: {len(project.analyses)}") diff --git a/examples/ex2b_canti2d_inelastic_section.osmodel b/examples/ex2b_canti2d_inelastic_section.osmodel index 13aa8cc..7a5e302 100644 --- a/examples/ex2b_canti2d_inelastic_section.osmodel +++ b/examples/ex2b_canti2d_inelastic_section.osmodel @@ -4,7 +4,7 @@ "name": "OpenSees Ex 2b - Nonlinear Cantilever Column", "description": "Nonlinear cantilever with an aggregated uniaxial section: elastic axial P plus bilinear inelastic Mz in one force-based element.", "author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -4166,7 +4166,8 @@ 0.0006658006, 0.0006636009 ], - "file_path": "BM68elc.acc" + "file_path": "BM68elc.acc", + "use_last": false } ], "load_patterns": [ @@ -4230,6 +4231,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -4247,6 +4249,7 @@ "pattern_ids": [ 200 ], + "pattern_factors": {}, "control_node": 2, "control_dof": 1, "target_disp": 21.6, @@ -4271,6 +4274,7 @@ "pattern_ids": [ 400 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 1000, "system": "SparseGeneral", @@ -4286,6 +4290,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/ex2b_canti2d_inelastic_section.py b/examples/ex2b_canti2d_inelastic_section.py index d8444b1..842199c 100644 --- a/examples/ex2b_canti2d_inelastic_section.py +++ b/examples/ex2b_canti2d_inelastic_section.py @@ -108,7 +108,7 @@ def build_ex2b_canti2d_inelastic_section() -> Project: "Nonlinear cantilever with an aggregated uniaxial section: " "elastic axial P plus bilinear inelastic Mz in one force-based element." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, @@ -253,7 +253,6 @@ def main() -> None: project = build_ex2b_canti2d_inelastic_section() project.validate_references() gm = next(ts for ts in project.time_series if ts.id == 400) - assert isinstance(gm, PathTimeSeries) print(f"Built '{project.meta.name}'") print( f" LCol={L_COL}, EA={EA_COL:.1f}, My={MY_COL:.1f}, " diff --git a/examples/ex2c_canti2d_inelastic_fiber_section.osmodel b/examples/ex2c_canti2d_inelastic_fiber_section.osmodel index db0c485..840d512 100644 --- a/examples/ex2c_canti2d_inelastic_fiber_section.osmodel +++ b/examples/ex2c_canti2d_inelastic_fiber_section.osmodel @@ -4,7 +4,7 @@ "name": "OpenSees Ex 2c - Inelastic Fiber-Section Cantilever", "description": "Nonlinear cantilever with a fiber section built from Concrete02 and Steel02 uniaxial materials.", "author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -4194,7 +4194,8 @@ 0.0006658006, 0.0006636009 ], - "file_path": "BM68elc.acc" + "file_path": "BM68elc.acc", + "use_last": false } ], "load_patterns": [ @@ -4258,6 +4259,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -4275,6 +4277,7 @@ "pattern_ids": [ 200 ], + "pattern_factors": {}, "control_node": 2, "control_dof": 1, "target_disp": 4.32, @@ -4299,6 +4302,7 @@ "pattern_ids": [ 400 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 1000, "system": "SparseGeneral", @@ -4314,6 +4318,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/ex2c_canti2d_inelastic_fiber_section.py b/examples/ex2c_canti2d_inelastic_fiber_section.py index 0f64fec..e2d06af 100644 --- a/examples/ex2c_canti2d_inelastic_fiber_section.py +++ b/examples/ex2c_canti2d_inelastic_fiber_section.py @@ -120,7 +120,7 @@ def build_ex2c_canti2d_inelastic_fiber_section() -> Project: "Nonlinear cantilever with a fiber section built from " "Concrete02 and Steel02 uniaxial materials." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, @@ -301,7 +301,6 @@ def main() -> None: project = build_ex2c_canti2d_inelastic_fiber_section() project.validate_references() gm = next(ts for ts in project.time_series if ts.id == 400) - assert isinstance(gm, PathTimeSeries) print(f"Built '{project.meta.name}'") print( f" LCol={L_COL}, ACol={A_COL:.1f}, Iz={IZ_COL:.1f}, " diff --git a/examples/ex3_canti2d_elastic_element.osmodel b/examples/ex3_canti2d_elastic_element.osmodel index 4481e5f..f50b215 100644 --- a/examples/ex3_canti2d_elastic_element.osmodel +++ b/examples/ex3_canti2d_elastic_element.osmodel @@ -4,7 +4,7 @@ "name": "OpenSees Ex 3 - Cantilever (Elastic Build)", "description": "Example 3 elastic cantilever build with unit-scaled geometry and shared push / uniform-EQ analysis files.", "author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -98,7 +98,8 @@ "Iz": 1080000.0, "Iy": 1080000.0, "G": 1.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -112,7 +113,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], "mp_constraints": [], @@ -4138,7 +4140,8 @@ 0.0006658006, 0.0006636009 ], - "file_path": "BM68elc.acc" + "file_path": "BM68elc.acc", + "use_last": false } ], "load_patterns": [ @@ -4202,6 +4205,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -4219,6 +4223,7 @@ "pattern_ids": [ 200 ], + "pattern_factors": {}, "control_node": 2, "control_dof": 1, "target_disp": 21.6, @@ -4243,6 +4248,7 @@ "pattern_ids": [ 400 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 1000, "system": "SparseGeneral", @@ -4258,6 +4264,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/ex3_canti2d_elastic_element.py b/examples/ex3_canti2d_elastic_element.py index f754360..b48deb7 100644 --- a/examples/ex3_canti2d_elastic_element.py +++ b/examples/ex3_canti2d_elastic_element.py @@ -87,7 +87,7 @@ def build_ex3_canti2d_elastic_element() -> Project: "Example 3 elastic cantilever build with unit-scaled geometry " "and shared push / uniform-EQ analysis files." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, diff --git a/examples/ex3_canti2d_inelastic_fiber_section.osmodel b/examples/ex3_canti2d_inelastic_fiber_section.osmodel index 37e9f33..b20ef17 100644 --- a/examples/ex3_canti2d_inelastic_fiber_section.osmodel +++ b/examples/ex3_canti2d_inelastic_fiber_section.osmodel @@ -4,7 +4,7 @@ "name": "OpenSees Ex 3 - Cantilever (Fiber Section Build)", "description": "Example 3 cantilever with inelastic fiber section and shared analysis files.", "author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -4194,7 +4194,8 @@ 0.0006658006, 0.0006636009 ], - "file_path": "BM68elc.acc" + "file_path": "BM68elc.acc", + "use_last": false } ], "load_patterns": [ @@ -4258,6 +4259,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -4275,6 +4277,7 @@ "pattern_ids": [ 200 ], + "pattern_factors": {}, "control_node": 2, "control_dof": 1, "target_disp": 21.6, @@ -4299,6 +4302,7 @@ "pattern_ids": [ 400 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 1000, "system": "SparseGeneral", @@ -4314,6 +4318,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/ex3_canti2d_inelastic_fiber_section.py b/examples/ex3_canti2d_inelastic_fiber_section.py index 83fe887..2a090fa 100644 --- a/examples/ex3_canti2d_inelastic_fiber_section.py +++ b/examples/ex3_canti2d_inelastic_fiber_section.py @@ -107,7 +107,7 @@ def build_ex3_canti2d_inelastic_fiber_section() -> Project: name="OpenSees Ex 3 - Cantilever (Fiber Section Build)", author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna", description="Example 3 cantilever with inelastic fiber section and shared analysis files.", - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, nodes=[ diff --git a/examples/ex3_canti2d_inelastic_section.osmodel b/examples/ex3_canti2d_inelastic_section.osmodel index 764d6f3..7cf5e13 100644 --- a/examples/ex3_canti2d_inelastic_section.osmodel +++ b/examples/ex3_canti2d_inelastic_section.osmodel @@ -4,7 +4,7 @@ "name": "OpenSees Ex 3 - Cantilever (Inelastic Section Build)", "description": "Example 3 cantilever with aggregated uniaxial inelastic section and shared analysis files.", "author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -4166,7 +4166,8 @@ 0.0006658006, 0.0006636009 ], - "file_path": "BM68elc.acc" + "file_path": "BM68elc.acc", + "use_last": false } ], "load_patterns": [ @@ -4230,6 +4231,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -4247,6 +4249,7 @@ "pattern_ids": [ 200 ], + "pattern_factors": {}, "control_node": 2, "control_dof": 1, "target_disp": 21.6, @@ -4271,6 +4274,7 @@ "pattern_ids": [ 400 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 1000, "system": "SparseGeneral", @@ -4286,6 +4290,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/ex3_canti2d_inelastic_section.py b/examples/ex3_canti2d_inelastic_section.py index ae97650..4a7f903 100644 --- a/examples/ex3_canti2d_inelastic_section.py +++ b/examples/ex3_canti2d_inelastic_section.py @@ -88,7 +88,7 @@ def build_ex3_canti2d_inelastic_section() -> Project: name="OpenSees Ex 3 - Cantilever (Inelastic Section Build)", author="OpenSees Wiki / Silvia Mazzoni & Frank McKenna", description="Example 3 cantilever with aggregated uniaxial inelastic section and shared analysis files.", - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, nodes=[ diff --git a/examples/ex4_portal2d_elastic_element.osmodel b/examples/ex4_portal2d_elastic_element.osmodel index d2426ff..33a80a4 100644 --- a/examples/ex4_portal2d_elastic_element.osmodel +++ b/examples/ex4_portal2d_elastic_element.osmodel @@ -4,7 +4,7 @@ "name": "OpenSees Ex 4 - Portal Frame (Elastic Build)", "description": "Example 4 elastic portal frame with shared gravity, push, and sine-wave support motion.", "author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -148,7 +148,8 @@ "Iz": 864000.0, "Iy": 864000.0, "G": 1.0, - "J": 1.0 + "J": 1.0, + "shape": null }, { "id": 2, @@ -159,7 +160,8 @@ "Iz": 4423680.0, "Iy": 4423680.0, "G": 1.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -173,7 +175,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 2, @@ -185,7 +188,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 3, @@ -197,7 +201,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], "mp_constraints": [], @@ -335,7 +340,7 @@ -83.82633839751217, -99.09214039482102, -113.56011074290575, - -127.11376185155243, + -127.11376185155244, -139.643967691452, -151.04984241282355, -161.23955261895566, @@ -428,7 +433,7 @@ -113.56011074290593, -99.0921403948212, -83.82633839751236, - -67.88561601251554, + -67.88561601251556, -51.398318563481766, -34.49719207509674, -17.318314480143496, @@ -511,7 +516,7 @@ 99.0921403948209, 113.56011074290566, 127.11376185155234, - 139.64396769145193, + 139.6439676914519, 151.04984241282366, 161.23955261895557, 170.1310567547217, @@ -540,12 +545,12 @@ 2.129415854357418e-13, -17.31831448014314, -34.49719207509707, - -51.398318563481425, + -51.39831856348143, -67.88561601251521, -83.82633839751205, -99.09214039482087, -113.5601107429062, - -127.11376185155231, + -127.11376185155234, -139.6439676914519, -151.04984241282324, -161.23955261895594, @@ -578,7 +583,7 @@ 51.3983185634814, 67.88561601251583, 83.82633839751202, - 99.09214039482086, + 99.09214039482087, 113.56011074290619, 127.11376185155231, 139.64396769145142, @@ -635,7 +640,7 @@ -151.04984241282355, -139.6439676914518, -127.1137618515527, - -113.56011074290548, + -113.5601107429055, -99.09214039482131, -83.82633839751311, -67.88561601251568, @@ -654,7 +659,7 @@ 139.64396769145233, 151.0498424128232, 161.2395526189559, - 170.13105675472167, + 170.13105675472164, 177.6527656569399, 183.7441189482236, 188.3560726335284, @@ -668,7 +673,7 @@ 170.1310567547216, 161.23955261895586, 151.04984241282312, - 139.64396769145227, + 139.64396769145225, 127.11376185155324, 113.56011074290606, 99.09214039482075, @@ -805,14 +810,14 @@ 188.35607263352813, 183.7441189482236, 177.65276565694043, - 170.13105675472167, + 170.13105675472164, 161.2395526189559, 151.0498424128232, 139.64396769145233, 127.11376185155329, 113.56011074290502, 99.09214039482083, - 83.82633839751259, + 83.8263383975126, 67.88561601251514, 51.39831856348202, 34.49719207509632, @@ -824,7 +829,8 @@ -67.88561601251568, -83.82633839751188 ], - "file_path": "generated:sine-wave" + "file_path": "generated:sine-wave", + "use_last": false }, { "id": 401, @@ -860,7 +866,7 @@ 5.09980167540335, 5.928784854272076, 6.7100329435632435, - 7.437255793028317, + 7.437255793028318, 8.104598231883518, 8.70668721120218, 9.238675064545419, @@ -911,7 +917,7 @@ -10.075813279025667, -9.696278538521907, -9.238675064545415, - -8.706687211202178, + -8.706687211202176, -8.10459823188352, -7.437255793028321, -6.710032943563246, @@ -941,7 +947,7 @@ 10.718732373371513, 10.762057251873964, 10.718732373371514, - 10.58910656431125, + 10.589106564311248, 10.37422349548898, 10.075813279025667, 9.696278538521907, @@ -1017,7 +1023,7 @@ 9.696278538521907, 9.238675064545422, 8.706687211202185, - 8.104598231883523, + 8.104598231883521, 7.437255793028323, 6.71003294356325, 5.928784854272083, @@ -1158,8 +1164,8 @@ 9.238675064545424, 8.706687211202166, 8.104598231883525, - 7.437255793028327, - 6.710032943563254, + 7.437255793028328, + 6.710032943563253, 5.928784854272055, 5.099801675403362, 4.229757892079114, @@ -1193,7 +1199,7 @@ -9.238675064545426, -8.706687211202167, -8.104598231883527, - -7.437255793028355, + -7.437255793028356, -6.710032943563254, -5.928784854272056, -5.099801675403363, @@ -1265,7 +1271,7 @@ -8.104598231883529, -7.437255793028303, -6.710032943563257, - -5.928784854272058, + -5.928784854272059, -5.0998016754033655, -4.229757892079153, -3.3256585852652147, @@ -1279,7 +1285,7 @@ 4.229757892079088, 5.09980167540337, 5.928784854272062, - 6.710032943563261, + 6.71003294356326, 7.4372557930283065, 8.104598231883482, 8.706687211202171, @@ -1316,7 +1322,7 @@ -5.92878485427203, -6.71003294356329, -7.437255793028332, - -8.104598231883505, + -8.104598231883507, -8.706687211202192, -9.23867506454541, -9.69627853852188, @@ -1328,7 +1334,7 @@ -10.71873237337151, -10.589106564311251, -10.374223495488975, - -10.075813279025672, + -10.075813279025674, -9.696278538521929, -9.238675064545427, -8.706687211202215, @@ -1355,7 +1361,7 @@ 8.70668721120217, 9.238675064545427, 9.696278538521897, - 10.075813279025672, + 10.075813279025674, 10.374223495488975, 10.589106564311237, 10.718732373371518, @@ -1402,7 +1408,7 @@ -9.696278538521899, -9.23867506454543, -8.706687211202215, - -8.104598231883534, + -8.104598231883532, -7.437255793028362, -6.710032943563201, -5.928784854272062, @@ -1418,11 +1424,11 @@ 3.3256585852652147, 4.229757892079083, 5.0998016754033655, - 5.928784854272058, + 5.928784854272059, 6.710032943563257, 7.437255793028303, 8.104598231883479, - 8.706687211202214, + 8.706687211202212, 9.238675064545427, 9.696278538521895, 10.075813279025672, @@ -1436,7 +1442,8 @@ 10.07581327902566, 9.696278538521915 ], - "file_path": "generated:sine-wave-velocity" + "file_path": "generated:sine-wave-velocity", + "use_last": false } ], "load_patterns": [ @@ -1506,6 +1513,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -1523,6 +1531,7 @@ "pattern_ids": [ 200 ], + "pattern_factors": {}, "control_node": 3, "control_dof": 1, "target_disp": 43.2, @@ -1548,6 +1557,7 @@ "pattern_ids": [ 400 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 1000, "system": "BandGeneral", @@ -1563,6 +1573,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/ex4_portal2d_inelastic_fiber_section.osmodel b/examples/ex4_portal2d_inelastic_fiber_section.osmodel index bc4102b..96119f6 100644 --- a/examples/ex4_portal2d_inelastic_fiber_section.osmodel +++ b/examples/ex4_portal2d_inelastic_fiber_section.osmodel @@ -4,7 +4,7 @@ "name": "OpenSees Ex 4 - Portal Frame (Fiber Section Build)", "description": "Example 4 portal frame with fiber-section columns and shared pushover / sine-wave analyses.", "author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -213,7 +213,8 @@ "Iz": 4423680.0, "Iy": 4423680.0, "G": 1.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -395,7 +396,7 @@ -83.82633839751217, -99.09214039482102, -113.56011074290575, - -127.11376185155243, + -127.11376185155244, -139.643967691452, -151.04984241282355, -161.23955261895566, @@ -488,7 +489,7 @@ -113.56011074290593, -99.0921403948212, -83.82633839751236, - -67.88561601251554, + -67.88561601251556, -51.398318563481766, -34.49719207509674, -17.318314480143496, @@ -571,7 +572,7 @@ 99.0921403948209, 113.56011074290566, 127.11376185155234, - 139.64396769145193, + 139.6439676914519, 151.04984241282366, 161.23955261895557, 170.1310567547217, @@ -600,12 +601,12 @@ 2.129415854357418e-13, -17.31831448014314, -34.49719207509707, - -51.398318563481425, + -51.39831856348143, -67.88561601251521, -83.82633839751205, -99.09214039482087, -113.5601107429062, - -127.11376185155231, + -127.11376185155234, -139.6439676914519, -151.04984241282324, -161.23955261895594, @@ -638,7 +639,7 @@ 51.3983185634814, 67.88561601251583, 83.82633839751202, - 99.09214039482086, + 99.09214039482087, 113.56011074290619, 127.11376185155231, 139.64396769145142, @@ -695,7 +696,7 @@ -151.04984241282355, -139.6439676914518, -127.1137618515527, - -113.56011074290548, + -113.5601107429055, -99.09214039482131, -83.82633839751311, -67.88561601251568, @@ -714,7 +715,7 @@ 139.64396769145233, 151.0498424128232, 161.2395526189559, - 170.13105675472167, + 170.13105675472164, 177.6527656569399, 183.7441189482236, 188.3560726335284, @@ -728,7 +729,7 @@ 170.1310567547216, 161.23955261895586, 151.04984241282312, - 139.64396769145227, + 139.64396769145225, 127.11376185155324, 113.56011074290606, 99.09214039482075, @@ -865,14 +866,14 @@ 188.35607263352813, 183.7441189482236, 177.65276565694043, - 170.13105675472167, + 170.13105675472164, 161.2395526189559, 151.0498424128232, 139.64396769145233, 127.11376185155329, 113.56011074290502, 99.09214039482083, - 83.82633839751259, + 83.8263383975126, 67.88561601251514, 51.39831856348202, 34.49719207509632, @@ -884,7 +885,8 @@ -67.88561601251568, -83.82633839751188 ], - "file_path": "generated:sine-wave" + "file_path": "generated:sine-wave", + "use_last": false }, { "id": 401, @@ -920,7 +922,7 @@ 5.09980167540335, 5.928784854272076, 6.7100329435632435, - 7.437255793028317, + 7.437255793028318, 8.104598231883518, 8.70668721120218, 9.238675064545419, @@ -971,7 +973,7 @@ -10.075813279025667, -9.696278538521907, -9.238675064545415, - -8.706687211202178, + -8.706687211202176, -8.10459823188352, -7.437255793028321, -6.710032943563246, @@ -1001,7 +1003,7 @@ 10.718732373371513, 10.762057251873964, 10.718732373371514, - 10.58910656431125, + 10.589106564311248, 10.37422349548898, 10.075813279025667, 9.696278538521907, @@ -1077,7 +1079,7 @@ 9.696278538521907, 9.238675064545422, 8.706687211202185, - 8.104598231883523, + 8.104598231883521, 7.437255793028323, 6.71003294356325, 5.928784854272083, @@ -1218,8 +1220,8 @@ 9.238675064545424, 8.706687211202166, 8.104598231883525, - 7.437255793028327, - 6.710032943563254, + 7.437255793028328, + 6.710032943563253, 5.928784854272055, 5.099801675403362, 4.229757892079114, @@ -1253,7 +1255,7 @@ -9.238675064545426, -8.706687211202167, -8.104598231883527, - -7.437255793028355, + -7.437255793028356, -6.710032943563254, -5.928784854272056, -5.099801675403363, @@ -1325,7 +1327,7 @@ -8.104598231883529, -7.437255793028303, -6.710032943563257, - -5.928784854272058, + -5.928784854272059, -5.0998016754033655, -4.229757892079153, -3.3256585852652147, @@ -1339,7 +1341,7 @@ 4.229757892079088, 5.09980167540337, 5.928784854272062, - 6.710032943563261, + 6.71003294356326, 7.4372557930283065, 8.104598231883482, 8.706687211202171, @@ -1376,7 +1378,7 @@ -5.92878485427203, -6.71003294356329, -7.437255793028332, - -8.104598231883505, + -8.104598231883507, -8.706687211202192, -9.23867506454541, -9.69627853852188, @@ -1388,7 +1390,7 @@ -10.71873237337151, -10.589106564311251, -10.374223495488975, - -10.075813279025672, + -10.075813279025674, -9.696278538521929, -9.238675064545427, -8.706687211202215, @@ -1415,7 +1417,7 @@ 8.70668721120217, 9.238675064545427, 9.696278538521897, - 10.075813279025672, + 10.075813279025674, 10.374223495488975, 10.589106564311237, 10.718732373371518, @@ -1462,7 +1464,7 @@ -9.696278538521899, -9.23867506454543, -8.706687211202215, - -8.104598231883534, + -8.104598231883532, -7.437255793028362, -6.710032943563201, -5.928784854272062, @@ -1478,11 +1480,11 @@ 3.3256585852652147, 4.229757892079083, 5.0998016754033655, - 5.928784854272058, + 5.928784854272059, 6.710032943563257, 7.437255793028303, 8.104598231883479, - 8.706687211202214, + 8.706687211202212, 9.238675064545427, 9.696278538521895, 10.075813279025672, @@ -1496,7 +1498,8 @@ 10.07581327902566, 9.696278538521915 ], - "file_path": "generated:sine-wave-velocity" + "file_path": "generated:sine-wave-velocity", + "use_last": false } ], "load_patterns": [ @@ -1566,6 +1569,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -1583,6 +1587,7 @@ "pattern_ids": [ 200 ], + "pattern_factors": {}, "control_node": 3, "control_dof": 1, "target_disp": 43.2, @@ -1608,6 +1613,7 @@ "pattern_ids": [ 400 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 1000, "system": "BandGeneral", @@ -1623,6 +1629,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/ex4_portal2d_inelastic_section.osmodel b/examples/ex4_portal2d_inelastic_section.osmodel index 66a4935..42c5487 100644 --- a/examples/ex4_portal2d_inelastic_section.osmodel +++ b/examples/ex4_portal2d_inelastic_section.osmodel @@ -4,7 +4,7 @@ "name": "OpenSees Ex 4 - Portal Frame (Inelastic Section Build)", "description": "Example 4 portal frame with aggregated uniaxial inelastic column sections and shared analyses.", "author": "OpenSees Wiki / Silvia Mazzoni & Frank McKenna", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -185,7 +185,8 @@ "Iz": 4423680.0, "Iy": 4423680.0, "G": 1.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -367,7 +368,7 @@ -83.82633839751217, -99.09214039482102, -113.56011074290575, - -127.11376185155243, + -127.11376185155244, -139.643967691452, -151.04984241282355, -161.23955261895566, @@ -460,7 +461,7 @@ -113.56011074290593, -99.0921403948212, -83.82633839751236, - -67.88561601251554, + -67.88561601251556, -51.398318563481766, -34.49719207509674, -17.318314480143496, @@ -543,7 +544,7 @@ 99.0921403948209, 113.56011074290566, 127.11376185155234, - 139.64396769145193, + 139.6439676914519, 151.04984241282366, 161.23955261895557, 170.1310567547217, @@ -572,12 +573,12 @@ 2.129415854357418e-13, -17.31831448014314, -34.49719207509707, - -51.398318563481425, + -51.39831856348143, -67.88561601251521, -83.82633839751205, -99.09214039482087, -113.5601107429062, - -127.11376185155231, + -127.11376185155234, -139.6439676914519, -151.04984241282324, -161.23955261895594, @@ -610,7 +611,7 @@ 51.3983185634814, 67.88561601251583, 83.82633839751202, - 99.09214039482086, + 99.09214039482087, 113.56011074290619, 127.11376185155231, 139.64396769145142, @@ -667,7 +668,7 @@ -151.04984241282355, -139.6439676914518, -127.1137618515527, - -113.56011074290548, + -113.5601107429055, -99.09214039482131, -83.82633839751311, -67.88561601251568, @@ -686,7 +687,7 @@ 139.64396769145233, 151.0498424128232, 161.2395526189559, - 170.13105675472167, + 170.13105675472164, 177.6527656569399, 183.7441189482236, 188.3560726335284, @@ -700,7 +701,7 @@ 170.1310567547216, 161.23955261895586, 151.04984241282312, - 139.64396769145227, + 139.64396769145225, 127.11376185155324, 113.56011074290606, 99.09214039482075, @@ -837,14 +838,14 @@ 188.35607263352813, 183.7441189482236, 177.65276565694043, - 170.13105675472167, + 170.13105675472164, 161.2395526189559, 151.0498424128232, 139.64396769145233, 127.11376185155329, 113.56011074290502, 99.09214039482083, - 83.82633839751259, + 83.8263383975126, 67.88561601251514, 51.39831856348202, 34.49719207509632, @@ -856,7 +857,8 @@ -67.88561601251568, -83.82633839751188 ], - "file_path": "generated:sine-wave" + "file_path": "generated:sine-wave", + "use_last": false }, { "id": 401, @@ -892,7 +894,7 @@ 5.09980167540335, 5.928784854272076, 6.7100329435632435, - 7.437255793028317, + 7.437255793028318, 8.104598231883518, 8.70668721120218, 9.238675064545419, @@ -943,7 +945,7 @@ -10.075813279025667, -9.696278538521907, -9.238675064545415, - -8.706687211202178, + -8.706687211202176, -8.10459823188352, -7.437255793028321, -6.710032943563246, @@ -973,7 +975,7 @@ 10.718732373371513, 10.762057251873964, 10.718732373371514, - 10.58910656431125, + 10.589106564311248, 10.37422349548898, 10.075813279025667, 9.696278538521907, @@ -1049,7 +1051,7 @@ 9.696278538521907, 9.238675064545422, 8.706687211202185, - 8.104598231883523, + 8.104598231883521, 7.437255793028323, 6.71003294356325, 5.928784854272083, @@ -1190,8 +1192,8 @@ 9.238675064545424, 8.706687211202166, 8.104598231883525, - 7.437255793028327, - 6.710032943563254, + 7.437255793028328, + 6.710032943563253, 5.928784854272055, 5.099801675403362, 4.229757892079114, @@ -1225,7 +1227,7 @@ -9.238675064545426, -8.706687211202167, -8.104598231883527, - -7.437255793028355, + -7.437255793028356, -6.710032943563254, -5.928784854272056, -5.099801675403363, @@ -1297,7 +1299,7 @@ -8.104598231883529, -7.437255793028303, -6.710032943563257, - -5.928784854272058, + -5.928784854272059, -5.0998016754033655, -4.229757892079153, -3.3256585852652147, @@ -1311,7 +1313,7 @@ 4.229757892079088, 5.09980167540337, 5.928784854272062, - 6.710032943563261, + 6.71003294356326, 7.4372557930283065, 8.104598231883482, 8.706687211202171, @@ -1348,7 +1350,7 @@ -5.92878485427203, -6.71003294356329, -7.437255793028332, - -8.104598231883505, + -8.104598231883507, -8.706687211202192, -9.23867506454541, -9.69627853852188, @@ -1360,7 +1362,7 @@ -10.71873237337151, -10.589106564311251, -10.374223495488975, - -10.075813279025672, + -10.075813279025674, -9.696278538521929, -9.238675064545427, -8.706687211202215, @@ -1387,7 +1389,7 @@ 8.70668721120217, 9.238675064545427, 9.696278538521897, - 10.075813279025672, + 10.075813279025674, 10.374223495488975, 10.589106564311237, 10.718732373371518, @@ -1434,7 +1436,7 @@ -9.696278538521899, -9.23867506454543, -8.706687211202215, - -8.104598231883534, + -8.104598231883532, -7.437255793028362, -6.710032943563201, -5.928784854272062, @@ -1450,11 +1452,11 @@ 3.3256585852652147, 4.229757892079083, 5.0998016754033655, - 5.928784854272058, + 5.928784854272059, 6.710032943563257, 7.437255793028303, 8.104598231883479, - 8.706687211202214, + 8.706687211202212, 9.238675064545427, 9.696278538521895, 10.075813279025672, @@ -1468,7 +1470,8 @@ 10.07581327902566, 9.696278538521915 ], - "file_path": "generated:sine-wave-velocity" + "file_path": "generated:sine-wave-velocity", + "use_last": false } ], "load_patterns": [ @@ -1538,6 +1541,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -1555,6 +1559,7 @@ "pattern_ids": [ 200 ], + "pattern_factors": {}, "control_node": 3, "control_dof": 1, "target_disp": 43.2, @@ -1580,6 +1585,7 @@ "pattern_ids": [ 400 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 1000, "system": "BandGeneral", @@ -1595,6 +1601,8 @@ "max_iter": 10, "rayleigh_alpha_m": 0.0, "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, "rayleigh_mode1_damping": 0.02, "preload_case_ids": [ 1 diff --git a/examples/moment_curvature.osmodel b/examples/moment_curvature.osmodel index 167c610..4a1975e 100644 --- a/examples/moment_curvature.osmodel +++ b/examples/moment_curvature.osmodel @@ -4,7 +4,7 @@ "name": "Moment-Curvature (OpenSees Ex 2)", "description": "RC column fibre section — constant axial P + DisplacementControl curvature pushover. Kip-in-ksi units throughout.", "author": "OpenSees Examples Manual", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -251,6 +251,7 @@ "section_id": 1 } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -317,6 +318,7 @@ 1, 2 ], + "pattern_factors": {}, "control_node": 2, "control_dof": 3, "target_disp": 0.0019047619047619052, @@ -329,7 +331,8 @@ "algorithm": "Newton", "test": "NormUnbalance", "tolerance": 1e-09, - "max_iter": 25 + "max_iter": 25, + "preload_case_ids": [] } ] } \ No newline at end of file diff --git a/examples/moment_curvature.py b/examples/moment_curvature.py index 66dd63f..ff6cec2 100644 --- a/examples/moment_curvature.py +++ b/examples/moment_curvature.py @@ -14,7 +14,7 @@ linear reference moment pattern (Mz = 1 kip·in) is added and DisplacementControl on DOF 3 ramps the curvature to μ·Ky where μ = 15 and Ky is the elastic yield curvature estimate. -Units: kip, in, ksi (UnitSystem.US_IN_KIP). +Units: kip, in, ksi (UnitSystem.IMPERIAL). GUI walkthrough: File → Open → moment_curvature.osmodel, Options → Set Display Units → US (in, kip, kip·s²/in, s, ksi), Analyze → Run → @@ -83,7 +83,7 @@ def build_moment_curvature() -> Project: "RC column fibre section — constant axial P + DisplacementControl " "curvature pushover. Kip-in-ksi units throughout." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, coord_systems=[ diff --git a/examples/portal_frame.osmodel b/examples/portal_frame.osmodel index 3e496c4..c438728 100644 --- a/examples/portal_frame.osmodel +++ b/examples/portal_frame.osmodel @@ -4,7 +4,7 @@ "name": "Portal Frame", "description": "", "author": "Ozan", - "units": "SI (m, N, kg, s, Pa)" + "units": "Metric (m, N, kg, s, Pa)" }, "ndm": 3, "ndf": 6, @@ -161,7 +161,8 @@ "Iz": 0.000416, "Iy": 0.000129, "G": 80000000000.0, - "J": 2.04e-06 + "J": 2.04e-06, + "shape": null } ], "elements": [ @@ -175,7 +176,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 2, @@ -187,7 +189,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 3, @@ -199,9 +202,11 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -418,7 +423,8 @@ 0.0, 0.0 ], - "file_path": null + "file_path": null, + "use_last": false } ], "load_patterns": [ @@ -472,6 +478,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 1, "load_factor_increment": 1.0, "system": "BandGeneral", @@ -496,6 +503,7 @@ "pattern_ids": [ 2 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 200, "system": "BandGeneral", @@ -510,7 +518,12 @@ "tolerance": 1e-06, "max_iter": 25, "rayleigh_alpha_m": 0.0, - "rayleigh_beta_k": 0.0 + "rayleigh_beta_k": 0.0, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, + "rayleigh_mode1_damping": null, + "preload_case_ids": [], + "remove_patterns": [] } ] } \ No newline at end of file diff --git a/examples/portal_frame.py b/examples/portal_frame.py index 5e7776d..e1bac48 100644 --- a/examples/portal_frame.py +++ b/examples/portal_frame.py @@ -59,7 +59,7 @@ def _sine_pulse_factors() -> list[float]: def build_portal_frame() -> Project: """A two-column, one-beam steel portal frame with static + dynamic cases.""" return Project( - meta=ProjectMeta(name="Portal Frame", author="Ozan", units=UnitSystem.SI_M_N), + meta=ProjectMeta(name="Portal Frame", author="Ozan", units=UnitSystem.METRIC), ndm=3, ndf=6, nodes=[ diff --git a/examples/portal_pushover.osmodel b/examples/portal_pushover.osmodel index 093937f..88e8370 100644 --- a/examples/portal_pushover.osmodel +++ b/examples/portal_pushover.osmodel @@ -4,7 +4,7 @@ "name": "Portal Pushover", "description": "", "author": "Ozan", - "units": "SI (m, N, kg, s, Pa)" + "units": "Metric (m, N, kg, s, Pa)" }, "ndm": 3, "ndf": 6, @@ -231,7 +231,8 @@ "Iz": 0.000416, "Iy": 0.000129, "G": 80000000000.0, - "J": 2.04e-06 + "J": 2.04e-06, + "shape": null } ], "elements": [ @@ -285,9 +286,11 @@ ], "section_id": 3, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -327,6 +330,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "control_node": 3, "control_dof": 1, "target_disp": 0.15, @@ -340,7 +344,8 @@ "algorithm": "Newton", "test": "NormDispIncr", "tolerance": 1e-06, - "max_iter": 25 + "max_iter": 25, + "preload_case_ids": [] }, { "id": 2, diff --git a/examples/portal_pushover.py b/examples/portal_pushover.py index 5be487e..e44eba0 100644 --- a/examples/portal_pushover.py +++ b/examples/portal_pushover.py @@ -64,7 +64,7 @@ def build_portal_pushover() -> Project: return Project( meta=ProjectMeta(name="Portal Pushover", author="Ozan", - units=UnitSystem.SI_M_N), + units=UnitSystem.METRIC), ndm=3, ndf=6, nodes=[ Node(id=1, name="Base-L", coords=(0.0, 0.0, 0.0), diff --git a/examples/rc_frame_earthquake.osmodel b/examples/rc_frame_earthquake.osmodel index 99fecd0..614efc7 100644 --- a/examples/rc_frame_earthquake.osmodel +++ b/examples/rc_frame_earthquake.osmodel @@ -4,7 +4,7 @@ "name": "RC Frame Earthquake (OpenSees Ex 3.3)", "description": "Ex 3 gravity + uniform base excitation (horizontal, 4-s synthetic record peaking at ~0.35 g) + Rayleigh beta_k", "author": "OpenSees Examples Manual", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -313,7 +313,8 @@ "Iz": 8640.0, "Iy": 8640.0, "G": 1500.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -355,9 +356,11 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -455,7 +458,7 @@ -0.07226305111525733, -0.08816778784387092, -0.10268206588930337, - -0.11557698641636836, + -0.11557698641636838, -0.12664918882530235, -0.13572405786990283, -0.142658477444273, @@ -489,7 +492,7 @@ 0.14970400926424074, 0.14734308760930329, 0.14265847744427299, - 0.13572405786990302, + 0.135724057869903, 0.12664918882530243, 0.11557698641636846, 0.10268206588930347, @@ -504,7 +507,7 @@ -0.0552186829027018, -0.0722630511152573, -0.08816778784387089, - -0.10268206588930313, + -0.10268206588930316, -0.11557698641636852, -0.12664918882530232, -0.13572405786990294, @@ -566,7 +569,7 @@ -0.14265847744427307, -0.1357240578699028, -0.12664918882530243, - -0.11557698641636868, + -0.11557698641636867, -0.10268206588930333, -0.08816778784387108, -0.0722630511152575, @@ -616,12 +619,12 @@ -0.08652674043787481, -0.08129819692830416, -0.07492000704231337, - -0.06752087821731563, - -0.05924239844431021, + -0.06752087821731562, + -0.059242398444310215, -0.05023649120795737, -0.04066277069740133, -0.030685841350052333, - -0.020472585578247403, + -0.0204725855782474, -0.010189482631897374, -9.832592927142318e-17, 0.009937903409330418, @@ -642,7 +645,7 @@ 0.05947903147792919, 0.05481264807294881, 0.04939932978399098, - 0.043342664598141574, + 0.04334266459814158, 0.03675380211118236, 0.029749518558492356, 0.02245024111907521, @@ -666,9 +669,9 @@ -0.04631442669186875, -0.04351578902386234, -0.04010178999412147, - -0.03614132172945704, + -0.036141321729457034, -0.03171017082019282, - -0.02688965604775296, + -0.026889656047752963, -0.021765212731031044, -0.016424947276336123, -0.010958185402089994, @@ -682,7 +685,7 @@ 0.027293196958740988, 0.03033908626567152, 0.03283256872166209, - 0.034748063307665326, + 0.03474806330766532, 0.03606971177513051, 0.03679138802438222, 0.03691655417444788, @@ -720,7 +723,7 @@ -0.016973231331797612, -0.014392995708504558, -0.011650078858431393, - -0.008791640742480778, + -0.008791640742480776, -0.005865493972298641, -0.002919335651562286, -3.943919054879792e-17, @@ -729,7 +732,7 @@ 0.008156387432977339, 0.010541427274178315, 0.012701774131668711, - 0.014608995592985858, + 0.01460899559298586, 0.016239342654524213, 0.01757400763590476, 0.01859929800432925, @@ -742,7 +745,7 @@ 0.01704102783102496, 0.015704086601509296, 0.014153144944791883, - 0.012417881316069912, + 0.012417881316069914, 0.010530140607703934, 0.008523379771289316, 0.00643210177128294, @@ -754,7 +757,7 @@ -0.005967340521710147, -0.007712272945193301, -0.009292816470078945, - -0.010688169499041026, + -0.010688169499041028, -0.011880956889869792, -0.012857418649655518, -0.013607537107403339, @@ -774,7 +777,8 @@ -0.003139572682582101, -0.0015626077711816495 ], - "file_path": null + "file_path": null, + "use_last": false } ], "load_patterns": [ @@ -830,6 +834,7 @@ 1, 2 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 400, "system": "BandGeneral", @@ -844,7 +849,12 @@ "tolerance": 1e-12, "max_iter": 10, "rayleigh_alpha_m": 0.0, - "rayleigh_beta_k": 0.000625 + "rayleigh_beta_k": 0.000625, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, + "rayleigh_mode1_damping": null, + "preload_case_ids": [], + "remove_patterns": [] } ] } \ No newline at end of file diff --git a/examples/rc_frame_gravity.osmodel b/examples/rc_frame_gravity.osmodel index aa2b143..04429eb 100644 --- a/examples/rc_frame_gravity.osmodel +++ b/examples/rc_frame_gravity.osmodel @@ -4,7 +4,7 @@ "name": "RC Frame Gravity (OpenSees Ex 3)", "description": "1-bay 1-storey portal frame, nonlinear fibre columns + elastic beam, 10-step LoadControl gravity pushdown.", "author": "OpenSees Examples Manual", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -313,7 +313,8 @@ "Iz": 8640.0, "Iy": 8640.0, "G": 1500.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -355,9 +356,11 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -408,6 +411,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", diff --git a/examples/rc_frame_gravity.py b/examples/rc_frame_gravity.py index 33761d0..4c3faf8 100644 --- a/examples/rc_frame_gravity.py +++ b/examples/rc_frame_gravity.py @@ -92,7 +92,7 @@ def build_rc_frame_gravity() -> Project: "1-bay 1-storey portal frame, nonlinear fibre columns + " "elastic beam, 10-step LoadControl gravity pushdown." ), - units=UnitSystem.US_IN_KIP, + units=UnitSystem.IMPERIAL, ), ndm=2, ndf=3, coord_systems=[ diff --git a/examples/rc_frame_pushover.osmodel b/examples/rc_frame_pushover.osmodel index 6bc6868..77baa9d 100644 --- a/examples/rc_frame_pushover.osmodel +++ b/examples/rc_frame_pushover.osmodel @@ -4,7 +4,7 @@ "name": "RC Frame Pushover (OpenSees Ex 3.2)", "description": "Ex 3 gravity preload (StaticCase) + lateral reference load + DisplacementControl pushover on node 3 (DOF 1) to 15 in, chained via preload_case_ids.", "author": "OpenSees Examples Manual", - "units": "US (in, kip, kip·s²/in, s, ksi)" + "units": "Imperial (in, kip, kip·s²/in, s, ksi)" }, "ndm": 2, "ndf": 3, @@ -313,7 +313,8 @@ "Iz": 8640.0, "Iy": 8640.0, "G": 1500.0, - "J": 1.0 + "J": 1.0, + "shape": null } ], "elements": [ @@ -355,9 +356,11 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -445,6 +448,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 10, "load_factor_increment": 0.1, "system": "BandGeneral", @@ -462,6 +466,7 @@ "pattern_ids": [ 2 ], + "pattern_factors": {}, "control_node": 3, "control_dof": 1, "target_disp": 15.0, diff --git a/examples/sdof_pushover.osmodel b/examples/sdof_pushover.osmodel index d9fa9bd..14df07a 100644 --- a/examples/sdof_pushover.osmodel +++ b/examples/sdof_pushover.osmodel @@ -4,7 +4,7 @@ "name": "SDOF Pushover", "description": "", "author": "Ozan", - "units": "SI (m, N, kg, s, Pa)" + "units": "Metric (m, N, kg, s, Pa)" }, "ndm": 3, "ndf": 6, @@ -121,7 +121,8 @@ "Iz": 0.0002, "Iy": 4.5e-05, "G": 80000000000.0, - "J": 8.5e-07 + "J": 8.5e-07, + "shape": null } ], "elements": [ @@ -135,9 +136,11 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -177,6 +180,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "control_node": 2, "control_dof": 1, "target_disp": 0.1, @@ -189,7 +193,8 @@ "algorithm": "Newton", "test": "NormDispIncr", "tolerance": 1e-06, - "max_iter": 25 + "max_iter": 25, + "preload_case_ids": [] }, { "id": 2, diff --git a/examples/sdof_pushover.py b/examples/sdof_pushover.py index 1fed62b..777b0ee 100644 --- a/examples/sdof_pushover.py +++ b/examples/sdof_pushover.py @@ -48,7 +48,7 @@ from otko.services import load_project, save_project def build_sdof() -> Project: return Project( meta=ProjectMeta(name="SDOF Pushover", author="Ozan", - units=UnitSystem.SI_M_N), + units=UnitSystem.METRIC), ndm=3, ndf=6, nodes=[ Node(id=1, name="Base", coords=(0.0, 0.0, 0.0), diff --git a/examples/space_frame_3d.osmodel b/examples/space_frame_3d.osmodel index 46ebbd9..783c598 100644 --- a/examples/space_frame_3d.osmodel +++ b/examples/space_frame_3d.osmodel @@ -4,7 +4,7 @@ "name": "Space Frame 3D", "description": "", "author": "Ozan", - "units": "SI (m, N, kg, s, Pa)" + "units": "Metric (m, N, kg, s, Pa)" }, "ndm": 3, "ndf": 6, @@ -348,7 +348,8 @@ "Iz": 0.00025, "Iy": 0.00025, "G": 80000000000.0, - "J": 0.0004 + "J": 0.0004, + "shape": null }, { "id": 2, @@ -359,7 +360,8 @@ "Iz": 0.0003, "Iy": 8e-05, "G": 80000000000.0, - "J": 1e-06 + "J": 1e-06, + "shape": null } ], "elements": [ @@ -373,7 +375,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 2, @@ -385,7 +388,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 3, @@ -397,7 +401,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 4, @@ -409,7 +414,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 5, @@ -421,7 +427,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 6, @@ -433,7 +440,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 7, @@ -445,7 +453,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 8, @@ -457,7 +466,8 @@ ], "section_id": 1, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 9, @@ -469,7 +479,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 10, @@ -481,7 +492,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 11, @@ -493,7 +505,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 12, @@ -505,7 +518,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 13, @@ -517,7 +531,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 14, @@ -529,7 +544,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 15, @@ -541,7 +557,8 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false }, { "id": 16, @@ -553,9 +570,11 @@ ], "section_id": 2, "geom_transf": "Linear", - "rho": 0.0 + "rho": 0.0, + "consistent_mass": false } ], + "mp_constraints": [], "time_series": [ { "id": 1, @@ -653,7 +672,7 @@ -0.29326050060188424, -0.3555645611718062, -0.4115042921209622, - -0.46028029767148104, + -0.4602802976714811, -0.5012157143407345, -0.5337653150252811, -0.557522431249662, @@ -663,7 +682,7 @@ -0.5615384725351046, -0.5402796151704672, -0.5107978730732473, - -0.473659120634178, + -0.47365912063417787, -0.42954246541985186, -0.37922816465313647, -0.3235841034221959, @@ -687,7 +706,7 @@ 0.4906740795248149, 0.4799109935380196, 0.46174240873333633, - 0.43654625061930163, + 0.4365462506193016, 0.40480613582123315, 0.367102454112839, 0.32410204140538085, @@ -702,7 +721,7 @@ -0.16470790295421026, -0.21419825881788915, -0.2597053123214179, - -0.3005638423432181, + -0.30056384234321815, -0.33618996805592183, -0.3660893065503416, -0.3898636224828937, @@ -764,10 +783,10 @@ -0.2882327405527284, -0.2725045822392421, -0.25269150009501856, - -0.2291557899194038, + -0.22915578991940377, -0.20231370964878448, -0.17262826564213357, - -0.14060135458277695, + -0.14060135458277698, -0.10676539250226193, -0.07167456884782708, -0.03589586768362362, @@ -814,8 +833,8 @@ -0.210525969348039, -0.1990380801906294, -0.18456655167452068, - -0.1673760056265102, - -0.14777047796350654, + -0.16737600562651017, + -0.14777047796350656, -0.12608814977607422, -0.10269560775234517, -0.07798172999451634, @@ -840,7 +859,7 @@ 0.17010510853326, 0.15773721930062054, 0.14304555980288974, - 0.1262899700796923, + 0.12628997007969234, 0.10775947186525829, 0.08776736334007407, 0.06664599372956057, @@ -864,9 +883,9 @@ -0.15376873454743234, -0.14537794939239213, -0.13480790602063883, - -0.12225188492657595, + -0.12225188492657593, -0.10793195476209022, - -0.09209512390578932, + -0.09209512390578933, -0.07500914826118861, -0.05695806544062365, -0.0382375289144052, @@ -875,7 +894,7 @@ 0.018910877417136193, 0.037288490913057575, 0.054850768151679684, - 0.07133196903235188, + 0.07133196903235187, 0.08648665679303691, 0.1000933005365811, 0.11195745718335284, @@ -904,7 +923,7 @@ -0.0739146103417395, -0.08554333791262553, -0.09568287328255036, - -0.10419251035749608, + -0.10419251035749609, -0.11095890755818626, -0.11589752681422662, -0.11895360205510225, @@ -918,7 +937,7 @@ -0.07883379020837138, -0.06726652633324699, -0.054786883743239034, - -0.0416023242747564, + -0.041602324274756394, -0.027928794018131982, -0.013987224628675814, -1.9014085379808582e-16, @@ -927,7 +946,7 @@ 0.040063148664072165, 0.05210106213902843, 0.06317008686143476, - 0.07310841606785966, + 0.07310841606785967, 0.0817740280096844, 0.08904667019366247, 0.09482947682594194, @@ -945,14 +964,14 @@ 0.04682284312839545, 0.035554843973652946, 0.023868952780831416, - 0.011953985695908748, + 0.011953985695908746, 5.108354497396276e-16, -0.011804707389018669, -0.023276536275777145, -0.034239409087743526, -0.04452744329813354, -0.05398743030141857, - -0.0624810842126653, + -0.06248108421266531, -0.06988702813284133, -0.076102489952229, -0.08104468467631713, @@ -972,7 +991,8 @@ -0.020399266308586933, -0.010216306509086727 ], - "file_path": null + "file_path": null, + "use_last": false } ], "load_patterns": [ @@ -1082,6 +1102,7 @@ "pattern_ids": [ 1 ], + "pattern_factors": {}, "n_steps": 1, "load_factor_increment": 1.0, "system": "BandGeneral", @@ -1106,6 +1127,7 @@ "pattern_ids": [ 2 ], + "pattern_factors": {}, "dt": 0.01, "n_steps": 400, "system": "BandGeneral", @@ -1120,7 +1142,12 @@ "tolerance": 1e-06, "max_iter": 25, "rayleigh_alpha_m": 0.524, - "rayleigh_beta_k": 0.00106 + "rayleigh_beta_k": 0.00106, + "rayleigh_beta_k_init": 0.0, + "rayleigh_beta_k_comm": 0.0, + "rayleigh_mode1_damping": null, + "preload_case_ids": [], + "remove_patterns": [] }, { "id": 4, diff --git a/examples/space_frame_3d.py b/examples/space_frame_3d.py index b154f25..c15f3c1 100644 --- a/examples/space_frame_3d.py +++ b/examples/space_frame_3d.py @@ -106,7 +106,7 @@ def build_space_frame() -> Project: add_el(b, d, sec=2, name=f"Beam-{story_base}-Y2") return Project( - meta=ProjectMeta(name="Space Frame 3D", author="Ozan", units=UnitSystem.SI_M_N), + meta=ProjectMeta(name="Space Frame 3D", author="Ozan", units=UnitSystem.METRIC), ndm=3, ndf=6, nodes=nodes, sections=[ diff --git a/pyproject.toml b/pyproject.toml index d67c45f..93bfc44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "otko" -dynamic = ["version"] +version = "0.1.0" description = "OTKO — a modern, SAP2000-style desktop GUI for OpenSeesPy." readme = "README.md" requires-python = ">=3.10" @@ -42,7 +42,10 @@ gui = [ "vtk>=9.3", "pyqtgraph>=0.13", "imageio[ffmpeg]>=2.34", - "plotly>=5.18", + # scipy and pandas are listed for forward-compatibility; they are not + # currently imported by any module in this repo (phantom deps as of 2026-06-15). + "scipy>=1.11", + "pandas>=2.1", ] dev = [ "pytest>=7.4", @@ -52,7 +55,6 @@ dev = [ "mypy>=1.8", "pre-commit>=3.6", "hatch>=1.9", - "tomli>=2.0; python_version < '3.11'", ] docs = [ "sphinx>=7.2", @@ -70,9 +72,6 @@ Issues = "https://github.com/ogunc/otko/issues" [tool.hatch.build.targets.wheel] packages = ["src/otko"] -[tool.hatch.version] -path = "src/otko/_const.py" - # ─────────── Ruff ─────────── [tool.ruff] line-length = 100 @@ -90,25 +89,7 @@ select = [ "SIM", # simplify "RUF", # ruff-specific ] -ignore = [ - "E501", # line-length handled by the formatter - "RUF001", # ambiguous unicode: engineering notation (×, ≈, ²) is deliberate - "RUF002", - "RUF003", -] - -[tool.ruff.lint.per-file-ignores] -# Engineering symbols (A, I, L, E, P) and `importorskip`-then-import blocks -# are normal in tests. -"tests/**" = ["E402", "E741", "N802", "N806", "N815"] -# Qt requires camelCase signal names and PascalCase event overrides. -"src/otko/views/**" = ["N802", "N815"] -"src/otko/viewmodels/**" = ["N815"] -# Material fields mirror the OpenSees parameter names verbatim (cR1, epsU, ...). -"src/otko/core/materials/__init__.py" = ["N815"] -# Engineering symbols in numeric code: L (length), Iz/Iy (second moments). -"src/otko/services/section_properties.py" = ["N806"] -"src/otko/views/canvas3d/model_renderer.py" = ["N802", "N806", "N815"] +ignore = ["E501"] # line length handled by formatter [tool.ruff.format] quote-style = "double" @@ -116,12 +97,11 @@ quote-style = "double" # ─────────── Mypy ─────────── [tool.mypy] python_version = "3.10" -warn_unused_configs = true -plugins = [] - -[[tool.mypy.overrides]] -module = ["otko.core.*", "otko.services.*", "otko.viewmodels.*"] strict = true +warn_unused_ignores = true +warn_return_any = true +disallow_untyped_defs = true +plugins = [] [[tool.mypy.overrides]] module = ["openseespy.*", "pyvista.*", "pyvistaqt.*", "vtk.*", "pyqtgraph.*"] @@ -141,8 +121,3 @@ markers = [ branch = true source = ["src/otko"] omit = ["src/otko/views/*"] # GUI excluded from coverage targets - -[tool.coverage.report] -exclude_lines = ["if TYPE_CHECKING:", "raise NotImplementedError"] -# Conservative floor for the headless suite; raise deliberately with evidence. -fail_under = 50 diff --git a/specifications/15-rebuild-adoption-plan.md b/specifications/15-rebuild-adoption-plan.md deleted file mode 100644 index f10ddfd..0000000 --- a/specifications/15-rebuild-adoption-plan.md +++ /dev/null @@ -1,383 +0,0 @@ -# 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 deleted file mode 100644 index 3c8f8d4..0000000 --- a/specifications/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# 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). diff --git a/src/otko/__init__.py b/src/otko/__init__.py index 6291478..d9d3102 100644 --- a/src/otko/__init__.py +++ b/src/otko/__init__.py @@ -1,5 +1,3 @@ """OTKO — a desktop GUI for OpenSeesPy.""" -from otko._const import __version__ - -__all__ = ["__version__"] +__version__ = "0.1.0" diff --git a/src/otko/_const.py b/src/otko/_const.py deleted file mode 100644 index 51e97ce..0000000 --- a/src/otko/_const.py +++ /dev/null @@ -1,13 +0,0 @@ -# Ported from otko-development (MIT), (c) 2026 OTKO contributors -"""Single source of truth for OTKO's version and the pinned solver version. - -Defines the package version and the pinned OpenseesPy version exactly once -so neither is duplicated as a literal anywhere else in the source. -""" - -from __future__ import annotations - -__version__ = "0.1.0" - -#: Pinned OpenseesPy version the application targets. -OPENSEESPY_VERSION = "3.8.0.0" diff --git a/src/otko/commands/__init__.py b/src/otko/commands/__init__.py index eddf86f..4d6647e 100644 --- a/src/otko/commands/__init__.py +++ b/src/otko/commands/__init__.py @@ -6,11 +6,6 @@ from otko.commands.analysis import ( UpdateAnalysisCaseCommand, ) from otko.commands.base import ProjectCommand -from otko.commands.combinations import ( - AddCombinationsCommand, - DeleteCombinationsCommand, - UpdateCombinationCommand, -) from otko.commands.constraints import ( AddEqualDOFConstraintCommand, DeleteEqualDOFConstraintCommand, @@ -29,6 +24,14 @@ from otko.commands.grid import ( SetCoordSystemsCommand, SetGridSystemCommand, ) +from otko.commands.series_and_patterns import ( + AddLoadPatternCommand, + AddTimeSeriesCommand, + DeleteLoadPatternCommand, + DeleteTimeSeriesCommand, + UpdateLoadPatternCommand, + UpdateTimeSeriesCommand, +) from otko.commands.loads import ( AddElementLoadsCommand, AddNodalLoadsCommand, @@ -55,14 +58,6 @@ from otko.commands.sections import ( DeleteSectionsCommand, UpdateSectionCommand, ) -from otko.commands.series_and_patterns import ( - AddLoadPatternCommand, - AddTimeSeriesCommand, - DeleteLoadPatternCommand, - DeleteTimeSeriesCommand, - UpdateLoadPatternCommand, - UpdateTimeSeriesCommand, -) from otko.commands.transforms import ( MirrorCommand, MoveNodesCommand, @@ -72,50 +67,25 @@ from otko.commands.transforms import ( __all__ = [ "ProjectCommand", - "AddNodesCommand", - "DeleteNodesCommand", - "SetRestraintCommand", - "SetMassCommand", + "AddNodesCommand", "DeleteNodesCommand", "SetRestraintCommand", "SetMassCommand", "UpdateNodeFieldsCommand", - "AddElementsCommand", - "DeleteElementsCommand", - "AssignSectionCommand", - "AssignMaterialCommand", - "ReplaceElementsCommand", - "ConvertElementTypeCommand", + "AddElementsCommand", "DeleteElementsCommand", + "AssignSectionCommand", "AssignMaterialCommand", + "ReplaceElementsCommand", "ConvertElementTypeCommand", "UpdateElementFieldsCommand", - "AddMaterialsCommand", - "DeleteMaterialsCommand", - "UpdateMaterialCommand", - "AddSectionsCommand", - "DeleteSectionsCommand", - "UpdateSectionCommand", - "AddNodalLoadsCommand", - "AddElementLoadsCommand", - "UpdateNodalLoadCommand", - "DeleteNodalLoadsCommand", - "UpdateElementLoadCommand", - "DeleteElementLoadsCommand", - "MoveNodesCommand", - "ReplicateCommand", - "MirrorCommand", - "Plane", - "AddAnalysisCasesCommand", - "DeleteAnalysisCasesCommand", - "UpdateAnalysisCaseCommand", - "AddCombinationsCommand", - "DeleteCombinationsCommand", - "UpdateCombinationCommand", + "AddMaterialsCommand", "DeleteMaterialsCommand", "UpdateMaterialCommand", + "AddSectionsCommand", "DeleteSectionsCommand", "UpdateSectionCommand", + "AddNodalLoadsCommand", "AddElementLoadsCommand", + "UpdateNodalLoadCommand", "DeleteNodalLoadsCommand", + "UpdateElementLoadCommand", "DeleteElementLoadsCommand", + "MoveNodesCommand", "ReplicateCommand", "MirrorCommand", "Plane", + "AddAnalysisCasesCommand", "DeleteAnalysisCasesCommand", "UpdateAnalysisCaseCommand", "SetGridSystemCommand", "SetCoordSystemsCommand", - "AddTimeSeriesCommand", - "AddLoadPatternCommand", - "UpdateTimeSeriesCommand", - "DeleteTimeSeriesCommand", - "UpdateLoadPatternCommand", - "DeleteLoadPatternCommand", + "AddTimeSeriesCommand", "AddLoadPatternCommand", + "UpdateTimeSeriesCommand", "DeleteTimeSeriesCommand", + "UpdateLoadPatternCommand", "DeleteLoadPatternCommand", "AddEqualDOFConstraintCommand", - "UpdateEqualDOFConstraintCommand", - "DeleteEqualDOFConstraintCommand", + "UpdateEqualDOFConstraintCommand", "DeleteEqualDOFConstraintCommand", "SetUnitsCommand", ] diff --git a/src/otko/commands/analysis.py b/src/otko/commands/analysis.py index 444f34c..c26dc22 100644 --- a/src/otko/commands/analysis.py +++ b/src/otko/commands/analysis.py @@ -13,7 +13,8 @@ if TYPE_CHECKING: class AddAnalysisCasesCommand(ProjectCommand): """Add one or more analysis cases.""" - def __init__(self, vm: ProjectViewModel, cases: list[Any], *, text: str | None = None) -> None: + def __init__(self, vm: "ProjectViewModel", cases: list[Any], *, + text: str | None = None) -> None: super().__init__(vm, text or f"Add {len(cases)} analysis case(s)") self._cases = list(cases) @@ -34,16 +35,19 @@ class AddAnalysisCasesCommand(ProjectCommand): class DeleteAnalysisCasesCommand(ProjectCommand): """Remove a set of analysis cases.""" - def __init__(self, vm: ProjectViewModel, case_ids: set[int]) -> None: + def __init__(self, vm: "ProjectViewModel", case_ids: set[int]) -> None: super().__init__(vm, f"Delete {len(case_ids)} analysis case(s)") self._case_ids = set(case_ids) self._removed: list[tuple[int, Any]] = [] def redo(self) -> None: self._removed = [ - (i, c) for i, c in enumerate(self.project.analyses) if c.id in self._case_ids + (i, c) for i, c in enumerate(self.project.analyses) + if c.id in self._case_ids + ] + self.project.analyses[:] = [ + c for c in self.project.analyses if c.id not in self._case_ids ] - self.project.analyses[:] = [c for c in self.project.analyses if c.id not in self._case_ids] self._notify() def undo(self) -> None: @@ -56,7 +60,7 @@ class DeleteAnalysisCasesCommand(ProjectCommand): class UpdateAnalysisCaseCommand(ProjectCommand): """Replace an analysis case at a given id.""" - def __init__(self, vm: ProjectViewModel, new_case: Any) -> None: + def __init__(self, vm: "ProjectViewModel", new_case: Any) -> None: super().__init__(vm, f"Edit analysis case {new_case.id}") self._new = new_case self._old: Any | None = None diff --git a/src/otko/commands/base.py b/src/otko/commands/base.py index 0e8b3dd..c740057 100644 --- a/src/otko/commands/base.py +++ b/src/otko/commands/base.py @@ -27,18 +27,20 @@ class ProjectCommand(QUndoCommand): :meth:`_notify` exactly once at the end of each. """ - def __init__(self, vm: ProjectViewModel, text: str) -> None: + def __init__(self, vm: "ProjectViewModel", text: str) -> None: super().__init__(text) self._vm = vm @property - def vm(self) -> ProjectViewModel: + def vm(self) -> "ProjectViewModel": return self._vm @property - def project(self) -> Project: + def project(self) -> "Project": if self._vm.project is None: - raise RuntimeError(f"Cannot apply '{self.text()}': no active project.") + raise RuntimeError( + f"Cannot apply '{self.text()}': no active project." + ) return self._vm.project def _notify(self) -> None: diff --git a/src/otko/commands/combinations.py b/src/otko/commands/combinations.py deleted file mode 100644 index 23f1aa8..0000000 --- a/src/otko/commands/combinations.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Commands for load-combination management.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from otko.commands.base import ProjectCommand - -if TYPE_CHECKING: - from otko.viewmodels import ProjectViewModel - - -class AddCombinationsCommand(ProjectCommand): - """Add one or more load combinations.""" - - def __init__(self, vm: ProjectViewModel, combos: list[Any], *, text: str | None = None) -> None: - super().__init__(vm, text or f"Add {len(combos)} load combination(s)") - self._combos = list(combos) - - def redo(self) -> None: - existing = {c.id for c in self.project.combinations} - for combo in self._combos: - if combo.id in existing: - raise ValueError(f"Load combination id {combo.id} already exists.") - self.project.combinations.extend(self._combos) - self._notify() - - def undo(self) -> None: - ids = {c.id for c in self._combos} - self.project.combinations[:] = [c for c in self.project.combinations if c.id not in ids] - self._notify() - - -class DeleteCombinationsCommand(ProjectCommand): - """Remove a set of load combinations.""" - - def __init__(self, vm: ProjectViewModel, combo_ids: set[int]) -> None: - super().__init__(vm, f"Delete {len(combo_ids)} load combination(s)") - self._combo_ids = set(combo_ids) - self._removed: list[tuple[int, Any]] = [] - - def redo(self) -> None: - self._removed = [ - (i, c) for i, c in enumerate(self.project.combinations) if c.id in self._combo_ids - ] - self.project.combinations[:] = [ - c for c in self.project.combinations if c.id not in self._combo_ids - ] - self._notify() - - def undo(self) -> None: - for i, c in self._removed: - self.project.combinations.insert(min(i, len(self.project.combinations)), c) - self._removed.clear() - self._notify() - - -class UpdateCombinationCommand(ProjectCommand): - """Replace a load combination at a given id.""" - - def __init__(self, vm: ProjectViewModel, new_combo: Any) -> None: - super().__init__(vm, f"Edit load combination {new_combo.id}") - self._new = new_combo - self._old: Any | None = None - self._index: int | None = None - - def redo(self) -> None: - for i, c in enumerate(self.project.combinations): - if c.id == self._new.id: - self._old = c - self._index = i - self.project.combinations[i] = self._new - self._notify() - return - raise KeyError(f"Load combination with id={self._new.id} not found.") - - def undo(self) -> None: - if self._old is not None and self._index is not None: - self.project.combinations[self._index] = self._old - self._notify() diff --git a/src/otko/commands/constraints.py b/src/otko/commands/constraints.py index 3f94205..2fb051b 100644 --- a/src/otko/commands/constraints.py +++ b/src/otko/commands/constraints.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: class AddEqualDOFConstraintCommand(ProjectCommand): """Append an ``equalDOF`` constraint to the project (undoable).""" - def __init__(self, vm: ProjectViewModel, constraint: EqualDOFConstraint) -> None: + def __init__(self, vm: "ProjectViewModel", constraint: EqualDOFConstraint) -> None: super().__init__( vm, f"Add equalDOF {constraint.retained_node}->{constraint.constrained_node}", @@ -34,7 +34,8 @@ class AddEqualDOFConstraintCommand(ProjectCommand): def undo(self) -> None: self.project.mp_constraints[:] = [ - mp for mp in self.project.mp_constraints if mp != self._constraint + mp for mp in self.project.mp_constraints + if mp != self._constraint ] self._notify() @@ -48,7 +49,7 @@ class UpdateEqualDOFConstraintCommand(ProjectCommand): """ def __init__( - self, vm: ProjectViewModel, index: int, new_constraint: EqualDOFConstraint + self, vm: "ProjectViewModel", index: int, new_constraint: EqualDOFConstraint ) -> None: super().__init__(vm, f"Edit equalDOF row {index + 1}") self._index = index @@ -71,7 +72,7 @@ class UpdateEqualDOFConstraintCommand(ProjectCommand): class DeleteEqualDOFConstraintCommand(ProjectCommand): """Remove ``equalDOF`` constraints by table-row index (no cascade).""" - def __init__(self, vm: ProjectViewModel, indices: set[int]) -> None: + def __init__(self, vm: "ProjectViewModel", indices: set[int]) -> None: super().__init__(vm, f"Delete {len(indices)} constraint(s)") self._indices = set(indices) self._removed: list[tuple[int, EqualDOFConstraint]] = [] diff --git a/src/otko/commands/elements.py b/src/otko/commands/elements.py index f1967ba..6487a9d 100644 --- a/src/otko/commands/elements.py +++ b/src/otko/commands/elements.py @@ -14,7 +14,7 @@ class AddElementsCommand(ProjectCommand): """Add one or more elements in a single undoable step.""" def __init__( - self, vm: ProjectViewModel, elements: list[Any], *, text: str | None = None + self, vm: "ProjectViewModel", elements: list[Any], *, text: str | None = None ) -> None: super().__init__(vm, text or f"Add {len(elements)} element(s)") self._elements = list(elements) @@ -40,7 +40,7 @@ class AddElementsCommand(ProjectCommand): class DeleteElementsCommand(ProjectCommand): """Remove a set of elements (no cascade — nodes are not affected).""" - def __init__(self, vm: ProjectViewModel, element_ids: set[int]) -> None: + def __init__(self, vm: "ProjectViewModel", element_ids: set[int]) -> None: super().__init__(vm, f"Delete {len(element_ids)} element(s)") self._element_ids = set(element_ids) self._removed: list[tuple[int, Any]] = [] @@ -68,7 +68,7 @@ class AssignSectionCommand(ProjectCommand): (e.g. trusses, zero-length elements). """ - def __init__(self, vm: ProjectViewModel, element_ids: set[int], section_id: int) -> None: + def __init__(self, vm: "ProjectViewModel", element_ids: set[int], section_id: int) -> None: super().__init__(vm, f"Assign section {section_id} to {len(element_ids)} element(s)") self._element_ids = set(element_ids) self._section_id = section_id @@ -106,7 +106,7 @@ class ConvertElementTypeCommand(ProjectCommand): def __init__( self, - vm: ProjectViewModel, + vm: "ProjectViewModel", element_ids: set[int], target_type: str, defaults: dict[str, Any] | None = None, @@ -185,7 +185,7 @@ class ReplaceElementsCommand(ProjectCommand): for another while preserving the id and node connectivity. """ - def __init__(self, vm: ProjectViewModel, replacements: list[Any]) -> None: + def __init__(self, vm: "ProjectViewModel", replacements: list[Any]) -> None: super().__init__(vm, f"Replace {len(replacements)} element(s)") self._replacements = {el.id: el for el in replacements} self._previous: dict[int, Any] = {} @@ -217,7 +217,7 @@ class UpdateElementFieldsCommand(ProjectCommand): def __init__( self, - vm: ProjectViewModel, + vm: "ProjectViewModel", element_id: int, fields: dict[str, Any], ) -> None: @@ -254,7 +254,7 @@ class UpdateElementFieldsCommand(ProjectCommand): class AssignMaterialCommand(ProjectCommand): """Set ``material_id`` on a set of elements (truss-style).""" - def __init__(self, vm: ProjectViewModel, element_ids: set[int], material_id: int) -> None: + def __init__(self, vm: "ProjectViewModel", element_ids: set[int], material_id: int) -> None: super().__init__(vm, f"Assign material {material_id} to {len(element_ids)} element(s)") self._element_ids = set(element_ids) self._material_id = material_id diff --git a/src/otko/commands/grid.py b/src/otko/commands/grid.py index e271746..894c56b 100644 --- a/src/otko/commands/grid.py +++ b/src/otko/commands/grid.py @@ -20,7 +20,7 @@ class SetCoordSystemsCommand(ProjectCommand): def __init__( self, - vm: ProjectViewModel, + vm: "ProjectViewModel", new_systems: list[CoordinateGridSystem], ) -> None: super().__init__(vm, "Update coordinate/grid systems") @@ -46,7 +46,7 @@ class SetGridSystemCommand(ProjectCommand): systems at once. """ - def __init__(self, vm: ProjectViewModel, new_grid: GridSystem) -> None: + def __init__(self, vm: "ProjectViewModel", new_grid: GridSystem) -> None: super().__init__(vm, "Update grid system") self._new_grid = new_grid self._previous: GridSystem | None = None diff --git a/src/otko/commands/loads.py b/src/otko/commands/loads.py index 789b6c8..7822caf 100644 --- a/src/otko/commands/loads.py +++ b/src/otko/commands/loads.py @@ -35,7 +35,7 @@ class AddNodalLoadsCommand(ProjectCommand): def __init__( self, - vm: ProjectViewModel, + vm: "ProjectViewModel", node_ids: set[int], forces: tuple[float, float, float, float, float, float], pattern_id: int | None = None, @@ -47,7 +47,7 @@ class AddNodalLoadsCommand(ProjectCommand): self._forces = forces self._pattern_id = pattern_id self._new_pattern_name = new_pattern_name - self._new_ts_type = new_ts_type # "Linear" or "Constant" + self._new_ts_type = new_ts_type # "Linear" or "Constant" self._created_ts: TimeSeries | None = None self._created_pattern: PlainLoadPattern | None = None self._added_loads: list[tuple[int, NodalLoad]] = [] # (pattern_id, load) @@ -101,10 +101,7 @@ class AddNodalLoadsCommand(ProjectCommand): break self._added_loads.clear() # Roll back any infrastructure we created. - if ( - self._created_pattern is not None - and self._created_pattern in self.project.load_patterns - ): + if self._created_pattern is not None and self._created_pattern in self.project.load_patterns: self.project.load_patterns.remove(self._created_pattern) self._created_pattern = None if self._created_ts is not None and self._created_ts in self.project.time_series: @@ -122,7 +119,7 @@ class AddElementLoadsCommand(ProjectCommand): def __init__( self, - vm: ProjectViewModel, + vm: "ProjectViewModel", element_ids: set[int], wy: float = 0.0, wz: float = 0.0, @@ -160,10 +157,7 @@ class AddElementLoadsCommand(ProjectCommand): pattern = self._resolve_pattern() for eid in self._element_ids: load = UniformElementLoad( - element_id=eid, - wy=self._wy, - wz=self._wz, - wx=self._wx, + element_id=eid, wy=self._wy, wz=self._wz, wx=self._wx, ) pattern.element_loads.append(load) self._added_loads.append((pattern.id, load)) @@ -177,10 +171,7 @@ class AddElementLoadsCommand(ProjectCommand): pat.element_loads.remove(load) break self._added_loads.clear() - if ( - self._created_pattern is not None - and self._created_pattern in self.project.load_patterns - ): + if self._created_pattern is not None and self._created_pattern in self.project.load_patterns: self.project.load_patterns.remove(self._created_pattern) self._created_pattern = None if self._created_ts is not None and self._created_ts in self.project.time_series: diff --git a/src/otko/commands/materials.py b/src/otko/commands/materials.py index 9729ced..bce4088 100644 --- a/src/otko/commands/materials.py +++ b/src/otko/commands/materials.py @@ -13,9 +13,7 @@ if TYPE_CHECKING: class AddMaterialsCommand(ProjectCommand): """Add one or more materials in a single undoable step.""" - def __init__( - self, vm: ProjectViewModel, materials: list[Any], *, text: str | None = None - ) -> None: + def __init__(self, vm: "ProjectViewModel", materials: list[Any], *, text: str | None = None) -> None: super().__init__(vm, text or f"Add {len(materials)} material(s)") self._materials = list(materials) @@ -36,14 +34,15 @@ class AddMaterialsCommand(ProjectCommand): class DeleteMaterialsCommand(ProjectCommand): """Remove materials (no cascade).""" - def __init__(self, vm: ProjectViewModel, material_ids: set[int]) -> None: + def __init__(self, vm: "ProjectViewModel", material_ids: set[int]) -> None: super().__init__(vm, f"Delete {len(material_ids)} material(s)") self._material_ids = set(material_ids) self._removed: list[tuple[int, Any]] = [] def redo(self) -> None: self._removed = [ - (i, m) for i, m in enumerate(self.project.materials) if m.id in self._material_ids + (i, m) for i, m in enumerate(self.project.materials) + if m.id in self._material_ids ] self.project.materials[:] = [ m for m in self.project.materials if m.id not in self._material_ids @@ -64,7 +63,7 @@ class UpdateMaterialCommand(ProjectCommand): otherwise this is an Add+Delete, not an update. """ - def __init__(self, vm: ProjectViewModel, new_material: Any) -> None: + def __init__(self, vm: "ProjectViewModel", new_material: Any) -> None: super().__init__(vm, f"Edit material {new_material.id}") self._new = new_material self._old: Any | None = None diff --git a/src/otko/commands/nodes.py b/src/otko/commands/nodes.py index e24840c..b285159 100644 --- a/src/otko/commands/nodes.py +++ b/src/otko/commands/nodes.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: class AddNodesCommand(ProjectCommand): """Add one or more nodes in a single undoable step.""" - def __init__(self, vm: ProjectViewModel, nodes: list[Node], *, text: str | None = None) -> None: + def __init__(self, vm: "ProjectViewModel", nodes: list[Node], *, text: str | None = None) -> None: super().__init__(vm, text or f"Add {len(nodes)} node(s)") self._nodes = list(nodes) @@ -39,18 +39,17 @@ class DeleteNodesCommand(ProjectCommand): them in their original positions. """ - def __init__(self, vm: ProjectViewModel, node_ids: set[int]) -> None: + def __init__(self, vm: "ProjectViewModel", node_ids: set[int]) -> None: super().__init__(vm, f"Delete {len(node_ids)} node(s)") self._node_ids = set(node_ids) - self._removed_nodes: list[tuple[int, Node]] = [] # (index, node) - self._removed_elements: list[tuple[int, object]] = [] # (index, element) + self._removed_nodes: list[tuple[int, Node]] = [] # (index, node) + self._removed_elements: list[tuple[int, object]] = [] # (index, element) self._removed_mp_constraints: list[tuple[int, object]] = [] def redo(self) -> None: # Cascade: snapshot every element that references a doomed node. self._removed_elements = [ - (i, el) - for i, el in enumerate(self.project.elements) + (i, el) for i, el in enumerate(self.project.elements) if any(nid in self._node_ids for nid in el.nodes) ] doomed_elem_ids = {el.id for _, el in self._removed_elements} @@ -59,8 +58,7 @@ class DeleteNodesCommand(ProjectCommand): ] self._removed_mp_constraints = [ - (i, mp) - for i, mp in enumerate(self.project.mp_constraints) + (i, mp) for i, mp in enumerate(self.project.mp_constraints) if mp.retained_node in self._node_ids or mp.constrained_node in self._node_ids ] doomed_mp = {id(mp) for _, mp in self._removed_mp_constraints} @@ -71,7 +69,9 @@ class DeleteNodesCommand(ProjectCommand): self._removed_nodes = [ (i, n) for i, n in enumerate(self.project.nodes) if n.id in self._node_ids ] - self.project.nodes[:] = [n for n in self.project.nodes if n.id not in self._node_ids] + self.project.nodes[:] = [ + n for n in self.project.nodes if n.id not in self._node_ids + ] self._notify() def undo(self) -> None: @@ -93,7 +93,7 @@ class SetRestraintCommand(ProjectCommand): def __init__( self, - vm: ProjectViewModel, + vm: "ProjectViewModel", node_ids: set[int], restraint: tuple[bool, bool, bool, bool, bool, bool], ) -> None: @@ -128,7 +128,7 @@ class UpdateNodeFieldsCommand(ProjectCommand): def __init__( self, - vm: ProjectViewModel, + vm: "ProjectViewModel", node_id: int, fields: dict[str, Any], ) -> None: @@ -166,7 +166,7 @@ class SetMassCommand(ProjectCommand): def __init__( self, - vm: ProjectViewModel, + vm: "ProjectViewModel", node_ids: set[int], mass: tuple[float, float, float, float, float, float], ) -> None: diff --git a/src/otko/commands/project.py b/src/otko/commands/project.py index 3fd0065..362e2a6 100644 --- a/src/otko/commands/project.py +++ b/src/otko/commands/project.py @@ -19,7 +19,7 @@ class SetUnitsCommand(ProjectCommand): so both redo and undo notify (dirty + modelMutated) to refresh. """ - def __init__(self, vm: ProjectViewModel, units: UnitSystem) -> None: + def __init__(self, vm: "ProjectViewModel", units: UnitSystem) -> None: super().__init__(vm, f"Set display units to {units.value}") self._units = units self._previous: UnitSystem | None = None diff --git a/src/otko/commands/sections.py b/src/otko/commands/sections.py index 94142a6..ecd6054 100644 --- a/src/otko/commands/sections.py +++ b/src/otko/commands/sections.py @@ -13,9 +13,7 @@ if TYPE_CHECKING: class AddSectionsCommand(ProjectCommand): """Add one or more sections in a single undoable step.""" - def __init__( - self, vm: ProjectViewModel, sections: list[Any], *, text: str | None = None - ) -> None: + def __init__(self, vm: "ProjectViewModel", sections: list[Any], *, text: str | None = None) -> None: super().__init__(vm, text or f"Add {len(sections)} section(s)") self._sections = list(sections) @@ -36,14 +34,15 @@ class AddSectionsCommand(ProjectCommand): class DeleteSectionsCommand(ProjectCommand): """Remove sections (no cascade — elements referencing them must be cleaned separately).""" - def __init__(self, vm: ProjectViewModel, section_ids: set[int]) -> None: + def __init__(self, vm: "ProjectViewModel", section_ids: set[int]) -> None: super().__init__(vm, f"Delete {len(section_ids)} section(s)") self._section_ids = set(section_ids) self._removed: list[tuple[int, Any]] = [] def redo(self) -> None: self._removed = [ - (i, s) for i, s in enumerate(self.project.sections) if s.id in self._section_ids + (i, s) for i, s in enumerate(self.project.sections) + if s.id in self._section_ids ] self.project.sections[:] = [ s for s in self.project.sections if s.id not in self._section_ids @@ -60,7 +59,7 @@ class DeleteSectionsCommand(ProjectCommand): class UpdateSectionCommand(ProjectCommand): """Replace a section's parameters at a given id.""" - def __init__(self, vm: ProjectViewModel, new_section: Any) -> None: + def __init__(self, vm: "ProjectViewModel", new_section: Any) -> None: super().__init__(vm, f"Edit section {new_section.id}") self._new = new_section self._old: Any | None = None diff --git a/src/otko/commands/series_and_patterns.py b/src/otko/commands/series_and_patterns.py index 1d03ab5..8e11b6b 100644 --- a/src/otko/commands/series_and_patterns.py +++ b/src/otko/commands/series_and_patterns.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: class AddTimeSeriesCommand(ProjectCommand): """Append a :class:`TimeSeries` to the project (undoable).""" - def __init__(self, vm: ProjectViewModel, ts: TimeSeries) -> None: + def __init__(self, vm: "ProjectViewModel", ts: "TimeSeries") -> None: super().__init__(vm, f"Add time series '{ts.name or ts.id}'") self._ts = ts @@ -31,7 +31,9 @@ class AddTimeSeriesCommand(ProjectCommand): self._notify() def undo(self) -> None: - self.project.time_series[:] = [t for t in self.project.time_series if t.id != self._ts.id] + self.project.time_series[:] = [ + t for t in self.project.time_series if t.id != self._ts.id + ] self._notify() @@ -41,7 +43,7 @@ class UpdateTimeSeriesCommand(ProjectCommand): The new series must carry the same id as the one being replaced. """ - def __init__(self, vm: ProjectViewModel, new_ts: Any) -> None: + def __init__(self, vm: "ProjectViewModel", new_ts: Any) -> None: super().__init__(vm, f"Edit time series {new_ts.id}") self._new = new_ts self._old: Any | None = None @@ -67,7 +69,7 @@ class DeleteTimeSeriesCommand(ProjectCommand): """Remove time series (no cascade — patterns referencing them must be cleaned separately; ``validate_references`` catches strays before save).""" - def __init__(self, vm: ProjectViewModel, ts_ids: set[int]) -> None: + def __init__(self, vm: "ProjectViewModel", ts_ids: set[int]) -> None: super().__init__(vm, f"Delete {len(ts_ids)} time serie(s)") self._ts_ids = set(ts_ids) self._removed: list[tuple[int, Any]] = [] @@ -91,7 +93,7 @@ class DeleteTimeSeriesCommand(ProjectCommand): class AddLoadPatternCommand(ProjectCommand): """Append a :class:`LoadPattern` to the project (undoable).""" - def __init__(self, vm: ProjectViewModel, pattern: LoadPattern) -> None: + def __init__(self, vm: "ProjectViewModel", pattern: "LoadPattern") -> None: super().__init__(vm, f"Add pattern '{pattern.name or pattern.id}'") self._pattern = pattern @@ -117,7 +119,7 @@ class UpdateLoadPatternCommand(ProjectCommand): drops the pattern's stored loads. """ - def __init__(self, vm: ProjectViewModel, new_pattern: Any) -> None: + def __init__(self, vm: "ProjectViewModel", new_pattern: Any) -> None: super().__init__(vm, f"Edit pattern {new_pattern.id}") self._new = new_pattern self._old: Any | None = None @@ -142,7 +144,7 @@ class UpdateLoadPatternCommand(ProjectCommand): class DeleteLoadPatternCommand(ProjectCommand): """Remove load patterns (their child nodal/element loads go with them).""" - def __init__(self, vm: ProjectViewModel, pattern_ids: set[int]) -> None: + def __init__(self, vm: "ProjectViewModel", pattern_ids: set[int]) -> None: super().__init__(vm, f"Delete {len(pattern_ids)} pattern(s)") self._pattern_ids = set(pattern_ids) self._removed: list[tuple[int, Any]] = [] diff --git a/src/otko/commands/transforms.py b/src/otko/commands/transforms.py index 07c335d..b8d60cc 100644 --- a/src/otko/commands/transforms.py +++ b/src/otko/commands/transforms.py @@ -15,9 +15,10 @@ All copies inherit the source node's restraint, mass, and name. from __future__ import annotations -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Any, Literal from otko.commands.base import ProjectCommand +from otko.core import Node if TYPE_CHECKING: from otko.viewmodels import ProjectViewModel @@ -29,7 +30,7 @@ class MoveNodesCommand(ProjectCommand): def __init__( self, - vm: ProjectViewModel, + vm: "ProjectViewModel", node_ids: set[int], offset: tuple[float, float, float], ) -> None: @@ -44,9 +45,9 @@ class MoveNodesCommand(ProjectCommand): for i, n in enumerate(self.project.nodes): if n.id in self._node_ids: self._previous[n.id] = n.coords - self.project.nodes[i] = n.model_copy( - update={"coords": (n.coords[0] + dx, n.coords[1] + dy, n.coords[2] + dz)} - ) + self.project.nodes[i] = n.model_copy(update={ + "coords": (n.coords[0] + dx, n.coords[1] + dy, n.coords[2] + dz) + }) self._notify() def undo(self) -> None: @@ -62,7 +63,7 @@ class ReplicateCommand(ProjectCommand): def __init__( self, - vm: ProjectViewModel, + vm: "ProjectViewModel", node_ids: set[int], element_ids: set[int], offset: tuple[float, float, float], @@ -83,8 +84,7 @@ class ReplicateCommand(ProjectCommand): # Snapshot the source nodes/elements once (won't change during redo). src_nodes = [n for n in self.project.nodes if n.id in self._node_ids] src_elements = [ - e - for e in self.project.elements + e for e in self.project.elements if e.id in self._element_ids and all(nid in self._node_ids for nid in e.nodes) ] next_node_id = self.project.next_node_id() @@ -95,34 +95,30 @@ class ReplicateCommand(ProjectCommand): for k in range(1, self._n_copies + 1): mapping: dict[int, int] = {} for orig in src_nodes: - new_node = orig.model_copy( - update={ - "id": next_node_id, - "coords": ( - orig.coords[0] + k * dx, - orig.coords[1] + k * dy, - orig.coords[2] + k * dz, - ), - } - ) + new_node = orig.model_copy(update={ + "id": next_node_id, + "coords": (orig.coords[0] + k * dx, + orig.coords[1] + k * dy, + orig.coords[2] + k * dz), + }) self.project.nodes.append(new_node) self._added_node_ids.add(next_node_id) mapping[orig.id] = next_node_id next_node_id += 1 for orig in src_elements: - new_elem = orig.model_copy( - update={ - "id": next_elem_id, - "nodes": tuple(mapping[nid] for nid in orig.nodes), - } - ) + new_elem = orig.model_copy(update={ + "id": next_elem_id, + "nodes": tuple(mapping[nid] for nid in orig.nodes), + }) self.project.elements.append(new_elem) self._added_element_ids.add(next_elem_id) next_elem_id += 1 self._notify() def undo(self) -> None: - self.project.nodes[:] = [n for n in self.project.nodes if n.id not in self._added_node_ids] + self.project.nodes[:] = [ + n for n in self.project.nodes if n.id not in self._added_node_ids + ] self.project.elements[:] = [ e for e in self.project.elements if e.id not in self._added_element_ids ] @@ -146,7 +142,7 @@ class MirrorCommand(ProjectCommand): def __init__( self, - vm: ProjectViewModel, + vm: "ProjectViewModel", node_ids: set[int], element_ids: set[int], plane: Plane, @@ -170,8 +166,7 @@ class MirrorCommand(ProjectCommand): def redo(self) -> None: src_nodes = [n for n in self.project.nodes if n.id in self._node_ids] src_elements = [ - e - for e in self.project.elements + e for e in self.project.elements if e.id in self._element_ids and all(nid in self._node_ids for nid in e.nodes) ] next_node_id = self.project.next_node_id() @@ -181,30 +176,28 @@ class MirrorCommand(ProjectCommand): mapping: dict[int, int] = {} for orig in src_nodes: - new_node = orig.model_copy( - update={ - "id": next_node_id, - "coords": self._reflect(orig.coords, self._plane), - } - ) + new_node = orig.model_copy(update={ + "id": next_node_id, + "coords": self._reflect(orig.coords, self._plane), + }) self.project.nodes.append(new_node) self._added_node_ids.add(next_node_id) mapping[orig.id] = next_node_id next_node_id += 1 for orig in src_elements: - new_elem = orig.model_copy( - update={ - "id": next_elem_id, - "nodes": tuple(mapping[nid] for nid in orig.nodes), - } - ) + new_elem = orig.model_copy(update={ + "id": next_elem_id, + "nodes": tuple(mapping[nid] for nid in orig.nodes), + }) self.project.elements.append(new_elem) self._added_element_ids.add(next_elem_id) next_elem_id += 1 self._notify() def undo(self) -> None: - self.project.nodes[:] = [n for n in self.project.nodes if n.id not in self._added_node_ids] + self.project.nodes[:] = [ + n for n in self.project.nodes if n.id not in self._added_node_ids + ] self.project.elements[:] = [ e for e in self.project.elements if e.id not in self._added_element_ids ] diff --git a/src/otko/core/__init__.py b/src/otko/core/__init__.py index 34e7482..8c033e9 100644 --- a/src/otko/core/__init__.py +++ b/src/otko/core/__init__.py @@ -35,6 +35,7 @@ from otko.core.geometry import ( DispBeamColumn, ElasticBeamColumn, Element, + ElementBase, ForceBeamColumn, GridLine, GridSystem, @@ -50,8 +51,6 @@ from otko.core.loads import ( ConstantTimeSeries, ImposedSupportMotionPattern, LinearTimeSeries, - LoadCombination, - LoadCombinationItem, LoadPattern, NodalLoad, PathTimeSeries, @@ -90,7 +89,24 @@ from otko.core.sections import ( SectionShape, StraightLayer, ) -from otko.core.units import UnitLabels, UnitSystem, labels_for +from otko.core.units import ( + DISPLAY, + LEGACY_UNIT_SYSTEMS, + NATIVE, + QUANTITIES, + RAD_TO_DEG, + UnitLabels, + UnitSystem, + display_factor, + display_for, + display_label, + display_with_alias, + format_display, + format_rotation, + labels_for, + native_label, + to_display, +) __all__ = [ # Project + meta @@ -99,6 +115,19 @@ __all__ = [ "UnitSystem", "UnitLabels", "labels_for", + "DISPLAY", + "LEGACY_UNIT_SYSTEMS", + "NATIVE", + "QUANTITIES", + "RAD_TO_DEG", + "display_for", + "display_label", + "display_factor", + "display_with_alias", + "format_display", + "format_rotation", + "native_label", + "to_display", # Geometry "Node", "EqualDOFConstraint", @@ -116,6 +145,7 @@ __all__ = [ "ensure_default_truss_material", "ensure_default_timeseries_and_pattern", "Element", + "ElementBase", "TrussElement", "CorotTrussElement", "ElasticBeamColumn", @@ -167,8 +197,6 @@ __all__ = [ "PlainLoadPattern", "UniformExcitationPattern", "ImposedSupportMotionPattern", - "LoadCombination", - "LoadCombinationItem", "NodalLoad", "UniformElementLoad", "ResponseSpectrum", diff --git a/src/otko/core/_base.py b/src/otko/core/_base.py index da3e9b5..41b88c0 100644 --- a/src/otko/core/_base.py +++ b/src/otko/core/_base.py @@ -26,3 +26,21 @@ class Entity(BaseModel): ..., description="Unique tag within its kind. Used as the OpenSees tag." ) name: str = Field(default="", description="Optional human-readable label.") + + @property + def default_name(self) -> str: + """Fallback label used when ``name`` is blank. + + Display-only — reading it never writes back into ``name`` and it + is never persisted. Subclasses override this with their prefix + (e.g. ``N`` for nodes, ``E`` for elements). + """ + return "" + + @property + def display_name(self) -> str: + """Explicit ``name`` stripped, or ``default_name`` when blank. + + Display-only fallback — reading this never mutates ``name``. + """ + return self.name.strip() or self.default_name diff --git a/src/otko/core/analysis/__init__.py b/src/otko/core/analysis/__init__.py index e8835b7..eba5330 100644 --- a/src/otko/core/analysis/__init__.py +++ b/src/otko/core/analysis/__init__.py @@ -9,7 +9,7 @@ in Phase 6/8. from __future__ import annotations -from typing import Annotated, Literal +from typing import Annotated, Literal, Union from pydantic import Field, PositiveFloat, PositiveInt, model_validator @@ -83,8 +83,7 @@ class TransientCase(Entity): constraints: str = "Plain" integrator: str = "Newmark" integrator_params: tuple[float, float] = Field( - default=(0.5, 0.25), - description="Newmark gamma, beta (default = average acceleration).", + default=(0.5, 0.25), description="Newmark gamma, beta (default = average acceleration).", ) algorithm: str = "Newton" test: str = "NormDispIncr" @@ -101,7 +100,7 @@ class TransientCase(Entity): rayleigh_beta_k: float = Field( default=0.0, description="Stiffness-proportional βK on CURRENT/tangent stiffness — " - "``rayleigh`` slot 2 (damps high frequencies).", + "``rayleigh`` slot 2 (damps high frequencies).", ) rayleigh_beta_k_init: float = Field( default=0.0, @@ -115,11 +114,11 @@ class TransientCase(Entity): ) rayleigh_beta_k_comm: float = Field( default=0.0, - description="Stiffness-proportional βK on COMMITTED stiffness — " "``rayleigh`` slot 4.", + description="Stiffness-proportional βK on COMMITTED stiffness — " + "``rayleigh`` slot 4.", ) rayleigh_mode1_damping: float | None = Field( - default=None, - ge=0.0, + default=None, ge=0.0, description=( "If set, βK is computed as 2·ζ/√λ₁ (first-mode eigenvalue) and " "overrides ``rayleigh_beta_k``. ``rayleigh_alpha_m`` still applies." @@ -196,7 +195,7 @@ class PushoverCase(Entity): base_nodes: list[PositiveInt] = Field( default_factory=list, description="Nodes whose reactions sum into the 'base shear' for the curve. " - "Leave empty to use every restrained node in the project.", + "Leave empty to use every restrained node in the project.", ) system: str = "BandGeneral" constraints: str = "Plain" @@ -245,30 +244,23 @@ class ResponseSpectrumCase(Entity): type: Literal["ResponseSpectrum"] = "ResponseSpectrum" modal_case_id: PositiveInt = Field( - ..., - description="ID of the ModalCase whose mode shapes drive this analysis.", + ..., description="ID of the ModalCase whose mode shapes drive this analysis.", ) spectrum_id: PositiveInt = Field( - ..., - description="ID of the ResponseSpectrum to apply.", + ..., description="ID of the ResponseSpectrum to apply.", ) direction: int = Field( - ..., - ge=1, - le=6, - description="DOF direction (1..6) for the seismic excitation.", + ..., ge=1, le=6, description="DOF direction (1..6) for the seismic excitation.", ) combination: Literal["SRSS", "CQC"] = "SRSS" damping_ratio: float | None = Field( - default=None, - ge=0.0, - le=1.0, + default=None, ge=0.0, le=1.0, description="Override the spectrum's damping for CQC correlation. " - "Defaults to the spectrum's damping_ratio.", + "Defaults to the spectrum's damping_ratio.", ) AnalysisCase = Annotated[ - StaticCase | ModalCase | TransientCase | PushoverCase | ResponseSpectrumCase, + Union[StaticCase, ModalCase, TransientCase, PushoverCase, ResponseSpectrumCase], Field(discriminator="type"), ] diff --git a/src/otko/core/catalog/__init__.py b/src/otko/core/catalog/__init__.py index 31978fc..1581566 100644 --- a/src/otko/core/catalog/__init__.py +++ b/src/otko/core/catalog/__init__.py @@ -8,9 +8,7 @@ from pydantic import BaseModel from otko.core.catalog.generated.elastic import ElasticSpec from otko.core.catalog.generated.elastic_perfectly_plastic import ElasticPerfectlyPlasticSpec -from otko.core.catalog.generated.elastic_perfectly_plastic_with_gap import ( - ElasticPerfectlyPlasticWithGapSpec, -) +from otko.core.catalog.generated.elastic_perfectly_plastic_with_gap import ElasticPerfectlyPlasticWithGapSpec from otko.core.catalog.generated.viscous import ViscousSpec from otko.core.catalog.generated.viscous_damper import ViscousDamperSpec from otko.core.catalog.generated.hyperbolic_gap import HyperbolicGapSpec @@ -23,12 +21,8 @@ from otko.core.catalog.generated.steel02 import Steel02Spec from otko.core.catalog.generated.reinforcing_steel import ReinforcingSteelSpec from otko.core.catalog.generated.hysteretic import HystereticSpec from otko.core.catalog.generated.ramberg_osgood_steel import RambergOsgoodSteelSpec -from otko.core.catalog.generated.concrete01_zero_tensile_strength import ( - Concrete01ZeroTensileStrengthSpec, -) -from otko.core.catalog.generated.concrete02_linear_tension_softening import ( - Concrete02LinearTensionSofteningSpec, -) +from otko.core.catalog.generated.concrete01_zero_tensile_strength import Concrete01ZeroTensileStrengthSpec +from otko.core.catalog.generated.concrete02_linear_tension_softening import Concrete02LinearTensionSofteningSpec from otko.core.catalog.generated.concrete04_popovics_concrete import Concrete04PopovicsConcreteSpec from otko.core.catalog.generated.concrete06 import Concrete06Spec from otko.core.catalog.generated.concretecm import ConcretecmSpec @@ -56,16 +50,10 @@ from otko.core.catalog.generated.elastic_membrane_plate import ElasticMembranePl from otko.core.catalog.generated.layeredshell import LayeredshellSpec from otko.core.catalog.generated.ground_motion_record import GroundMotionRecordSpec from otko.core.catalog.generated.elastic_beam_column import ElasticBeamColumnSpec -from otko.core.catalog.generated.elastic_timoshenko_beam_column import ( - ElasticTimoshenkoBeamColumnSpec, -) +from otko.core.catalog.generated.elastic_timoshenko_beam_column import ElasticTimoshenkoBeamColumnSpec from otko.core.catalog.generated.force_based_beam_column import ForceBasedBeamColumnSpec -from otko.core.catalog.generated.displacement_based_beam_column import ( - DisplacementBasedBeamColumnSpec, -) -from otko.core.catalog.generated.flexure_shear_interaction_displacement_based_beam_column import ( - FlexureShearInteractionDisplacementBasedBeamColumnSpec, -) +from otko.core.catalog.generated.displacement_based_beam_column import DisplacementBasedBeamColumnSpec +from otko.core.catalog.generated.flexure_shear_interaction_displacement_based_beam_column import FlexureShearInteractionDisplacementBasedBeamColumnSpec from otko.core.catalog.generated.truss import TrussSpec from otko.core.catalog.generated.corotational_truss import CorotationalTrussSpec from otko.core.catalog.generated.quad import QuadSpec @@ -79,124 +67,64 @@ from otko.core.catalog.generated.auto_equal_constraint import AutoEqualConstrain CATALOG: dict[str, type[BaseModel]] = { - "Elastic": ElasticSpec, - "Elastic_Perfectly_Plastic": ElasticPerfectlyPlasticSpec, - "Elastic_Perfectly_Plastic_with_Gap": ElasticPerfectlyPlasticWithGapSpec, - "Viscous": ViscousSpec, - "Viscous_Damper": ViscousDamperSpec, - "Hyperbolic_Gap": HyperbolicGapSpec, - "PySimple1": Pysimple1Spec, - "TzSimple1": Tzsimple1Spec, - "QzSimple1": Qzsimple1Spec, - "BondSP01": Bondsp01Spec, - "Steel01": Steel01Spec, - "Steel02": Steel02Spec, - "Reinforcing_steel": ReinforcingSteelSpec, - "Hysteretic": HystereticSpec, - "Ramberg-Osgood steel": RambergOsgoodSteelSpec, - "Concrete01_(Zero_tensile_strength)": Concrete01ZeroTensileStrengthSpec, - "Concrete02_(Linear_tension_softening)": Concrete02LinearTensionSofteningSpec, - "Concrete04_(Popovics_concrete)": Concrete04PopovicsConcreteSpec, - "Concrete06": Concrete06Spec, - "ConcreteCM": ConcretecmSpec, - "Series": SeriesSpec, - "Parallel": ParallelSpec, - "Section_Aggregator": SectionAggregatorSpec, - "Initial_Strain": InitialStrainSpec, - "Initial_Stress": InitialStressSpec, - "MinMax": MinmaxSpec, - "User_Material": UserMaterialSpec, - "Elastic_Isotropic": ElasticIsotropicSpec, - "Elastic_Orthotropic": ElasticOrthotropicSpec, - "J2Plasticity": J2plasticitySpec, - "Damage2p": Damage2pSpec, - "PressureIndependMultiYield": PressureindependmultiyieldSpec, - "PressureDependMultiYield": PressuredependmultiyieldSpec, - "PressureDependMultiYield02": Pressuredependmultiyield02Spec, - "Contact": ContactSpec, - "Elastic_Section": ElasticSectionSpec, - "Fiber": FiberSpec, - "Fiber_Custom": FiberCustomSpec, - "FiberInt": FiberintSpec, - "Plate_Fiber": PlateFiberSpec, - "Elastic_Membrane_Plate": ElasticMembranePlateSpec, - "LayeredShell": LayeredshellSpec, - "Ground_Motion_Record": GroundMotionRecordSpec, - "Elastic_Beam-Column": ElasticBeamColumnSpec, - "Elastic_Timoshenko_Beam-Column": ElasticTimoshenkoBeamColumnSpec, - "Force-Based_Beam-Column": ForceBasedBeamColumnSpec, - "Displacement-Based_Beam-Column": DisplacementBasedBeamColumnSpec, - "Flexure-Shear_Interaction_Displacement-Based_Beam-Column": FlexureShearInteractionDisplacementBasedBeamColumnSpec, - "Truss": TrussSpec, - "Corotational_Truss": CorotationalTrussSpec, - "Quad": QuadSpec, - "Shell": ShellSpec, - "ShellDKGQ": ShelldkgqSpec, - "Tri31": Tri31Spec, - "QuadUP": QuadupSpec, - "Standard_Brick_Element": StandardBrickElementSpec, - "Auto_Zero_Length": AutoZeroLengthSpec, - "Auto_equal_constraint": AutoEqualConstraintSpec, + 'Elastic': ElasticSpec, + 'Elastic_Perfectly_Plastic': ElasticPerfectlyPlasticSpec, + 'Elastic_Perfectly_Plastic_with_Gap': ElasticPerfectlyPlasticWithGapSpec, + 'Viscous': ViscousSpec, + 'Viscous_Damper': ViscousDamperSpec, + 'Hyperbolic_Gap': HyperbolicGapSpec, + 'PySimple1': Pysimple1Spec, + 'TzSimple1': Tzsimple1Spec, + 'QzSimple1': Qzsimple1Spec, + 'BondSP01': Bondsp01Spec, + 'Steel01': Steel01Spec, + 'Steel02': Steel02Spec, + 'Reinforcing_steel': ReinforcingSteelSpec, + 'Hysteretic': HystereticSpec, + 'Ramberg-Osgood steel': RambergOsgoodSteelSpec, + 'Concrete01_(Zero_tensile_strength)': Concrete01ZeroTensileStrengthSpec, + 'Concrete02_(Linear_tension_softening)': Concrete02LinearTensionSofteningSpec, + 'Concrete04_(Popovics_concrete)': Concrete04PopovicsConcreteSpec, + 'Concrete06': Concrete06Spec, + 'ConcreteCM': ConcretecmSpec, + 'Series': SeriesSpec, + 'Parallel': ParallelSpec, + 'Section_Aggregator': SectionAggregatorSpec, + 'Initial_Strain': InitialStrainSpec, + 'Initial_Stress': InitialStressSpec, + 'MinMax': MinmaxSpec, + 'User_Material': UserMaterialSpec, + 'Elastic_Isotropic': ElasticIsotropicSpec, + 'Elastic_Orthotropic': ElasticOrthotropicSpec, + 'J2Plasticity': J2plasticitySpec, + 'Damage2p': Damage2pSpec, + 'PressureIndependMultiYield': PressureindependmultiyieldSpec, + 'PressureDependMultiYield': PressuredependmultiyieldSpec, + 'PressureDependMultiYield02': Pressuredependmultiyield02Spec, + 'Contact': ContactSpec, + 'Elastic_Section': ElasticSectionSpec, + 'Fiber': FiberSpec, + 'Fiber_Custom': FiberCustomSpec, + 'FiberInt': FiberintSpec, + 'Plate_Fiber': PlateFiberSpec, + 'Elastic_Membrane_Plate': ElasticMembranePlateSpec, + 'LayeredShell': LayeredshellSpec, + 'Ground_Motion_Record': GroundMotionRecordSpec, + 'Elastic_Beam-Column': ElasticBeamColumnSpec, + 'Elastic_Timoshenko_Beam-Column': ElasticTimoshenkoBeamColumnSpec, + 'Force-Based_Beam-Column': ForceBasedBeamColumnSpec, + 'Displacement-Based_Beam-Column': DisplacementBasedBeamColumnSpec, + 'Flexure-Shear_Interaction_Displacement-Based_Beam-Column': FlexureShearInteractionDisplacementBasedBeamColumnSpec, + 'Truss': TrussSpec, + 'Corotational_Truss': CorotationalTrussSpec, + 'Quad': QuadSpec, + 'Shell': ShellSpec, + 'ShellDKGQ': ShelldkgqSpec, + 'Tri31': Tri31Spec, + 'QuadUP': QuadupSpec, + 'Standard_Brick_Element': StandardBrickElementSpec, + 'Auto_Zero_Length': AutoZeroLengthSpec, + 'Auto_equal_constraint': AutoEqualConstraintSpec, } -__all__ = [ - "CATALOG", - "ElasticSpec", - "ElasticPerfectlyPlasticSpec", - "ElasticPerfectlyPlasticWithGapSpec", - "ViscousSpec", - "ViscousDamperSpec", - "HyperbolicGapSpec", - "Pysimple1Spec", - "Tzsimple1Spec", - "Qzsimple1Spec", - "Bondsp01Spec", - "Steel01Spec", - "Steel02Spec", - "ReinforcingSteelSpec", - "HystereticSpec", - "RambergOsgoodSteelSpec", - "Concrete01ZeroTensileStrengthSpec", - "Concrete02LinearTensionSofteningSpec", - "Concrete04PopovicsConcreteSpec", - "Concrete06Spec", - "ConcretecmSpec", - "SeriesSpec", - "ParallelSpec", - "SectionAggregatorSpec", - "InitialStrainSpec", - "InitialStressSpec", - "MinmaxSpec", - "UserMaterialSpec", - "ElasticIsotropicSpec", - "ElasticOrthotropicSpec", - "J2plasticitySpec", - "Damage2pSpec", - "PressureindependmultiyieldSpec", - "PressuredependmultiyieldSpec", - "Pressuredependmultiyield02Spec", - "ContactSpec", - "ElasticSectionSpec", - "FiberSpec", - "FiberCustomSpec", - "FiberintSpec", - "PlateFiberSpec", - "ElasticMembranePlateSpec", - "LayeredshellSpec", - "GroundMotionRecordSpec", - "ElasticBeamColumnSpec", - "ElasticTimoshenkoBeamColumnSpec", - "ForceBasedBeamColumnSpec", - "DisplacementBasedBeamColumnSpec", - "FlexureShearInteractionDisplacementBasedBeamColumnSpec", - "TrussSpec", - "CorotationalTrussSpec", - "QuadSpec", - "ShellSpec", - "ShelldkgqSpec", - "Tri31Spec", - "QuadupSpec", - "StandardBrickElementSpec", - "AutoZeroLengthSpec", - "AutoEqualConstraintSpec", -] +__all__ = ["CATALOG", 'ElasticSpec', 'ElasticPerfectlyPlasticSpec', 'ElasticPerfectlyPlasticWithGapSpec', 'ViscousSpec', 'ViscousDamperSpec', 'HyperbolicGapSpec', 'Pysimple1Spec', 'Tzsimple1Spec', 'Qzsimple1Spec', 'Bondsp01Spec', 'Steel01Spec', 'Steel02Spec', 'ReinforcingSteelSpec', 'HystereticSpec', 'RambergOsgoodSteelSpec', 'Concrete01ZeroTensileStrengthSpec', 'Concrete02LinearTensionSofteningSpec', 'Concrete04PopovicsConcreteSpec', 'Concrete06Spec', 'ConcretecmSpec', 'SeriesSpec', 'ParallelSpec', 'SectionAggregatorSpec', 'InitialStrainSpec', 'InitialStressSpec', 'MinmaxSpec', 'UserMaterialSpec', 'ElasticIsotropicSpec', 'ElasticOrthotropicSpec', 'J2plasticitySpec', 'Damage2pSpec', 'PressureindependmultiyieldSpec', 'PressuredependmultiyieldSpec', 'Pressuredependmultiyield02Spec', 'ContactSpec', 'ElasticSectionSpec', 'FiberSpec', 'FiberCustomSpec', 'FiberintSpec', 'PlateFiberSpec', 'ElasticMembranePlateSpec', 'LayeredshellSpec', 'GroundMotionRecordSpec', 'ElasticBeamColumnSpec', 'ElasticTimoshenkoBeamColumnSpec', 'ForceBasedBeamColumnSpec', 'DisplacementBasedBeamColumnSpec', 'FlexureShearInteractionDisplacementBasedBeamColumnSpec', 'TrussSpec', 'CorotationalTrussSpec', 'QuadSpec', 'ShellSpec', 'ShelldkgqSpec', 'Tri31Spec', 'QuadupSpec', 'StandardBrickElementSpec', 'AutoZeroLengthSpec', 'AutoEqualConstraintSpec'] diff --git a/src/otko/core/catalog/generated/__init__.py b/src/otko/core/catalog/generated/__init__.py index 170a391..d0aed63 100644 --- a/src/otko/core/catalog/generated/__init__.py +++ b/src/otko/core/catalog/generated/__init__.py @@ -7,9 +7,7 @@ from typing import Union from otko.core.catalog.generated.elastic import ElasticSpec from otko.core.catalog.generated.elastic_perfectly_plastic import ElasticPerfectlyPlasticSpec -from otko.core.catalog.generated.elastic_perfectly_plastic_with_gap import ( - ElasticPerfectlyPlasticWithGapSpec, -) +from otko.core.catalog.generated.elastic_perfectly_plastic_with_gap import ElasticPerfectlyPlasticWithGapSpec from otko.core.catalog.generated.viscous import ViscousSpec from otko.core.catalog.generated.viscous_damper import ViscousDamperSpec from otko.core.catalog.generated.hyperbolic_gap import HyperbolicGapSpec @@ -22,12 +20,8 @@ from otko.core.catalog.generated.steel02 import Steel02Spec from otko.core.catalog.generated.reinforcing_steel import ReinforcingSteelSpec from otko.core.catalog.generated.hysteretic import HystereticSpec from otko.core.catalog.generated.ramberg_osgood_steel import RambergOsgoodSteelSpec -from otko.core.catalog.generated.concrete01_zero_tensile_strength import ( - Concrete01ZeroTensileStrengthSpec, -) -from otko.core.catalog.generated.concrete02_linear_tension_softening import ( - Concrete02LinearTensionSofteningSpec, -) +from otko.core.catalog.generated.concrete01_zero_tensile_strength import Concrete01ZeroTensileStrengthSpec +from otko.core.catalog.generated.concrete02_linear_tension_softening import Concrete02LinearTensionSofteningSpec from otko.core.catalog.generated.concrete04_popovics_concrete import Concrete04PopovicsConcreteSpec from otko.core.catalog.generated.concrete06 import Concrete06Spec from otko.core.catalog.generated.concretecm import ConcretecmSpec @@ -55,16 +49,10 @@ from otko.core.catalog.generated.elastic_membrane_plate import ElasticMembranePl from otko.core.catalog.generated.layeredshell import LayeredshellSpec from otko.core.catalog.generated.ground_motion_record import GroundMotionRecordSpec from otko.core.catalog.generated.elastic_beam_column import ElasticBeamColumnSpec -from otko.core.catalog.generated.elastic_timoshenko_beam_column import ( - ElasticTimoshenkoBeamColumnSpec, -) +from otko.core.catalog.generated.elastic_timoshenko_beam_column import ElasticTimoshenkoBeamColumnSpec from otko.core.catalog.generated.force_based_beam_column import ForceBasedBeamColumnSpec -from otko.core.catalog.generated.displacement_based_beam_column import ( - DisplacementBasedBeamColumnSpec, -) -from otko.core.catalog.generated.flexure_shear_interaction_displacement_based_beam_column import ( - FlexureShearInteractionDisplacementBasedBeamColumnSpec, -) +from otko.core.catalog.generated.displacement_based_beam_column import DisplacementBasedBeamColumnSpec +from otko.core.catalog.generated.flexure_shear_interaction_displacement_based_beam_column import FlexureShearInteractionDisplacementBasedBeamColumnSpec from otko.core.catalog.generated.truss import TrussSpec from otko.core.catalog.generated.corotational_truss import CorotationalTrussSpec from otko.core.catalog.generated.quad import QuadSpec @@ -77,141 +65,19 @@ from otko.core.catalog.generated.auto_zero_length import AutoZeroLengthSpec from otko.core.catalog.generated.auto_equal_constraint import AutoEqualConstraintSpec -StandardUniaxialMaterials = Union[ - ElasticSpec, - ElasticPerfectlyPlasticSpec, - ElasticPerfectlyPlasticWithGapSpec, - ViscousSpec, - ViscousDamperSpec, - HyperbolicGapSpec, - Pysimple1Spec, - Tzsimple1Spec, - Qzsimple1Spec, - Bondsp01Spec, -] -UniaxialSteelMaterials = Union[ - Steel01Spec, Steel02Spec, ReinforcingSteelSpec, HystereticSpec, RambergOsgoodSteelSpec -] -UniaxialConcreteMaterials = Union[ - Concrete01ZeroTensileStrengthSpec, - Concrete02LinearTensionSofteningSpec, - Concrete04PopovicsConcreteSpec, - Concrete06Spec, - ConcretecmSpec, -] -CombinedMaterials = Union[ - SeriesSpec, - ParallelSpec, - SectionAggregatorSpec, - InitialStrainSpec, - InitialStressSpec, - MinmaxSpec, -] +StandardUniaxialMaterials = Union[ElasticSpec, ElasticPerfectlyPlasticSpec, ElasticPerfectlyPlasticWithGapSpec, ViscousSpec, ViscousDamperSpec, HyperbolicGapSpec, Pysimple1Spec, Tzsimple1Spec, Qzsimple1Spec, Bondsp01Spec] +UniaxialSteelMaterials = Union[Steel01Spec, Steel02Spec, ReinforcingSteelSpec, HystereticSpec, RambergOsgoodSteelSpec] +UniaxialConcreteMaterials = Union[Concrete01ZeroTensileStrengthSpec, Concrete02LinearTensionSofteningSpec, Concrete04PopovicsConcreteSpec, Concrete06Spec, ConcretecmSpec] +CombinedMaterials = Union[SeriesSpec, ParallelSpec, SectionAggregatorSpec, InitialStrainSpec, InitialStressSpec, MinmaxSpec] UserMaterials = UserMaterialSpec -MultidimensionalNdMaterials = Union[ - ElasticIsotropicSpec, - ElasticOrthotropicSpec, - J2plasticitySpec, - Damage2pSpec, - PressureindependmultiyieldSpec, - PressuredependmultiyieldSpec, - Pressuredependmultiyield02Spec, - ContactSpec, -] -SectionForceDeformation = Union[ - ElasticSectionSpec, - FiberSpec, - FiberCustomSpec, - FiberintSpec, - PlateFiberSpec, - ElasticMembranePlateSpec, - LayeredshellSpec, -] +MultidimensionalNdMaterials = Union[ElasticIsotropicSpec, ElasticOrthotropicSpec, J2plasticitySpec, Damage2pSpec, PressureindependmultiyieldSpec, PressuredependmultiyieldSpec, Pressuredependmultiyield02Spec, ContactSpec] +SectionForceDeformation = Union[ElasticSectionSpec, FiberSpec, FiberCustomSpec, FiberintSpec, PlateFiberSpec, ElasticMembranePlateSpec, LayeredshellSpec] Records = GroundMotionRecordSpec -BeamColumnElements = Union[ - ElasticBeamColumnSpec, - ElasticTimoshenkoBeamColumnSpec, - ForceBasedBeamColumnSpec, - DisplacementBasedBeamColumnSpec, - FlexureShearInteractionDisplacementBasedBeamColumnSpec, -] +BeamColumnElements = Union[ElasticBeamColumnSpec, ElasticTimoshenkoBeamColumnSpec, ForceBasedBeamColumnSpec, DisplacementBasedBeamColumnSpec, FlexureShearInteractionDisplacementBasedBeamColumnSpec] TrussElements = Union[TrussSpec, CorotationalTrussSpec] SurfaceElements = Union[QuadSpec, ShellSpec, ShelldkgqSpec, Tri31Spec, QuadupSpec] SolidElements = StandardBrickElementSpec Autozl = AutoZeroLengthSpec Autoedof = AutoEqualConstraintSpec -__all__ = [ - "ElasticSpec", - "ElasticPerfectlyPlasticSpec", - "ElasticPerfectlyPlasticWithGapSpec", - "ViscousSpec", - "ViscousDamperSpec", - "HyperbolicGapSpec", - "Pysimple1Spec", - "Tzsimple1Spec", - "Qzsimple1Spec", - "Bondsp01Spec", - "Steel01Spec", - "Steel02Spec", - "ReinforcingSteelSpec", - "HystereticSpec", - "RambergOsgoodSteelSpec", - "Concrete01ZeroTensileStrengthSpec", - "Concrete02LinearTensionSofteningSpec", - "Concrete04PopovicsConcreteSpec", - "Concrete06Spec", - "ConcretecmSpec", - "SeriesSpec", - "ParallelSpec", - "SectionAggregatorSpec", - "InitialStrainSpec", - "InitialStressSpec", - "MinmaxSpec", - "UserMaterialSpec", - "ElasticIsotropicSpec", - "ElasticOrthotropicSpec", - "J2plasticitySpec", - "Damage2pSpec", - "PressureindependmultiyieldSpec", - "PressuredependmultiyieldSpec", - "Pressuredependmultiyield02Spec", - "ContactSpec", - "ElasticSectionSpec", - "FiberSpec", - "FiberCustomSpec", - "FiberintSpec", - "PlateFiberSpec", - "ElasticMembranePlateSpec", - "LayeredshellSpec", - "GroundMotionRecordSpec", - "ElasticBeamColumnSpec", - "ElasticTimoshenkoBeamColumnSpec", - "ForceBasedBeamColumnSpec", - "DisplacementBasedBeamColumnSpec", - "FlexureShearInteractionDisplacementBasedBeamColumnSpec", - "TrussSpec", - "CorotationalTrussSpec", - "QuadSpec", - "ShellSpec", - "ShelldkgqSpec", - "Tri31Spec", - "QuadupSpec", - "StandardBrickElementSpec", - "AutoZeroLengthSpec", - "AutoEqualConstraintSpec", - "StandardUniaxialMaterials", - "UniaxialSteelMaterials", - "UniaxialConcreteMaterials", - "CombinedMaterials", - "UserMaterials", - "MultidimensionalNdMaterials", - "SectionForceDeformation", - "Records", - "BeamColumnElements", - "TrussElements", - "SurfaceElements", - "SolidElements", - "Autozl", - "Autoedof", -] +__all__ = ['ElasticSpec', 'ElasticPerfectlyPlasticSpec', 'ElasticPerfectlyPlasticWithGapSpec', 'ViscousSpec', 'ViscousDamperSpec', 'HyperbolicGapSpec', 'Pysimple1Spec', 'Tzsimple1Spec', 'Qzsimple1Spec', 'Bondsp01Spec', 'Steel01Spec', 'Steel02Spec', 'ReinforcingSteelSpec', 'HystereticSpec', 'RambergOsgoodSteelSpec', 'Concrete01ZeroTensileStrengthSpec', 'Concrete02LinearTensionSofteningSpec', 'Concrete04PopovicsConcreteSpec', 'Concrete06Spec', 'ConcretecmSpec', 'SeriesSpec', 'ParallelSpec', 'SectionAggregatorSpec', 'InitialStrainSpec', 'InitialStressSpec', 'MinmaxSpec', 'UserMaterialSpec', 'ElasticIsotropicSpec', 'ElasticOrthotropicSpec', 'J2plasticitySpec', 'Damage2pSpec', 'PressureindependmultiyieldSpec', 'PressuredependmultiyieldSpec', 'Pressuredependmultiyield02Spec', 'ContactSpec', 'ElasticSectionSpec', 'FiberSpec', 'FiberCustomSpec', 'FiberintSpec', 'PlateFiberSpec', 'ElasticMembranePlateSpec', 'LayeredshellSpec', 'GroundMotionRecordSpec', 'ElasticBeamColumnSpec', 'ElasticTimoshenkoBeamColumnSpec', 'ForceBasedBeamColumnSpec', 'DisplacementBasedBeamColumnSpec', 'FlexureShearInteractionDisplacementBasedBeamColumnSpec', 'TrussSpec', 'CorotationalTrussSpec', 'QuadSpec', 'ShellSpec', 'ShelldkgqSpec', 'Tri31Spec', 'QuadupSpec', 'StandardBrickElementSpec', 'AutoZeroLengthSpec', 'AutoEqualConstraintSpec', 'StandardUniaxialMaterials', 'UniaxialSteelMaterials', 'UniaxialConcreteMaterials', 'CombinedMaterials', 'UserMaterials', 'MultidimensionalNdMaterials', 'SectionForceDeformation', 'Records', 'BeamColumnElements', 'TrussElements', 'SurfaceElements', 'SolidElements', 'Autozl', 'Autoedof'] diff --git a/src/otko/core/catalog/generated/auto_equal_constraint.py b/src/otko/core/catalog/generated/auto_equal_constraint.py index 52c13e1..63a05a4 100644 --- a/src/otko/core/catalog/generated/auto_equal_constraint.py +++ b/src/otko/core/catalog/generated/auto_equal_constraint.py @@ -17,478 +17,478 @@ from pydantic import BaseModel, ConfigDict, Field class AutoEqualConstraintSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Auto_equal_constraint", - "x-book": "AutoEDOF", - "dependencies": [ + 'x-gid-name': 'Auto_equal_constraint', + 'x-book': 'AutoEDOF', + 'dependencies': [ { - "field": "source", - "gid_name": "Source", - "rules": [ + 'field': 'source', + 'gid_name': 'Source', + 'rules': [ { - "trigger": "Layer", - "actions": [ + 'trigger': 'Layer', + 'actions': [ { - "action": "RESTORE", - "field": "Layer_name", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Layer_name', + 'target': '#CURRENT#', }, ], }, { - "trigger": "All", - "actions": [ + 'trigger': 'All', + 'actions': [ { - "action": "HIDE", - "field": "Layer_name", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Layer_name', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Visible", - "actions": [ + 'trigger': 'Visible', + 'actions': [ { - "action": "HIDE", - "field": "Layer_name", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Layer_name', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "x_criteria", - "gid_name": "X-Criteria", - "rules": [ + 'field': 'x_criteria', + 'gid_name': 'X-Criteria', + 'rules': [ { - "trigger": "Any", - "actions": [ + 'trigger': 'Any', + 'actions': [ { - "action": "HIDE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Fixed", - "actions": [ + 'trigger': 'Fixed', + 'actions': [ { - "action": "RESTORE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GT", - "actions": [ + 'trigger': 'GT', + 'actions': [ { - "action": "RESTORE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GET", - "actions": [ + 'trigger': 'GET', + 'actions': [ { - "action": "RESTORE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LT", - "actions": [ + 'trigger': 'LT', + 'actions': [ { - "action": "RESTORE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LET", - "actions": [ + 'trigger': 'LET', + 'actions': [ { - "action": "RESTORE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Between", - "actions": [ + 'trigger': 'Between', + 'actions': [ { - "action": "HIDE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "y_criteria", - "gid_name": "Y-Criteria", - "rules": [ + 'field': 'y_criteria', + 'gid_name': 'Y-Criteria', + 'rules': [ { - "trigger": "Any", - "actions": [ + 'trigger': 'Any', + 'actions': [ { - "action": "HIDE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Fixed", - "actions": [ + 'trigger': 'Fixed', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GT", - "actions": [ + 'trigger': 'GT', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GET", - "actions": [ + 'trigger': 'GET', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LT", - "actions": [ + 'trigger': 'LT', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LET", - "actions": [ + 'trigger': 'LET', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Between", - "actions": [ + 'trigger': 'Between', + 'actions': [ { - "action": "HIDE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "z_criteria", - "gid_name": "Z-Criteria", - "rules": [ + 'field': 'z_criteria', + 'gid_name': 'Z-Criteria', + 'rules': [ { - "trigger": "Any", - "actions": [ + 'trigger': 'Any', + 'actions': [ { - "action": "HIDE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Fixed", - "actions": [ + 'trigger': 'Fixed', + 'actions': [ { - "action": "RESTORE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GT", - "actions": [ + 'trigger': 'GT', + 'actions': [ { - "action": "RESTORE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GET", - "actions": [ + 'trigger': 'GET', + 'actions': [ { - "action": "RESTORE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LT", - "actions": [ + 'trigger': 'LT', + 'actions': [ { - "action": "RESTORE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LET", - "actions": [ + 'trigger': 'LET', + 'actions': [ { - "action": "RESTORE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Between", - "actions": [ + 'trigger': 'Between', + 'actions': [ { - "action": "HIDE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, @@ -498,40 +498,29 @@ class AutoEqualConstraintSpec(BaseModel): } ) - determine_master_node: Literal["Existed", "Extra"] = "Existed" - source: Literal["Layer", "All", "Visible"] = Field( - "Layer", - description="Layer: Points or Nodes from specific layer regardless the visibility\\nAll: all points or nodes regardless the visibility\\nVisible: Only visible points or nodes", - ) - layer_name: str = Field("Layer0", description='Layer with name "AutoZL" will be ignored.') - x_criteria: Literal["Any", "Fixed", "GT", "GET", "LT", "LET", "Between"] = Field( - "Any", - description="GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries", - ) + determine_master_node: Literal['Existed', 'Extra'] = 'Existed' + source: Literal['Layer', 'All', 'Visible'] = Field('Layer', description='Layer: Points or Nodes from specific layer regardless the visibility\\nAll: all points or nodes regardless the visibility\\nVisible: Only visible points or nodes') + layer_name: str = Field('Layer0', description='Layer with name "AutoZL" will be ignored.') + x_criteria: Literal['Any', 'Fixed', 'GT', 'GET', 'LT', 'LET', 'Between'] = Field('Any', description='GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries') x_coordinate: float = 0.0 min_x_coordinate: float = 0.0 max_x_coordinate: float = 0.0 - y_criteria: Literal["Any", "Fixed", "GT", "GET", "LT", "LET", "Between"] = Field( - "Any", - description="GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries", - ) + y_criteria: Literal['Any', 'Fixed', 'GT', 'GET', 'LT', 'LET', 'Between'] = Field('Any', description='GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries') y_coordinate: float = 0.0 min_y_coordinate: float = 0.0 max_y_coordinate: float = 0.0 - z_criteria: Literal["Any", "Fixed", "GT", "GET", "LT", "LET", "Between"] = Field( - "Any", - description="GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries", - ) + z_criteria: Literal['Any', 'Fixed', 'GT', 'GET', 'LT', 'LET', 'Between'] = Field('Any', description='GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries') z_coordinate: float = 0.0 min_z_coordinate: float = 0.0 max_z_coordinate: float = 0.0 - extra_point_node_offset: Literal["_"] = "_" + extra_point_node_offset: Literal['_'] = '_' x_offset: float = 0.0 y_offset: float = 0.0 z_offset: float = 0.0 - x_translation: Literal["1", "0"] = "1" - y_translation: Literal["1", "0"] = "1" - z_translation: Literal["1", "0"] = "1" - x_rotation: Literal["1", "0"] = "1" - y_rotation: Literal["1", "0"] = "1" - z_rotation: Literal["1", "0"] = "1" + x_translation: Literal['1', '0'] = '1' + y_translation: Literal['1', '0'] = '1' + z_translation: Literal['1', '0'] = '1' + x_rotation: Literal['1', '0'] = '1' + y_rotation: Literal['1', '0'] = '1' + z_rotation: Literal['1', '0'] = '1' + diff --git a/src/otko/core/catalog/generated/auto_zero_length.py b/src/otko/core/catalog/generated/auto_zero_length.py index e25d1d4..880646f 100644 --- a/src/otko/core/catalog/generated/auto_zero_length.py +++ b/src/otko/core/catalog/generated/auto_zero_length.py @@ -17,634 +17,634 @@ from pydantic import BaseModel, ConfigDict, Field class AutoZeroLengthSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Auto_Zero_Length", - "x-book": "AutoZL", - "dependencies": [ + 'x-gid-name': 'Auto_Zero_Length', + 'x-book': 'AutoZL', + 'dependencies': [ { - "field": "source", - "gid_name": "Source", - "rules": [ + 'field': 'source', + 'gid_name': 'Source', + 'rules': [ { - "trigger": "Layer", - "actions": [ + 'trigger': 'Layer', + 'actions': [ { - "action": "RESTORE", - "field": "Layer_name", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Layer_name', + 'target': '#CURRENT#', }, ], }, { - "trigger": "All", - "actions": [ + 'trigger': 'All', + 'actions': [ { - "action": "HIDE", - "field": "Layer_name", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Layer_name', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Visible", - "actions": [ + 'trigger': 'Visible', + 'actions': [ { - "action": "HIDE", - "field": "Layer_name", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Layer_name', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "x_criteria", - "gid_name": "X-Criteria", - "rules": [ + 'field': 'x_criteria', + 'gid_name': 'X-Criteria', + 'rules': [ { - "trigger": "Any", - "actions": [ + 'trigger': 'Any', + 'actions': [ { - "action": "HIDE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Fixed", - "actions": [ + 'trigger': 'Fixed', + 'actions': [ { - "action": "RESTORE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GT", - "actions": [ + 'trigger': 'GT', + 'actions': [ { - "action": "RESTORE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GET", - "actions": [ + 'trigger': 'GET', + 'actions': [ { - "action": "RESTORE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LT", - "actions": [ + 'trigger': 'LT', + 'actions': [ { - "action": "RESTORE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LET", - "actions": [ + 'trigger': 'LET', + 'actions': [ { - "action": "RESTORE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Between", - "actions": [ + 'trigger': 'Between', + 'actions': [ { - "action": "HIDE", - "field": "X-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Min_X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Min_X-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Max_X-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Max_X-Coordinate', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "y_criteria", - "gid_name": "Y-Criteria", - "rules": [ + 'field': 'y_criteria', + 'gid_name': 'Y-Criteria', + 'rules': [ { - "trigger": "Any", - "actions": [ + 'trigger': 'Any', + 'actions': [ { - "action": "HIDE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Fixed", - "actions": [ + 'trigger': 'Fixed', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GT", - "actions": [ + 'trigger': 'GT', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GET", - "actions": [ + 'trigger': 'GET', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LT", - "actions": [ + 'trigger': 'LT', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LET", - "actions": [ + 'trigger': 'LET', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Between", - "actions": [ + 'trigger': 'Between', + 'actions': [ { - "action": "HIDE", - "field": "Y-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Min_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Min_Y-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Max_Y-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Max_Y-Coordinate', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "z_criteria", - "gid_name": "Z-Criteria", - "rules": [ + 'field': 'z_criteria', + 'gid_name': 'Z-Criteria', + 'rules': [ { - "trigger": "Any", - "actions": [ + 'trigger': 'Any', + 'actions': [ { - "action": "HIDE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Fixed", - "actions": [ + 'trigger': 'Fixed', + 'actions': [ { - "action": "RESTORE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GT", - "actions": [ + 'trigger': 'GT', + 'actions': [ { - "action": "RESTORE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "GET", - "actions": [ + 'trigger': 'GET', + 'actions': [ { - "action": "RESTORE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LT", - "actions": [ + 'trigger': 'LT', + 'actions': [ { - "action": "RESTORE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "LET", - "actions": [ + 'trigger': 'LET', + 'actions': [ { - "action": "RESTORE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Between", - "actions": [ + 'trigger': 'Between', + 'actions': [ { - "action": "HIDE", - "field": "Z-Coordinate", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Min_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Min_Z-Coordinate', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Max_Z-Coordinate", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Max_Z-Coordinate', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_ux", - "gid_name": "Activate_Ux", - "rules": [ + 'field': 'activate_ux', + 'gid_name': 'Activate_Ux', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Ux_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Ux_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Ux_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Ux_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_uy", - "gid_name": "Activate_Uy", - "rules": [ + 'field': 'activate_uy', + 'gid_name': 'Activate_Uy', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Uy_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Uy_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Uy_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Uy_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_uz", - "gid_name": "Activate_Uz", - "rules": [ + 'field': 'activate_uz', + 'gid_name': 'Activate_Uz', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Uz_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Uz_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Uz_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Uz_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_rx", - "gid_name": "Activate_Rx", - "rules": [ + 'field': 'activate_rx', + 'gid_name': 'Activate_Rx', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Rx_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Rx_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Rx_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rx_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_ry", - "gid_name": "Activate_Ry", - "rules": [ + 'field': 'activate_ry', + 'gid_name': 'Activate_Ry', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Ry_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Ry_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Ry_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Ry_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_rz", - "gid_name": "Activate_Rz", - "rules": [ + 'field': 'activate_rz', + 'gid_name': 'Activate_Rz', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Rz_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Rz_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Rz_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rz_material', + 'target': '#CURRENT#', }, ], }, @@ -654,45 +654,34 @@ class AutoZeroLengthSpec(BaseModel): } ) - source: Literal["Layer", "All", "Visible"] = Field( - "Layer", - description="Layer: Points or Nodes from specific layer regardless the visibility\\nAll: all points or nodes regardless the visibility\\nVisible: Only visible points or nodes", - ) - layer_name: str = Field("Layer0", description='Layer with name "AutoZL" will be ignored.') - x_criteria: Literal["Any", "Fixed", "GT", "GET", "LT", "LET", "Between"] = Field( - "Any", - description="GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries", - ) + source: Literal['Layer', 'All', 'Visible'] = Field('Layer', description='Layer: Points or Nodes from specific layer regardless the visibility\\nAll: all points or nodes regardless the visibility\\nVisible: Only visible points or nodes') + layer_name: str = Field('Layer0', description='Layer with name "AutoZL" will be ignored.') + x_criteria: Literal['Any', 'Fixed', 'GT', 'GET', 'LT', 'LET', 'Between'] = Field('Any', description='GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries') x_coordinate: float = 0.0 min_x_coordinate: float = 0.0 max_x_coordinate: float = 0.0 - y_criteria: Literal["Any", "Fixed", "GT", "GET", "LT", "LET", "Between"] = Field( - "Any", - description="GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries", - ) + y_criteria: Literal['Any', 'Fixed', 'GT', 'GET', 'LT', 'LET', 'Between'] = Field('Any', description='GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries') y_coordinate: float = 0.0 min_y_coordinate: float = 0.0 max_y_coordinate: float = 0.0 - z_criteria: Literal["Any", "Fixed", "GT", "GET", "LT", "LET", "Between"] = Field( - "Any", - description="GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries", - ) + z_criteria: Literal['Any', 'Fixed', 'GT', 'GET', 'LT', 'LET', 'Between'] = Field('Any', description='GT: Greater than\\nGET: Greater/Equal than\\nLT: Less than\\nLET: Less/Equal than\\nBetween: inside (min,max) space including boundaries') z_coordinate: float = 0.0 min_z_coordinate: float = 0.0 max_z_coordinate: float = 0.0 - extra_point_node_offset: Literal["_"] = "_" + extra_point_node_offset: Literal['_'] = '_' x_offset: float = 0.0 y_offset: float = 0.0 z_offset: float = 0.0 - activate_ux: Literal["0", "1"] = "1" - ux_material: str = "Elastic" - activate_uy: Literal["0", "1"] = "0" - uy_material: str = "Elastic" - activate_uz: Literal["0", "1"] = "0" - uz_material: str = "Elastic" - activate_rx: Literal["0", "1"] = "0" - rx_material: str = "Elastic" - activate_ry: Literal["0", "1"] = "0" - ry_material: str = "Elastic" - activate_rz: Literal["0", "1"] = "0" - rz_material: str = "Elastic" + activate_ux: Literal['0', '1'] = '1' + ux_material: str = 'Elastic' + activate_uy: Literal['0', '1'] = '0' + uy_material: str = 'Elastic' + activate_uz: Literal['0', '1'] = '0' + uz_material: str = 'Elastic' + activate_rx: Literal['0', '1'] = '0' + rx_material: str = 'Elastic' + activate_ry: Literal['0', '1'] = '0' + ry_material: str = 'Elastic' + activate_rz: Literal['0', '1'] = '0' + rz_material: str = 'Elastic' + diff --git a/src/otko/core/catalog/generated/bondsp01.py b/src/otko/core/catalog/generated/bondsp01.py index 032d37f..884e4f0 100644 --- a/src/otko/core/catalog/generated/bondsp01.py +++ b/src/otko/core/catalog/generated/bondsp01.py @@ -23,86 +23,86 @@ class CyclicDataRow(BaseModel): class Bondsp01Spec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "BondSP01", - "x-book": "Standard_Uniaxial_Materials", - "dependencies": [ + 'x-gid-name': 'BondSP01', + 'x-book': 'Standard_Uniaxial_Materials', + 'dependencies': [ { - "field": "formulation", - "gid_name": "Formulation", - "rules": [ + 'field': 'formulation', + 'gid_name': 'Formulation', + 'rules': [ { - "trigger": "Stress-Slip", - "actions": [ + 'trigger': 'Stress-Slip', + 'actions': [ { - "action": "RESTORE", - "field": "Yield_stress_Fy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Yield_stress_Fy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Ultimate_stress_Fu", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Ultimate_stress_Fu', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Yield_force_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Yield_force_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Ultimate_Force_Fu", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Ultimate_Force_Fu', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Force-Slip", - "actions": [ + 'trigger': 'Force-Slip', + 'actions': [ { - "action": "RESTORE", - "field": "Yield_force_Fy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Yield_force_Fy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Ultimate_Force_Fu", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Ultimate_Force_Fu', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Yield_stress_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Yield_stress_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Ultimate_stress_Fu", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Ultimate_stress_Fu', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -112,15 +112,16 @@ class Bondsp01Spec(BaseModel): } ) - material: Literal["_"] = "_" - formulation: Literal["Stress-Slip", "Force-Slip"] = "Stress-Slip" - yield_stress_fy: str = "500 MPa" # TODO: unit-aware type - yield_force_fy: str = "157 kN" # TODO: unit-aware type - rebar_slip_at_fy: str = "1 mm" # TODO: unit-aware type - ultimate_stress_fu: str = "675 MPa" # TODO: unit-aware type - ultimate_force_fu: str = "212 kN" # TODO: unit-aware type - rebar_slip_at_the_loaded_end_at_the_bar_fracture_strength: str = "2 mm" # TODO: unit-aware type + material: Literal['_'] = '_' + formulation: Literal['Stress-Slip', 'Force-Slip'] = 'Stress-Slip' + yield_stress_fy: str = '500 MPa' # TODO: unit-aware type + yield_force_fy: str = '157 kN' # TODO: unit-aware type + rebar_slip_at_fy: str = '1 mm' # TODO: unit-aware type + ultimate_stress_fu: str = '675 MPa' # TODO: unit-aware type + ultimate_force_fu: str = '212 kN' # TODO: unit-aware type + rebar_slip_at_the_loaded_end_at_the_bar_fracture_strength: str = '2 mm' # TODO: unit-aware type initial_hardening: float = 0.4 pinching_factor_for_the_cyclic_slip_vs_bar_response: float = 0.5 - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) + diff --git a/src/otko/core/catalog/generated/concrete01_zero_tensile_strength.py b/src/otko/core/catalog/generated/concrete01_zero_tensile_strength.py index 7316257..9e51c2b 100644 --- a/src/otko/core/catalog/generated/concrete01_zero_tensile_strength.py +++ b/src/otko/core/catalog/generated/concrete01_zero_tensile_strength.py @@ -23,176 +23,176 @@ class CyclicDataRow(BaseModel): class Concrete01ZeroTensileStrengthSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Concrete01_(Zero_tensile_strength)", - "x-book": "Uniaxial_Concrete_Materials", - "dependencies": [ + 'x-gid-name': 'Concrete01_(Zero_tensile_strength)', + 'x-book': 'Uniaxial_Concrete_Materials', + 'dependencies': [ { - "field": "strength_class", - "gid_name": "Strength_class", - "rules": [ + 'field': 'strength_class', + 'gid_name': 'Strength_class', + 'rules': [ { - "trigger": "C12/15", - "actions": [ + 'trigger': 'C12/15', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C16/20", - "actions": [ + 'trigger': 'C16/20', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C20/25", - "actions": [ + 'trigger': 'C20/25', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C25/30", - "actions": [ + 'trigger': 'C25/30', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C30/37", - "actions": [ + 'trigger': 'C30/37', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C35/45", - "actions": [ + 'trigger': 'C35/45', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C40/50", - "actions": [ + 'trigger': 'C40/50', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C45/55", - "actions": [ + 'trigger': 'C45/55', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C50/60", - "actions": [ + 'trigger': 'C50/60', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C60/75", - "actions": [ + 'trigger': 'C60/75', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C70/85", - "actions": [ + 'trigger': 'C70/85', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C80/95", - "actions": [ + 'trigger': 'C80/95', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C90/105", - "actions": [ + 'trigger': 'C90/105', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Custom", - "actions": [ + 'trigger': 'Custom', + 'actions': [ { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -202,36 +202,15 @@ class Concrete01ZeroTensileStrengthSpec(BaseModel): } ) - material: Literal["Concrete01"] = Field( - "Concrete01", - description="uniaxial Kent-Scott-Park concrete material object with degraded linear unloading/reloading stiffness according to the work of Karsan-Jirsa and no tensile strength", - ) - strength_class: Literal[ - "C12/15", - "C16/20", - "C20/25", - "C25/30", - "C30/37", - "C35/45", - "C40/50", - "C45/55", - "C50/60", - "C55/67", - "C60/75", - "C70/85", - "C80/95", - "C90/105", - "Custom", - ] = Field( - "Custom", - description="Choose concrete strength category, to take some standard values in the following fields.", - ) - strength_type: Literal["Mean", "Characteristic"] = "Characteristic" - compressive_strength_fpc: str = "-20 MPa" # TODO: unit-aware type + material: Literal['Concrete01'] = Field('Concrete01', description='uniaxial Kent-Scott-Park concrete material object with degraded linear unloading/reloading stiffness according to the work of Karsan-Jirsa and no tensile strength') + strength_class: Literal['C12/15', 'C16/20', 'C20/25', 'C25/30', 'C30/37', 'C35/45', 'C40/50', 'C45/55', 'C50/60', 'C55/67', 'C60/75', 'C70/85', 'C80/95', 'C90/105', 'Custom'] = Field('Custom', description='Choose concrete strength category, to take some standard values in the following fields.') + strength_type: Literal['Mean', 'Characteristic'] = 'Characteristic' + compressive_strength_fpc: str = '-20 MPa' # TODO: unit-aware type strain_at_maximum_strength_epsc0: float = -2.0e-3 - crushing_strength_fpcu: str = "-17.0 MPa" # TODO: unit-aware type + crushing_strength_fpcu: str = '-17.0 MPa' # TODO: unit-aware type strain_at_crushing_strength_epscu: float = -3.5e-3 - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/concrete02_linear_tension_softening.py b/src/otko/core/catalog/generated/concrete02_linear_tension_softening.py index 13c9c60..cf44214 100644 --- a/src/otko/core/catalog/generated/concrete02_linear_tension_softening.py +++ b/src/otko/core/catalog/generated/concrete02_linear_tension_softening.py @@ -23,176 +23,176 @@ class CyclicDataRow(BaseModel): class Concrete02LinearTensionSofteningSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Concrete02_(Linear_tension_softening)", - "x-book": "Uniaxial_Concrete_Materials", - "dependencies": [ + 'x-gid-name': 'Concrete02_(Linear_tension_softening)', + 'x-book': 'Uniaxial_Concrete_Materials', + 'dependencies': [ { - "field": "strength_class", - "gid_name": "Strength_class", - "rules": [ + 'field': 'strength_class', + 'gid_name': 'Strength_class', + 'rules': [ { - "trigger": "C12/15", - "actions": [ + 'trigger': 'C12/15', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C16/20", - "actions": [ + 'trigger': 'C16/20', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C20/25", - "actions": [ + 'trigger': 'C20/25', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C25/30", - "actions": [ + 'trigger': 'C25/30', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C30/37", - "actions": [ + 'trigger': 'C30/37', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C35/45", - "actions": [ + 'trigger': 'C35/45', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C40/50", - "actions": [ + 'trigger': 'C40/50', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C45/55", - "actions": [ + 'trigger': 'C45/55', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C50/60", - "actions": [ + 'trigger': 'C50/60', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C60/75", - "actions": [ + 'trigger': 'C60/75', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C70/85", - "actions": [ + 'trigger': 'C70/85', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C80/95", - "actions": [ + 'trigger': 'C80/95', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C90/105", - "actions": [ + 'trigger': 'C90/105', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Custom", - "actions": [ + 'trigger': 'Custom', + 'actions': [ { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -202,36 +202,18 @@ class Concrete02LinearTensionSofteningSpec(BaseModel): } ) - material: Literal["Concrete02"] = "Concrete02" - strength_class: Literal[ - "C12/15", - "C16/20", - "C20/25", - "C25/30", - "C30/37", - "C35/45", - "C40/50", - "C45/55", - "C50/60", - "C55/67", - "C60/75", - "C70/85", - "C80/95", - "C90/105", - "Custom", - ] = Field( - "Custom", - description="Choose concrete strength category, to generate fast properties in the following fields.", - ) - strength_type: Literal["Mean", "Characteristic"] = "Characteristic" - compressive_strength_fpc: str = "-20 MPa" # TODO: unit-aware type + material: Literal['Concrete02'] = 'Concrete02' + strength_class: Literal['C12/15', 'C16/20', 'C20/25', 'C25/30', 'C30/37', 'C35/45', 'C40/50', 'C45/55', 'C50/60', 'C55/67', 'C60/75', 'C70/85', 'C80/95', 'C90/105', 'Custom'] = Field('Custom', description='Choose concrete strength category, to generate fast properties in the following fields.') + strength_type: Literal['Mean', 'Characteristic'] = 'Characteristic' + compressive_strength_fpc: str = '-20 MPa' # TODO: unit-aware type strain_at_maximum_strength_epsc0: float = -2.0e-3 - crushing_strength_fpcu: str = "-17.0 MPa" # TODO: unit-aware type + crushing_strength_fpcu: str = '-17.0 MPa' # TODO: unit-aware type strain_at_crushing_strength_epscu: float = -3.5e-3 ratio_between_unloading_slope_at_epscu_and_initial_slope_lambda: float = 0.5 - tensile_strength_ft: str = "2.2 MPa" # TODO: unit-aware type - tension_softening_stiffness_ets: str = "2 GPa" # TODO: unit-aware type - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + tensile_strength_ft: str = '2.2 MPa' # TODO: unit-aware type + tension_softening_stiffness_ets: str = '2 GPa' # TODO: unit-aware type + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/concrete04_popovics_concrete.py b/src/otko/core/catalog/generated/concrete04_popovics_concrete.py index f4df18b..061e782 100644 --- a/src/otko/core/catalog/generated/concrete04_popovics_concrete.py +++ b/src/otko/core/catalog/generated/concrete04_popovics_concrete.py @@ -23,176 +23,176 @@ class CyclicDataRow(BaseModel): class Concrete04PopovicsConcreteSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Concrete04_(Popovics_concrete)", - "x-book": "Uniaxial_Concrete_Materials", - "dependencies": [ + 'x-gid-name': 'Concrete04_(Popovics_concrete)', + 'x-book': 'Uniaxial_Concrete_Materials', + 'dependencies': [ { - "field": "strength_class", - "gid_name": "Strength_class", - "rules": [ + 'field': 'strength_class', + 'gid_name': 'Strength_class', + 'rules': [ { - "trigger": "C12/15", - "actions": [ + 'trigger': 'C12/15', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C16/20", - "actions": [ + 'trigger': 'C16/20', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C20/25", - "actions": [ + 'trigger': 'C20/25', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C25/30", - "actions": [ + 'trigger': 'C25/30', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C30/37", - "actions": [ + 'trigger': 'C30/37', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C35/45", - "actions": [ + 'trigger': 'C35/45', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C40/50", - "actions": [ + 'trigger': 'C40/50', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C45/55", - "actions": [ + 'trigger': 'C45/55', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C50/60", - "actions": [ + 'trigger': 'C50/60', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C60/75", - "actions": [ + 'trigger': 'C60/75', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C70/85", - "actions": [ + 'trigger': 'C70/85', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C80/95", - "actions": [ + 'trigger': 'C80/95', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C90/105", - "actions": [ + 'trigger': 'C90/105', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Custom", - "actions": [ + 'trigger': 'Custom', + 'actions': [ { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -202,35 +202,17 @@ class Concrete04PopovicsConcreteSpec(BaseModel): } ) - material: Literal["Concrete04"] = "Concrete04" - strength_class: Literal[ - "C12/15", - "C16/20", - "C20/25", - "C25/30", - "C30/37", - "C35/45", - "C40/50", - "C45/55", - "C50/60", - "C55/67", - "C60/75", - "C70/85", - "C80/95", - "C90/105", - "Custom", - ] = Field( - "Custom", - description="Choose concrete strength category, to generate fast properties in the following fields.", - ) - strength_type: Literal["Mean", "Characteristic"] = "Characteristic" - compressive_strength: str = "-20 MPa" # TODO: unit-aware type + material: Literal['Concrete04'] = 'Concrete04' + strength_class: Literal['C12/15', 'C16/20', 'C20/25', 'C25/30', 'C30/37', 'C35/45', 'C40/50', 'C45/55', 'C50/60', 'C55/67', 'C60/75', 'C70/85', 'C80/95', 'C90/105', 'Custom'] = Field('Custom', description='Choose concrete strength category, to generate fast properties in the following fields.') + strength_type: Literal['Mean', 'Characteristic'] = 'Characteristic' + compressive_strength: str = '-20 MPa' # TODO: unit-aware type strain_at_maximum_strength: float = -2.0e-3 strain_at_crushing_strength: float = -3.5e-3 - initial_stiffness: str = "30 GPa" # TODO: unit-aware type - maximum_tensile_strength: str = "2.2 MPa" # TODO: unit-aware type + initial_stiffness: str = '30 GPa' # TODO: unit-aware type + maximum_tensile_strength: str = '2.2 MPa' # TODO: unit-aware type ultimate_tensile_strain: float = 1e-3 - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/concrete06.py b/src/otko/core/catalog/generated/concrete06.py index 771a25b..4fce8b6 100644 --- a/src/otko/core/catalog/generated/concrete06.py +++ b/src/otko/core/catalog/generated/concrete06.py @@ -23,176 +23,176 @@ class CyclicDataRow(BaseModel): class Concrete06Spec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Concrete06", - "x-book": "Uniaxial_Concrete_Materials", - "dependencies": [ + 'x-gid-name': 'Concrete06', + 'x-book': 'Uniaxial_Concrete_Materials', + 'dependencies': [ { - "field": "strength_class", - "gid_name": "Strength_class", - "rules": [ + 'field': 'strength_class', + 'gid_name': 'Strength_class', + 'rules': [ { - "trigger": "C12/15", - "actions": [ + 'trigger': 'C12/15', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C16/20", - "actions": [ + 'trigger': 'C16/20', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C20/25", - "actions": [ + 'trigger': 'C20/25', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C25/30", - "actions": [ + 'trigger': 'C25/30', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C30/37", - "actions": [ + 'trigger': 'C30/37', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C35/45", - "actions": [ + 'trigger': 'C35/45', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C40/50", - "actions": [ + 'trigger': 'C40/50', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C45/55", - "actions": [ + 'trigger': 'C45/55', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C50/60", - "actions": [ + 'trigger': 'C50/60', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C60/75", - "actions": [ + 'trigger': 'C60/75', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C70/85", - "actions": [ + 'trigger': 'C70/85', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C80/95", - "actions": [ + 'trigger': 'C80/95', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C90/105", - "actions": [ + 'trigger': 'C90/105', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Custom", - "actions": [ + 'trigger': 'Custom', + 'actions': [ { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -202,38 +202,20 @@ class Concrete06Spec(BaseModel): } ) - material: Literal["Concrete06"] = "Concrete06" - strength_class: Literal[ - "C12/15", - "C16/20", - "C20/25", - "C25/30", - "C30/37", - "C35/45", - "C40/50", - "C45/55", - "C50/60", - "C55/67", - "C60/75", - "C70/85", - "C80/95", - "C90/105", - "Custom", - ] = Field( - "Custom", - description="Choose concrete strength category, to generate fast properties in the following fields.", - ) - strength_type: Literal["Mean", "Characteristic"] = "Characteristic" - concrete_compressive_strength_fc: str = "-20 MPa" # TODO: unit-aware type + material: Literal['Concrete06'] = 'Concrete06' + strength_class: Literal['C12/15', 'C16/20', 'C20/25', 'C25/30', 'C30/37', 'C35/45', 'C40/50', 'C45/55', 'C50/60', 'C55/67', 'C60/75', 'C70/85', 'C80/95', 'C90/105', 'Custom'] = Field('Custom', description='Choose concrete strength category, to generate fast properties in the following fields.') + strength_type: Literal['Mean', 'Characteristic'] = 'Characteristic' + concrete_compressive_strength_fc: str = '-20 MPa' # TODO: unit-aware type strain_at_compressive_strength_e0: float = -2.0e-3 compressive_shape_factor_n: int = 1 post_peak_compressive_shape_factor_k: int = 1 parameter_a1_for_compressive_plastic_strain_definition: float = -2.500e-003 - tensile_strength_fcr: str = "2.2 MPa" # TODO: unit-aware type + tensile_strength_fcr: str = '2.2 MPa' # TODO: unit-aware type tensile_strain_at_peak_stress_ecr: float = 1.100e-004 exponent_of_the_tension_stiffering_curve_b: int = 1 parameter_a2_for_tensile_plastic_strain_definition: float = 7.700e-004 - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/concretecm.py b/src/otko/core/catalog/generated/concretecm.py index 224cecf..a8a3593 100644 --- a/src/otko/core/catalog/generated/concretecm.py +++ b/src/otko/core/catalog/generated/concretecm.py @@ -23,202 +23,202 @@ class CyclicDataRow(BaseModel): class ConcretecmSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "ConcreteCM", - "x-book": "Uniaxial_Concrete_Materials", - "dependencies": [ + 'x-gid-name': 'ConcreteCM', + 'x-book': 'Uniaxial_Concrete_Materials', + 'dependencies': [ { - "field": "strength_class", - "gid_name": "Strength_class", - "rules": [ + 'field': 'strength_class', + 'gid_name': 'Strength_class', + 'rules': [ { - "trigger": "C12/15", - "actions": [ + 'trigger': 'C12/15', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C16/20", - "actions": [ + 'trigger': 'C16/20', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C20/25", - "actions": [ + 'trigger': 'C20/25', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C25/30", - "actions": [ + 'trigger': 'C25/30', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C30/37", - "actions": [ + 'trigger': 'C30/37', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C35/45", - "actions": [ + 'trigger': 'C35/45', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C40/50", - "actions": [ + 'trigger': 'C40/50', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C45/55", - "actions": [ + 'trigger': 'C45/55', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C50/60", - "actions": [ + 'trigger': 'C50/60', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C60/75", - "actions": [ + 'trigger': 'C60/75', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C70/85", - "actions": [ + 'trigger': 'C70/85', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C80/95", - "actions": [ + 'trigger': 'C80/95', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "C90/105", - "actions": [ + 'trigger': 'C90/105', + 'actions': [ { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Custom", - "actions": [ + 'trigger': 'Custom', + 'actions': [ { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "consider_gap_closure", - "gid_name": "Consider_gap_closure", - "rules": [ + 'field': 'consider_gap_closure', + 'gid_name': 'Consider_gap_closure', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Gap_parameter", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Gap_parameter', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Gap_parameter", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Gap_parameter', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -228,37 +228,19 @@ class ConcretecmSpec(BaseModel): } ) - material: Literal["ConcreteCM"] = "ConcreteCM" - strength_class: Literal[ - "C12/15", - "C16/20", - "C20/25", - "C25/30", - "C30/37", - "C35/45", - "C40/50", - "C45/55", - "C50/60", - "C55/67", - "C60/75", - "C70/85", - "C80/95", - "C90/105", - "Custom", - ] = Field( - "Custom", - description="Choose concrete strength category, to generate fast properties in the following fields.", - ) - strength_type: Literal["Mean", "Characteristic"] = "Characteristic" - concrete_compressive_strength_fpcc: str = "-20 MPa" # TODO: unit-aware type + material: Literal['ConcreteCM'] = 'ConcreteCM' + strength_class: Literal['C12/15', 'C16/20', 'C20/25', 'C25/30', 'C30/37', 'C35/45', 'C40/50', 'C45/55', 'C50/60', 'C55/67', 'C60/75', 'C70/85', 'C80/95', 'C90/105', 'Custom'] = Field('Custom', description='Choose concrete strength category, to generate fast properties in the following fields.') + strength_type: Literal['Mean', 'Characteristic'] = 'Characteristic' + concrete_compressive_strength_fpcc: str = '-20 MPa' # TODO: unit-aware type strain_at_compressive_strength_epcc: float = -2.0e-3 - initial_tangent_modulus_ec: str = "30 GPa" # TODO: unit-aware type + initial_tangent_modulus_ec: str = '30 GPa' # TODO: unit-aware type shape_parameter_rc_in_tsai_s_equation_for_compression: int = 7 critical_strain_on_compression_envelope: float = 1.035 - tensile_strength_ft: str = "2.2 MPa" # TODO: unit-aware type + tensile_strength_ft: str = '2.2 MPa' # TODO: unit-aware type strain_at_tensile_strength_et: float = 1.100e-004 shape_parameter_rc_in_tsai_s_equation_for_tension: float = 1.2 critical_strain_on_tension_envelope: int = 10000 - consider_gap_closure: Literal["1", "0"] = "0" - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + consider_gap_closure: Literal['1', '0'] = '0' + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) + diff --git a/src/otko/core/catalog/generated/conditions/__init__.py b/src/otko/core/catalog/generated/conditions/__init__.py index fd52e19..3f4328b 100644 --- a/src/otko/core/catalog/generated/conditions/__init__.py +++ b/src/otko/core/catalog/generated/conditions/__init__.py @@ -15,173 +15,42 @@ from otko.core.catalog.generated.conditions.line_uniform_forces import LineUnifo from otko.core.catalog.generated.conditions.point_displacements import PointDisplacementsSpec from otko.core.catalog.generated.conditions.line_displacements import LineDisplacementsSpec from otko.core.catalog.generated.conditions.surface_displacements import SurfaceDisplacementsSpec -from otko.core.catalog.generated.conditions.point_ground_motion_from_record import ( - PointGroundMotionFromRecordSpec, -) -from otko.core.catalog.generated.conditions.point_sine_ground_motion import ( - PointSineGroundMotionSpec, -) -from otko.core.catalog.generated.conditions.point_equal_constraint_master_node import ( - PointEqualConstraintMasterNodeSpec, -) -from otko.core.catalog.generated.conditions.point_equal_constraint_slave_nodes import ( - PointEqualConstraintSlaveNodesSpec, -) -from otko.core.catalog.generated.conditions.line_equal_constraint_slave_nodes import ( - LineEqualConstraintSlaveNodesSpec, -) -from otko.core.catalog.generated.conditions.point_rigid_link_master_node import ( - PointRigidLinkMasterNodeSpec, -) -from otko.core.catalog.generated.conditions.point_rigid_link_slave_nodes import ( - PointRigidLinkSlaveNodesSpec, -) -from otko.core.catalog.generated.conditions.line_rigid_link_slave_nodes import ( - LineRigidLinkSlaveNodesSpec, -) -from otko.core.catalog.generated.conditions.point_rigid_diaphragm_master_node import ( - PointRigidDiaphragmMasterNodeSpec, -) -from otko.core.catalog.generated.conditions.point_rigid_diaphragm_slave_nodes import ( - PointRigidDiaphragmSlaveNodesSpec, -) -from otko.core.catalog.generated.conditions.line_rigid_diaphragm_slave_nodes import ( - LineRigidDiaphragmSlaveNodesSpec, -) +from otko.core.catalog.generated.conditions.point_ground_motion_from_record import PointGroundMotionFromRecordSpec +from otko.core.catalog.generated.conditions.point_sine_ground_motion import PointSineGroundMotionSpec +from otko.core.catalog.generated.conditions.point_equal_constraint_master_node import PointEqualConstraintMasterNodeSpec +from otko.core.catalog.generated.conditions.point_equal_constraint_slave_nodes import PointEqualConstraintSlaveNodesSpec +from otko.core.catalog.generated.conditions.line_equal_constraint_slave_nodes import LineEqualConstraintSlaveNodesSpec +from otko.core.catalog.generated.conditions.point_rigid_link_master_node import PointRigidLinkMasterNodeSpec +from otko.core.catalog.generated.conditions.point_rigid_link_slave_nodes import PointRigidLinkSlaveNodesSpec +from otko.core.catalog.generated.conditions.line_rigid_link_slave_nodes import LineRigidLinkSlaveNodesSpec +from otko.core.catalog.generated.conditions.point_rigid_diaphragm_master_node import PointRigidDiaphragmMasterNodeSpec +from otko.core.catalog.generated.conditions.point_rigid_diaphragm_slave_nodes import PointRigidDiaphragmSlaveNodesSpec +from otko.core.catalog.generated.conditions.line_rigid_diaphragm_slave_nodes import LineRigidDiaphragmSlaveNodesSpec from otko.core.catalog.generated.conditions.point_mass import PointMassSpec from otko.core.catalog.generated.conditions.line_mass import LineMassSpec from otko.core.catalog.generated.conditions.surface_mass import SurfaceMassSpec from otko.core.catalog.generated.conditions.volume_mass import VolumeMassSpec -from otko.core.catalog.generated.conditions.line_rayleigh_damping_elements import ( - LineRayleighDampingElementsSpec, -) -from otko.core.catalog.generated.conditions.surface_rayleigh_damping_elements import ( - SurfaceRayleighDampingElementsSpec, -) -from otko.core.catalog.generated.conditions.volume_rayleigh_damping_elements import ( - VolumeRayleighDampingElementsSpec, -) -from otko.core.catalog.generated.conditions.point_rayleigh_damping_nodes import ( - PointRayleighDampingNodesSpec, -) -from otko.core.catalog.generated.conditions.line_rayleigh_damping_nodes import ( - LineRayleighDampingNodesSpec, -) -from otko.core.catalog.generated.conditions.surface_rayleigh_damping_nodes import ( - SurfaceRayleighDampingNodesSpec, -) -from otko.core.catalog.generated.conditions.volume_rayleigh_damping_nodes import ( - VolumeRayleighDampingNodesSpec, -) +from otko.core.catalog.generated.conditions.line_rayleigh_damping_elements import LineRayleighDampingElementsSpec +from otko.core.catalog.generated.conditions.surface_rayleigh_damping_elements import SurfaceRayleighDampingElementsSpec +from otko.core.catalog.generated.conditions.volume_rayleigh_damping_elements import VolumeRayleighDampingElementsSpec +from otko.core.catalog.generated.conditions.point_rayleigh_damping_nodes import PointRayleighDampingNodesSpec +from otko.core.catalog.generated.conditions.line_rayleigh_damping_nodes import LineRayleighDampingNodesSpec +from otko.core.catalog.generated.conditions.surface_rayleigh_damping_nodes import SurfaceRayleighDampingNodesSpec +from otko.core.catalog.generated.conditions.volume_rayleigh_damping_nodes import VolumeRayleighDampingNodesSpec from otko.core.catalog.generated.conditions.point_zerolength import PointZerolengthSpec -from otko.core.catalog.generated.conditions.line_beam_contact_master_end_nodes import ( - LineBeamContactMasterEndNodesSpec, -) -from otko.core.catalog.generated.conditions.point_beam_contact_slave_nodes import ( - PointBeamContactSlaveNodesSpec, -) -from otko.core.catalog.generated.conditions.point_body_constraint_master_node import ( - PointBodyConstraintMasterNodeSpec, -) -from otko.core.catalog.generated.conditions.point_body_constraint_slave_nodes import ( - PointBodyConstraintSlaveNodesSpec, -) -from otko.core.catalog.generated.conditions.line_body_constraint_slave_nodes import ( - LineBodyConstraintSlaveNodesSpec, -) +from otko.core.catalog.generated.conditions.line_beam_contact_master_end_nodes import LineBeamContactMasterEndNodesSpec +from otko.core.catalog.generated.conditions.point_beam_contact_slave_nodes import PointBeamContactSlaveNodesSpec +from otko.core.catalog.generated.conditions.point_body_constraint_master_node import PointBodyConstraintMasterNodeSpec +from otko.core.catalog.generated.conditions.point_body_constraint_slave_nodes import PointBodyConstraintSlaveNodesSpec +from otko.core.catalog.generated.conditions.line_body_constraint_slave_nodes import LineBodyConstraintSlaveNodesSpec from otko.core.catalog.generated.conditions.zerolength import ZerolengthSpec Restraints = Union[PointRestraintsSpec, LineRestraintsSpec, SurfaceRestraintsSpec] -Loads = Union[ - PointForcesSpec, - LineForcesSpec, - SurfaceForcesSpec, - LineUniformForcesSpec, - PointDisplacementsSpec, - LineDisplacementsSpec, - SurfaceDisplacementsSpec, - PointGroundMotionFromRecordSpec, - PointSineGroundMotionSpec, -] -Constraints = Union[ - PointEqualConstraintMasterNodeSpec, - PointEqualConstraintSlaveNodesSpec, - LineEqualConstraintSlaveNodesSpec, - PointRigidLinkMasterNodeSpec, - PointRigidLinkSlaveNodesSpec, - LineRigidLinkSlaveNodesSpec, - PointRigidDiaphragmMasterNodeSpec, - PointRigidDiaphragmSlaveNodesSpec, - LineRigidDiaphragmSlaveNodesSpec, -] -MassDamping = Union[ - PointMassSpec, - LineMassSpec, - SurfaceMassSpec, - VolumeMassSpec, - LineRayleighDampingElementsSpec, - SurfaceRayleighDampingElementsSpec, - VolumeRayleighDampingElementsSpec, - PointRayleighDampingNodesSpec, - LineRayleighDampingNodesSpec, - SurfaceRayleighDampingNodesSpec, - VolumeRayleighDampingNodesSpec, -] -ZerolengthElements = Union[ - PointZerolengthSpec, LineBeamContactMasterEndNodesSpec, PointBeamContactSlaveNodesSpec -] -OldConditions = Union[ - PointBodyConstraintMasterNodeSpec, - PointBodyConstraintSlaveNodesSpec, - LineBodyConstraintSlaveNodesSpec, - ZerolengthSpec, -] +Loads = Union[PointForcesSpec, LineForcesSpec, SurfaceForcesSpec, LineUniformForcesSpec, PointDisplacementsSpec, LineDisplacementsSpec, SurfaceDisplacementsSpec, PointGroundMotionFromRecordSpec, PointSineGroundMotionSpec] +Constraints = Union[PointEqualConstraintMasterNodeSpec, PointEqualConstraintSlaveNodesSpec, LineEqualConstraintSlaveNodesSpec, PointRigidLinkMasterNodeSpec, PointRigidLinkSlaveNodesSpec, LineRigidLinkSlaveNodesSpec, PointRigidDiaphragmMasterNodeSpec, PointRigidDiaphragmSlaveNodesSpec, LineRigidDiaphragmSlaveNodesSpec] +MassDamping = Union[PointMassSpec, LineMassSpec, SurfaceMassSpec, VolumeMassSpec, LineRayleighDampingElementsSpec, SurfaceRayleighDampingElementsSpec, VolumeRayleighDampingElementsSpec, PointRayleighDampingNodesSpec, LineRayleighDampingNodesSpec, SurfaceRayleighDampingNodesSpec, VolumeRayleighDampingNodesSpec] +ZerolengthElements = Union[PointZerolengthSpec, LineBeamContactMasterEndNodesSpec, PointBeamContactSlaveNodesSpec] +OldConditions = Union[PointBodyConstraintMasterNodeSpec, PointBodyConstraintSlaveNodesSpec, LineBodyConstraintSlaveNodesSpec, ZerolengthSpec] -__all__ = [ - "PointRestraintsSpec", - "LineRestraintsSpec", - "SurfaceRestraintsSpec", - "PointForcesSpec", - "LineForcesSpec", - "SurfaceForcesSpec", - "LineUniformForcesSpec", - "PointDisplacementsSpec", - "LineDisplacementsSpec", - "SurfaceDisplacementsSpec", - "PointGroundMotionFromRecordSpec", - "PointSineGroundMotionSpec", - "PointEqualConstraintMasterNodeSpec", - "PointEqualConstraintSlaveNodesSpec", - "LineEqualConstraintSlaveNodesSpec", - "PointRigidLinkMasterNodeSpec", - "PointRigidLinkSlaveNodesSpec", - "LineRigidLinkSlaveNodesSpec", - "PointRigidDiaphragmMasterNodeSpec", - "PointRigidDiaphragmSlaveNodesSpec", - "LineRigidDiaphragmSlaveNodesSpec", - "PointMassSpec", - "LineMassSpec", - "SurfaceMassSpec", - "VolumeMassSpec", - "LineRayleighDampingElementsSpec", - "SurfaceRayleighDampingElementsSpec", - "VolumeRayleighDampingElementsSpec", - "PointRayleighDampingNodesSpec", - "LineRayleighDampingNodesSpec", - "SurfaceRayleighDampingNodesSpec", - "VolumeRayleighDampingNodesSpec", - "PointZerolengthSpec", - "LineBeamContactMasterEndNodesSpec", - "PointBeamContactSlaveNodesSpec", - "PointBodyConstraintMasterNodeSpec", - "PointBodyConstraintSlaveNodesSpec", - "LineBodyConstraintSlaveNodesSpec", - "ZerolengthSpec", - "Restraints", - "Loads", - "Constraints", - "MassDamping", - "ZerolengthElements", - "OldConditions", -] +__all__ = ['PointRestraintsSpec', 'LineRestraintsSpec', 'SurfaceRestraintsSpec', 'PointForcesSpec', 'LineForcesSpec', 'SurfaceForcesSpec', 'LineUniformForcesSpec', 'PointDisplacementsSpec', 'LineDisplacementsSpec', 'SurfaceDisplacementsSpec', 'PointGroundMotionFromRecordSpec', 'PointSineGroundMotionSpec', 'PointEqualConstraintMasterNodeSpec', 'PointEqualConstraintSlaveNodesSpec', 'LineEqualConstraintSlaveNodesSpec', 'PointRigidLinkMasterNodeSpec', 'PointRigidLinkSlaveNodesSpec', 'LineRigidLinkSlaveNodesSpec', 'PointRigidDiaphragmMasterNodeSpec', 'PointRigidDiaphragmSlaveNodesSpec', 'LineRigidDiaphragmSlaveNodesSpec', 'PointMassSpec', 'LineMassSpec', 'SurfaceMassSpec', 'VolumeMassSpec', 'LineRayleighDampingElementsSpec', 'SurfaceRayleighDampingElementsSpec', 'VolumeRayleighDampingElementsSpec', 'PointRayleighDampingNodesSpec', 'LineRayleighDampingNodesSpec', 'SurfaceRayleighDampingNodesSpec', 'VolumeRayleighDampingNodesSpec', 'PointZerolengthSpec', 'LineBeamContactMasterEndNodesSpec', 'PointBeamContactSlaveNodesSpec', 'PointBodyConstraintMasterNodeSpec', 'PointBodyConstraintSlaveNodesSpec', 'LineBodyConstraintSlaveNodesSpec', 'ZerolengthSpec', 'Restraints', 'Loads', 'Constraints', 'MassDamping', 'ZerolengthElements', 'OldConditions'] diff --git a/src/otko/core/catalog/generated/conditions/line_beam_contact_master_end_nodes.py b/src/otko/core/catalog/generated/conditions/line_beam_contact_master_end_nodes.py index 3c063d7..e1c021d 100644 --- a/src/otko/core/catalog/generated/conditions/line_beam_contact_master_end_nodes.py +++ b/src/otko/core/catalog/generated/conditions/line_beam_contact_master_end_nodes.py @@ -15,9 +15,10 @@ from pydantic import BaseModel, ConfigDict, Field class LineBeamContactMasterEndNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Beam_contact_master_end_nodes", - "x-book": "ZeroLength_Elements", + 'x-gid-name': 'Line_Beam_contact_master_end_nodes', + 'x-book': 'ZeroLength_Elements', } ) beam_contact_id: int = 1 + diff --git a/src/otko/core/catalog/generated/conditions/line_body_constraint_slave_nodes.py b/src/otko/core/catalog/generated/conditions/line_body_constraint_slave_nodes.py index 16131a0..250dbec 100644 --- a/src/otko/core/catalog/generated/conditions/line_body_constraint_slave_nodes.py +++ b/src/otko/core/catalog/generated/conditions/line_body_constraint_slave_nodes.py @@ -17,80 +17,80 @@ from pydantic import BaseModel, ConfigDict, Field class LineBodyConstraintSlaveNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Body_constraint_slave_nodes", - "x-book": "Old_Conditions", - "dependencies": [ + 'x-gid-name': 'Line_Body_constraint_slave_nodes', + 'x-book': 'Old_Conditions', + 'dependencies': [ { - "field": "fast_constraints", - "gid_name": "Fast_constraints", - "rules": [ + 'field': 'fast_constraints', + 'gid_name': 'Fast_constraints', + 'rules': [ { - "trigger": "Pinned_Connection", - "actions": [ + 'trigger': 'Pinned_Connection', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Fixed_Connection", - "actions": [ + 'trigger': 'Fixed_Connection', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '1', }, ], }, @@ -100,13 +100,12 @@ class LineBodyConstraintSlaveNodesSpec(BaseModel): } ) - line_constraint_id_number: int = Field( - 1, description="Enter a positive integer number for the body constraint." - ) - x_translation: Literal["1", "0"] = "1" - y_translation: Literal["1", "0"] = "1" - z_translation: Literal["1", "0"] = "1" - x_rotation: Literal["1", "0"] = "1" - y_rotation: Literal["1", "0"] = "1" - z_rotation: Literal["1", "0"] = "1" - fast_constraints: Literal["Pinned_Connection", "Fixed_Connection"] = "Pinned_Connection" + line_constraint_id_number: int = Field(1, description='Enter a positive integer number for the body constraint.') + x_translation: Literal['1', '0'] = '1' + y_translation: Literal['1', '0'] = '1' + z_translation: Literal['1', '0'] = '1' + x_rotation: Literal['1', '0'] = '1' + y_rotation: Literal['1', '0'] = '1' + z_rotation: Literal['1', '0'] = '1' + fast_constraints: Literal['Pinned_Connection', 'Fixed_Connection'] = 'Pinned_Connection' + diff --git a/src/otko/core/catalog/generated/conditions/line_displacements.py b/src/otko/core/catalog/generated/conditions/line_displacements.py index 140d8f2..d00e49a 100644 --- a/src/otko/core/catalog/generated/conditions/line_displacements.py +++ b/src/otko/core/catalog/generated/conditions/line_displacements.py @@ -15,14 +15,15 @@ from pydantic import BaseModel, ConfigDict, Field class LineDisplacementsSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Displacements", - "x-book": "Loads", + 'x-gid-name': 'Line_Displacements', + 'x-book': 'Loads', } ) - x_displacement: str = "0.0m" # TODO: unit-aware type - y_displacement: str = "0.0m" # TODO: unit-aware type - z_displacement: str = "0.0m" # TODO: unit-aware type - x_rotation: str = "0.0 rad" # TODO: unit-aware type - y_rotation: str = "0.0 rad" # TODO: unit-aware type - z_rotation: str = "0.0 rad" # TODO: unit-aware type + x_displacement: str = '0.0m' # TODO: unit-aware type + y_displacement: str = '0.0m' # TODO: unit-aware type + z_displacement: str = '0.0m' # TODO: unit-aware type + x_rotation: str = '0.0 rad' # TODO: unit-aware type + y_rotation: str = '0.0 rad' # TODO: unit-aware type + z_rotation: str = '0.0 rad' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/conditions/line_equal_constraint_slave_nodes.py b/src/otko/core/catalog/generated/conditions/line_equal_constraint_slave_nodes.py index 43d050a..cdfa971 100644 --- a/src/otko/core/catalog/generated/conditions/line_equal_constraint_slave_nodes.py +++ b/src/otko/core/catalog/generated/conditions/line_equal_constraint_slave_nodes.py @@ -17,17 +17,16 @@ from pydantic import BaseModel, ConfigDict, Field class LineEqualConstraintSlaveNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Equal_constraint_slave_nodes", - "x-book": "Constraints", + 'x-gid-name': 'Line_Equal_constraint_slave_nodes', + 'x-book': 'Constraints', } ) - equal_constraint_id: int = Field( - 1, description="Enter a positive integer number for the equal constraint." - ) - x_translation: Literal["1", "0"] = "1" - y_translation: Literal["1", "0"] = "1" - z_translation: Literal["1", "0"] = "1" - x_rotation: Literal["1", "0"] = "1" - y_rotation: Literal["1", "0"] = "1" - z_rotation: Literal["1", "0"] = "1" + equal_constraint_id: int = Field(1, description='Enter a positive integer number for the equal constraint.') + x_translation: Literal['1', '0'] = '1' + y_translation: Literal['1', '0'] = '1' + z_translation: Literal['1', '0'] = '1' + x_rotation: Literal['1', '0'] = '1' + y_rotation: Literal['1', '0'] = '1' + z_rotation: Literal['1', '0'] = '1' + diff --git a/src/otko/core/catalog/generated/conditions/line_forces.py b/src/otko/core/catalog/generated/conditions/line_forces.py index 69a5422..e03e48a 100644 --- a/src/otko/core/catalog/generated/conditions/line_forces.py +++ b/src/otko/core/catalog/generated/conditions/line_forces.py @@ -15,14 +15,15 @@ from pydantic import BaseModel, ConfigDict, Field class LineForcesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Forces", - "x-book": "Loads", + 'x-gid-name': 'Line_Forces', + 'x-book': 'Loads', } ) - x_force: str = "0.0 kN" # TODO: unit-aware type - y_force: str = "0.0 kN" # TODO: unit-aware type - z_force: str = "0.0 kN" # TODO: unit-aware type - x_moment: str = "0.0 kNm" # TODO: unit-aware type - y_moment: str = "0.0 kNm" # TODO: unit-aware type - z_moment: str = "0.0 kNm" # TODO: unit-aware type + x_force: str = '0.0 kN' # TODO: unit-aware type + y_force: str = '0.0 kN' # TODO: unit-aware type + z_force: str = '0.0 kN' # TODO: unit-aware type + x_moment: str = '0.0 kNm' # TODO: unit-aware type + y_moment: str = '0.0 kNm' # TODO: unit-aware type + z_moment: str = '0.0 kNm' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/conditions/line_mass.py b/src/otko/core/catalog/generated/conditions/line_mass.py index c4b57a2..891c038 100644 --- a/src/otko/core/catalog/generated/conditions/line_mass.py +++ b/src/otko/core/catalog/generated/conditions/line_mass.py @@ -17,15 +17,16 @@ from pydantic import BaseModel, ConfigDict, Field class LineMassSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Mass", - "x-book": "Mass/Damping", + 'x-gid-name': 'Line_Mass', + 'x-book': 'Mass/Damping', } ) - mass_dof_values: Literal[":"] = ":" - x_direction: str = "0.0ton" # TODO: unit-aware type - y_direction: str = "0.0ton" # TODO: unit-aware type - z_direction: str = "0.0ton" # TODO: unit-aware type - x_rotation: str = "0.0ton" # TODO: unit-aware type - y_rotation: str = "0.0ton" # TODO: unit-aware type - z_rotation: str = "0.0ton" # TODO: unit-aware type + mass_dof_values: Literal[':'] = ':' + x_direction: str = '0.0ton' # TODO: unit-aware type + y_direction: str = '0.0ton' # TODO: unit-aware type + z_direction: str = '0.0ton' # TODO: unit-aware type + x_rotation: str = '0.0ton' # TODO: unit-aware type + y_rotation: str = '0.0ton' # TODO: unit-aware type + z_rotation: str = '0.0ton' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/conditions/line_rayleigh_damping_elements.py b/src/otko/core/catalog/generated/conditions/line_rayleigh_damping_elements.py index dc8a0b1..5f24808 100644 --- a/src/otko/core/catalog/generated/conditions/line_rayleigh_damping_elements.py +++ b/src/otko/core/catalog/generated/conditions/line_rayleigh_damping_elements.py @@ -15,8 +15,8 @@ from pydantic import BaseModel, ConfigDict, Field class LineRayleighDampingElementsSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Rayleigh_Damping-Elements", - "x-book": "Mass/Damping", + 'x-gid-name': 'Line_Rayleigh_Damping-Elements', + 'x-book': 'Mass/Damping', } ) @@ -25,3 +25,4 @@ class LineRayleighDampingElementsSpec(BaseModel): betak: float = 0.0 betakinit: float = 0.0 betakcomm: float = 0.0 + diff --git a/src/otko/core/catalog/generated/conditions/line_rayleigh_damping_nodes.py b/src/otko/core/catalog/generated/conditions/line_rayleigh_damping_nodes.py index 57679ed..fbf062d 100644 --- a/src/otko/core/catalog/generated/conditions/line_rayleigh_damping_nodes.py +++ b/src/otko/core/catalog/generated/conditions/line_rayleigh_damping_nodes.py @@ -15,8 +15,8 @@ from pydantic import BaseModel, ConfigDict, Field class LineRayleighDampingNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Rayleigh_Damping-Nodes", - "x-book": "Mass/Damping", + 'x-gid-name': 'Line_Rayleigh_Damping-Nodes', + 'x-book': 'Mass/Damping', } ) @@ -25,3 +25,4 @@ class LineRayleighDampingNodesSpec(BaseModel): betak: float = 0.0 betakinit: float = 0.0 betakcomm: float = 0.0 + diff --git a/src/otko/core/catalog/generated/conditions/line_restraints.py b/src/otko/core/catalog/generated/conditions/line_restraints.py index 15ef27e..74455ee 100644 --- a/src/otko/core/catalog/generated/conditions/line_restraints.py +++ b/src/otko/core/catalog/generated/conditions/line_restraints.py @@ -17,290 +17,290 @@ from pydantic import BaseModel, ConfigDict, Field class LineRestraintsSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Restraints", - "x-book": "Restraints", - "dependencies": [ + 'x-gid-name': 'Line_Restraints', + 'x-book': 'Restraints', + 'dependencies': [ { - "field": "fast_restraints", - "gid_name": "Fast_restraints", - "rules": [ + 'field': 'fast_restraints', + 'gid_name': 'Fast_restraints', + 'rules': [ { - "trigger": "Fix_XYZ", - "actions": [ + 'trigger': 'Fix_XYZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '1', }, ], }, { - "trigger": "Fix_XY", - "actions": [ + 'trigger': 'Fix_XY', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, ], }, { - "trigger": "Fix_XZ", - "actions": [ + 'trigger': 'Fix_XZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Fix_YZ", - "actions": [ + 'trigger': 'Fix_YZ', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_XYZ", - "actions": [ + 'trigger': 'Pin_XYZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_XY", - "actions": [ + 'trigger': 'Pin_XY', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_XZ", - "actions": [ + 'trigger': 'Pin_XZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_YZ", - "actions": [ + 'trigger': 'Pin_YZ', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, @@ -310,13 +310,12 @@ class LineRestraintsSpec(BaseModel): } ) - x_translation: Literal["1", "0"] = "1" - y_translation: Literal["1", "0"] = "1" - z_translation: Literal["1", "0"] = "1" - x_rotation: Literal["1", "0"] = "1" - y_rotation: Literal["1", "0"] = "1" - z_rotation: Literal["1", "0"] = "1" - fluid_pressure: Literal["1", "0"] = "0" - fast_restraints: Literal[ - "Fix_XYZ", "Fix_XY", "Fix_XZ", "Fix_YZ", "Pin_XYZ", "Pin_XY", "Pin_XZ", "Pin_YZ" - ] = "Fix_XYZ" + x_translation: Literal['1', '0'] = '1' + y_translation: Literal['1', '0'] = '1' + z_translation: Literal['1', '0'] = '1' + x_rotation: Literal['1', '0'] = '1' + y_rotation: Literal['1', '0'] = '1' + z_rotation: Literal['1', '0'] = '1' + fluid_pressure: Literal['1', '0'] = '0' + fast_restraints: Literal['Fix_XYZ', 'Fix_XY', 'Fix_XZ', 'Fix_YZ', 'Pin_XYZ', 'Pin_XY', 'Pin_XZ', 'Pin_YZ'] = 'Fix_XYZ' + diff --git a/src/otko/core/catalog/generated/conditions/line_rigid_diaphragm_slave_nodes.py b/src/otko/core/catalog/generated/conditions/line_rigid_diaphragm_slave_nodes.py index aa0b5b9..107a4fb 100644 --- a/src/otko/core/catalog/generated/conditions/line_rigid_diaphragm_slave_nodes.py +++ b/src/otko/core/catalog/generated/conditions/line_rigid_diaphragm_slave_nodes.py @@ -17,10 +17,11 @@ from pydantic import BaseModel, ConfigDict, Field class LineRigidDiaphragmSlaveNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Rigid_diaphragm_slave_nodes", - "x-book": "Constraints", + 'x-gid-name': 'Line_Rigid_diaphragm_slave_nodes', + 'x-book': 'Constraints', } ) rigid_diaphragm_id: int = 1 - rigid_plane: Literal["XY", "YZ", "ZX"] = "XY" + rigid_plane: Literal['XY', 'YZ', 'ZX'] = 'XY' + diff --git a/src/otko/core/catalog/generated/conditions/line_rigid_link_slave_nodes.py b/src/otko/core/catalog/generated/conditions/line_rigid_link_slave_nodes.py index d105126..abfff93 100644 --- a/src/otko/core/catalog/generated/conditions/line_rigid_link_slave_nodes.py +++ b/src/otko/core/catalog/generated/conditions/line_rigid_link_slave_nodes.py @@ -17,12 +17,11 @@ from pydantic import BaseModel, ConfigDict, Field class LineRigidLinkSlaveNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Rigid_link_slave_nodes", - "x-book": "Constraints", + 'x-gid-name': 'Line_Rigid_link_slave_nodes', + 'x-book': 'Constraints', } ) - rigid_link_id: int = Field( - 1, description="Give a positive integer number for body constraint group." - ) - rigid_link_type: Literal["Bar", "Beam"] = "Beam" + rigid_link_id: int = Field(1, description='Give a positive integer number for body constraint group.') + rigid_link_type: Literal['Bar', 'Beam'] = 'Beam' + diff --git a/src/otko/core/catalog/generated/conditions/line_uniform_forces.py b/src/otko/core/catalog/generated/conditions/line_uniform_forces.py index 3303b6b..0a0cbc2 100644 --- a/src/otko/core/catalog/generated/conditions/line_uniform_forces.py +++ b/src/otko/core/catalog/generated/conditions/line_uniform_forces.py @@ -15,11 +15,12 @@ from pydantic import BaseModel, ConfigDict, Field class LineUniformForcesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Line_Uniform_Forces", - "x-book": "Loads", + 'x-gid-name': 'Line_Uniform_Forces', + 'x-book': 'Loads', } ) - uniform_load_along_line_longitudinal_axis: str = "0.0 kN/m" # TODO: unit-aware type - uniform_load_acting_in_local_y_direction: str = "0.0 kN/m" # TODO: unit-aware type - uniform_load_acting_in_local_z_direction: str = "0.0 kN/m" # TODO: unit-aware type + uniform_load_along_line_longitudinal_axis: str = '0.0 kN/m' # TODO: unit-aware type + uniform_load_acting_in_local_y_direction: str = '0.0 kN/m' # TODO: unit-aware type + uniform_load_acting_in_local_z_direction: str = '0.0 kN/m' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/conditions/point_beam_contact_slave_nodes.py b/src/otko/core/catalog/generated/conditions/point_beam_contact_slave_nodes.py index 600c99e..5d96720 100644 --- a/src/otko/core/catalog/generated/conditions/point_beam_contact_slave_nodes.py +++ b/src/otko/core/catalog/generated/conditions/point_beam_contact_slave_nodes.py @@ -17,14 +17,15 @@ from pydantic import BaseModel, ConfigDict, Field class PointBeamContactSlaveNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Beam_contact_slave_nodes", - "x-book": "ZeroLength_Elements", + 'x-gid-name': 'Point_Beam_contact_slave_nodes', + 'x-book': 'ZeroLength_Elements', } ) beam_contact_id: int = 1 - contact_material_mat_multidimensional_nd_materials_user_materials: str = "Contact" - width_radius: str = "0.25m" # TODO: unit-aware type + contact_material_mat_multidimensional_nd_materials_user_materials: str = 'Contact' + width_radius: str = '0.25m' # TODO: unit-aware type gap_tolerance: float = 1.0e-10 force_tolerance: float = 1.0e-10 - initial_assumption: Literal["Contact", "No_Contact"] = "Contact" + initial_assumption: Literal['Contact', 'No_Contact'] = 'Contact' + diff --git a/src/otko/core/catalog/generated/conditions/point_body_constraint_master_node.py b/src/otko/core/catalog/generated/conditions/point_body_constraint_master_node.py index 8a52b87..b8403c9 100644 --- a/src/otko/core/catalog/generated/conditions/point_body_constraint_master_node.py +++ b/src/otko/core/catalog/generated/conditions/point_body_constraint_master_node.py @@ -15,11 +15,10 @@ from pydantic import BaseModel, ConfigDict, Field class PointBodyConstraintMasterNodeSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Body_constraint_master_node", - "x-book": "Old_Conditions", + 'x-gid-name': 'Point_Body_constraint_master_node', + 'x-book': 'Old_Conditions', } ) - body_constraint_id_number: int = Field( - 1, description="Give a positive integer number for body constraint group." - ) + body_constraint_id_number: int = Field(1, description='Give a positive integer number for body constraint group.') + diff --git a/src/otko/core/catalog/generated/conditions/point_body_constraint_slave_nodes.py b/src/otko/core/catalog/generated/conditions/point_body_constraint_slave_nodes.py index 01c115d..c51ad33 100644 --- a/src/otko/core/catalog/generated/conditions/point_body_constraint_slave_nodes.py +++ b/src/otko/core/catalog/generated/conditions/point_body_constraint_slave_nodes.py @@ -17,80 +17,80 @@ from pydantic import BaseModel, ConfigDict, Field class PointBodyConstraintSlaveNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Body_constraint_slave_nodes", - "x-book": "Old_Conditions", - "dependencies": [ + 'x-gid-name': 'Point_Body_constraint_slave_nodes', + 'x-book': 'Old_Conditions', + 'dependencies': [ { - "field": "fast_constraints", - "gid_name": "Fast_constraints", - "rules": [ + 'field': 'fast_constraints', + 'gid_name': 'Fast_constraints', + 'rules': [ { - "trigger": "Pinned_Connection", - "actions": [ + 'trigger': 'Pinned_Connection', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Fixed_Connection", - "actions": [ + 'trigger': 'Fixed_Connection', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '1', }, ], }, @@ -100,13 +100,12 @@ class PointBodyConstraintSlaveNodesSpec(BaseModel): } ) - body_constraint_id_number: int = Field( - 1, description="Give a positive integer number for body constraint group." - ) - x_translation: Literal["1", "0"] = "1" - y_translation: Literal["1", "0"] = "1" - z_translation: Literal["1", "0"] = "1" - x_rotation: Literal["1", "0"] = "1" - y_rotation: Literal["1", "0"] = "1" - z_rotation: Literal["1", "0"] = "1" - fast_constraints: Literal["Pinned_Connection", "Fixed_Connection"] = "Pinned_Connection" + body_constraint_id_number: int = Field(1, description='Give a positive integer number for body constraint group.') + x_translation: Literal['1', '0'] = '1' + y_translation: Literal['1', '0'] = '1' + z_translation: Literal['1', '0'] = '1' + x_rotation: Literal['1', '0'] = '1' + y_rotation: Literal['1', '0'] = '1' + z_rotation: Literal['1', '0'] = '1' + fast_constraints: Literal['Pinned_Connection', 'Fixed_Connection'] = 'Pinned_Connection' + diff --git a/src/otko/core/catalog/generated/conditions/point_displacements.py b/src/otko/core/catalog/generated/conditions/point_displacements.py index e95eb4a..45073fd 100644 --- a/src/otko/core/catalog/generated/conditions/point_displacements.py +++ b/src/otko/core/catalog/generated/conditions/point_displacements.py @@ -15,14 +15,15 @@ from pydantic import BaseModel, ConfigDict, Field class PointDisplacementsSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Displacements", - "x-book": "Loads", + 'x-gid-name': 'Point_Displacements', + 'x-book': 'Loads', } ) - x_displacement: str = "0.0m" # TODO: unit-aware type - y_displacement: str = "0.0m" # TODO: unit-aware type - z_displacement: str = "0.0m" # TODO: unit-aware type - x_rotation: str = "0.0 rad" # TODO: unit-aware type - y_rotation: str = "0.0 rad" # TODO: unit-aware type - z_rotation: str = "0.0 rad" # TODO: unit-aware type + x_displacement: str = '0.0m' # TODO: unit-aware type + y_displacement: str = '0.0m' # TODO: unit-aware type + z_displacement: str = '0.0m' # TODO: unit-aware type + x_rotation: str = '0.0 rad' # TODO: unit-aware type + y_rotation: str = '0.0 rad' # TODO: unit-aware type + z_rotation: str = '0.0 rad' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/conditions/point_equal_constraint_master_node.py b/src/otko/core/catalog/generated/conditions/point_equal_constraint_master_node.py index 5c8465e..b5166b0 100644 --- a/src/otko/core/catalog/generated/conditions/point_equal_constraint_master_node.py +++ b/src/otko/core/catalog/generated/conditions/point_equal_constraint_master_node.py @@ -15,11 +15,10 @@ from pydantic import BaseModel, ConfigDict, Field class PointEqualConstraintMasterNodeSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Equal_constraint_master_node", - "x-book": "Constraints", + 'x-gid-name': 'Point_Equal_constraint_master_node', + 'x-book': 'Constraints', } ) - equal_constraint_id: int = Field( - 1, description="Give a positive integer number for body constraint group." - ) + equal_constraint_id: int = Field(1, description='Give a positive integer number for body constraint group.') + diff --git a/src/otko/core/catalog/generated/conditions/point_equal_constraint_slave_nodes.py b/src/otko/core/catalog/generated/conditions/point_equal_constraint_slave_nodes.py index 969e08e..b5106af 100644 --- a/src/otko/core/catalog/generated/conditions/point_equal_constraint_slave_nodes.py +++ b/src/otko/core/catalog/generated/conditions/point_equal_constraint_slave_nodes.py @@ -17,17 +17,16 @@ from pydantic import BaseModel, ConfigDict, Field class PointEqualConstraintSlaveNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Equal_constraint_slave_nodes", - "x-book": "Constraints", + 'x-gid-name': 'Point_Equal_constraint_slave_nodes', + 'x-book': 'Constraints', } ) - equal_constraint_id: int = Field( - 1, description="Give a positive integer number for body constraint group." - ) - x_translation: Literal["1", "0"] = "1" - y_translation: Literal["1", "0"] = "1" - z_translation: Literal["1", "0"] = "1" - x_rotation: Literal["1", "0"] = "1" - y_rotation: Literal["1", "0"] = "1" - z_rotation: Literal["1", "0"] = "1" + equal_constraint_id: int = Field(1, description='Give a positive integer number for body constraint group.') + x_translation: Literal['1', '0'] = '1' + y_translation: Literal['1', '0'] = '1' + z_translation: Literal['1', '0'] = '1' + x_rotation: Literal['1', '0'] = '1' + y_rotation: Literal['1', '0'] = '1' + z_rotation: Literal['1', '0'] = '1' + diff --git a/src/otko/core/catalog/generated/conditions/point_forces.py b/src/otko/core/catalog/generated/conditions/point_forces.py index 2bf40d5..599b530 100644 --- a/src/otko/core/catalog/generated/conditions/point_forces.py +++ b/src/otko/core/catalog/generated/conditions/point_forces.py @@ -15,14 +15,15 @@ from pydantic import BaseModel, ConfigDict, Field class PointForcesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Forces", - "x-book": "Loads", + 'x-gid-name': 'Point_Forces', + 'x-book': 'Loads', } ) - x_force: str = "0.0 kN" # TODO: unit-aware type - y_force: str = "0.0 kN" # TODO: unit-aware type - z_force: str = "0.0 kN" # TODO: unit-aware type - x_moment: str = "0.0 kNm" # TODO: unit-aware type - y_moment: str = "0.0 kNm" # TODO: unit-aware type - z_moment: str = "0.0 kNm" # TODO: unit-aware type + x_force: str = '0.0 kN' # TODO: unit-aware type + y_force: str = '0.0 kN' # TODO: unit-aware type + z_force: str = '0.0 kN' # TODO: unit-aware type + x_moment: str = '0.0 kNm' # TODO: unit-aware type + y_moment: str = '0.0 kNm' # TODO: unit-aware type + z_moment: str = '0.0 kNm' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/conditions/point_ground_motion_from_record.py b/src/otko/core/catalog/generated/conditions/point_ground_motion_from_record.py index 9898b6d..da6ddef 100644 --- a/src/otko/core/catalog/generated/conditions/point_ground_motion_from_record.py +++ b/src/otko/core/catalog/generated/conditions/point_ground_motion_from_record.py @@ -17,10 +17,11 @@ from pydantic import BaseModel, ConfigDict, Field class PointGroundMotionFromRecordSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Ground_Motion_from_Record", - "x-book": "Loads", + 'x-gid-name': 'Point_Ground_Motion_from_Record', + 'x-book': 'Loads', } ) - record_file: str = "Ground_Motion_Record" - ground_motion_direction: Literal["Ux", "Uy", "Uz", "Rx", "Ry", "Rz"] = "Ux" + record_file: str = 'Ground_Motion_Record' + ground_motion_direction: Literal['Ux', 'Uy', 'Uz', 'Rx', 'Ry', 'Rz'] = 'Ux' + diff --git a/src/otko/core/catalog/generated/conditions/point_mass.py b/src/otko/core/catalog/generated/conditions/point_mass.py index 5afb059..d4143f8 100644 --- a/src/otko/core/catalog/generated/conditions/point_mass.py +++ b/src/otko/core/catalog/generated/conditions/point_mass.py @@ -17,15 +17,16 @@ from pydantic import BaseModel, ConfigDict, Field class PointMassSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Mass", - "x-book": "Mass/Damping", + 'x-gid-name': 'Point_Mass', + 'x-book': 'Mass/Damping', } ) - mass_dof_values: Literal[":"] = ":" - x_direction: str = "0.0ton" # TODO: unit-aware type - y_direction: str = "0.0ton" # TODO: unit-aware type - z_direction: str = "0.0ton" # TODO: unit-aware type - x_rotation: str = "0.0ton" # TODO: unit-aware type - y_rotation: str = "0.0ton" # TODO: unit-aware type - z_rotation: str = "0.0ton" # TODO: unit-aware type + mass_dof_values: Literal[':'] = ':' + x_direction: str = '0.0ton' # TODO: unit-aware type + y_direction: str = '0.0ton' # TODO: unit-aware type + z_direction: str = '0.0ton' # TODO: unit-aware type + x_rotation: str = '0.0ton' # TODO: unit-aware type + y_rotation: str = '0.0ton' # TODO: unit-aware type + z_rotation: str = '0.0ton' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/conditions/point_rayleigh_damping_nodes.py b/src/otko/core/catalog/generated/conditions/point_rayleigh_damping_nodes.py index 70155a1..24dd864 100644 --- a/src/otko/core/catalog/generated/conditions/point_rayleigh_damping_nodes.py +++ b/src/otko/core/catalog/generated/conditions/point_rayleigh_damping_nodes.py @@ -15,8 +15,8 @@ from pydantic import BaseModel, ConfigDict, Field class PointRayleighDampingNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Rayleigh_Damping-Nodes", - "x-book": "Mass/Damping", + 'x-gid-name': 'Point_Rayleigh_Damping-Nodes', + 'x-book': 'Mass/Damping', } ) @@ -25,3 +25,4 @@ class PointRayleighDampingNodesSpec(BaseModel): betak: float = 0.0 betakinit: float = 0.0 betakcomm: float = 0.0 + diff --git a/src/otko/core/catalog/generated/conditions/point_restraints.py b/src/otko/core/catalog/generated/conditions/point_restraints.py index 4a45da1..ea4fbd6 100644 --- a/src/otko/core/catalog/generated/conditions/point_restraints.py +++ b/src/otko/core/catalog/generated/conditions/point_restraints.py @@ -17,290 +17,290 @@ from pydantic import BaseModel, ConfigDict, Field class PointRestraintsSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Restraints", - "x-book": "Restraints", - "dependencies": [ + 'x-gid-name': 'Point_Restraints', + 'x-book': 'Restraints', + 'dependencies': [ { - "field": "fast_restraints", - "gid_name": "Fast_restraints", - "rules": [ + 'field': 'fast_restraints', + 'gid_name': 'Fast_restraints', + 'rules': [ { - "trigger": "Fix_XYZ", - "actions": [ + 'trigger': 'Fix_XYZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '1', }, ], }, { - "trigger": "Fix_XY", - "actions": [ + 'trigger': 'Fix_XY', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, ], }, { - "trigger": "Fix_XZ", - "actions": [ + 'trigger': 'Fix_XZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Fix_YZ", - "actions": [ + 'trigger': 'Fix_YZ', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_XYZ", - "actions": [ + 'trigger': 'Pin_XYZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_XY", - "actions": [ + 'trigger': 'Pin_XY', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_XZ", - "actions": [ + 'trigger': 'Pin_XZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_YZ", - "actions": [ + 'trigger': 'Pin_YZ', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, @@ -310,13 +310,12 @@ class PointRestraintsSpec(BaseModel): } ) - x_translation: Literal["1", "0"] = "1" - y_translation: Literal["1", "0"] = "1" - z_translation: Literal["1", "0"] = "1" - x_rotation: Literal["1", "0"] = "1" - y_rotation: Literal["1", "0"] = "1" - z_rotation: Literal["1", "0"] = "1" - fluid_pressure: Literal["1", "0"] = "0" - fast_restraints: Literal[ - "Fix_XYZ", "Fix_XY", "Fix_XZ", "Fix_YZ", "Pin_XYZ", "Pin_XY", "Pin_XZ", "Pin_YZ" - ] = "Fix_XYZ" + x_translation: Literal['1', '0'] = '1' + y_translation: Literal['1', '0'] = '1' + z_translation: Literal['1', '0'] = '1' + x_rotation: Literal['1', '0'] = '1' + y_rotation: Literal['1', '0'] = '1' + z_rotation: Literal['1', '0'] = '1' + fluid_pressure: Literal['1', '0'] = '0' + fast_restraints: Literal['Fix_XYZ', 'Fix_XY', 'Fix_XZ', 'Fix_YZ', 'Pin_XYZ', 'Pin_XY', 'Pin_XZ', 'Pin_YZ'] = 'Fix_XYZ' + diff --git a/src/otko/core/catalog/generated/conditions/point_rigid_diaphragm_master_node.py b/src/otko/core/catalog/generated/conditions/point_rigid_diaphragm_master_node.py index 3bdb1f3..1996c5f 100644 --- a/src/otko/core/catalog/generated/conditions/point_rigid_diaphragm_master_node.py +++ b/src/otko/core/catalog/generated/conditions/point_rigid_diaphragm_master_node.py @@ -15,9 +15,10 @@ from pydantic import BaseModel, ConfigDict, Field class PointRigidDiaphragmMasterNodeSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Rigid_diaphragm_master_node", - "x-book": "Constraints", + 'x-gid-name': 'Point_Rigid_diaphragm_master_node', + 'x-book': 'Constraints', } ) rigid_diaphragm_id: int = 1 + diff --git a/src/otko/core/catalog/generated/conditions/point_rigid_diaphragm_slave_nodes.py b/src/otko/core/catalog/generated/conditions/point_rigid_diaphragm_slave_nodes.py index bc69de3..115514d 100644 --- a/src/otko/core/catalog/generated/conditions/point_rigid_diaphragm_slave_nodes.py +++ b/src/otko/core/catalog/generated/conditions/point_rigid_diaphragm_slave_nodes.py @@ -17,10 +17,11 @@ from pydantic import BaseModel, ConfigDict, Field class PointRigidDiaphragmSlaveNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Rigid_diaphragm_slave_nodes", - "x-book": "Constraints", + 'x-gid-name': 'Point_Rigid_diaphragm_slave_nodes', + 'x-book': 'Constraints', } ) rigid_diaphragm_id: int = 1 - rigid_plane: Literal["XY", "YZ", "ZX"] = "XY" + rigid_plane: Literal['XY', 'YZ', 'ZX'] = 'XY' + diff --git a/src/otko/core/catalog/generated/conditions/point_rigid_link_master_node.py b/src/otko/core/catalog/generated/conditions/point_rigid_link_master_node.py index cb56d02..8c0bad6 100644 --- a/src/otko/core/catalog/generated/conditions/point_rigid_link_master_node.py +++ b/src/otko/core/catalog/generated/conditions/point_rigid_link_master_node.py @@ -15,9 +15,10 @@ from pydantic import BaseModel, ConfigDict, Field class PointRigidLinkMasterNodeSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Rigid_link_master_node", - "x-book": "Constraints", + 'x-gid-name': 'Point_Rigid_link_master_node', + 'x-book': 'Constraints', } ) rigid_link_id: int = 1 + diff --git a/src/otko/core/catalog/generated/conditions/point_rigid_link_slave_nodes.py b/src/otko/core/catalog/generated/conditions/point_rigid_link_slave_nodes.py index 6d9cdc9..fac5ffd 100644 --- a/src/otko/core/catalog/generated/conditions/point_rigid_link_slave_nodes.py +++ b/src/otko/core/catalog/generated/conditions/point_rigid_link_slave_nodes.py @@ -17,12 +17,11 @@ from pydantic import BaseModel, ConfigDict, Field class PointRigidLinkSlaveNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Rigid_link_slave_nodes", - "x-book": "Constraints", + 'x-gid-name': 'Point_Rigid_link_slave_nodes', + 'x-book': 'Constraints', } ) - rigid_link_id: int = Field( - 1, description="Give a positive integer number for body constraint group." - ) - rigid_link_type: Literal["Bar", "Beam"] = "Beam" + rigid_link_id: int = Field(1, description='Give a positive integer number for body constraint group.') + rigid_link_type: Literal['Bar', 'Beam'] = 'Beam' + diff --git a/src/otko/core/catalog/generated/conditions/point_sine_ground_motion.py b/src/otko/core/catalog/generated/conditions/point_sine_ground_motion.py index aa82b6f..7be402d 100644 --- a/src/otko/core/catalog/generated/conditions/point_sine_ground_motion.py +++ b/src/otko/core/catalog/generated/conditions/point_sine_ground_motion.py @@ -17,40 +17,40 @@ from pydantic import BaseModel, ConfigDict, Field class PointSineGroundMotionSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_Sine_Ground_Motion", - "x-book": "Loads", - "dependencies": [ + 'x-gid-name': 'Point_Sine_Ground_Motion', + 'x-book': 'Loads', + 'dependencies': [ { - "field": "type", - "gid_name": "Type", - "rules": [ + 'field': 'type', + 'gid_name': 'Type', + 'rules': [ { - "trigger": "Acceleration", - "actions": [ + 'trigger': 'Acceleration', + 'actions': [ { - "action": "RESTORE", - "field": "Acceleration_Amplitude", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Acceleration_Amplitude', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_Amplitude", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_Amplitude', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Displacement", - "actions": [ + 'trigger': 'Displacement', + 'actions': [ { - "action": "HIDE", - "field": "Acceleration_Amplitude", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Acceleration_Amplitude', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Displacement_Amplitude", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Displacement_Amplitude', + 'target': '#CURRENT#', }, ], }, @@ -60,10 +60,11 @@ class PointSineGroundMotionSpec(BaseModel): } ) - type: Literal["Acceleration", "Displacement"] = "Displacement" - ground_motion_direction: Literal["Ux", "Uy", "Uz", "Rx", "Ry", "Rz"] = "Ux" - acceleration_amplitude: str = "1.0m/s^2" # TODO: unit-aware type - displacement_amplitude: str = "1.0m" # TODO: unit-aware type - period: str = "0.6sec" # TODO: unit-aware type - duration: str = "10sec" # TODO: unit-aware type + type: Literal['Acceleration', 'Displacement'] = 'Displacement' + ground_motion_direction: Literal['Ux', 'Uy', 'Uz', 'Rx', 'Ry', 'Rz'] = 'Ux' + acceleration_amplitude: str = '1.0m/s^2' # TODO: unit-aware type + displacement_amplitude: str = '1.0m' # TODO: unit-aware type + period: str = '0.6sec' # TODO: unit-aware type + duration: str = '10sec' # TODO: unit-aware type shift: float = 0.0 + diff --git a/src/otko/core/catalog/generated/conditions/point_zerolength.py b/src/otko/core/catalog/generated/conditions/point_zerolength.py index 813ace9..c0aa22e 100644 --- a/src/otko/core/catalog/generated/conditions/point_zerolength.py +++ b/src/otko/core/catalog/generated/conditions/point_zerolength.py @@ -17,160 +17,160 @@ from pydantic import BaseModel, ConfigDict, Field class PointZerolengthSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Point_ZeroLength", - "x-book": "ZeroLength_Elements", - "dependencies": [ + 'x-gid-name': 'Point_ZeroLength', + 'x-book': 'ZeroLength_Elements', + 'dependencies': [ { - "field": "activate_ux", - "gid_name": "Activate_Ux", - "rules": [ + 'field': 'activate_ux', + 'gid_name': 'Activate_Ux', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Ux_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Ux_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Ux_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Ux_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_uy", - "gid_name": "Activate_Uy", - "rules": [ + 'field': 'activate_uy', + 'gid_name': 'Activate_Uy', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Uy_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Uy_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Uy_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Uy_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_uz", - "gid_name": "Activate_Uz", - "rules": [ + 'field': 'activate_uz', + 'gid_name': 'Activate_Uz', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Uz_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Uz_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Uz_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Uz_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_rx", - "gid_name": "Activate_Rx", - "rules": [ + 'field': 'activate_rx', + 'gid_name': 'Activate_Rx', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Rx_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Rx_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Rx_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rx_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_ry", - "gid_name": "Activate_Ry", - "rules": [ + 'field': 'activate_ry', + 'gid_name': 'Activate_Ry', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Ry_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Ry_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Ry_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Ry_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_rz", - "gid_name": "Activate_Rz", - "rules": [ + 'field': 'activate_rz', + 'gid_name': 'Activate_Rz', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Rz_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Rz_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Rz_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rz_material', + 'target': '#CURRENT#', }, ], }, @@ -181,15 +181,16 @@ class PointZerolengthSpec(BaseModel): ) zerolength_id: int = 1 - activate_ux: Literal["0", "1"] = "1" - ux_material: str = "Elastic" - activate_uy: Literal["0", "1"] = "0" - uy_material: str = "Elastic" - activate_uz: Literal["0", "1"] = "0" - uz_material: str = "Elastic" - activate_rx: Literal["0", "1"] = "0" - rx_material: str = "Elastic" - activate_ry: Literal["0", "1"] = "0" - ry_material: str = "Elastic" - activate_rz: Literal["0", "1"] = "0" - rz_material: str = "Elastic" + activate_ux: Literal['0', '1'] = '1' + ux_material: str = 'Elastic' + activate_uy: Literal['0', '1'] = '0' + uy_material: str = 'Elastic' + activate_uz: Literal['0', '1'] = '0' + uz_material: str = 'Elastic' + activate_rx: Literal['0', '1'] = '0' + rx_material: str = 'Elastic' + activate_ry: Literal['0', '1'] = '0' + ry_material: str = 'Elastic' + activate_rz: Literal['0', '1'] = '0' + rz_material: str = 'Elastic' + diff --git a/src/otko/core/catalog/generated/conditions/surface_displacements.py b/src/otko/core/catalog/generated/conditions/surface_displacements.py index c9c7b25..a2c2a1b 100644 --- a/src/otko/core/catalog/generated/conditions/surface_displacements.py +++ b/src/otko/core/catalog/generated/conditions/surface_displacements.py @@ -15,14 +15,15 @@ from pydantic import BaseModel, ConfigDict, Field class SurfaceDisplacementsSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Surface_Displacements", - "x-book": "Loads", + 'x-gid-name': 'Surface_Displacements', + 'x-book': 'Loads', } ) - x_displacement: str = "0.0m" # TODO: unit-aware type - y_displacement: str = "0.0m" # TODO: unit-aware type - z_displacement: str = "0.0m" # TODO: unit-aware type - x_rotation: str = "0.0 rad" # TODO: unit-aware type - y_rotation: str = "0.0 rad" # TODO: unit-aware type - z_rotation: str = "0.0 rad" # TODO: unit-aware type + x_displacement: str = '0.0m' # TODO: unit-aware type + y_displacement: str = '0.0m' # TODO: unit-aware type + z_displacement: str = '0.0m' # TODO: unit-aware type + x_rotation: str = '0.0 rad' # TODO: unit-aware type + y_rotation: str = '0.0 rad' # TODO: unit-aware type + z_rotation: str = '0.0 rad' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/conditions/surface_forces.py b/src/otko/core/catalog/generated/conditions/surface_forces.py index dbf8ea9..ac53c76 100644 --- a/src/otko/core/catalog/generated/conditions/surface_forces.py +++ b/src/otko/core/catalog/generated/conditions/surface_forces.py @@ -15,14 +15,15 @@ from pydantic import BaseModel, ConfigDict, Field class SurfaceForcesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Surface_Forces", - "x-book": "Loads", + 'x-gid-name': 'Surface_Forces', + 'x-book': 'Loads', } ) - x_force: str = "0.0 kN" # TODO: unit-aware type - y_force: str = "0.0 kN" # TODO: unit-aware type - z_force: str = "0.0 kN" # TODO: unit-aware type - x_moment: str = "0.0 kNm" # TODO: unit-aware type - y_moment: str = "0.0 kNm" # TODO: unit-aware type - z_moment: str = "0.0 kNm" # TODO: unit-aware type + x_force: str = '0.0 kN' # TODO: unit-aware type + y_force: str = '0.0 kN' # TODO: unit-aware type + z_force: str = '0.0 kN' # TODO: unit-aware type + x_moment: str = '0.0 kNm' # TODO: unit-aware type + y_moment: str = '0.0 kNm' # TODO: unit-aware type + z_moment: str = '0.0 kNm' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/conditions/surface_mass.py b/src/otko/core/catalog/generated/conditions/surface_mass.py index 1001fb0..b7c08ec 100644 --- a/src/otko/core/catalog/generated/conditions/surface_mass.py +++ b/src/otko/core/catalog/generated/conditions/surface_mass.py @@ -17,15 +17,16 @@ from pydantic import BaseModel, ConfigDict, Field class SurfaceMassSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Surface_Mass", - "x-book": "Mass/Damping", + 'x-gid-name': 'Surface_Mass', + 'x-book': 'Mass/Damping', } ) - mass_dof_values: Literal[":"] = ":" - x_direction: str = "0.0ton" # TODO: unit-aware type - y_direction: str = "0.0ton" # TODO: unit-aware type - z_direction: str = "0.0ton" # TODO: unit-aware type - x_rotation: str = "0.0ton" # TODO: unit-aware type - y_rotation: str = "0.0ton" # TODO: unit-aware type - z_rotation: str = "0.0ton" # TODO: unit-aware type + mass_dof_values: Literal[':'] = ':' + x_direction: str = '0.0ton' # TODO: unit-aware type + y_direction: str = '0.0ton' # TODO: unit-aware type + z_direction: str = '0.0ton' # TODO: unit-aware type + x_rotation: str = '0.0ton' # TODO: unit-aware type + y_rotation: str = '0.0ton' # TODO: unit-aware type + z_rotation: str = '0.0ton' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/conditions/surface_rayleigh_damping_elements.py b/src/otko/core/catalog/generated/conditions/surface_rayleigh_damping_elements.py index d32d2aa..e33f80f 100644 --- a/src/otko/core/catalog/generated/conditions/surface_rayleigh_damping_elements.py +++ b/src/otko/core/catalog/generated/conditions/surface_rayleigh_damping_elements.py @@ -15,8 +15,8 @@ from pydantic import BaseModel, ConfigDict, Field class SurfaceRayleighDampingElementsSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Surface_Rayleigh_Damping-Elements", - "x-book": "Mass/Damping", + 'x-gid-name': 'Surface_Rayleigh_Damping-Elements', + 'x-book': 'Mass/Damping', } ) @@ -25,3 +25,4 @@ class SurfaceRayleighDampingElementsSpec(BaseModel): betak: float = 0.0 betakinit: float = 0.0 betakcomm: float = 0.0 + diff --git a/src/otko/core/catalog/generated/conditions/surface_rayleigh_damping_nodes.py b/src/otko/core/catalog/generated/conditions/surface_rayleigh_damping_nodes.py index f3ed8c9..d086691 100644 --- a/src/otko/core/catalog/generated/conditions/surface_rayleigh_damping_nodes.py +++ b/src/otko/core/catalog/generated/conditions/surface_rayleigh_damping_nodes.py @@ -15,8 +15,8 @@ from pydantic import BaseModel, ConfigDict, Field class SurfaceRayleighDampingNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Surface_Rayleigh_Damping-Nodes", - "x-book": "Mass/Damping", + 'x-gid-name': 'Surface_Rayleigh_Damping-Nodes', + 'x-book': 'Mass/Damping', } ) @@ -25,3 +25,4 @@ class SurfaceRayleighDampingNodesSpec(BaseModel): betak: float = 0.0 betakinit: float = 0.0 betakcomm: float = 0.0 + diff --git a/src/otko/core/catalog/generated/conditions/surface_restraints.py b/src/otko/core/catalog/generated/conditions/surface_restraints.py index c88e9d1..7c40c31 100644 --- a/src/otko/core/catalog/generated/conditions/surface_restraints.py +++ b/src/otko/core/catalog/generated/conditions/surface_restraints.py @@ -17,290 +17,290 @@ from pydantic import BaseModel, ConfigDict, Field class SurfaceRestraintsSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Surface_Restraints", - "x-book": "Restraints", - "dependencies": [ + 'x-gid-name': 'Surface_Restraints', + 'x-book': 'Restraints', + 'dependencies': [ { - "field": "fast_restraints", - "gid_name": "Fast_restraints", - "rules": [ + 'field': 'fast_restraints', + 'gid_name': 'Fast_restraints', + 'rules': [ { - "trigger": "Fix_XYZ", - "actions": [ + 'trigger': 'Fix_XYZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '1', }, ], }, { - "trigger": "Fix_XY", - "actions": [ + 'trigger': 'Fix_XY', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, ], }, { - "trigger": "Fix_XZ", - "actions": [ + 'trigger': 'Fix_XZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Fix_YZ", - "actions": [ + 'trigger': 'Fix_YZ', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_XYZ", - "actions": [ + 'trigger': 'Pin_XYZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_XY", - "actions": [ + 'trigger': 'Pin_XY', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_XZ", - "actions": [ + 'trigger': 'Pin_XZ', + 'actions': [ { - "action": "RESTORE", - "field": "X-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Y-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, { - "trigger": "Pin_YZ", - "actions": [ + 'trigger': 'Pin_YZ', + 'actions': [ { - "action": "RESTORE", - "field": "Y-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Y-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "Z-Translation", - "target": "1", + 'action': 'RESTORE', + 'field': 'Z-Translation', + 'target': '1', }, { - "action": "RESTORE", - "field": "X-Translation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Translation', + 'target': '0', }, { - "action": "RESTORE", - "field": "X-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'X-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Y-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Y-Rotation', + 'target': '0', }, { - "action": "RESTORE", - "field": "Z-Rotation", - "target": "0", + 'action': 'RESTORE', + 'field': 'Z-Rotation', + 'target': '0', }, ], }, @@ -310,13 +310,12 @@ class SurfaceRestraintsSpec(BaseModel): } ) - x_translation: Literal["1", "0"] = "1" - y_translation: Literal["1", "0"] = "1" - z_translation: Literal["1", "0"] = "1" - x_rotation: Literal["1", "0"] = "1" - y_rotation: Literal["1", "0"] = "1" - z_rotation: Literal["1", "0"] = "1" - fluid_pressure: Literal["1", "0"] = "0" - fast_restraints: Literal[ - "Fix_XYZ", "Fix_XY", "Fix_XZ", "Fix_YZ", "Pin_XYZ", "Pin_XY", "Pin_XZ", "Pin_YZ" - ] = "Fix_XYZ" + x_translation: Literal['1', '0'] = '1' + y_translation: Literal['1', '0'] = '1' + z_translation: Literal['1', '0'] = '1' + x_rotation: Literal['1', '0'] = '1' + y_rotation: Literal['1', '0'] = '1' + z_rotation: Literal['1', '0'] = '1' + fluid_pressure: Literal['1', '0'] = '0' + fast_restraints: Literal['Fix_XYZ', 'Fix_XY', 'Fix_XZ', 'Fix_YZ', 'Pin_XYZ', 'Pin_XY', 'Pin_XZ', 'Pin_YZ'] = 'Fix_XYZ' + diff --git a/src/otko/core/catalog/generated/conditions/volume_mass.py b/src/otko/core/catalog/generated/conditions/volume_mass.py index a527d58..2281556 100644 --- a/src/otko/core/catalog/generated/conditions/volume_mass.py +++ b/src/otko/core/catalog/generated/conditions/volume_mass.py @@ -17,15 +17,16 @@ from pydantic import BaseModel, ConfigDict, Field class VolumeMassSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Volume_Mass", - "x-book": "Mass/Damping", + 'x-gid-name': 'Volume_Mass', + 'x-book': 'Mass/Damping', } ) - mass_dof_values: Literal[":"] = ":" - x_direction: str = "0.0ton" # TODO: unit-aware type - y_direction: str = "0.0ton" # TODO: unit-aware type - z_direction: str = "0.0ton" # TODO: unit-aware type - x_rotation: str = "0.0ton" # TODO: unit-aware type - y_rotation: str = "0.0ton" # TODO: unit-aware type - z_rotation: str = "0.0ton" # TODO: unit-aware type + mass_dof_values: Literal[':'] = ':' + x_direction: str = '0.0ton' # TODO: unit-aware type + y_direction: str = '0.0ton' # TODO: unit-aware type + z_direction: str = '0.0ton' # TODO: unit-aware type + x_rotation: str = '0.0ton' # TODO: unit-aware type + y_rotation: str = '0.0ton' # TODO: unit-aware type + z_rotation: str = '0.0ton' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/conditions/volume_rayleigh_damping_elements.py b/src/otko/core/catalog/generated/conditions/volume_rayleigh_damping_elements.py index 6f25b2f..48a38d0 100644 --- a/src/otko/core/catalog/generated/conditions/volume_rayleigh_damping_elements.py +++ b/src/otko/core/catalog/generated/conditions/volume_rayleigh_damping_elements.py @@ -15,8 +15,8 @@ from pydantic import BaseModel, ConfigDict, Field class VolumeRayleighDampingElementsSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Volume_Rayleigh_Damping-Elements", - "x-book": "Mass/Damping", + 'x-gid-name': 'Volume_Rayleigh_Damping-Elements', + 'x-book': 'Mass/Damping', } ) @@ -25,3 +25,4 @@ class VolumeRayleighDampingElementsSpec(BaseModel): betak: float = 0.0 betakinit: float = 0.0 betakcomm: float = 0.0 + diff --git a/src/otko/core/catalog/generated/conditions/volume_rayleigh_damping_nodes.py b/src/otko/core/catalog/generated/conditions/volume_rayleigh_damping_nodes.py index c268826..eb57cdd 100644 --- a/src/otko/core/catalog/generated/conditions/volume_rayleigh_damping_nodes.py +++ b/src/otko/core/catalog/generated/conditions/volume_rayleigh_damping_nodes.py @@ -15,8 +15,8 @@ from pydantic import BaseModel, ConfigDict, Field class VolumeRayleighDampingNodesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Volume_Rayleigh_Damping-Nodes", - "x-book": "Mass/Damping", + 'x-gid-name': 'Volume_Rayleigh_Damping-Nodes', + 'x-book': 'Mass/Damping', } ) @@ -25,3 +25,4 @@ class VolumeRayleighDampingNodesSpec(BaseModel): betak: float = 0.0 betakinit: float = 0.0 betakcomm: float = 0.0 + diff --git a/src/otko/core/catalog/generated/conditions/zerolength.py b/src/otko/core/catalog/generated/conditions/zerolength.py index 7b59d3b..361c6c8 100644 --- a/src/otko/core/catalog/generated/conditions/zerolength.py +++ b/src/otko/core/catalog/generated/conditions/zerolength.py @@ -17,160 +17,160 @@ from pydantic import BaseModel, ConfigDict, Field class ZerolengthSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "ZeroLength", - "x-book": "Old_Conditions", - "dependencies": [ + 'x-gid-name': 'ZeroLength', + 'x-book': 'Old_Conditions', + 'dependencies': [ { - "field": "activate_ux", - "gid_name": "Activate_Ux", - "rules": [ + 'field': 'activate_ux', + 'gid_name': 'Activate_Ux', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Ux_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Ux_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Ux_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Ux_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_uy", - "gid_name": "Activate_Uy", - "rules": [ + 'field': 'activate_uy', + 'gid_name': 'Activate_Uy', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Uy_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Uy_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Uy_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Uy_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_uz", - "gid_name": "Activate_Uz", - "rules": [ + 'field': 'activate_uz', + 'gid_name': 'Activate_Uz', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Uz_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Uz_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Uz_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Uz_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_rx", - "gid_name": "Activate_Rx", - "rules": [ + 'field': 'activate_rx', + 'gid_name': 'Activate_Rx', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Rx_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Rx_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Rx_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rx_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_ry", - "gid_name": "Activate_Ry", - "rules": [ + 'field': 'activate_ry', + 'gid_name': 'Activate_Ry', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Ry_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Ry_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Ry_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Ry_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_rz", - "gid_name": "Activate_Rz", - "rules": [ + 'field': 'activate_rz', + 'gid_name': 'Activate_Rz', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Rz_material", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Rz_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Rz_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rz_material', + 'target': '#CURRENT#', }, ], }, @@ -181,15 +181,16 @@ class ZerolengthSpec(BaseModel): ) zerolength_id_number: int = 1 - activate_ux: Literal["0", "1"] = "1" - ux_material: str = "Elastic" - activate_uy: Literal["0", "1"] = "0" - uy_material: str = "Elastic" - activate_uz: Literal["0", "1"] = "0" - uz_material: str = "Elastic" - activate_rx: Literal["0", "1"] = "0" - rx_material: str = "Elastic" - activate_ry: Literal["0", "1"] = "0" - ry_material: str = "Elastic" - activate_rz: Literal["0", "1"] = "0" - rz_material: str = "Elastic" + activate_ux: Literal['0', '1'] = '1' + ux_material: str = 'Elastic' + activate_uy: Literal['0', '1'] = '0' + uy_material: str = 'Elastic' + activate_uz: Literal['0', '1'] = '0' + uz_material: str = 'Elastic' + activate_rx: Literal['0', '1'] = '0' + rx_material: str = 'Elastic' + activate_ry: Literal['0', '1'] = '0' + ry_material: str = 'Elastic' + activate_rz: Literal['0', '1'] = '0' + rz_material: str = 'Elastic' + diff --git a/src/otko/core/catalog/generated/contact.py b/src/otko/core/catalog/generated/contact.py index 6e685b9..040344a 100644 --- a/src/otko/core/catalog/generated/contact.py +++ b/src/otko/core/catalog/generated/contact.py @@ -17,13 +17,14 @@ from pydantic import BaseModel, ConfigDict, Field class ContactSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Contact", - "x-book": "Multidimensional_(nD)_Materials", + 'x-gid-name': 'Contact', + 'x-book': 'Multidimensional_(nD)_Materials', } ) - material: Literal["Contact"] = "Contact" + material: Literal['Contact'] = 'Contact' frictional_coefficient: float = 0.1 - stiffness_parameter: str = "1000.0kN/m" # TODO: unit-aware type + stiffness_parameter: str = '1000.0kN/m' # TODO: unit-aware type cohesive_intercept: float = 0.0 - tensile_strength: str = "0.0kN" # TODO: unit-aware type + tensile_strength: str = '0.0kN' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/corotational_truss.py b/src/otko/core/catalog/generated/corotational_truss.py index 04bdb57..2638cb1 100644 --- a/src/otko/core/catalog/generated/corotational_truss.py +++ b/src/otko/core/catalog/generated/corotational_truss.py @@ -17,196 +17,196 @@ from pydantic import BaseModel, ConfigDict, Field class CorotationalTrussSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Corotational_Truss", - "x-book": "Truss_Elements", - "dependencies": [ + 'x-gid-name': 'Corotational_Truss', + 'x-book': 'Truss_Elements', + 'dependencies': [ { - "field": "cross_section", - "gid_name": "Cross_section", - "rules": [ + 'field': 'cross_section', + 'gid_name': 'Cross_section', + 'rules': [ { - "trigger": "Rectangular", - "actions": [ + 'trigger': 'Rectangular', + 'actions': [ { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_Bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_Bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Circular", - "actions": [ + 'trigger': 'Circular', + 'actions': [ { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_Bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_Bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Tee", - "actions": [ + 'trigger': 'Tee', + 'actions': [ { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_Bw", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_Bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, ], }, { - "trigger": "General", - "actions": [ + 'trigger': 'General', + 'actions': [ { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_Bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_Bw', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "set_modification_factors", - "gid_name": "Set_modification_factors", - "rules": [ + 'field': 'set_modification_factors', + 'gid_name': 'Set_modification_factors', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "mod._A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._A', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "mod._A", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._A', + 'target': '#CURRENT#', }, ], }, @@ -216,22 +216,21 @@ class CorotationalTrussSpec(BaseModel): } ) - element_type: Literal["CorotationalTruss"] = "CorotationalTruss" - compatible_materials: Literal["_"] = "_" - material: str = Field( - "Elastic", description="Choose one existing material to use its parameters." - ) - mass_density: str = "0.0ton/m^3" # TODO: unit-aware type - consider_consistent_mass_matrix: Literal["1", "0"] = "0" - include_rayleigh_damping: Literal["1", "0"] = "0" - weight_density: str = "0.0 kN/m^3" # TODO: unit-aware type - cross_section: Literal["Rectangular", "Tee", "Circular", "General"] = "General" - height_h: str = "0.5m" # TODO: unit-aware type - width_b: str = "0.3m" # TODO: unit-aware type - diameter_d: str = "0.5m" # TODO: unit-aware type - width_bf: str = "0.4m" # TODO: unit-aware type - height_hf: str = "0.15m" # TODO: unit-aware type - width_bw: str = "0.25m" # TODO: unit-aware type - area_a: str = "1 m^2" # TODO: unit-aware type - set_modification_factors: Literal["0", "1"] = "0" + element_type: Literal['CorotationalTruss'] = 'CorotationalTruss' + compatible_materials: Literal['_'] = '_' + material: str = Field('Elastic', description='Choose one existing material to use its parameters.') + mass_density: str = '0.0ton/m^3' # TODO: unit-aware type + consider_consistent_mass_matrix: Literal['1', '0'] = '0' + include_rayleigh_damping: Literal['1', '0'] = '0' + weight_density: str = '0.0 kN/m^3' # TODO: unit-aware type + cross_section: Literal['Rectangular', 'Tee', 'Circular', 'General'] = 'General' + height_h: str = '0.5m' # TODO: unit-aware type + width_b: str = '0.3m' # TODO: unit-aware type + diameter_d: str = '0.5m' # TODO: unit-aware type + width_bf: str = '0.4m' # TODO: unit-aware type + height_hf: str = '0.15m' # TODO: unit-aware type + width_bw: str = '0.25m' # TODO: unit-aware type + area_a: str = '1 m^2' # TODO: unit-aware type + set_modification_factors: Literal['0', '1'] = '0' mod_a: int = 1 + diff --git a/src/otko/core/catalog/generated/damage2p.py b/src/otko/core/catalog/generated/damage2p.py index 58bf76b..77e3c1a 100644 --- a/src/otko/core/catalog/generated/damage2p.py +++ b/src/otko/core/catalog/generated/damage2p.py @@ -17,22 +17,21 @@ from pydantic import BaseModel, ConfigDict, Field class Damage2pSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Damage2p", - "x-book": "Multidimensional_(nD)_Materials", + 'x-gid-name': 'Damage2p', + 'x-book': 'Multidimensional_(nD)_Materials', } ) - material: Literal["Damage2p"] = "Damage2p" - concrete_compressive_strength: str = "-20MPa" # TODO: unit-aware type + material: Literal['Damage2p'] = 'Damage2p' + concrete_compressive_strength: str = '-20MPa' # TODO: unit-aware type calculate_default_values_update_to_apply_cb_1_0: int = 1 - young_modulus: str = "21.243GPa" # TODO: unit-aware type - concrete_tensile_strength: str = "2.0MPa" # TODO: unit-aware type + young_modulus: str = '21.243GPa' # TODO: unit-aware type + concrete_tensile_strength: str = '2.0MPa' # TODO: unit-aware type poisson_coefficient: float = 0.15 - tension_fracture_energy_density: str = "3.465e-004GPa" # TODO: unit-aware type - comp_fracture_energy_density: str = "1.177e-001GPa" # TODO: unit-aware type + tension_fracture_energy_density: str = '3.465e-004GPa' # TODO: unit-aware type + comp_fracture_energy_density: str = '1.177e-001GPa' # TODO: unit-aware type parameter_of_plastic_volume_change: float = 0.20 - linear_hardening_parameter: str = "5.311GPa" # TODO: unit-aware type + linear_hardening_parameter: str = '5.311GPa' # TODO: unit-aware type isotropic_kinematic_hardening_ratio: float = 0.50 - computational_stiffness_matrix: Literal["Computational_tangent", "Damaged_secant_stiffness"] = ( - "Computational_tangent" - ) + computational_stiffness_matrix: Literal['Computational_tangent', 'Damaged_secant_stiffness'] = 'Computational_tangent' + diff --git a/src/otko/core/catalog/generated/displacement_based_beam_column.py b/src/otko/core/catalog/generated/displacement_based_beam_column.py index 1ccfce7..e23893a 100644 --- a/src/otko/core/catalog/generated/displacement_based_beam_column.py +++ b/src/otko/core/catalog/generated/displacement_based_beam_column.py @@ -17,19 +17,18 @@ from pydantic import BaseModel, ConfigDict, Field class DisplacementBasedBeamColumnSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Displacement-Based_Beam-Column", - "x-book": "Beam-Column_Elements", + 'x-gid-name': 'Displacement-Based_Beam-Column', + 'x-book': 'Beam-Column_Elements', } ) - element_type: Literal["dispBeamColumn"] = "dispBeamColumn" - compatible_sections: Literal["_"] = "_" - number_of_integration_points: int = Field( - 3, description="number of integration points along the element" - ) - the_number_of_integration: Literal["_"] = "_" - to_increase_analysis_accuracy: Literal["_"] = "_" - section: str = "Fiber" - geometric_transformation: Literal["Linear", "P-Delta", "Corotational"] = "Linear" - mass_density: str = "0.0ton/m^3" # TODO: unit-aware type - weight_density: str = "25 kN/m^3" # TODO: unit-aware type + element_type: Literal['dispBeamColumn'] = 'dispBeamColumn' + compatible_sections: Literal['_'] = '_' + number_of_integration_points: int = Field(3, description='number of integration points along the element') + the_number_of_integration: Literal['_'] = '_' + to_increase_analysis_accuracy: Literal['_'] = '_' + section: str = 'Fiber' + geometric_transformation: Literal['Linear', 'P-Delta', 'Corotational'] = 'Linear' + mass_density: str = '0.0ton/m^3' # TODO: unit-aware type + weight_density: str = '25 kN/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/elastic.py b/src/otko/core/catalog/generated/elastic.py index ad411c7..79b6ce1 100644 --- a/src/otko/core/catalog/generated/elastic.py +++ b/src/otko/core/catalog/generated/elastic.py @@ -23,187 +23,187 @@ class CyclicDataRow(BaseModel): class ElasticSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Elastic", - "x-book": "Standard_Uniaxial_Materials", - "dependencies": [ + 'x-gid-name': 'Elastic', + 'x-book': 'Standard_Uniaxial_Materials', + 'dependencies': [ { - "field": "formulation", - "gid_name": "Formulation", - "rules": [ + 'field': 'formulation', + 'gid_name': 'Formulation', + 'rules': [ { - "trigger": "Stress-Strain", - "actions": [ + 'trigger': 'Stress-Strain', + 'actions': [ { - "action": "RESTORE", - "field": "Material_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_type', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stiffness_K", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stiffness_K', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Elastic_modulus_E", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Elastic_modulus_E', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Force-Deformation", - "actions": [ + 'trigger': 'Force-Deformation', + 'actions': [ { - "action": "HIDE", - "field": "Material_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_type', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Elastic_modulus_E", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Elastic_modulus_E', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Stiffness_K", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Stiffness_K', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Moment-Rotation", - "actions": [ + 'trigger': 'Moment-Rotation', + 'actions': [ { - "action": "HIDE", - "field": "Material_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_type', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Elastic_modulus_E", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Elastic_modulus_E', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stiffness_K", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stiffness_K', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "material_type", - "gid_name": "Material_type", - "rules": [ + 'field': 'material_type', + 'gid_name': 'Material_type', + 'rules': [ { - "trigger": "Concrete", - "actions": [ + 'trigger': 'Concrete', + 'actions': [ { - "action": "RESTORE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Steel", - "actions": [ + 'trigger': 'Steel', + 'actions': [ { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Custom", - "actions": [ + 'trigger': 'Custom', + 'actions': [ { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -213,32 +213,16 @@ class ElasticSpec(BaseModel): } ) - material: Literal["Elastic"] = Field("Elastic", description="Elastic uniaxial material object") - formulation: Literal["Stress-Strain", "Force-Deformation", "Moment-Rotation"] = "Stress-Strain" - material_type: Literal["Concrete", "Steel", "Custom"] = "Custom" - concrete_class: Literal[ - "C12/15", - "C16/20", - "C20/25", - "C25/30", - "C30/37", - "C35/45", - "C40/50", - "C45/55", - "C50/60", - "C55/67", - "C60/75", - "C70/85", - "C80/95", - "C90/105", - ] = "C20/25" - steel_grade: Literal["S235", "S275", "S355", "S450", "B500"] = "S235" - elastic_modulus_e: str = Field( - "30 GPa", description="Young modulus of elasticity E, considering stress-strain diagram" - ) # TODO: unit-aware type - stiffness_k: str = "4000 kN/m" # TODO: unit-aware type - moment_per_rotation_unit: str = "10000 kNm" # TODO: unit-aware type - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + material: Literal['Elastic'] = Field('Elastic', description='Elastic uniaxial material object') + formulation: Literal['Stress-Strain', 'Force-Deformation', 'Moment-Rotation'] = 'Stress-Strain' + material_type: Literal['Concrete', 'Steel', 'Custom'] = 'Custom' + concrete_class: Literal['C12/15', 'C16/20', 'C20/25', 'C25/30', 'C30/37', 'C35/45', 'C40/50', 'C45/55', 'C50/60', 'C55/67', 'C60/75', 'C70/85', 'C80/95', 'C90/105'] = 'C20/25' + steel_grade: Literal['S235', 'S275', 'S355', 'S450', 'B500'] = 'S235' + elastic_modulus_e: str = Field('30 GPa', description='Young modulus of elasticity E, considering stress-strain diagram') # TODO: unit-aware type + stiffness_k: str = '4000 kN/m' # TODO: unit-aware type + moment_per_rotation_unit: str = '10000 kNm' # TODO: unit-aware type + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/elastic_beam_column.py b/src/otko/core/catalog/generated/elastic_beam_column.py index 0e575f5..4321f87 100644 --- a/src/otko/core/catalog/generated/elastic_beam_column.py +++ b/src/otko/core/catalog/generated/elastic_beam_column.py @@ -17,286 +17,286 @@ from pydantic import BaseModel, ConfigDict, Field class ElasticBeamColumnSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Elastic_Beam-Column", - "x-book": "Beam-Column_Elements", - "dependencies": [ + 'x-gid-name': 'Elastic_Beam-Column', + 'x-book': 'Beam-Column_Elements', + 'dependencies': [ { - "field": "cross_section", - "gid_name": "Cross_section", - "rules": [ + 'field': 'cross_section', + 'gid_name': 'Cross_section', + 'rules': [ { - "trigger": "Rectangular", - "actions": [ + 'trigger': 'Rectangular', + 'actions': [ { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Circular", - "actions": [ + 'trigger': 'Circular', + 'actions': [ { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Tee", - "actions": [ + 'trigger': 'Tee', + 'actions': [ { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "General", - "actions": [ + 'trigger': 'General', + 'actions': [ { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "set_modification_factors", - "gid_name": "Set_modification_factors", - "rules": [ + 'field': 'set_modification_factors', + 'gid_name': 'Set_modification_factors', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "mod._A", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._A', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Iyy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Iyy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Izz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Izz', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Iyz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Iyz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "mod._A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Iyz', + 'target': '#CURRENT#', }, ], }, @@ -306,24 +306,25 @@ class ElasticBeamColumnSpec(BaseModel): } ) - element_type: Literal["ElasticBeamColumn"] = "ElasticBeamColumn" - compatible_materials: Literal["Elastic_Isotropic"] = "Elastic_Isotropic" - material_mat_multidimensional_nd_materials_user_materials: str = "Elastic_Isotropic" - cross_section: Literal["Rectangular", "Tee", "Circular", "General"] = "Rectangular" - height_h: str = "0.5m" # TODO: unit-aware type - width_b: str = "0.3m" # TODO: unit-aware type - diameter_d: str = "0.5m" # TODO: unit-aware type - width_bf: str = "0.4m" # TODO: unit-aware type - height_hf: str = "0.15m" # TODO: unit-aware type - width_bw: str = "0.25m" # TODO: unit-aware type - area_a: str = "1.0 m^2" # TODO: unit-aware type - moment_of_inertia_about_local_y_iyy: str = "0.5 m^4" # TODO: unit-aware type - moment_of_inertia_about_local_z_izz: str = "0.5 m^4" # TODO: unit-aware type - torsional_moment_of_inertia_iyz: str = "1.0 m^4" # TODO: unit-aware type - set_modification_factors: Literal["1", "0"] = "0" + element_type: Literal['ElasticBeamColumn'] = 'ElasticBeamColumn' + compatible_materials: Literal['Elastic_Isotropic'] = 'Elastic_Isotropic' + material_mat_multidimensional_nd_materials_user_materials: str = 'Elastic_Isotropic' + cross_section: Literal['Rectangular', 'Tee', 'Circular', 'General'] = 'Rectangular' + height_h: str = '0.5m' # TODO: unit-aware type + width_b: str = '0.3m' # TODO: unit-aware type + diameter_d: str = '0.5m' # TODO: unit-aware type + width_bf: str = '0.4m' # TODO: unit-aware type + height_hf: str = '0.15m' # TODO: unit-aware type + width_bw: str = '0.25m' # TODO: unit-aware type + area_a: str = '1.0 m^2' # TODO: unit-aware type + moment_of_inertia_about_local_y_iyy: str = '0.5 m^4' # TODO: unit-aware type + moment_of_inertia_about_local_z_izz: str = '0.5 m^4' # TODO: unit-aware type + torsional_moment_of_inertia_iyz: str = '1.0 m^4' # TODO: unit-aware type + set_modification_factors: Literal['1', '0'] = '0' mod_a: int = 1 mod_iyy: int = 1 mod_izz: int = 1 mod_iyz: int = 1 - geometric_transformation: Literal["Linear", "P-Delta", "Corotational"] = "Linear" - weight_density: str = "25 kN/m^3" # TODO: unit-aware type + geometric_transformation: Literal['Linear', 'P-Delta', 'Corotational'] = 'Linear' + weight_density: str = '25 kN/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/elastic_isotropic.py b/src/otko/core/catalog/generated/elastic_isotropic.py index 86cbeb5..8443902 100644 --- a/src/otko/core/catalog/generated/elastic_isotropic.py +++ b/src/otko/core/catalog/generated/elastic_isotropic.py @@ -17,40 +17,40 @@ from pydantic import BaseModel, ConfigDict, Field class ElasticIsotropicSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Elastic_Isotropic", - "x-book": "Multidimensional_(nD)_Materials", - "dependencies": [ + 'x-gid-name': 'Elastic_Isotropic', + 'x-book': 'Multidimensional_(nD)_Materials', + 'dependencies': [ { - "field": "material_type", - "gid_name": "Material_type", - "rules": [ + 'field': 'material_type', + 'gid_name': 'Material_type', + 'rules': [ { - "trigger": "Concrete", - "actions": [ + 'trigger': 'Concrete', + 'actions': [ { - "action": "RESTORE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Steel", - "actions": [ + 'trigger': 'Steel', + 'actions': [ { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Custom", - "actions": [ + 'trigger': 'Custom', + 'actions': [ { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, ], }, @@ -60,24 +60,10 @@ class ElasticIsotropicSpec(BaseModel): } ) - material: Literal["ElasticIsotropic"] = "ElasticIsotropic" - material_type: Literal["Concrete", "Steel", "Custom"] = "Custom" - concrete_class: Literal[ - "C12/15", - "C16/20", - "C20/25", - "C25/30", - "C30/37", - "C35/45", - "C40/50", - "C45/55", - "C50/60", - "C55/67", - "C60/75", - "C70/85", - "C80/95", - "C90/105", - ] = "C20/25" - elastic_modulus_e: str = "30GPa" # TODO: unit-aware type + material: Literal['ElasticIsotropic'] = 'ElasticIsotropic' + material_type: Literal['Concrete', 'Steel', 'Custom'] = 'Custom' + concrete_class: Literal['C12/15', 'C16/20', 'C20/25', 'C25/30', 'C30/37', 'C35/45', 'C40/50', 'C45/55', 'C50/60', 'C55/67', 'C60/75', 'C70/85', 'C80/95', 'C90/105'] = 'C20/25' + elastic_modulus_e: str = '30GPa' # TODO: unit-aware type poisson_s_ratio: float = 0.20 - mass_density: str = "0.0ton/m^3" # TODO: unit-aware type + mass_density: str = '0.0ton/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/elastic_membrane_plate.py b/src/otko/core/catalog/generated/elastic_membrane_plate.py index af479db..a19ab57 100644 --- a/src/otko/core/catalog/generated/elastic_membrane_plate.py +++ b/src/otko/core/catalog/generated/elastic_membrane_plate.py @@ -17,13 +17,14 @@ from pydantic import BaseModel, ConfigDict, Field class ElasticMembranePlateSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Elastic_Membrane_Plate", - "x-book": "Section_Force-Deformation", + 'x-gid-name': 'Elastic_Membrane_Plate', + 'x-book': 'Section_Force-Deformation', } ) - section: Literal["ElasticMembranePlate"] = "ElasticMembranePlate" - elastic_modulus_e: str = "28GPa" # TODO: unit-aware type + section: Literal['ElasticMembranePlate'] = 'ElasticMembranePlate' + elastic_modulus_e: str = '28GPa' # TODO: unit-aware type poisson_s_ratio: float = 0.2 - section_depth_h: str = "0.25m" # TODO: unit-aware type - mass_density: str = "0.0ton/m^3" # TODO: unit-aware type + section_depth_h: str = '0.25m' # TODO: unit-aware type + mass_density: str = '0.0ton/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/elastic_orthotropic.py b/src/otko/core/catalog/generated/elastic_orthotropic.py index f1a225a..5a3f863 100644 --- a/src/otko/core/catalog/generated/elastic_orthotropic.py +++ b/src/otko/core/catalog/generated/elastic_orthotropic.py @@ -17,19 +17,20 @@ from pydantic import BaseModel, ConfigDict, Field class ElasticOrthotropicSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Elastic_Orthotropic", - "x-book": "Multidimensional_(nD)_Materials", + 'x-gid-name': 'Elastic_Orthotropic', + 'x-book': 'Multidimensional_(nD)_Materials', } ) - material: Literal["ElasticOrthotropic"] = "ElasticOrthotropic" - elastic_modulus_ex: str = "30GPa" # TODO: unit-aware type - elastic_modulus_ey: str = "30GPa" # TODO: unit-aware type - elastic_modulus_ez: str = "30GPa" # TODO: unit-aware type + material: Literal['ElasticOrthotropic'] = 'ElasticOrthotropic' + elastic_modulus_ex: str = '30GPa' # TODO: unit-aware type + elastic_modulus_ey: str = '30GPa' # TODO: unit-aware type + elastic_modulus_ez: str = '30GPa' # TODO: unit-aware type poisson_s_ratio_vxy: float = 0.20 poisson_s_ratio_vyz: float = 0.20 poisson_s_ratio_vzx: float = 0.20 - shear_modulus_gxy: str = "11GPa" # TODO: unit-aware type - shear_modulus_gyz: str = "11GPa" # TODO: unit-aware type - shear_modulus_gzx: str = "11GPa" # TODO: unit-aware type - mass_density: str = "0.0ton/m^3" # TODO: unit-aware type + shear_modulus_gxy: str = '11GPa' # TODO: unit-aware type + shear_modulus_gyz: str = '11GPa' # TODO: unit-aware type + shear_modulus_gzx: str = '11GPa' # TODO: unit-aware type + mass_density: str = '0.0ton/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/elastic_perfectly_plastic.py b/src/otko/core/catalog/generated/elastic_perfectly_plastic.py index fee4bf3..4b36761 100644 --- a/src/otko/core/catalog/generated/elastic_perfectly_plastic.py +++ b/src/otko/core/catalog/generated/elastic_perfectly_plastic.py @@ -23,332 +23,332 @@ class CyclicDataRow(BaseModel): class ElasticPerfectlyPlasticSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Elastic_Perfectly_Plastic", - "x-book": "Standard_Uniaxial_Materials", - "dependencies": [ + 'x-gid-name': 'Elastic_Perfectly_Plastic', + 'x-book': 'Standard_Uniaxial_Materials', + 'dependencies': [ { - "field": "formulation", - "gid_name": "Formulation", - "rules": [ + 'field': 'formulation', + 'gid_name': 'Formulation', + 'rules': [ { - "trigger": "Stress-Strain", - "actions": [ + 'trigger': 'Stress-Strain', + 'actions': [ { - "action": "RESTORE", - "field": "Material_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_type', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stiffness_K", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stiffness_K', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Elastic_modulus_E", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Elastic_modulus_E', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Deformation_epsN", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Deformation_epsN', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Deformation_epsP", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Deformation_epsP', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Initial_strain_eps0", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Initial_strain_eps0', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_deformation_eps0", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_deformation_eps0', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_epsP", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_epsP', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_epsN", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_epsN', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_rotation_eps0", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_rotation_eps0', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Force-Deformation", - "actions": [ + 'trigger': 'Force-Deformation', + 'actions': [ { - "action": "HIDE", - "field": "Material_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_type', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Elastic_modulus_E", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Elastic_modulus_E', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Stiffness_K", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Stiffness_K', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Deformation_epsN", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Deformation_epsN', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Deformation_epsP", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Deformation_epsP', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_epsN", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_epsN', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_epsP", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_epsP', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_strain_eps0", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_strain_eps0', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Initial_deformation_eps0", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Initial_deformation_eps0', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_epsP", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_epsP', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_epsN", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_epsN', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_rotation_eps0", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_rotation_eps0', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Moment-Rotation", - "actions": [ + 'trigger': 'Moment-Rotation', + 'actions': [ { - "action": "HIDE", - "field": "Material_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_type', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Elastic_modulus_E", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Elastic_modulus_E', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stiffness_K", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stiffness_K', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Deformation_epsN", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Deformation_epsN', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Deformation_epsP", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Deformation_epsP', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_epsN", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_epsN', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_epsP", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_epsP', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_strain_eps0", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_strain_eps0', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_deformation_eps0", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_deformation_eps0', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Rotation_epsP", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rotation_epsP', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Rotation_epsN", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rotation_epsN', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Initial_rotation_eps0", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Initial_rotation_eps0', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "material_type", - "gid_name": "Material_type", - "rules": [ + 'field': 'material_type', + 'gid_name': 'Material_type', + 'rules': [ { - "trigger": "Concrete", - "actions": [ + 'trigger': 'Concrete', + 'actions': [ { - "action": "RESTORE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Steel", - "actions": [ + 'trigger': 'Steel', + 'actions': [ { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Custom", - "actions": [ + 'trigger': 'Custom', + 'actions': [ { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -358,50 +358,26 @@ class ElasticPerfectlyPlasticSpec(BaseModel): } ) - material: Literal["ElasticPerfectlyPlastic"] = Field( - "ElasticPerfectlyPlastic", description="elastic perfectly-plastic uniaxial material object" - ) - formulation: Literal["Stress-Strain", "Force-Deformation", "Moment-Rotation"] = "Stress-Strain" - material_type: Literal["Concrete", "Steel", "Custom"] = "Custom" - concrete_class: Literal[ - "C12/15", - "C16/20", - "C20/25", - "C25/30", - "C30/37", - "C35/45", - "C40/50", - "C45/55", - "C50/60", - "C55/67", - "C60/75", - "C70/85", - "C80/95", - "C90/105", - ] = "C20/25" - strength_type: Literal["Mean", "Characteristic"] = "Characteristic" - steel_grade: Literal["S235", "S275", "S355", "S450", "B500"] = "B500" - elastic_modulus_e: str = Field( - "20GPa", description="young modulus of elasticity E" - ) # TODO: unit-aware type - stiffness_k: str = "4000 kN/m" # TODO: unit-aware type - moment_per_rotation_unit: str = "10000 kNm" # TODO: unit-aware type - strain_epsp: float = Field( - 1.10e-004, - description="strain or deformation at which material reaches plastic state in tension", - ) - strain_epsn: float = Field( - -2.0e-3, - description="strain or deformation at which material reaches plastic state in compression", - ) - deformation_epsp: str = "1m" # TODO: unit-aware type - deformation_epsn: str = "-1m" # TODO: unit-aware type - rotation_epsp: str = "0.5 rad" # TODO: unit-aware type - rotation_epsn: str = "-0.5 rad" # TODO: unit-aware type + material: Literal['ElasticPerfectlyPlastic'] = Field('ElasticPerfectlyPlastic', description='elastic perfectly-plastic uniaxial material object') + formulation: Literal['Stress-Strain', 'Force-Deformation', 'Moment-Rotation'] = 'Stress-Strain' + material_type: Literal['Concrete', 'Steel', 'Custom'] = 'Custom' + concrete_class: Literal['C12/15', 'C16/20', 'C20/25', 'C25/30', 'C30/37', 'C35/45', 'C40/50', 'C45/55', 'C50/60', 'C55/67', 'C60/75', 'C70/85', 'C80/95', 'C90/105'] = 'C20/25' + strength_type: Literal['Mean', 'Characteristic'] = 'Characteristic' + steel_grade: Literal['S235', 'S275', 'S355', 'S450', 'B500'] = 'B500' + elastic_modulus_e: str = Field('20GPa', description='young modulus of elasticity E') # TODO: unit-aware type + stiffness_k: str = '4000 kN/m' # TODO: unit-aware type + moment_per_rotation_unit: str = '10000 kNm' # TODO: unit-aware type + strain_epsp: float = Field(1.10e-004, description='strain or deformation at which material reaches plastic state in tension') + strain_epsn: float = Field(-2.0e-3, description='strain or deformation at which material reaches plastic state in compression') + deformation_epsp: str = '1m' # TODO: unit-aware type + deformation_epsn: str = '-1m' # TODO: unit-aware type + rotation_epsp: str = '0.5 rad' # TODO: unit-aware type + rotation_epsn: str = '-0.5 rad' # TODO: unit-aware type initial_strain_eps0: float = 0.0 - initial_deformation_eps0: str = "0.0m" # TODO: unit-aware type - initial_rotation_eps0: str = "0.0 rad" # TODO: unit-aware type - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + initial_deformation_eps0: str = '0.0m' # TODO: unit-aware type + initial_rotation_eps0: str = '0.0 rad' # TODO: unit-aware type + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/elastic_perfectly_plastic_with_gap.py b/src/otko/core/catalog/generated/elastic_perfectly_plastic_with_gap.py index e460af0..27221b1 100644 --- a/src/otko/core/catalog/generated/elastic_perfectly_plastic_with_gap.py +++ b/src/otko/core/catalog/generated/elastic_perfectly_plastic_with_gap.py @@ -23,307 +23,307 @@ class CyclicDataRow(BaseModel): class ElasticPerfectlyPlasticWithGapSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Elastic_Perfectly_Plastic_with_Gap", - "x-book": "Standard_Uniaxial_Materials", - "dependencies": [ + 'x-gid-name': 'Elastic_Perfectly_Plastic_with_Gap', + 'x-book': 'Standard_Uniaxial_Materials', + 'dependencies': [ { - "field": "formulation", - "gid_name": "Formulation", - "rules": [ + 'field': 'formulation', + 'gid_name': 'Formulation', + 'rules': [ { - "trigger": "Stress-Strain", - "actions": [ + 'trigger': 'Stress-Strain', + 'actions': [ { - "action": "RESTORE", - "field": "Elastic_modulus_E", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Elastic_modulus_E', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Yield_Stress_Fy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Yield_Stress_Fy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Material_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_type', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Strain_gap", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strain_gap', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Deformation_gap", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Deformation_gap', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stiffness_K", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stiffness_K', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_My", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_My', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_gap", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_gap', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Force-Deformation", - "actions": [ + 'trigger': 'Force-Deformation', + 'actions': [ { - "action": "HIDE", - "field": "Elastic_modulus_E", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Elastic_modulus_E', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_gap", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_gap', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Yield_Stress_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Yield_Stress_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Material_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_type', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Stiffness_K", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Stiffness_K', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Force_Fy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Force_Fy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Deformation_gap", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Deformation_gap', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_My", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_My', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_gap", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_gap', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Moment-Rotation", - "actions": [ + 'trigger': 'Moment-Rotation', + 'actions': [ { - "action": "HIDE", - "field": "Elastic_modulus_E", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Elastic_modulus_E', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_gap", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_gap', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Yield_Stress_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Yield_Stress_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Material_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_type', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stiffness_K", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stiffness_K', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Deformation_gap", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Deformation_gap', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_My", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_My', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Rotation_gap", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rotation_gap', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "material_type", - "gid_name": "Material_type", - "rules": [ + 'field': 'material_type', + 'gid_name': 'Material_type', + 'rules': [ { - "trigger": "Concrete", - "actions": [ + 'trigger': 'Concrete', + 'actions': [ { - "action": "RESTORE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Steel", - "actions": [ + 'trigger': 'Steel', + 'actions': [ { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Yield_Stress_Fy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Yield_Stress_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Custom", - "actions": [ + 'trigger': 'Custom', + 'actions': [ { - "action": "HIDE", - "field": "Concrete_class", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Concrete_class', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Yield_Stress_Fy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Yield_Stress_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strength_type", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strength_type', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -333,45 +333,23 @@ class ElasticPerfectlyPlasticWithGapSpec(BaseModel): } ) - material: Literal["ElasticPerfectlyPlasticwithGap"] = Field( - "ElasticPerfectlyPlasticwithGap", - description="elastic perfectly-plastic gap uniaxial material object", - ) - formulation: Literal["Stress-Strain", "Force-Deformation", "Moment-Rotation"] = "Stress-Strain" - material_type: Literal["Concrete", "Steel", "Custom"] = "Custom" - concrete_class: Literal[ - "C12/15", - "C16/20", - "C20/25", - "C25/30", - "C30/37", - "C35/45", - "C40/50", - "C45/55", - "C50/60", - "C55/67", - "C60/75", - "C70/85", - "C80/95", - "C90/105", - ] = "C20/25" - strength_type: Literal["Mean", "Characteristic"] = "Characteristic" - steel_grade: Literal["S235", "S275", "S355", "S450", "B500"] = "B500" - elastic_modulus_e: str = Field( - "20GPa", description="young modulus of elasticity E" - ) # TODO: unit-aware type - stiffness_k: str = "4000 kN/m" # TODO: unit-aware type - moment_per_rotation_unit: str = "10000 kNm" # TODO: unit-aware type - yield_stress_fy: str = "-20MPa" # TODO: unit-aware type - force_fy: str = Field( - "1000 kN", - description="Yield stress at which material reaches the plastic plate. Negative value for compression.", - ) # TODO: unit-aware type - moment_my: str = "150 kNm" # TODO: unit-aware type + material: Literal['ElasticPerfectlyPlasticwithGap'] = Field('ElasticPerfectlyPlasticwithGap', description='elastic perfectly-plastic gap uniaxial material object') + formulation: Literal['Stress-Strain', 'Force-Deformation', 'Moment-Rotation'] = 'Stress-Strain' + material_type: Literal['Concrete', 'Steel', 'Custom'] = 'Custom' + concrete_class: Literal['C12/15', 'C16/20', 'C20/25', 'C25/30', 'C30/37', 'C35/45', 'C40/50', 'C45/55', 'C50/60', 'C55/67', 'C60/75', 'C70/85', 'C80/95', 'C90/105'] = 'C20/25' + strength_type: Literal['Mean', 'Characteristic'] = 'Characteristic' + steel_grade: Literal['S235', 'S275', 'S355', 'S450', 'B500'] = 'B500' + elastic_modulus_e: str = Field('20GPa', description='young modulus of elasticity E') # TODO: unit-aware type + stiffness_k: str = '4000 kN/m' # TODO: unit-aware type + moment_per_rotation_unit: str = '10000 kNm' # TODO: unit-aware type + yield_stress_fy: str = '-20MPa' # TODO: unit-aware type + force_fy: str = Field('1000 kN', description='Yield stress at which material reaches the plastic plate. Negative value for compression.') # TODO: unit-aware type + moment_my: str = '150 kNm' # TODO: unit-aware type strain_gap: float = 0.0 - deformation_gap: str = "0.0m" # TODO: unit-aware type - rotation_gap: str = "0.0 rad" # TODO: unit-aware type - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + deformation_gap: str = '0.0m' # TODO: unit-aware type + rotation_gap: str = '0.0 rad' # TODO: unit-aware type + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/elastic_section.py b/src/otko/core/catalog/generated/elastic_section.py index dc17b4f..2d98fb1 100644 --- a/src/otko/core/catalog/generated/elastic_section.py +++ b/src/otko/core/catalog/generated/elastic_section.py @@ -17,332 +17,332 @@ from pydantic import BaseModel, ConfigDict, Field class ElasticSectionSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Elastic_Section", - "x-book": "Section_Force-Deformation", - "dependencies": [ + 'x-gid-name': 'Elastic_Section', + 'x-book': 'Section_Force-Deformation', + 'dependencies': [ { - "field": "calculate_shear_modulus_update_to_apply_cb_0_1", - "gid_name": "Calculate_Shear_modulus_(update_to_apply)#CB#(0,1)", - "rules": [ + 'field': 'calculate_shear_modulus_update_to_apply_cb_0_1', + 'gid_name': 'Calculate_Shear_modulus_(update_to_apply)#CB#(0,1)', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Poisson's_ratio", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': "Poisson's_ratio", + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Poisson's_ratio", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': "Poisson's_ratio", + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "cross_section", - "gid_name": "Cross_section", - "rules": [ + 'field': 'cross_section', + 'gid_name': 'Cross_section', + 'rules': [ { - "trigger": "Rectangular", - "actions": [ + 'trigger': 'Rectangular', + 'actions': [ { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Circular", - "actions": [ + 'trigger': 'Circular', + 'actions': [ { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Tee", - "actions": [ + 'trigger': 'Tee', + 'actions': [ { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "General", - "actions": [ + 'trigger': 'General', + 'actions': [ { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "set_modification_factors", - "gid_name": "Set_modification_factors", - "rules": [ + 'field': 'set_modification_factors', + 'gid_name': 'Set_modification_factors', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "mod._A", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._A', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Iyy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Iyy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Izz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Izz', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Iyz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Iyz', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Sy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Sy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Sz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Sz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "mod._A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Iyz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Sy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Sy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Sz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Sz', + 'target': '#CURRENT#', }, ], }, @@ -352,25 +352,26 @@ class ElasticSectionSpec(BaseModel): } ) - section: Literal["ElasticSection"] = "ElasticSection" - elastic_modulus_e: str = "28 GPa" # TODO: unit-aware type + section: Literal['ElasticSection'] = 'ElasticSection' + elastic_modulus_e: str = '28 GPa' # TODO: unit-aware type calculate_shear_modulus_update_to_apply_cb_0_1: int = 0 - shear_modulus_g: str = "11.66666667 GPa" # TODO: unit-aware type - cross_section: Literal["Rectangular", "Tee", "Circular", "General"] = "Rectangular" - height_h: str = "0.5m" # TODO: unit-aware type - width_b: str = "0.3m" # TODO: unit-aware type - diameter_d: str = "0.5m" # TODO: unit-aware type - width_bf: str = "0.4m" # TODO: unit-aware type - height_hf: str = "0.15m" # TODO: unit-aware type - width_bw: str = "0.25m" # TODO: unit-aware type - area_a: str = "1.0 m^2" # TODO: unit-aware type - moment_of_inertia_about_local_y_iyy: str = "0.5 m^4" # TODO: unit-aware type - moment_of_inertia_about_local_z_izz: str = "0.5 m^4" # TODO: unit-aware type - torsional_moment_of_inertia_iyz: str = "1.0 m^4" # TODO: unit-aware type - set_modification_factors: Literal["1", "0"] = "0" + shear_modulus_g: str = '11.66666667 GPa' # TODO: unit-aware type + cross_section: Literal['Rectangular', 'Tee', 'Circular', 'General'] = 'Rectangular' + height_h: str = '0.5m' # TODO: unit-aware type + width_b: str = '0.3m' # TODO: unit-aware type + diameter_d: str = '0.5m' # TODO: unit-aware type + width_bf: str = '0.4m' # TODO: unit-aware type + height_hf: str = '0.15m' # TODO: unit-aware type + width_bw: str = '0.25m' # TODO: unit-aware type + area_a: str = '1.0 m^2' # TODO: unit-aware type + moment_of_inertia_about_local_y_iyy: str = '0.5 m^4' # TODO: unit-aware type + moment_of_inertia_about_local_z_izz: str = '0.5 m^4' # TODO: unit-aware type + torsional_moment_of_inertia_iyz: str = '1.0 m^4' # TODO: unit-aware type + set_modification_factors: Literal['1', '0'] = '0' mod_a: int = 1 mod_sy: int = 1 mod_sz: int = 1 mod_iyy: int = 1 mod_izz: int = 1 mod_iyz: int = 1 + diff --git a/src/otko/core/catalog/generated/elastic_timoshenko_beam_column.py b/src/otko/core/catalog/generated/elastic_timoshenko_beam_column.py index ca8b6fc..8e02988 100644 --- a/src/otko/core/catalog/generated/elastic_timoshenko_beam_column.py +++ b/src/otko/core/catalog/generated/elastic_timoshenko_beam_column.py @@ -17,306 +17,306 @@ from pydantic import BaseModel, ConfigDict, Field class ElasticTimoshenkoBeamColumnSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Elastic_Timoshenko_Beam-Column", - "x-book": "Beam-Column_Elements", - "dependencies": [ + 'x-gid-name': 'Elastic_Timoshenko_Beam-Column', + 'x-book': 'Beam-Column_Elements', + 'dependencies': [ { - "field": "cross_section", - "gid_name": "Cross_section", - "rules": [ + 'field': 'cross_section', + 'gid_name': 'Cross_section', + 'rules': [ { - "trigger": "Rectangular", - "actions": [ + 'trigger': 'Rectangular', + 'actions': [ { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Circular", - "actions": [ + 'trigger': 'Circular', + 'actions': [ { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Tee", - "actions": [ + 'trigger': 'Tee', + 'actions': [ { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "General", - "actions": [ + 'trigger': 'General', + 'actions': [ { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bw', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_of_inertia_about_local-y_Iyy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_of_inertia_about_local-y_Iyy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_of_inertia_about_local-z_Izz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_of_inertia_about_local-z_Izz', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Torsional_moment_of_inertia_Iyz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Torsional_moment_of_inertia_Iyz', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "set_modification_factors", - "gid_name": "Set_modification_factors", - "rules": [ + 'field': 'set_modification_factors', + 'gid_name': 'Set_modification_factors', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "mod._A", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._A', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Sy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Sy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Sz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Sz', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Iyy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Iyy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Izz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Izz', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "mod._Iyz", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._Iyz', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "mod._A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._A', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Sy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Sy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Sz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Sz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Iyy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Iyy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Izz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Izz', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "mod._Iyz", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._Iyz', + 'target': '#CURRENT#', }, ], }, @@ -326,33 +326,27 @@ class ElasticTimoshenkoBeamColumnSpec(BaseModel): } ) - element_type: Literal["ElasticTimoshenkoBeamColumn"] = Field( - "ElasticTimoshenkoBeamColumn", - description="A Timoshenko beam is a frame member that accounts for shear deformations.", - ) - compatible_materials: Literal["Elastic_Isotropic"] = "Elastic_Isotropic" - material_mat_multidimensional_nd_materials_user_materials: str = "Elastic_Isotropic" - cross_section: Literal["Rectangular", "Tee", "Circular", "General"] = "Rectangular" - height_h: str = "0.5m" # TODO: unit-aware type - width_b: str = "0.3m" # TODO: unit-aware type - diameter_d: str = "0.5m" # TODO: unit-aware type - width_bf: str = "0.4m" # TODO: unit-aware type - height_hf: str = "0.15m" # TODO: unit-aware type - width_bw: str = "0.25m" # TODO: unit-aware type - area_a: str = "1.0 m^2" # TODO: unit-aware type - moment_of_inertia_about_local_y_iyy: str = "0.5 m^4" # TODO: unit-aware type - moment_of_inertia_about_local_z_izz: str = "0.5 m^4" # TODO: unit-aware type - torsional_moment_of_inertia_iyz: str = "1.0 m^4" # TODO: unit-aware type - set_modification_factors: Literal["1", "0"] = "0" + element_type: Literal['ElasticTimoshenkoBeamColumn'] = Field('ElasticTimoshenkoBeamColumn', description='A Timoshenko beam is a frame member that accounts for shear deformations.') + compatible_materials: Literal['Elastic_Isotropic'] = 'Elastic_Isotropic' + material_mat_multidimensional_nd_materials_user_materials: str = 'Elastic_Isotropic' + cross_section: Literal['Rectangular', 'Tee', 'Circular', 'General'] = 'Rectangular' + height_h: str = '0.5m' # TODO: unit-aware type + width_b: str = '0.3m' # TODO: unit-aware type + diameter_d: str = '0.5m' # TODO: unit-aware type + width_bf: str = '0.4m' # TODO: unit-aware type + height_hf: str = '0.15m' # TODO: unit-aware type + width_bw: str = '0.25m' # TODO: unit-aware type + area_a: str = '1.0 m^2' # TODO: unit-aware type + moment_of_inertia_about_local_y_iyy: str = '0.5 m^4' # TODO: unit-aware type + moment_of_inertia_about_local_z_izz: str = '0.5 m^4' # TODO: unit-aware type + torsional_moment_of_inertia_iyz: str = '1.0 m^4' # TODO: unit-aware type + set_modification_factors: Literal['1', '0'] = '0' mod_a: int = 1 - mod_sy: int = Field( - 1, description="This value will be multiplied by the Area (A) for local-y Shear Area (Sy)" - ) - mod_sz: int = Field( - 1, description="This value will be multiplied by the Area (A) for local-z Shear Area (Sz)" - ) + mod_sy: int = Field(1, description='This value will be multiplied by the Area (A) for local-y Shear Area (Sy)') + mod_sz: int = Field(1, description='This value will be multiplied by the Area (A) for local-z Shear Area (Sz)') mod_iyy: int = 1 mod_izz: int = 1 mod_iyz: int = 1 - geometric_transformation: Literal["Linear", "P-Delta", "Corotational"] = "Linear" - weight_density: str = "25 kN/m^3" # TODO: unit-aware type + geometric_transformation: Literal['Linear', 'P-Delta', 'Corotational'] = 'Linear' + weight_density: str = '25 kN/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/fiber.py b/src/otko/core/catalog/generated/fiber.py index 5ee6e3f..a047e2d 100644 --- a/src/otko/core/catalog/generated/fiber.py +++ b/src/otko/core/catalog/generated/fiber.py @@ -17,1968 +17,1968 @@ from pydantic import BaseModel, ConfigDict, Field class FiberSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Fiber", - "x-book": "Section_Force-Deformation", - "dependencies": [ + 'x-gid-name': 'Fiber', + 'x-book': 'Section_Force-Deformation', + 'dependencies': [ { - "field": "cross_section", - "gid_name": "Cross_section", - "rules": [ + 'field': 'cross_section', + 'gid_name': 'Cross_section', + 'rules': [ { - "trigger": "Rectangular_Column", - "actions": [ + 'trigger': 'Rectangular_Column', + 'actions': [ { - "action": "RESTORE", - "field": "Core_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Core_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Cover_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cover_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Web_width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Web_width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_thickness_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_thickness_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_b", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_b', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_arc", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_arc', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_in_the_circumferential_direction", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_in_the_circumferential_direction', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_in_the_radial_direction", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_in_the_radial_direction', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_along_h_length", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_along_h_length', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_along_b_length", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_along_b_length', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Corner_bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Corner_bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Middle_bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Middle_bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Corner_bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Corner_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Middle_bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Middle_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_h_length", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_h_length', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_b_length", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_b_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bw_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bw_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_width_wt", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_width_wt', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_width_wb", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_width_wb', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_thickness_ts1", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_thickness_ts1', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_thickness_ts2", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_thickness_ts2', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hv", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hv', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "External_web_thickness_tw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'External_web_thickness_tw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Include_additional_part", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Include_additional_part', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Solid_Section", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Solid_Section', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_top_layer_of_top_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_top_layer_of_top_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_bottom_layer_of_top_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_bottom_layer_of_top_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_top_layer_of_bottom_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_top_layer_of_bottom_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_bottom_layer_of_bottom_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_bottom_layer_of_bottom_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_beam_height", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_beam_height', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_beam_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_beam_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_top_slab_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_top_slab_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_top_slab_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_top_slab_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bottom_slab_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bottom_slab_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bottom_slab_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bottom_slab_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_external_web_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_external_web_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_internal_web_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_internal_web_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_web_height", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_web_height', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Main_section_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Main_section_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Circular_Column", - "actions": [ + 'trigger': 'Circular_Column', + 'actions': [ { - "action": "RESTORE", - "field": "Core_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Core_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Cover_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cover_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Web_width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Web_width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_thickness_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_thickness_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_b", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_b', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_along_arc", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_along_arc', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_in_the_circumferential_direction", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_in_the_circumferential_direction', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_in_the_radial_direction", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_in_the_radial_direction', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_h_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_h_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_b_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_b_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Corner_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Corner_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Middle_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Middle_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Corner_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Corner_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Middle_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Middle_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_h_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_h_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_b_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_b_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bw_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bw_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_width_wt", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_width_wt', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_width_wb", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_width_wb', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_thickness_ts1", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_thickness_ts1', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_thickness_ts2", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_thickness_ts2', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hv", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hv', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "External_web_thickness_tw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'External_web_thickness_tw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Include_additional_part", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Include_additional_part', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Solid_Section", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Solid_Section', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_top_layer_of_top_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_top_layer_of_top_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_bottom_layer_of_top_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_bottom_layer_of_top_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_top_layer_of_bottom_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_top_layer_of_bottom_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_bottom_layer_of_bottom_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_bottom_layer_of_bottom_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_beam_height", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_beam_height', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_beam_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_beam_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_top_slab_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_top_slab_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_top_slab_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_top_slab_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bottom_slab_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bottom_slab_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bottom_slab_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bottom_slab_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_external_web_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_external_web_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_internal_web_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_internal_web_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_web_height", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_web_height', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Main_section_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Main_section_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Rectangular_Beam", - "actions": [ + 'trigger': 'Rectangular_Beam', + 'actions': [ { - "action": "RESTORE", - "field": "Core_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Core_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Cover_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cover_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Web_width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Web_width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_thickness_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_thickness_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Corner_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Corner_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Middle_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Middle_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Corner_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Corner_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Middle_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Middle_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_b_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_b_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_h_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_h_length', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_b", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_b', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_arc", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_arc', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Top_bars", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Top_bars', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bottom_bars", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bottom_bars', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Top_bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Top_bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bottom_bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bottom_bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Top_bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Top_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bottom_bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bottom_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_h_length", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_h_length', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_b_length", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_b_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bw_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bw_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_in_the_circumferential_direction", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_in_the_circumferential_direction', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_in_the_radial_direction", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_in_the_radial_direction', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_width_wt", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_width_wt', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_width_wb", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_width_wb', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_thickness_ts1", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_thickness_ts1', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_thickness_ts2", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_thickness_ts2', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hv", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hv', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "External_web_thickness_tw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'External_web_thickness_tw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Include_additional_part", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Include_additional_part', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Solid_Section", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Solid_Section', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_top_layer_of_top_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_top_layer_of_top_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_bottom_layer_of_top_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_bottom_layer_of_top_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_top_layer_of_bottom_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_top_layer_of_bottom_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_bottom_layer_of_bottom_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_bottom_layer_of_bottom_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_beam_height", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_beam_height', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_beam_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_beam_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_top_slab_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_top_slab_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_top_slab_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_top_slab_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bottom_slab_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bottom_slab_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bottom_slab_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bottom_slab_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_external_web_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_external_web_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_internal_web_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_internal_web_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_web_height", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_web_height', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Main_section_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Main_section_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Tee_Beam", - "actions": [ + 'trigger': 'Tee_Beam', + 'actions': [ { - "action": "RESTORE", - "field": "Core_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Core_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Cover_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cover_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bottom_beam_bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bottom_beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bottom_beam_bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bottom_beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bottom_beam_bars", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bottom_beam_bars', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Top_beam_bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Top_beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Slab_bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Slab_bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Top_beam_bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Top_beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Web_width_bw", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Web_width_bw', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Slab_thickness_hf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Slab_thickness_hf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Top_beam_bars", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Top_beam_bars', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Slab_bars", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Slab_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Corner_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Corner_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Middle_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Middle_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Corner_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Corner_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Middle_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Middle_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_b_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_b_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_h_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_h_length', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_b", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_b', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_arc", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_arc', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_h_length", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_h_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_b_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_b_length', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_bw_length", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_bw_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_in_the_circumferential_direction", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_in_the_circumferential_direction', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_in_the_radial_direction", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_in_the_radial_direction', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_width_wt", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_width_wt', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_width_wb", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_width_wb', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_thickness_ts1", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_thickness_ts1', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_thickness_ts2", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_thickness_ts2', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hv", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hv', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "External_web_thickness_tw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'External_web_thickness_tw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Include_additional_part", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Include_additional_part', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Solid_Section", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Solid_Section', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_top_layer_of_top_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_top_layer_of_top_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_bottom_layer_of_top_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_bottom_layer_of_top_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_top_layer_of_bottom_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_top_layer_of_bottom_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_bottom_layer_of_bottom_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_bottom_layer_of_bottom_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_beam_height", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_beam_height', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_beam_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_beam_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_top_slab_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_top_slab_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_top_slab_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_top_slab_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bottom_slab_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bottom_slab_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bottom_slab_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bottom_slab_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_external_web_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_external_web_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_internal_web_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_internal_web_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_web_height", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_web_height', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Main_section_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Main_section_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Bridge_Deck", - "actions": [ + 'trigger': 'Bridge_Deck', + 'actions': [ { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_b", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_b', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Web_width_bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Web_width_bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_thickness_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_thickness_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bars", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bars', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_h_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_h_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_b_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_b_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_arc", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_arc', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Corner_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Corner_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Middle_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Middle_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Corner_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Corner_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Middle_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Middle_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Top_beam_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Top_beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bottom_beam_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bottom_beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_h_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_h_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_b_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_b_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_bw_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_bw_length', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_in_the_circumferential_direction", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_in_the_circumferential_direction', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_in_the_radial_direction", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_in_the_radial_direction', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Core_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Core_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Cover_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cover_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Top_slab_width_wt", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Top_slab_width_wt', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bottom_slab_width_wb", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bottom_slab_width_wb', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Top_slab_thickness_ts1", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Top_slab_thickness_ts1', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bottom_slab_thickness_ts2", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bottom_slab_thickness_ts2', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Height_hv", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_hv', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "External_web_thickness_tw", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'External_web_thickness_tw', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Include_additional_part", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Include_additional_part', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Solid_Section", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Solid_Section', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_on_top_layer_of_top_slab", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_on_top_layer_of_top_slab', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_on_bottom_layer_of_top_slab", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_on_bottom_layer_of_top_slab', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_on_top_layer_of_bottom_slab", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_on_top_layer_of_bottom_slab', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_on_bottom_layer_of_bottom_slab", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_on_bottom_layer_of_bottom_slab', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_along_beam_height", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_along_beam_height', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_along_beam_width", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_along_beam_width', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Top_slab_bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Top_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bottom_slab_bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bottom_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Top_slab_bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Top_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bottom_slab_bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bottom_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_top_slab_width", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_top_slab_width', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_top_slab_thickness", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_top_slab_thickness', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_bottom_slab_width", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_bottom_slab_width', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_bottom_slab_thickness", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_bottom_slab_thickness', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_external_web_thickness", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_external_web_thickness', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_internal_web_thickness", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_internal_web_thickness', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_web_height", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_web_height', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Main_section_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Main_section_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Top_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Top_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bottom_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bottom_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "include_additional_part", - "gid_name": "Include_additional_part", - "rules": [ + 'field': 'include_additional_part', + 'gid_name': 'Include_additional_part', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Additional_slab_thickness_ts3", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Additional_slab_thickness_ts3', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Sidewalk_width_bsw", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Sidewalk_width_bsw', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Sidewalk_thickness", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Sidewalk_thickness', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Beam_width_b", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Beam_width_b', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Beam_height", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Beam_height', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_beam_height", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_beam_height', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_beam_width", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_beam_width', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_on_top_layer_of_additional_slab", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_on_top_layer_of_additional_slab', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_on_bottom_layer_of_additional_slab", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_on_bottom_layer_of_additional_slab', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_along_beam_height", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_along_beam_height', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Bars_along_beam_width", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Bars_along_beam_width', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Additional_slab_bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Additional_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Beam_bar_size", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Additional_slab_bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Additional_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Beam_bar_area", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Additional_part_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Additional_part_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Additional_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Additional_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Beam_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Beam_reinforcing_bar_material', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Additional_slab_thickness_ts3", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Additional_slab_thickness_ts3', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Sidewalk_width_bsw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Sidewalk_width_bsw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Sidewalk_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Sidewalk_thickness', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Beam_width_b", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Beam_width_b', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Beam_height", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Beam_height', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_beam_height", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_beam_height', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_beam_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_beam_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_top_layer_of_additional_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_top_layer_of_additional_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_on_bottom_layer_of_additional_slab", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_on_bottom_layer_of_additional_slab', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_beam_height", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_beam_height', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Bars_along_beam_width", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Bars_along_beam_width', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Additional_slab_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Additional_slab_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Beam_bar_size", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Beam_bar_size', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Additional_slab_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Additional_slab_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Beam_bar_area", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Beam_bar_area', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Additional_part_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Additional_part_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Additional_slab_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Additional_slab_reinforcing_bar_material', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Beam_reinforcing_bar_material", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Beam_reinforcing_bar_material', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "solid_section", - "gid_name": "Solid_Section", - "rules": [ + 'field': 'solid_section', + 'gid_name': 'Solid_Section', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "SET", - "field": "Void_width_dv", - "target": "0.0m", + 'action': 'SET', + 'field': 'Void_width_dv', + 'target': '0.0m', }, { - "action": "SET", - "field": "Number_of_voids", - "target": "0", + 'action': 'SET', + 'field': 'Number_of_voids', + 'target': '0', }, { - "action": "HIDE", - "field": "External_web_thickness_tw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'External_web_thickness_tw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Fibers_along_internal_web_thickness", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Fibers_along_internal_web_thickness', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "RESTORE", - "field": "Void_width_dv", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Void_width_dv', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Number_of_voids", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Number_of_voids', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "External_web_thickness_tw", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'External_web_thickness_tw', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Fibers_along_internal_web_thickness", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Fibers_along_internal_web_thickness', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_torsional_stiffness", - "gid_name": "Activate_torsional_stiffness", - "rules": [ + 'field': 'activate_torsional_stiffness', + 'gid_name': 'Activate_torsional_stiffness', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Torsional_stiffness_GJ", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Torsional_stiffness_GJ', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Torsional_stiffness_GJ", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Torsional_stiffness_GJ', + 'target': '#CURRENT#', }, ], }, @@ -1988,36 +1988,34 @@ class FiberSpec(BaseModel): } ) - section: Literal["Fiber"] = "Fiber" - compatible_materials: Literal["_"] = "_" - cross_section: Literal[ - "Rectangular_Column", "Rectangular_Beam", "Tee_Beam", "Circular_Column", "Bridge_Deck" - ] = "Rectangular_Column" - height_h: str = "0.5 m" # TODO: unit-aware type - width_b: str = "0.3 m" # TODO: unit-aware type - width_bf: str = "0.8 m" # TODO: unit-aware type - diameter_d: str = "0.5 m" # TODO: unit-aware type - web_width_bw: str = "0.3 m" # TODO: unit-aware type - slab_thickness_hf: str = "0.15 m" # TODO: unit-aware type - top_slab_width_wt: str = "7.0 m" # TODO: unit-aware type - bottom_slab_width_wb: str = "4.0 m" # TODO: unit-aware type - top_slab_thickness_ts1: str = "0.5 m" # TODO: unit-aware type - bottom_slab_thickness_ts2: str = "0.5 m" # TODO: unit-aware type - height_hv: str = "0.7 m" # TODO: unit-aware type - external_web_thickness_tw: str = "0.4 m" # TODO: unit-aware type - include_additional_part: Literal["1", "0"] = "1" - additional_slab_thickness_ts3: str = "0.5 m" # TODO: unit-aware type - sidewalk_thickness_ts4: str = "0.2 m" # TODO: unit-aware type - sidewalk_width_bsw: str = "1.10 m" # TODO: unit-aware type - beam_width_b: str = "0.4 m" # TODO: unit-aware type - beam_height_h: str = "0.6 m" # TODO: unit-aware type - solid_section: Literal["1", "0"] = "0" - activate_torsional_stiffness: Literal["1", "0"] = "0" - torsional_stiffness_gj: str = "10000 kNm^2" # TODO: unit-aware type + section: Literal['Fiber'] = 'Fiber' + compatible_materials: Literal['_'] = '_' + cross_section: Literal['Rectangular_Column', 'Rectangular_Beam', 'Tee_Beam', 'Circular_Column', 'Bridge_Deck'] = 'Rectangular_Column' + height_h: str = '0.5 m' # TODO: unit-aware type + width_b: str = '0.3 m' # TODO: unit-aware type + width_bf: str = '0.8 m' # TODO: unit-aware type + diameter_d: str = '0.5 m' # TODO: unit-aware type + web_width_bw: str = '0.3 m' # TODO: unit-aware type + slab_thickness_hf: str = '0.15 m' # TODO: unit-aware type + top_slab_width_wt: str = '7.0 m' # TODO: unit-aware type + bottom_slab_width_wb: str = '4.0 m' # TODO: unit-aware type + top_slab_thickness_ts1: str = '0.5 m' # TODO: unit-aware type + bottom_slab_thickness_ts2: str = '0.5 m' # TODO: unit-aware type + height_hv: str = '0.7 m' # TODO: unit-aware type + external_web_thickness_tw: str = '0.4 m' # TODO: unit-aware type + include_additional_part: Literal['1', '0'] = '1' + additional_slab_thickness_ts3: str = '0.5 m' # TODO: unit-aware type + sidewalk_thickness_ts4: str = '0.2 m' # TODO: unit-aware type + sidewalk_width_bsw: str = '1.10 m' # TODO: unit-aware type + beam_width_b: str = '0.4 m' # TODO: unit-aware type + beam_height_h: str = '0.6 m' # TODO: unit-aware type + solid_section: Literal['1', '0'] = '0' + activate_torsional_stiffness: Literal['1', '0'] = '0' + torsional_stiffness_gj: str = '10000 kNm^2' # TODO: unit-aware type number_of_voids: int = 2 - void_width_dv: str = "0.3 m" # TODO: unit-aware type - reinforcement: Literal["_"] = "_" - cover_depth_for_bars: str = "0.05 m" # TODO: unit-aware type + void_width_dv: str = '0.3 m' # TODO: unit-aware type + reinforcement: Literal['_'] = '_' + cover_depth_for_bars: str = '0.05 m' # TODO: unit-aware type top_bars: int = 2 top_beam_bars: int = 2 bottom_beam_bars: int = 3 @@ -2026,14 +2024,14 @@ class FiberSpec(BaseModel): bars_along_h_length: int = 3 bars_along_b_length: int = 3 bars_along_arc: int = 16 - corner_bar_size: str = "18 mm" # TODO: unit-aware type - middle_bar_size: str = "16 mm" # TODO: unit-aware type - bar_size: str = "8 mm" # TODO: unit-aware type - top_bar_size: str = "14 mm" # TODO: unit-aware type - top_beam_bar_size: str = "14 mm" # TODO: unit-aware type - bottom_bar_size: str = "18 mm" # TODO: unit-aware type - bottom_beam_bar_size: str = "18 mm" # TODO: unit-aware type - slab_bar_size: str = "8 mm" # TODO: unit-aware type + corner_bar_size: str = '18 mm' # TODO: unit-aware type + middle_bar_size: str = '16 mm' # TODO: unit-aware type + bar_size: str = '8 mm' # TODO: unit-aware type + top_bar_size: str = '14 mm' # TODO: unit-aware type + top_beam_bar_size: str = '14 mm' # TODO: unit-aware type + bottom_bar_size: str = '18 mm' # TODO: unit-aware type + bottom_beam_bar_size: str = '18 mm' # TODO: unit-aware type + slab_bar_size: str = '8 mm' # TODO: unit-aware type bars_on_top_layer_of_top_slab: int = 10 bars_on_bottom_layer_of_top_slab: int = 10 bars_on_top_layer_of_bottom_slab: int = 10 @@ -2042,24 +2040,24 @@ class FiberSpec(BaseModel): bars_on_bottom_layer_of_additional_slab: int = 10 bars_along_beam_height: int = 5 bars_along_beam_width: int = 5 - top_slab_bar_size: str = "20 mm" # TODO: unit-aware type - bottom_slab_bar_size: str = "20 mm" # TODO: unit-aware type - additional_slab_bar_size: str = "20 mm" # TODO: unit-aware type - beam_bar_size: str = "20 mm" # TODO: unit-aware type + top_slab_bar_size: str = '20 mm' # TODO: unit-aware type + bottom_slab_bar_size: str = '20 mm' # TODO: unit-aware type + additional_slab_bar_size: str = '20 mm' # TODO: unit-aware type + beam_bar_size: str = '20 mm' # TODO: unit-aware type calculate_bar_areas_update_to_apply_cb_0_1: int = 1 - corner_bar_area: str = "2.545e-04 m^2" # TODO: unit-aware type - middle_bar_area: str = "2.011e-04 m^2" # TODO: unit-aware type - bar_area: str = "5.027e-05 m^2" # TODO: unit-aware type - top_bar_area: str = "1.539e-04 m^2" # TODO: unit-aware type - top_beam_bar_area: str = "1.539e-04 m^2" # TODO: unit-aware type - bottom_bar_area: str = "2.545e-04 m^2" # TODO: unit-aware type - bottom_beam_bar_area: str = "2.545e-04 m^2" # TODO: unit-aware type - slab_bar_area: str = "5.027e-05 m^2" # TODO: unit-aware type - top_slab_bar_area: str = "3.142e-04 m^2" # TODO: unit-aware type - bottom_slab_bar_area: str = "3.142e-04 m^2" # TODO: unit-aware type - additional_slab_bar_area: str = "3.142e-04 m^2" # TODO: unit-aware type - beam_bar_area: str = "3.142e-04 m^2" # TODO: unit-aware type - fibers: Literal["_"] = "_" + corner_bar_area: str = '2.545e-04 m^2' # TODO: unit-aware type + middle_bar_area: str = '2.011e-04 m^2' # TODO: unit-aware type + bar_area: str = '5.027e-05 m^2' # TODO: unit-aware type + top_bar_area: str = '1.539e-04 m^2' # TODO: unit-aware type + top_beam_bar_area: str = '1.539e-04 m^2' # TODO: unit-aware type + bottom_bar_area: str = '2.545e-04 m^2' # TODO: unit-aware type + bottom_beam_bar_area: str = '2.545e-04 m^2' # TODO: unit-aware type + slab_bar_area: str = '5.027e-05 m^2' # TODO: unit-aware type + top_slab_bar_area: str = '3.142e-04 m^2' # TODO: unit-aware type + bottom_slab_bar_area: str = '3.142e-04 m^2' # TODO: unit-aware type + additional_slab_bar_area: str = '3.142e-04 m^2' # TODO: unit-aware type + beam_bar_area: str = '3.142e-04 m^2' # TODO: unit-aware type + fibers: Literal['_'] = '_' suggest_fiber_mesh_update_to_apply_cb_0_1: int = 0 fibers_along_h_length: int = 17 fibers_along_b_length: int = 10 @@ -2075,12 +2073,13 @@ class FiberSpec(BaseModel): fibers_along_web_height: int = 10 fibers_along_beam_height: int = 6 fibers_along_beam_width: int = 5 - core_material: str = "Concrete01_(Zero_tensile_strength)" - cover_material: str = "Concrete01_(Zero_tensile_strength)" - reinforcing_bar_material: str = "Steel01" - main_section_material: str = "Concrete01_(Zero_tensile_strength)" - top_slab_reinforcing_bar_material: str = "Steel01" - bottom_slab_reinforcing_bar_material: str = "Steel01" - additional_part_material: str = "Concrete01_(Zero_tensile_strength)" - additional_slab_reinforcing_bar_material: str = "Steel01" - beam_reinforcing_bar_material: str = "Steel01" + core_material: str = 'Concrete01_(Zero_tensile_strength)' + cover_material: str = 'Concrete01_(Zero_tensile_strength)' + reinforcing_bar_material: str = 'Steel01' + main_section_material: str = 'Concrete01_(Zero_tensile_strength)' + top_slab_reinforcing_bar_material: str = 'Steel01' + bottom_slab_reinforcing_bar_material: str = 'Steel01' + additional_part_material: str = 'Concrete01_(Zero_tensile_strength)' + additional_slab_reinforcing_bar_material: str = 'Steel01' + beam_reinforcing_bar_material: str = 'Steel01' + diff --git a/src/otko/core/catalog/generated/fiber_custom.py b/src/otko/core/catalog/generated/fiber_custom.py index 4850326..bce9428 100644 --- a/src/otko/core/catalog/generated/fiber_custom.py +++ b/src/otko/core/catalog/generated/fiber_custom.py @@ -17,30 +17,30 @@ from pydantic import BaseModel, ConfigDict, Field class FiberCustomSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Fiber_Custom", - "x-book": "Section_Force-Deformation", - "dependencies": [ + 'x-gid-name': 'Fiber_Custom', + 'x-book': 'Section_Force-Deformation', + 'dependencies': [ { - "field": "activate_torsional_stiffness", - "gid_name": "Activate_torsional_stiffness", - "rules": [ + 'field': 'activate_torsional_stiffness', + 'gid_name': 'Activate_torsional_stiffness', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Torsional_stiffness_GJ", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Torsional_stiffness_GJ', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Torsional_stiffness_GJ", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Torsional_stiffness_GJ', + 'target': '#CURRENT#', }, ], }, @@ -50,17 +50,18 @@ class FiberCustomSpec(BaseModel): } ) - section: Literal["FiberCustom"] = "FiberCustom" - cross_section_area: str = "0.0 m^2" # TODO: unit-aware type - activate_torsional_stiffness: Literal["1", "0"] = "0" - torsional_stiffness_gj: str = "10000 kNm^2" # TODO: unit-aware type - region_1_material: str = "Concrete01_(Zero_tensile_strength)" - region_2_material: str = "Concrete01_(Zero_tensile_strength)" - region_3_material: str = "Steel02" - region_4_material: str = "Steel02" - region_5_material: str = "Steel02" - region_6_material: str = "Steel02" - region_7_material: str = "Steel02" - region_8_material: str = "Steel02" - region_9_material: str = "Steel02" - region_10_material: str = "Steel02" + section: Literal['FiberCustom'] = 'FiberCustom' + cross_section_area: str = '0.0 m^2' # TODO: unit-aware type + activate_torsional_stiffness: Literal['1', '0'] = '0' + torsional_stiffness_gj: str = '10000 kNm^2' # TODO: unit-aware type + region_1_material: str = 'Concrete01_(Zero_tensile_strength)' + region_2_material: str = 'Concrete01_(Zero_tensile_strength)' + region_3_material: str = 'Steel02' + region_4_material: str = 'Steel02' + region_5_material: str = 'Steel02' + region_6_material: str = 'Steel02' + region_7_material: str = 'Steel02' + region_8_material: str = 'Steel02' + region_9_material: str = 'Steel02' + region_10_material: str = 'Steel02' + diff --git a/src/otko/core/catalog/generated/fiberint.py b/src/otko/core/catalog/generated/fiberint.py index 9f9a812..193d671 100644 --- a/src/otko/core/catalog/generated/fiberint.py +++ b/src/otko/core/catalog/generated/fiberint.py @@ -17,32 +17,33 @@ from pydantic import BaseModel, ConfigDict, Field class FiberintSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "FiberInt", - "x-book": "Section_Force-Deformation", + 'x-gid-name': 'FiberInt', + 'x-book': 'Section_Force-Deformation', } ) - section: Literal["FiberInt"] = "FiberInt" - compatible_materials: Literal["_"] = "_" + section: Literal['FiberInt'] = 'FiberInt' + compatible_materials: Literal['_'] = '_' left_subsection_y_direction_cb: str = "" - thickness: str = "0.5 m" # TODO: unit-aware type - width: str = "0.5 m" # TODO: unit-aware type - cover: str = "0.05 m" # TODO: unit-aware type + thickness: str = '0.5 m' # TODO: unit-aware type + width: str = '0.5 m' # TODO: unit-aware type + cover: str = '0.05 m' # TODO: unit-aware type strips: int = 2 - steel_area: str = "16.08 m^2" # TODO: unit-aware type - middle_subsection: Literal["_"] = "_" - thickness_1: str = "0.25 m" # TODO: unit-aware type - width_1: str = "2 m" # TODO: unit-aware type + steel_area: str = '16.08 m^2' # TODO: unit-aware type + middle_subsection: Literal['_'] = '_' + thickness_1: str = '0.25 m' # TODO: unit-aware type + width_1: str = '2 m' # TODO: unit-aware type strips_1: int = 4 - steel_area_1: str = "10.05 m^2" # TODO: unit-aware type + steel_area_1: str = '10.05 m^2' # TODO: unit-aware type right_subsection_y_direction_cb: str = "" - thickness_2: str = "0.5 m" # TODO: unit-aware type - width_2: str = "0.5 m" # TODO: unit-aware type - cover_1: str = "0.05 m" # TODO: unit-aware type + thickness_2: str = '0.5 m' # TODO: unit-aware type + width_2: str = '0.5 m' # TODO: unit-aware type + cover_1: str = '0.05 m' # TODO: unit-aware type strips_2: int = 2 - steel_area_2: str = "16.08 m^2" # TODO: unit-aware type - horizontal_reinforcement: Literal["_"] = "_" - steel_area_3: str = "10 m^2" # TODO: unit-aware type - core_material: str = "Concrete01_(Zero_tensile_strength)" - cover_material: str = "Concrete01_(Zero_tensile_strength)" - reinforcing_bar_material: str = "Steel01" + steel_area_2: str = '16.08 m^2' # TODO: unit-aware type + horizontal_reinforcement: Literal['_'] = '_' + steel_area_3: str = '10 m^2' # TODO: unit-aware type + core_material: str = 'Concrete01_(Zero_tensile_strength)' + cover_material: str = 'Concrete01_(Zero_tensile_strength)' + reinforcing_bar_material: str = 'Steel01' + diff --git a/src/otko/core/catalog/generated/flexure_shear_interaction_displacement_based_beam_column.py b/src/otko/core/catalog/generated/flexure_shear_interaction_displacement_based_beam_column.py index 10de81b..c0a6567 100644 --- a/src/otko/core/catalog/generated/flexure_shear_interaction_displacement_based_beam_column.py +++ b/src/otko/core/catalog/generated/flexure_shear_interaction_displacement_based_beam_column.py @@ -17,20 +17,19 @@ from pydantic import BaseModel, ConfigDict, Field class FlexureShearInteractionDisplacementBasedBeamColumnSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Flexure-Shear_Interaction_Displacement-Based_Beam-Column", - "x-book": "Beam-Column_Elements", + 'x-gid-name': 'Flexure-Shear_Interaction_Displacement-Based_Beam-Column', + 'x-book': 'Beam-Column_Elements', } ) - element_type: Literal["dispBeamColumnInt"] = "dispBeamColumnInt" - compatible_sections: Literal["_"] = "_" - number_of_integration_points: Literal["3"] = Field( - "3", description="number of integration points along the element" - ) - the_number_of_integration: Literal["_"] = "_" - to_increase_analysis_accuracy: Literal["_"] = "_" - section: str = "FiberInt" + element_type: Literal['dispBeamColumnInt'] = 'dispBeamColumnInt' + compatible_sections: Literal['_'] = '_' + number_of_integration_points: Literal['3'] = Field('3', description='number of integration points along the element') + the_number_of_integration: Literal['_'] = '_' + to_increase_analysis_accuracy: Literal['_'] = '_' + section: str = 'FiberInt' fraction_of_the_height_from_bottom_to_the_rotation_center: float = 0.4 - geometric_transformation: Literal["LinearInt"] = "LinearInt" - mass_density: str = "0.0ton/m^3" # TODO: unit-aware type - weight_density: str = "25 kN/m^3" # TODO: unit-aware type + geometric_transformation: Literal['LinearInt'] = 'LinearInt' + mass_density: str = '0.0ton/m^3' # TODO: unit-aware type + weight_density: str = '25 kN/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/force_based_beam_column.py b/src/otko/core/catalog/generated/force_based_beam_column.py index 183019a..e26d416 100644 --- a/src/otko/core/catalog/generated/force_based_beam_column.py +++ b/src/otko/core/catalog/generated/force_based_beam_column.py @@ -17,40 +17,40 @@ from pydantic import BaseModel, ConfigDict, Field class ForceBasedBeamColumnSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Force-Based_Beam-Column", - "x-book": "Beam-Column_Elements", - "dependencies": [ + 'x-gid-name': 'Force-Based_Beam-Column', + 'x-book': 'Beam-Column_Elements', + 'dependencies': [ { - "field": "activate_iterative_scheme_for_satisfying_element_compatibility", - "gid_name": "Activate_iterative_scheme_for_satisfying_element_compatibility", - "rules": [ + 'field': 'activate_iterative_scheme_for_satisfying_element_compatibility', + 'gid_name': 'Activate_iterative_scheme_for_satisfying_element_compatibility', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Maximum_Iterations", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Maximum_Iterations', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Tolerance", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Tolerance', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Maximum_Iterations", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Maximum_Iterations', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Tolerance", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Tolerance', + 'target': '#CURRENT#', }, ], }, @@ -60,22 +60,17 @@ class ForceBasedBeamColumnSpec(BaseModel): } ) - element_type: Literal["forceBeamColumn"] = "forceBeamColumn" - compatible_sections: Literal["_"] = "_" - integration_type: Literal["Lobatto"] = "Lobatto" - number_of_integration_points: int = Field( - 3, description="number of Gauss-Lobatto integration points along the element" - ) - the_number_of_integration: Literal["_"] = "_" - to_increase_analysis_accuracy: Literal["_"] = "_" - section: str = "Fiber" - geometric_transformation: Literal["Linear", "P-Delta", "Corotational"] = "Linear" - activate_iterative_scheme_for_satisfying_element_compatibility: Literal["1", "0"] = "1" - maximum_iterations: int = Field( - 10, description="maximum number of iterations to undertake to satisfy element compatibility" - ) - tolerance: float = Field( - 1.0e-6, description="tolerance for satisfaction of element compatibility" - ) - mass_density: str = "0.0ton/m^3" # TODO: unit-aware type - weight_density: str = "25 kN/m^3" # TODO: unit-aware type + element_type: Literal['forceBeamColumn'] = 'forceBeamColumn' + compatible_sections: Literal['_'] = '_' + integration_type: Literal['Lobatto'] = 'Lobatto' + number_of_integration_points: int = Field(3, description='number of Gauss-Lobatto integration points along the element') + the_number_of_integration: Literal['_'] = '_' + to_increase_analysis_accuracy: Literal['_'] = '_' + section: str = 'Fiber' + geometric_transformation: Literal['Linear', 'P-Delta', 'Corotational'] = 'Linear' + activate_iterative_scheme_for_satisfying_element_compatibility: Literal['1', '0'] = '1' + maximum_iterations: int = Field(10, description='maximum number of iterations to undertake to satisfy element compatibility') + tolerance: float = Field(1.0e-6, description='tolerance for satisfaction of element compatibility') + mass_density: str = '0.0ton/m^3' # TODO: unit-aware type + weight_density: str = '25 kN/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/ground_motion_record.py b/src/otko/core/catalog/generated/ground_motion_record.py index b21d04d..e41c545 100644 --- a/src/otko/core/catalog/generated/ground_motion_record.py +++ b/src/otko/core/catalog/generated/ground_motion_record.py @@ -17,85 +17,85 @@ from pydantic import BaseModel, ConfigDict, Field class GroundMotionRecordSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Ground_Motion_Record", - "x-book": "Records", - "dependencies": [ + 'x-gid-name': 'Ground_Motion_Record', + 'x-book': 'Records', + 'dependencies': [ { - "field": "record_file_format", - "gid_name": "Record_file_format", - "rules": [ + 'field': 'record_file_format', + 'gid_name': 'Record_file_format', + 'rules': [ { - "trigger": "PEER_format", - "actions": [ + 'trigger': 'PEER_format', + 'actions': [ { - "action": "HIDE", - "field": "Lines_to_skip", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Lines_to_skip', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Time_step", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Time_step', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Time_column", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Time_column', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Value_column", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Value_column', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Single_value_per_line", - "actions": [ + 'trigger': 'Single_value_per_line', + 'actions': [ { - "action": "HIDE", - "field": "Time_column", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Time_column', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Value_column", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Value_column', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Lines_to_skip", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Lines_to_skip', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Time_step", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Time_step', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Time_and_value_per_line", - "actions": [ + 'trigger': 'Time_and_value_per_line', + 'actions': [ { - "action": "RESTORE", - "field": "Lines_to_skip", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Lines_to_skip', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Time_step", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Time_step', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Time_column", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Time_column', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Value_column", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Value_column', + 'target': '#CURRENT#', }, ], }, @@ -106,12 +106,11 @@ class GroundMotionRecordSpec(BaseModel): ) record_file: str = "" - record_type: Literal["Acceleration", "Displacement", "Velocity", "Function"] = "Acceleration" - record_file_format: Literal[ - "PEER_format", "Single_value_per_line", "Time_and_value_per_line" - ] = "Time_and_value_per_line" + record_type: Literal['Acceleration', 'Displacement', 'Velocity', 'Function'] = 'Acceleration' + record_file_format: Literal['PEER_format', 'Single_value_per_line', 'Time_and_value_per_line'] = 'Time_and_value_per_line' lines_to_skip: int = 1 time_step: float = 0.005 - time_column: Literal["1", "2", "3", "4"] = "1" - value_column: Literal["1", "2", "3", "4"] = "2" + time_column: Literal['1', '2', '3', '4'] = '1' + value_column: Literal['1', '2', '3', '4'] = '2' scale_factor: float = 9.81 + diff --git a/src/otko/core/catalog/generated/hyperbolic_gap.py b/src/otko/core/catalog/generated/hyperbolic_gap.py index 809b4b7..bed462d 100644 --- a/src/otko/core/catalog/generated/hyperbolic_gap.py +++ b/src/otko/core/catalog/generated/hyperbolic_gap.py @@ -23,30 +23,30 @@ class CyclicDataRow(BaseModel): class HyperbolicGapSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Hyperbolic_Gap", - "x-book": "Standard_Uniaxial_Materials", - "dependencies": [ + 'x-gid-name': 'Hyperbolic_Gap', + 'x-book': 'Standard_Uniaxial_Materials', + 'dependencies': [ { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -56,11 +56,12 @@ class HyperbolicGapSpec(BaseModel): } ) - material: Literal["_"] = "_" - initial_stiffness: str = "20300 kN/m" # TODO: unit-aware type - unloading_reloading_stiffness: str = "20300 kN/m" # TODO: unit-aware type + material: Literal['_'] = '_' + initial_stiffness: str = '20300 kN/m' # TODO: unit-aware type + unloading_reloading_stiffness: str = '20300 kN/m' # TODO: unit-aware type failure_ratio: float = 0.7 - ultimate_passive_resistance: str = "-326 kN" # TODO: unit-aware type - initial_gap: str = "-2.54 cm" # TODO: unit-aware type - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + ultimate_passive_resistance: str = '-326 kN' # TODO: unit-aware type + initial_gap: str = '-2.54 cm' # TODO: unit-aware type + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) + diff --git a/src/otko/core/catalog/generated/hysteretic.py b/src/otko/core/catalog/generated/hysteretic.py index 1f6d3ad..66b7f0e 100644 --- a/src/otko/core/catalog/generated/hysteretic.py +++ b/src/otko/core/catalog/generated/hysteretic.py @@ -23,591 +23,591 @@ class CyclicDataRow(BaseModel): class HystereticSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Hysteretic", - "x-book": "Uniaxial_Steel_Materials", - "dependencies": [ + 'x-gid-name': 'Hysteretic', + 'x-book': 'Uniaxial_Steel_Materials', + 'dependencies': [ { - "field": "formulation", - "gid_name": "Formulation", - "rules": [ + 'field': 'formulation', + 'gid_name': 'Formulation', + 'rules': [ { - "trigger": "Stress-Strain", - "actions": [ + 'trigger': 'Stress-Strain', + 'actions': [ { - "action": "RESTORE", - "field": "Stress_s1p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Stress_s1p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Stress_s2p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Stress_s2p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Stress_s3p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Stress_s3p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Stress_s1n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Stress_s1n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Stress_s2n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Stress_s2n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Stress_s3n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Stress_s3n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Strain_e1p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strain_e1p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Strain_e2p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strain_e2p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Strain_e3p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strain_e3p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Strain_e1n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strain_e1n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Strain_e2n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strain_e2n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Strain_e3n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Strain_e3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e3n', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Force-Deformation", - "actions": [ + 'trigger': 'Force-Deformation', + 'actions': [ { - "action": "RESTORE", - "field": "Force_s1p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Force_s1p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Force_s2p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Force_s2p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Force_s3p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Force_s3p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Force_s1n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Force_s1n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Force_s2n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Force_s2n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Force_s3n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Force_s3n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Displacement_e1p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Displacement_e1p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Displacement_e2p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Displacement_e2p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Displacement_e3p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Displacement_e3p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Displacement_e1n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Displacement_e1n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Displacement_e2n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Displacement_e2n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Displacement_e3n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Displacement_e3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_s3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_s3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Rotation_e3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Rotation_e3n', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Moment-Rotation", - "actions": [ + 'trigger': 'Moment-Rotation', + 'actions': [ { - "action": "RESTORE", - "field": "Moment_s1p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_s1p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_s2p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_s2p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_s3p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_s3p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_s1n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_s1n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_s2n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_s2n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_s3n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_s3n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Rotation_e1p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rotation_e1p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Rotation_e2p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rotation_e2p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Rotation_e3p", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rotation_e3p', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Rotation_e1n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rotation_e1n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Rotation_e2n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rotation_e2n', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Rotation_e3n", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Rotation_e3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Stress_s3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Stress_s3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Strain_e3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Strain_e3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_s3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_s3n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e1p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e1p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e2p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e2p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e3p", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e3p', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e1n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e1n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e2n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e2n', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Displacement_e3n", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Displacement_e3n', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -617,48 +617,49 @@ class HystereticSpec(BaseModel): } ) - material: Literal["_"] = "_" - formulation: Literal["Stress-Strain", "Force-Deformation", "Moment-Rotation"] = "Stress-Strain" - stress_s1p: str = "400MPa" # TODO: unit-aware type + material: Literal['_'] = '_' + formulation: Literal['Stress-Strain', 'Force-Deformation', 'Moment-Rotation'] = 'Stress-Strain' + stress_s1p: str = '400MPa' # TODO: unit-aware type strain_e1p: float = 2.0e-3 - force_s1p: str = "0.0kN" # TODO: unit-aware type - displacement_e1p: str = "0.0m" # TODO: unit-aware type - moment_s1p: str = "0.0kNm" # TODO: unit-aware type - rotation_e1p: str = "0.0 rad" # TODO: unit-aware type - stress_s2p: str = "500MPa" # TODO: unit-aware type + force_s1p: str = '0.0kN' # TODO: unit-aware type + displacement_e1p: str = '0.0m' # TODO: unit-aware type + moment_s1p: str = '0.0kNm' # TODO: unit-aware type + rotation_e1p: str = '0.0 rad' # TODO: unit-aware type + stress_s2p: str = '500MPa' # TODO: unit-aware type strain_e2p: float = 2.5e-3 - force_s2p: str = "0.0kN" # TODO: unit-aware type - displacement_e2p: str = "0.0m" # TODO: unit-aware type - moment_s2p: str = "0.0kNm" # TODO: unit-aware type - rotation_e2p: str = "0.0 rad" # TODO: unit-aware type - stress_s3p: str = "575MPa" # TODO: unit-aware type + force_s2p: str = '0.0kN' # TODO: unit-aware type + displacement_e2p: str = '0.0m' # TODO: unit-aware type + moment_s2p: str = '0.0kNm' # TODO: unit-aware type + rotation_e2p: str = '0.0 rad' # TODO: unit-aware type + stress_s3p: str = '575MPa' # TODO: unit-aware type strain_e3p: float = 7.5e-2 - force_s3p: str = "0.0kN" # TODO: unit-aware type - displacement_e3p: str = "0.0m" # TODO: unit-aware type - moment_s3p: str = "0.0kNm" # TODO: unit-aware type - rotation_e3p: str = "0.0 rad" # TODO: unit-aware type - stress_s1n: str = "-400MPa" # TODO: unit-aware type + force_s3p: str = '0.0kN' # TODO: unit-aware type + displacement_e3p: str = '0.0m' # TODO: unit-aware type + moment_s3p: str = '0.0kNm' # TODO: unit-aware type + rotation_e3p: str = '0.0 rad' # TODO: unit-aware type + stress_s1n: str = '-400MPa' # TODO: unit-aware type strain_e1n: float = -2.0e-3 - force_s1n: str = "0.0kN" # TODO: unit-aware type - displacement_e1n: str = "0.0m" # TODO: unit-aware type - moment_s1n: str = "0.0kNm" # TODO: unit-aware type - rotation_e1n: str = "0.0 rad" # TODO: unit-aware type - stress_s2n: str = "-500MPa" # TODO: unit-aware type + force_s1n: str = '0.0kN' # TODO: unit-aware type + displacement_e1n: str = '0.0m' # TODO: unit-aware type + moment_s1n: str = '0.0kNm' # TODO: unit-aware type + rotation_e1n: str = '0.0 rad' # TODO: unit-aware type + stress_s2n: str = '-500MPa' # TODO: unit-aware type strain_e2n: float = -2.5e-3 - force_s2n: str = "0.0kN" # TODO: unit-aware type - displacement_e2n: str = "0.0m" # TODO: unit-aware type - moment_s2n: str = "0.0kNm" # TODO: unit-aware type - rotation_e2n: str = "0.0 rad" # TODO: unit-aware type - stress_s3n: str = "-575MPa" # TODO: unit-aware type + force_s2n: str = '0.0kN' # TODO: unit-aware type + displacement_e2n: str = '0.0m' # TODO: unit-aware type + moment_s2n: str = '0.0kNm' # TODO: unit-aware type + rotation_e2n: str = '0.0 rad' # TODO: unit-aware type + stress_s3n: str = '-575MPa' # TODO: unit-aware type strain_e3n: float = -7.5e-2 - force_s3n: str = "0.0kN" # TODO: unit-aware type - displacement_e3n: str = "0.0m" # TODO: unit-aware type - moment_s3n: str = "0.0kNm" # TODO: unit-aware type - rotation_e3n: str = "0.0 rad" # TODO: unit-aware type + force_s3n: str = '0.0kN' # TODO: unit-aware type + displacement_e3n: str = '0.0m' # TODO: unit-aware type + moment_s3n: str = '0.0kNm' # TODO: unit-aware type + rotation_e3n: str = '0.0 rad' # TODO: unit-aware type pinching_factor_for_strain_deformation: int = 1 pinching_factor_for_stress_force: int = 1 damage_due_to_ductility: float = 0.0 damage_due_to_energy: float = 0.2 beta_power: float = 0.0 - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) + diff --git a/src/otko/core/catalog/generated/initial_strain.py b/src/otko/core/catalog/generated/initial_strain.py index e5cc1e9..1ea1283 100644 --- a/src/otko/core/catalog/generated/initial_strain.py +++ b/src/otko/core/catalog/generated/initial_strain.py @@ -17,11 +17,12 @@ from pydantic import BaseModel, ConfigDict, Field class InitialStrainSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Initial_Strain", - "x-book": "Combined_Materials", + 'x-gid-name': 'Initial_Strain', + 'x-book': 'Combined_Materials', } ) - material: Literal["_"] = "_" - material_for_defining_initial_strain: str = "Elastic" + material: Literal['_'] = '_' + material_for_defining_initial_strain: str = 'Elastic' initial_strain: float = 0.0004 + diff --git a/src/otko/core/catalog/generated/initial_stress.py b/src/otko/core/catalog/generated/initial_stress.py index 4938462..cd31467 100644 --- a/src/otko/core/catalog/generated/initial_stress.py +++ b/src/otko/core/catalog/generated/initial_stress.py @@ -17,11 +17,12 @@ from pydantic import BaseModel, ConfigDict, Field class InitialStressSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Initial_Stress", - "x-book": "Combined_Materials", + 'x-gid-name': 'Initial_Stress', + 'x-book': 'Combined_Materials', } ) - material: Literal["_"] = "_" - material_for_defining_initial_stress: str = "Elastic" - initial_stress: str = "-10 MPa" # TODO: unit-aware type + material: Literal['_'] = '_' + material_for_defining_initial_stress: str = 'Elastic' + initial_stress: str = '-10 MPa' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/j2plasticity.py b/src/otko/core/catalog/generated/j2plasticity.py index 1dc77e9..9cea9ee 100644 --- a/src/otko/core/catalog/generated/j2plasticity.py +++ b/src/otko/core/catalog/generated/j2plasticity.py @@ -17,15 +17,16 @@ from pydantic import BaseModel, ConfigDict, Field class J2plasticitySpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "J2Plasticity", - "x-book": "Multidimensional_(nD)_Materials", + 'x-gid-name': 'J2Plasticity', + 'x-book': 'Multidimensional_(nD)_Materials', } ) - material: Literal["J2Plasticity"] = "J2Plasticity" - bulk_modulus: str = "300MPa" # TODO: unit-aware type - shear_modulus: str = "60MPa" # TODO: unit-aware type - initial_yield_stress: str = "0.0kPa" # TODO: unit-aware type - final_saturation_yield_stress: str = "10kPa" # TODO: unit-aware type + material: Literal['J2Plasticity'] = 'J2Plasticity' + bulk_modulus: str = '300MPa' # TODO: unit-aware type + shear_modulus: str = '60MPa' # TODO: unit-aware type + initial_yield_stress: str = '0.0kPa' # TODO: unit-aware type + final_saturation_yield_stress: str = '10kPa' # TODO: unit-aware type exp_hardening_parameter_delta: float = 1.0 linear_hardening_parameter: float = 1.0 + diff --git a/src/otko/core/catalog/generated/layeredshell.py b/src/otko/core/catalog/generated/layeredshell.py index 3b9f2d2..515478e 100644 --- a/src/otko/core/catalog/generated/layeredshell.py +++ b/src/otko/core/catalog/generated/layeredshell.py @@ -17,40 +17,41 @@ from pydantic import BaseModel, ConfigDict, Field class LayeredshellSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "LayeredShell", - "x-book": "Section_Force-Deformation", + 'x-gid-name': 'LayeredShell', + 'x-book': 'Section_Force-Deformation', } ) - section: Literal["LayeredShell"] = "LayeredShell" - wall_width: str = "0.25 m" # TODO: unit-aware type - reinforcement_cover: str = "5 cm" # TODO: unit-aware type + section: Literal['LayeredShell'] = 'LayeredShell' + wall_width: str = '0.25 m' # TODO: unit-aware type + reinforcement_cover: str = '5 cm' # TODO: unit-aware type longitudinal_bars: int = 2 - longitudinal_bar_diameter: str = "16 mm" # TODO: unit-aware type - longitudinal_reinforcement_spacing: str = "100 mm" # TODO: unit-aware type + longitudinal_bar_diameter: str = '16 mm' # TODO: unit-aware type + longitudinal_reinforcement_spacing: str = '100 mm' # TODO: unit-aware type transverse_bars: int = 2 - transverse_bar_diameter: str = "8 mm" # TODO: unit-aware type - transverse_reinforcement_spacing: str = "100 mm" # TODO: unit-aware type + transverse_bar_diameter: str = '8 mm' # TODO: unit-aware type + transverse_reinforcement_spacing: str = '100 mm' # TODO: unit-aware type calculate_reinforcement_ratios_update_to_apply_cb_0_1: int = 1 longitudinal_reinforcement_ratio: float = 1.608e-02 transverse_reinforcement_ratio: float = 4.021e-03 cover_layers: int = 1 core_layers: int = 4 - cover_fc: str = Field("20 MPa", description="positive value") # TODO: unit-aware type - cover_ft: str = Field("2.2 MPa", description="positive value") # TODO: unit-aware type - cover_fu: str = Field("-17.0 MPa", description="negative value") # TODO: unit-aware type - cover_ec: float = Field(-2.0e-3, description="negative value") - cover_ecu: float = Field(-3.5e-3, description="negative value") - cover_etu: float = Field(1.0e-3, description="positive value") + cover_fc: str = Field('20 MPa', description='positive value') # TODO: unit-aware type + cover_ft: str = Field('2.2 MPa', description='positive value') # TODO: unit-aware type + cover_fu: str = Field('-17.0 MPa', description='negative value') # TODO: unit-aware type + cover_ec: float = Field(-2.0e-3, description='negative value') + cover_ecu: float = Field(-3.5e-3, description='negative value') + cover_etu: float = Field(1.0e-3, description='positive value') cover_shear_ret_factor: float = 0.1 - cover_g: str = "12.5 GPa" # TODO: unit-aware type - core_fc: str = Field("20 MPa", description="positive value") # TODO: unit-aware type - core_ft: str = Field("2.2 MPa", description="positive value") # TODO: unit-aware type - core_fu: str = Field("-17.0 MPa", description="negative value") # TODO: unit-aware type - core_ec: float = Field(-2.0e-3, description="negative value") - core_ecu: float = Field(-3.5e-3, description="negative value") - core_etu: float = Field(1.0e-3, description="positive value") + cover_g: str = '12.5 GPa' # TODO: unit-aware type + core_fc: str = Field('20 MPa', description='positive value') # TODO: unit-aware type + core_ft: str = Field('2.2 MPa', description='positive value') # TODO: unit-aware type + core_fu: str = Field('-17.0 MPa', description='negative value') # TODO: unit-aware type + core_ec: float = Field(-2.0e-3, description='negative value') + core_ecu: float = Field(-3.5e-3, description='negative value') + core_etu: float = Field(1.0e-3, description='positive value') core_shear_ret_factor: float = 0.1 - core_g: str = "12.5 GPa" # TODO: unit-aware type - longitudinal_steel_material: str = "Steel02" - transverse_steel_material: str = "Steel02" + core_g: str = '12.5 GPa' # TODO: unit-aware type + longitudinal_steel_material: str = 'Steel02' + transverse_steel_material: str = 'Steel02' + diff --git a/src/otko/core/catalog/generated/minmax.py b/src/otko/core/catalog/generated/minmax.py index a066d64..343b55d 100644 --- a/src/otko/core/catalog/generated/minmax.py +++ b/src/otko/core/catalog/generated/minmax.py @@ -17,160 +17,160 @@ from pydantic import BaseModel, ConfigDict, Field class MinmaxSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "MinMax", - "x-book": "Combined_Materials", - "dependencies": [ + 'x-gid-name': 'MinMax', + 'x-book': 'Combined_Materials', + 'dependencies': [ { - "field": "formulation", - "gid_name": "Formulation", - "rules": [ + 'field': 'formulation', + 'gid_name': 'Formulation', + 'rules': [ { - "trigger": "Stress-Strain", - "actions": [ + 'trigger': 'Stress-Strain', + 'actions': [ { - "action": "RESTORE", - "field": "Minimum_strain", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Minimum_strain', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Maximum_strain", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Maximum_strain', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Minimum_deformation", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Minimum_deformation', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Maximum_deformation", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Maximum_deformation', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Minimum_rotation", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Minimum_rotation', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Maximum_rotation", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Maximum_rotation', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Material_for_defining_stress-strain_behaviour", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_for_defining_stress-strain_behaviour', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Material_for_defining_force-deformation_behaviour", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_for_defining_force-deformation_behaviour', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Material_for_defining_moment-rotation_behaviour", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_for_defining_moment-rotation_behaviour', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Force-Deformation", - "actions": [ + 'trigger': 'Force-Deformation', + 'actions': [ { - "action": "HIDE", - "field": "Minimum_strain", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Minimum_strain', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Maximum_strain", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Maximum_strain', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Minimum_deformation", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Minimum_deformation', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Maximum_deformation", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Maximum_deformation', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Minimum_rotation", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Minimum_rotation', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Maximum_rotation", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Maximum_rotation', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Material_for_defining_stress-strain_behaviour", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_for_defining_stress-strain_behaviour', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Material_for_defining_force-deformation_behaviour", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_for_defining_force-deformation_behaviour', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Material_for_defining_moment-rotation_behaviour", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_for_defining_moment-rotation_behaviour', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Moment-Rotation", - "actions": [ + 'trigger': 'Moment-Rotation', + 'actions': [ { - "action": "HIDE", - "field": "Minimum_strain", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Minimum_strain', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Maximum_strain", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Maximum_strain', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Minimum_deformation", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Minimum_deformation', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Maximum_deformation", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Maximum_deformation', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Minimum_rotation", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Minimum_rotation', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Maximum_rotation", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Maximum_rotation', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Material_for_defining_stress-strain_behaviour", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_for_defining_stress-strain_behaviour', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Material_for_defining_force-deformation_behaviour", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Material_for_defining_force-deformation_behaviour', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Material_for_defining_moment-rotation_behaviour", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_for_defining_moment-rotation_behaviour', + 'target': '#CURRENT#', }, ], }, @@ -180,14 +180,15 @@ class MinmaxSpec(BaseModel): } ) - material: Literal["_"] = "_" - formulation: Literal["Stress-Strain", "Force-Deformation", "Moment-Rotation"] = "Stress-Strain" - material_for_defining_stress_strain_behaviour: str = "Steel01" - material_for_defining_force_deformation_behaviour: str = "Steel01" - material_for_defining_moment_rotation_behaviour: str = "Steel01" + material: Literal['_'] = '_' + formulation: Literal['Stress-Strain', 'Force-Deformation', 'Moment-Rotation'] = 'Stress-Strain' + material_for_defining_stress_strain_behaviour: str = 'Steel01' + material_for_defining_force_deformation_behaviour: str = 'Steel01' + material_for_defining_moment_rotation_behaviour: str = 'Steel01' minimum_strain: float = -1.0e16 maximum_strain: float = 1.0e16 - minimum_deformation: str = "-1.0e16 m" # TODO: unit-aware type - maximum_deformation: str = "1.0e16 m" # TODO: unit-aware type - minimum_rotation: str = "-1.0e16 rad" # TODO: unit-aware type - maximum_rotation: str = "1.0e16 rad" # TODO: unit-aware type + minimum_deformation: str = '-1.0e16 m' # TODO: unit-aware type + maximum_deformation: str = '1.0e16 m' # TODO: unit-aware type + minimum_rotation: str = '-1.0e16 rad' # TODO: unit-aware type + maximum_rotation: str = '1.0e16 rad' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/parallel.py b/src/otko/core/catalog/generated/parallel.py index 813e7e1..b613566 100644 --- a/src/otko/core/catalog/generated/parallel.py +++ b/src/otko/core/catalog/generated/parallel.py @@ -17,134 +17,134 @@ from pydantic import BaseModel, ConfigDict, Field class ParallelSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Parallel", - "x-book": "Combined_Materials", - "dependencies": [ + 'x-gid-name': 'Parallel', + 'x-book': 'Combined_Materials', + 'dependencies': [ { - "field": "activate_1st_material", - "gid_name": "Activate_1st_material", - "rules": [ + 'field': 'activate_1st_material', + 'gid_name': 'Activate_1st_material', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Material_1", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Material_1', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Material_1", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_1', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_2nd_material", - "gid_name": "Activate_2nd_material", - "rules": [ + 'field': 'activate_2nd_material', + 'gid_name': 'Activate_2nd_material', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Material_2", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Material_2', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Material_2", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_2', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_3rd_material", - "gid_name": "Activate_3rd_material", - "rules": [ + 'field': 'activate_3rd_material', + 'gid_name': 'Activate_3rd_material', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Material_3", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Material_3', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Material_3", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_3', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_4th_material", - "gid_name": "Activate_4th_Material", - "rules": [ + 'field': 'activate_4th_material', + 'gid_name': 'Activate_4th_Material', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Material_4", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Material_4', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Material_4", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_4', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_5th_material", - "gid_name": "Activate_5th_material", - "rules": [ + 'field': 'activate_5th_material', + 'gid_name': 'Activate_5th_material', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Material_5", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Material_5', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Material_5", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_5', + 'target': '#CURRENT#', }, ], }, @@ -154,14 +154,15 @@ class ParallelSpec(BaseModel): } ) - material: Literal["Parallel"] = "Parallel" - activate_1st_material: Literal["0", "1"] = "1" - material_1: str = "Elastic" - activate_2nd_material: Literal["0", "1"] = "1" - material_2: str = "Elastic" - activate_3rd_material: Literal["0", "1"] = "0" - material_3: str = "Elastic" - activate_4th_material: Literal["0", "1"] = "0" - material_4: str = "Elastic" - activate_5th_material: Literal["0", "1"] = "0" - material_5: str = "Elastic" + material: Literal['Parallel'] = 'Parallel' + activate_1st_material: Literal['0', '1'] = '1' + material_1: str = 'Elastic' + activate_2nd_material: Literal['0', '1'] = '1' + material_2: str = 'Elastic' + activate_3rd_material: Literal['0', '1'] = '0' + material_3: str = 'Elastic' + activate_4th_material: Literal['0', '1'] = '0' + material_4: str = 'Elastic' + activate_5th_material: Literal['0', '1'] = '0' + material_5: str = 'Elastic' + diff --git a/src/otko/core/catalog/generated/plate_fiber.py b/src/otko/core/catalog/generated/plate_fiber.py index 93c2073..45facaa 100644 --- a/src/otko/core/catalog/generated/plate_fiber.py +++ b/src/otko/core/catalog/generated/plate_fiber.py @@ -17,14 +17,13 @@ from pydantic import BaseModel, ConfigDict, Field class PlateFiberSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Plate_Fiber", - "x-book": "Section_Force-Deformation", + 'x-gid-name': 'Plate_Fiber', + 'x-book': 'Section_Force-Deformation', } ) - section: Literal["PlateFiber"] = "PlateFiber" - compatible_materials: Literal["nD_Materials"] = "nD_Materials" - material_mat_multidimensional_nd_materials: str = Field( - "Elastic_Isotropic", description="All nD materials are listed here" - ) - plate_thickness_h: str = "0.25m" # TODO: unit-aware type + section: Literal['PlateFiber'] = 'PlateFiber' + compatible_materials: Literal['nD_Materials'] = 'nD_Materials' + material_mat_multidimensional_nd_materials: str = Field('Elastic_Isotropic', description='All nD materials are listed here') + plate_thickness_h: str = '0.25m' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/pressuredependmultiyield.py b/src/otko/core/catalog/generated/pressuredependmultiyield.py index e510fa8..e62d0c3 100644 --- a/src/otko/core/catalog/generated/pressuredependmultiyield.py +++ b/src/otko/core/catalog/generated/pressuredependmultiyield.py @@ -17,56 +17,56 @@ from pydantic import BaseModel, ConfigDict, Field class PressuredependmultiyieldSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "PressureDependMultiYield", - "x-book": "Multidimensional_(nD)_Materials", - "dependencies": [ + 'x-gid-name': 'PressureDependMultiYield', + 'x-book': 'Multidimensional_(nD)_Materials', + 'dependencies': [ { - "field": "activate_plastic_response", - "gid_name": "Activate_plastic_response", - "rules": [ + 'field': 'activate_plastic_response', + 'gid_name': 'Activate_plastic_response', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Activate_plastic_response_at_interval", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Activate_plastic_response_at_interval', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Activate_plastic_response_at_interval", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Activate_plastic_response_at_interval', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "automatic_surface_generation", - "gid_name": "Automatic_surface_generation", - "rules": [ + 'field': 'automatic_surface_generation', + 'gid_name': 'Automatic_surface_generation', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "RESTORE", - "field": "Define_yield_surfaces_based_on_shear_modulus_reduction_curve", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Define_yield_surfaces_based_on_shear_modulus_reduction_curve', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "HIDE", - "field": "Define_yield_surfaces_based_on_shear_modulus_reduction_curve", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Define_yield_surfaces_based_on_shear_modulus_reduction_curve', + 'target': '#CURRENT#', }, ], }, @@ -76,36 +76,30 @@ class PressuredependmultiyieldSpec(BaseModel): } ) - material: Literal["PressureDependMultiYield"] = "PressureDependMultiYield" - soil_type: Literal["Loose_Sand", "Medium_Sand", "Medium-dense_Sand", "Dense_Sand", "Custom"] = ( - "Custom" - ) - saturated_soil_mass_density: str = "1.7ton/m^3" # TODO: unit-aware type - reference_shear_modulus_gr: str = "55MPa" # TODO: unit-aware type - reference_bulk_modulus: str = "150MPa" # TODO: unit-aware type + material: Literal['PressureDependMultiYield'] = 'PressureDependMultiYield' + soil_type: Literal['Loose_Sand', 'Medium_Sand', 'Medium-dense_Sand', 'Dense_Sand', 'Custom'] = 'Custom' + saturated_soil_mass_density: str = '1.7ton/m^3' # TODO: unit-aware type + reference_shear_modulus_gr: str = '55MPa' # TODO: unit-aware type + reference_bulk_modulus: str = '150MPa' # TODO: unit-aware type shear_strain_at_which_maximum_stress_is_reached: float = 0.1 - friction_angle: int = Field(29, description="Enter angle in degrees") - reference_mean_effective_confining_pressure_pr: str = "80kPa" # TODO: unit-aware type + friction_angle: int = Field(29, description='Enter angle in degrees') + reference_mean_effective_confining_pressure_pr: str = '80kPa' # TODO: unit-aware type positive_constant_d: float = 0.5 - phase_transformation_angle: int = Field(29, description="Enter angle in degrees") - contraction_rate_constant: float = Field( - 0.21, - description="A non-negative constant defining the rate of shear-induced volume decrease (contraction) or pore pressure buildup. A larger value corresponds to faster contraction rate.", - ) + phase_transformation_angle: int = Field(29, description='Enter angle in degrees') + contraction_rate_constant: float = Field(0.21, description='A non-negative constant defining the rate of shear-induced volume decrease (contraction) or pore pressure buildup. A larger value corresponds to faster contraction rate.') dilation_rate_constant_1: float = 0.0 dilation_rate_constant_2: float = 0.0 - liquefaction_parameter_1: str = "10kPa" # TODO: unit-aware type + liquefaction_parameter_1: str = '10kPa' # TODO: unit-aware type liquefaction_parameter_2: float = 0.02 - liquefaction_parameter_3: float = Field( - 1.0, description="liquefac3 takes a value between 0.0 and 3.0." - ) + liquefaction_parameter_3: float = Field(1.0, description='liquefac3 takes a value between 0.0 and 3.0.') initial_void_ratio: float = 0.85 critical_straight_line_parameter_cs1: float = 0.9 critical_straight_line_parameter_cs2: float = 0.02 critical_straight_line_parameter_cs3: float = 0.07 - atmospheric_pressure_for_normalization: str = "101kPa" # TODO: unit-aware type - numerical_constant: str = "0.3kPa" # TODO: unit-aware type - yield_surfaces: int = Field(20, description="Yield surfaces must be less than 40!") - activate_plastic_response: Literal["1", "0"] = "0" + atmospheric_pressure_for_normalization: str = '101kPa' # TODO: unit-aware type + numerical_constant: str = '0.3kPa' # TODO: unit-aware type + yield_surfaces: int = Field(20, description='Yield surfaces must be less than 40!') + activate_plastic_response: Literal['1', '0'] = '0' activate_plastic_response_at_interval: int = 2 - automatic_surface_generation: Literal["0", "1"] = "1" + automatic_surface_generation: Literal['0', '1'] = '1' + diff --git a/src/otko/core/catalog/generated/pressuredependmultiyield02.py b/src/otko/core/catalog/generated/pressuredependmultiyield02.py index b60cd29..253454d 100644 --- a/src/otko/core/catalog/generated/pressuredependmultiyield02.py +++ b/src/otko/core/catalog/generated/pressuredependmultiyield02.py @@ -17,56 +17,56 @@ from pydantic import BaseModel, ConfigDict, Field class Pressuredependmultiyield02Spec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "PressureDependMultiYield02", - "x-book": "Multidimensional_(nD)_Materials", - "dependencies": [ + 'x-gid-name': 'PressureDependMultiYield02', + 'x-book': 'Multidimensional_(nD)_Materials', + 'dependencies': [ { - "field": "activate_plastic_response", - "gid_name": "Activate_plastic_response", - "rules": [ + 'field': 'activate_plastic_response', + 'gid_name': 'Activate_plastic_response', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Activate_plastic_response_at_interval", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Activate_plastic_response_at_interval', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Activate_plastic_response_at_interval", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Activate_plastic_response_at_interval', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "automatic_surface_generation", - "gid_name": "Automatic_surface_generation", - "rules": [ + 'field': 'automatic_surface_generation', + 'gid_name': 'Automatic_surface_generation', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "RESTORE", - "field": "Define_yield_surfaces_based_on_shear_modulus_reduction_curve", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Define_yield_surfaces_based_on_shear_modulus_reduction_curve', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "HIDE", - "field": "Define_yield_surfaces_based_on_shear_modulus_reduction_curve", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Define_yield_surfaces_based_on_shear_modulus_reduction_curve', + 'target': '#CURRENT#', }, ], }, @@ -76,33 +76,32 @@ class Pressuredependmultiyield02Spec(BaseModel): } ) - material: Literal["PressureDependMultiYield02"] = "PressureDependMultiYield02" - soil_type: Literal["Dr=30%", "Dr=40%", "Dr=50%", "Dr=60%", "Dr=75%", "Custom"] = "Custom" - saturated_soil_mass_density: str = "1.7ton/m^3" # TODO: unit-aware type - reference_shear_modulus_gr: str = "55MPa" # TODO: unit-aware type - reference_bulk_modulus: str = "150MPa" # TODO: unit-aware type + material: Literal['PressureDependMultiYield02'] = 'PressureDependMultiYield02' + soil_type: Literal['Dr=30%', 'Dr=40%', 'Dr=50%', 'Dr=60%', 'Dr=75%', 'Custom'] = 'Custom' + saturated_soil_mass_density: str = '1.7ton/m^3' # TODO: unit-aware type + reference_shear_modulus_gr: str = '55MPa' # TODO: unit-aware type + reference_bulk_modulus: str = '150MPa' # TODO: unit-aware type shear_strain_at_which_maximum_stress_is_reached: float = 0.1 - friction_angle: int = Field(29, description="Enter angle in degrees") - reference_mean_effective_confining_pressure_pr: str = "80kPa" # TODO: unit-aware type + friction_angle: int = Field(29, description='Enter angle in degrees') + reference_mean_effective_confining_pressure_pr: str = '80kPa' # TODO: unit-aware type positive_constant_d: float = 0.5 - phase_transformation_angle: int = Field(29, description="Enter angle in degrees") + phase_transformation_angle: int = Field(29, description='Enter angle in degrees') contraction_constant_1: float = 0.87 contraction_constant_2: int = 5 - contraction_constant_3: float = Field( - 0.18, description="A non-negative constant reflecting Ks effect" - ) + contraction_constant_3: float = Field(0.18, description='A non-negative constant reflecting Ks effect') dilation_rate_constant_1: float = 0.0 dilation_rate_constant_2: float = 0.0 dilation_rate_constant_3: float = 0.0 - liquefaction_parameter_1: str = "10kPa" # TODO: unit-aware type + liquefaction_parameter_1: str = '10kPa' # TODO: unit-aware type liquefaction_parameter_2: float = 0.02 initial_void_ratio: float = 0.85 critical_straight_line_parameter_cs1: float = 0.9 critical_straight_line_parameter_cs2: float = 0.02 critical_straight_line_parameter_cs3: float = 0.07 - atmospheric_pressure_for_normalization: str = "101kPa" # TODO: unit-aware type - numerical_constant: str = "0.3kPa" # TODO: unit-aware type - yield_surfaces: int = Field(20, description="Yield surfaces must be less than 40!") - activate_plastic_response: Literal["1", "0"] = "0" + atmospheric_pressure_for_normalization: str = '101kPa' # TODO: unit-aware type + numerical_constant: str = '0.3kPa' # TODO: unit-aware type + yield_surfaces: int = Field(20, description='Yield surfaces must be less than 40!') + activate_plastic_response: Literal['1', '0'] = '0' activate_plastic_response_at_interval: int = 2 - automatic_surface_generation: Literal["0", "1"] = "1" + automatic_surface_generation: Literal['0', '1'] = '1' + diff --git a/src/otko/core/catalog/generated/pressureindependmultiyield.py b/src/otko/core/catalog/generated/pressureindependmultiyield.py index d239293..89a9a68 100644 --- a/src/otko/core/catalog/generated/pressureindependmultiyield.py +++ b/src/otko/core/catalog/generated/pressureindependmultiyield.py @@ -17,56 +17,56 @@ from pydantic import BaseModel, ConfigDict, Field class PressureindependmultiyieldSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "PressureIndependMultiYield", - "x-book": "Multidimensional_(nD)_Materials", - "dependencies": [ + 'x-gid-name': 'PressureIndependMultiYield', + 'x-book': 'Multidimensional_(nD)_Materials', + 'dependencies': [ { - "field": "activate_plastic_response", - "gid_name": "Activate_plastic_response", - "rules": [ + 'field': 'activate_plastic_response', + 'gid_name': 'Activate_plastic_response', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Activate_plastic_response_at_interval", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Activate_plastic_response_at_interval', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Activate_plastic_response_at_interval", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Activate_plastic_response_at_interval', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "automatic_surface_generation", - "gid_name": "Automatic_surface_generation", - "rules": [ + 'field': 'automatic_surface_generation', + 'gid_name': 'Automatic_surface_generation', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "RESTORE", - "field": "Define_yield_surfaces_based_on_shear_modulus_reduction_curve", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Define_yield_surfaces_based_on_shear_modulus_reduction_curve', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "HIDE", - "field": "Define_yield_surfaces_based_on_shear_modulus_reduction_curve", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Define_yield_surfaces_based_on_shear_modulus_reduction_curve', + 'target': '#CURRENT#', }, ], }, @@ -76,17 +76,18 @@ class PressureindependmultiyieldSpec(BaseModel): } ) - material: Literal["PressureIndependMultiYield"] = "PressureIndependMultiYield" - soil_type: Literal["Soft_Clay", "Medium_Clay", "Stiff_Clay", "Custom"] = "Custom" - saturated_soil_mass_density: str = "1.5ton/m^3" # TODO: unit-aware type - reference_shear_modulus_gr: str = "60MPa" # TODO: unit-aware type - reference_bulk_modulus: str = "300MPa" # TODO: unit-aware type - apparent_cohesion: str = "37kPa" # TODO: unit-aware type + material: Literal['PressureIndependMultiYield'] = 'PressureIndependMultiYield' + soil_type: Literal['Soft_Clay', 'Medium_Clay', 'Stiff_Clay', 'Custom'] = 'Custom' + saturated_soil_mass_density: str = '1.5ton/m^3' # TODO: unit-aware type + reference_shear_modulus_gr: str = '60MPa' # TODO: unit-aware type + reference_bulk_modulus: str = '300MPa' # TODO: unit-aware type + apparent_cohesion: str = '37kPa' # TODO: unit-aware type shear_strain_at_which_maximum_stress_is_reached: float = 0.1 - friction_angle: float = Field(0.0, description="Enter angle in degrees") - reference_mean_effective_confining_pressure_pr: str = "100kPa" # TODO: unit-aware type + friction_angle: float = Field(0.0, description='Enter angle in degrees') + reference_mean_effective_confining_pressure_pr: str = '100kPa' # TODO: unit-aware type positive_constant_d: float = 0.0 - yield_surfaces: int = Field(20, description="Yield surfaces must be less than 40!") - activate_plastic_response: Literal["1", "0"] = "0" + yield_surfaces: int = Field(20, description='Yield surfaces must be less than 40!') + activate_plastic_response: Literal['1', '0'] = '0' activate_plastic_response_at_interval: int = 2 - automatic_surface_generation: Literal["0", "1"] = "1" + automatic_surface_generation: Literal['0', '1'] = '1' + diff --git a/src/otko/core/catalog/generated/pysimple1.py b/src/otko/core/catalog/generated/pysimple1.py index 73e8724..d2c6230 100644 --- a/src/otko/core/catalog/generated/pysimple1.py +++ b/src/otko/core/catalog/generated/pysimple1.py @@ -23,30 +23,30 @@ class CyclicDataRow(BaseModel): class Pysimple1Spec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "PySimple1", - "x-book": "Standard_Uniaxial_Materials", - "dependencies": [ + 'x-gid-name': 'PySimple1', + 'x-book': 'Standard_Uniaxial_Materials', + 'dependencies': [ { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -56,21 +56,12 @@ class Pysimple1Spec(BaseModel): } ) - material: Literal["_"] = "_" - p_y_backbone_curve: Literal["Soft_clay", "Sand"] = Field( - "Soft_clay", - description="Soft clay corresponds to Matlock (1970) relation. Sand corresponds to API (1993) relation", - ) - ultimate_capacity_pult: str = "4000 kN" # TODO: unit-aware type - displacement_at_which_the_50_of_pult_is_mobilized_in_monotonic_loading: str = Field( - "0.02 m", - description="Displacement at which the 50% of pult is mobilized in monotonic loading", - ) # TODO: unit-aware type - coefficient_cd: float = Field( - 0.0, description="Sets the drag resistance within a fully-mobilized gap as Cd*pult" - ) - viscous_damping_coefficient: str = Field( - "0.0 kNsec/m", description="Nonzero values are used to represent radiation damping effects" - ) # TODO: unit-aware type - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + material: Literal['_'] = '_' + p_y_backbone_curve: Literal['Soft_clay', 'Sand'] = Field('Soft_clay', description='Soft clay corresponds to Matlock (1970) relation. Sand corresponds to API (1993) relation') + ultimate_capacity_pult: str = '4000 kN' # TODO: unit-aware type + displacement_at_which_the_50_of_pult_is_mobilized_in_monotonic_loading: str = Field('0.02 m', description='Displacement at which the 50% of pult is mobilized in monotonic loading') # TODO: unit-aware type + coefficient_cd: float = Field(0.0, description='Sets the drag resistance within a fully-mobilized gap as Cd*pult') + viscous_damping_coefficient: str = Field('0.0 kNsec/m', description='Nonzero values are used to represent radiation damping effects') # TODO: unit-aware type + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) + diff --git a/src/otko/core/catalog/generated/quad.py b/src/otko/core/catalog/generated/quad.py index c283988..2b31ef5 100644 --- a/src/otko/core/catalog/generated/quad.py +++ b/src/otko/core/catalog/generated/quad.py @@ -17,25 +17,20 @@ from pydantic import BaseModel, ConfigDict, Field class QuadSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Quad", - "x-book": "Surface_Elements", + 'x-gid-name': 'Quad', + 'x-book': 'Surface_Elements', } ) - element_type: Literal["Quad"] = Field( - "Quad", - description="Four node quadrilateral element object which uses a bilinear isoparametric formulation", - ) - compatible_materials: Literal["nD_Materials"] = "nD_Materials" - define_equaldof_between_nodes_of_the_same_vertical_location: Literal["0", "1"] = Field( - "0", - description="equalDOF commands are automatically printed for element nodes which share the same vertical (Y) location", - ) - material_mat_multidimensional_nd_materials_user_materials: str = "Elastic_Isotropic" - plane_behavior: Literal["PlaneStrain", "PlaneStress"] = "PlaneStrain" - thickness: str = "1m" # TODO: unit-aware type - surface_pressure: str = "0.0kPa" # TODO: unit-aware type - applied_on_the_global_axes: Literal["_"] = "_" - x_direction: str = "0.0 kN/m^3" # TODO: unit-aware type - y_direction: str = "0.0 kN/m^3" # TODO: unit-aware type - mass_density: str = "0.0 ton/m^3" # TODO: unit-aware type + element_type: Literal['Quad'] = Field('Quad', description='Four node quadrilateral element object which uses a bilinear isoparametric formulation') + compatible_materials: Literal['nD_Materials'] = 'nD_Materials' + define_equaldof_between_nodes_of_the_same_vertical_location: Literal['0', '1'] = Field('0', description='equalDOF commands are automatically printed for element nodes which share the same vertical (Y) location') + material_mat_multidimensional_nd_materials_user_materials: str = 'Elastic_Isotropic' + plane_behavior: Literal['PlaneStrain', 'PlaneStress'] = 'PlaneStrain' + thickness: str = '1m' # TODO: unit-aware type + surface_pressure: str = '0.0kPa' # TODO: unit-aware type + applied_on_the_global_axes: Literal['_'] = '_' + x_direction: str = '0.0 kN/m^3' # TODO: unit-aware type + y_direction: str = '0.0 kN/m^3' # TODO: unit-aware type + mass_density: str = '0.0 ton/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/quadup.py b/src/otko/core/catalog/generated/quadup.py index 6393d90..a1fb541 100644 --- a/src/otko/core/catalog/generated/quadup.py +++ b/src/otko/core/catalog/generated/quadup.py @@ -17,30 +17,30 @@ from pydantic import BaseModel, ConfigDict, Field class QuadupSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "QuadUP", - "x-book": "Surface_Elements", - "dependencies": [ + 'x-gid-name': 'QuadUP', + 'x-book': 'Surface_Elements', + 'dependencies': [ { - "field": "update_permeabilities", - "gid_name": "Update_permeabilities", - "rules": [ + 'field': 'update_permeabilities', + 'gid_name': 'Update_permeabilities', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Update_permeabilities_at_interval", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Update_permeabilities_at_interval', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Update_permeabilities_at_interval", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Update_permeabilities_at_interval', + 'target': '#CURRENT#', }, ], }, @@ -50,28 +50,18 @@ class QuadupSpec(BaseModel): } ) - element_type: Literal["QuadUP"] = Field( - "QuadUP", - description="Four-node plane-strain element using bilinear isoparametric formulation", - ) - compatible_materials: Literal["_"] = "_" - define_equal_translational_dof_between_nodes_of_the_same_vertical_location: Literal[ - "0", "1" - ] = Field( - "0", - description="equalDOF commands are automatically printed for element nodes which share the same vertical (Y) location", - ) - material_mat_multidimensional_nd_materials_user_materials: str = "PressureIndependMultiYield" - thickness: str = "1.0m" # TODO: unit-aware type - combined_undrained_bulk_modulus_bc: str = "2.2GPa" # TODO: unit-aware type - fluid_mass_density: str = "1.0ton/m^3" # TODO: unit-aware type - permeability_coefficient_in_horizontal_direction: str = "1.0e-3 m/sec" # TODO: unit-aware type - permeability_coefficient_in_vertical_direction: str = "1.0e-3 m/sec" # TODO: unit-aware type - update_permeabilities: Literal["1", "0"] = Field( - "0", - description="Update permeabilities at the start of the selected interval (before analysis options)", - ) + element_type: Literal['QuadUP'] = Field('QuadUP', description='Four-node plane-strain element using bilinear isoparametric formulation') + compatible_materials: Literal['_'] = '_' + define_equal_translational_dof_between_nodes_of_the_same_vertical_location: Literal['0', '1'] = Field('0', description='equalDOF commands are automatically printed for element nodes which share the same vertical (Y) location') + material_mat_multidimensional_nd_materials_user_materials: str = 'PressureIndependMultiYield' + thickness: str = '1.0m' # TODO: unit-aware type + combined_undrained_bulk_modulus_bc: str = '2.2GPa' # TODO: unit-aware type + fluid_mass_density: str = '1.0ton/m^3' # TODO: unit-aware type + permeability_coefficient_in_horizontal_direction: str = '1.0e-3 m/sec' # TODO: unit-aware type + permeability_coefficient_in_vertical_direction: str = '1.0e-3 m/sec' # TODO: unit-aware type + update_permeabilities: Literal['1', '0'] = Field('0', description='Update permeabilities at the start of the selected interval (before analysis options)') update_permeabilities_at_interval: int = 1 - uniform_normal_traction: str = "0.0kPa" # TODO: unit-aware type - x_acceleration: str = "0.0m/sec^2" # TODO: unit-aware type - y_acceleration: str = "0.0m/sec^2" # TODO: unit-aware type + uniform_normal_traction: str = '0.0kPa' # TODO: unit-aware type + x_acceleration: str = '0.0m/sec^2' # TODO: unit-aware type + y_acceleration: str = '0.0m/sec^2' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/qzsimple1.py b/src/otko/core/catalog/generated/qzsimple1.py index 61fdbbc..4641469 100644 --- a/src/otko/core/catalog/generated/qzsimple1.py +++ b/src/otko/core/catalog/generated/qzsimple1.py @@ -23,30 +23,30 @@ class CyclicDataRow(BaseModel): class Qzsimple1Spec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "QzSimple1", - "x-book": "Standard_Uniaxial_Materials", - "dependencies": [ + 'x-gid-name': 'QzSimple1', + 'x-book': 'Standard_Uniaxial_Materials', + 'dependencies': [ { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -56,22 +56,12 @@ class Qzsimple1Spec(BaseModel): } ) - material: Literal["_"] = "_" - q_z_backbone_curve: Literal["Reese_and_O'Neill_relation", "Vijayvergiya_relation"] = Field( - "Reese_and_O'Neill_relation", - description="Reese and O'Neil relation is for drilled shafts in clay. Vijayvergiya relation is for piles in sand.", - ) - ultimate_capacity_qult: str = "4000 kN" # TODO: unit-aware type - displacement_at_which_the_50_of_qult_is_mobilized_in_monotonic_loading: str = Field( - "0.02 m", - description="Displacement at which the 50% of qult is mobilized in monotonic loading", - ) # TODO: unit-aware type - suction: float = Field( - 0.0, - description="Uplift resistance is equal to suction*qult. Valid values: between 0.0 - 0.1", - ) - viscous_damping_coefficient: str = Field( - "0.0 kNsec/m", description="Nonzero values are used to represent radiation damping effects" - ) # TODO: unit-aware type - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + material: Literal['_'] = '_' + q_z_backbone_curve: Literal["Reese_and_O'Neill_relation", 'Vijayvergiya_relation'] = Field("Reese_and_O'Neill_relation", description="Reese and O'Neil relation is for drilled shafts in clay. Vijayvergiya relation is for piles in sand.") + ultimate_capacity_qult: str = '4000 kN' # TODO: unit-aware type + displacement_at_which_the_50_of_qult_is_mobilized_in_monotonic_loading: str = Field('0.02 m', description='Displacement at which the 50% of qult is mobilized in monotonic loading') # TODO: unit-aware type + suction: float = Field(0.0, description='Uplift resistance is equal to suction*qult. Valid values: between 0.0 - 0.1') + viscous_damping_coefficient: str = Field('0.0 kNsec/m', description='Nonzero values are used to represent radiation damping effects') # TODO: unit-aware type + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) + diff --git a/src/otko/core/catalog/generated/ramberg_osgood_steel.py b/src/otko/core/catalog/generated/ramberg_osgood_steel.py index e029923..2b451c3 100644 --- a/src/otko/core/catalog/generated/ramberg_osgood_steel.py +++ b/src/otko/core/catalog/generated/ramberg_osgood_steel.py @@ -23,30 +23,30 @@ class CyclicDataRow(BaseModel): class RambergOsgoodSteelSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Ramberg-Osgood steel", - "x-book": "Uniaxial_Steel_Materials", - "dependencies": [ + 'x-gid-name': 'Ramberg-Osgood steel', + 'x-book': 'Uniaxial_Steel_Materials', + 'dependencies': [ { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -56,13 +56,14 @@ class RambergOsgoodSteelSpec(BaseModel): } ) - material: Literal["_"] = "_" - steel_grade: Literal["B500C", "Custom"] = "Custom" - yield_stress_fy: str = "500 MPa" # TODO: unit-aware type - initial_elastic_tangent_e0: str = "200 GPa" # TODO: unit-aware type + material: Literal['_'] = '_' + steel_grade: Literal['B500C', 'Custom'] = 'Custom' + yield_stress_fy: str = '500 MPa' # TODO: unit-aware type + initial_elastic_tangent_e0: str = '200 GPa' # TODO: unit-aware type yield_offset: float = 0.002 parameter_n_to_control_transition_from_elastic_to_plastic_branches: int = 5 - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/reinforcing_steel.py b/src/otko/core/catalog/generated/reinforcing_steel.py index cf6f384..0cfb1ec 100644 --- a/src/otko/core/catalog/generated/reinforcing_steel.py +++ b/src/otko/core/catalog/generated/reinforcing_steel.py @@ -23,30 +23,30 @@ class CyclicDataRow(BaseModel): class ReinforcingSteelSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Reinforcing_steel", - "x-book": "Uniaxial_Steel_Materials", - "dependencies": [ + 'x-gid-name': 'Reinforcing_steel', + 'x-book': 'Uniaxial_Steel_Materials', + 'dependencies': [ { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -56,15 +56,16 @@ class ReinforcingSteelSpec(BaseModel): } ) - material: Literal["_"] = "_" - steel_grade: Literal["B500C", "Custom"] = "Custom" - yield_stress_fy: str = "500 MPa" # TODO: unit-aware type - ultimate_stress_fsu: str = "575 MPa" # TODO: unit-aware type - initial_elastic_tangent_es: str = "200 GPa" # TODO: unit-aware type - tangent_at_initial_strain_hardening_esh: str = "20 GPa" # TODO: unit-aware type + material: Literal['_'] = '_' + steel_grade: Literal['B500C', 'Custom'] = 'Custom' + yield_stress_fy: str = '500 MPa' # TODO: unit-aware type + ultimate_stress_fsu: str = '575 MPa' # TODO: unit-aware type + initial_elastic_tangent_es: str = '200 GPa' # TODO: unit-aware type + tangent_at_initial_strain_hardening_esh: str = '20 GPa' # TODO: unit-aware type strain_corresponding_to_initial_strain_hardening_esh: float = 5e-3 strain_at_peak_stress_esu: float = 7.5e-2 - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/section_aggregator.py b/src/otko/core/catalog/generated/section_aggregator.py index dc5c4d3..9a6b95a 100644 --- a/src/otko/core/catalog/generated/section_aggregator.py +++ b/src/otko/core/catalog/generated/section_aggregator.py @@ -17,186 +17,186 @@ from pydantic import BaseModel, ConfigDict, Field class SectionAggregatorSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Section_Aggregator", - "x-book": "Combined_Materials", - "dependencies": [ + 'x-gid-name': 'Section_Aggregator', + 'x-book': 'Combined_Materials', + 'dependencies': [ { - "field": "select_section", - "gid_name": "Select_section", - "rules": [ + 'field': 'select_section', + 'gid_name': 'Select_section', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Section_to_be_aggregated", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Section_to_be_aggregated', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Section_to_be_aggregated", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Section_to_be_aggregated', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_p", - "gid_name": "Activate_P", - "rules": [ + 'field': 'activate_p', + 'gid_name': 'Activate_P', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Axial_force-deformation", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Axial_force-deformation', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Axial_force-deformation", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Axial_force-deformation', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_mz", - "gid_name": "Activate_Mz", - "rules": [ + 'field': 'activate_mz', + 'gid_name': 'Activate_Mz', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Moment-curvature_about_local_z-z", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment-curvature_about_local_z-z', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Moment-curvature_about_local_z-z", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Moment-curvature_about_local_z-z', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_vy", - "gid_name": "Activate_Vy", - "rules": [ + 'field': 'activate_vy', + 'gid_name': 'Activate_Vy', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Shear_force-deformation_along_local_y-y", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Shear_force-deformation_along_local_y-y', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Shear_force-deformation_along_local_y-y", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Shear_force-deformation_along_local_y-y', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_my", - "gid_name": "Activate_My", - "rules": [ + 'field': 'activate_my', + 'gid_name': 'Activate_My', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Moment-curvature_about_local_y-y", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment-curvature_about_local_y-y', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Moment-curvature_about_local_y-y", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Moment-curvature_about_local_y-y', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_vz", - "gid_name": "Activate_Vz", - "rules": [ + 'field': 'activate_vz', + 'gid_name': 'Activate_Vz', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Shear_force-deformation_along_local_z-z", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Shear_force-deformation_along_local_z-z', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Shear_force-deformation_along_local_z-z", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Shear_force-deformation_along_local_z-z', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_t", - "gid_name": "Activate_T", - "rules": [ + 'field': 'activate_t', + 'gid_name': 'Activate_T', + 'rules': [ { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Torsion_force-deformation", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Torsion_force-deformation', + 'target': '#CURRENT#', }, ], }, { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Torsion_force-deformation", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Torsion_force-deformation', + 'target': '#CURRENT#', }, ], }, @@ -206,20 +206,21 @@ class SectionAggregatorSpec(BaseModel): } ) - section: Literal["SectionAggregator"] = "SectionAggregator" - compatible_materials: Literal["_"] = "_" - compatible_sections: Literal["_"] = "_" - select_section: Literal["1", "0"] = "1" - section_to_be_aggregated: str = "Fiber" - activate_p: Literal["1", "0"] = "0" - axial_force_deformation: str = "Elastic" - activate_mz: Literal["1", "0"] = "0" - moment_curvature_about_local_z_z: str = "Elastic" - activate_vy: Literal["1", "0"] = "0" - shear_force_deformation_along_local_y_y: str = "Elastic" - activate_my: Literal["1", "0"] = "0" - moment_curvature_about_local_y_y: str = "Elastic" - activate_vz: Literal["1", "0"] = "0" - shear_force_deformation_along_local_z_z: str = "Elastic" - activate_t: Literal["1", "0"] = "0" - torsion_force_deformation: str = "Elastic" + section: Literal['SectionAggregator'] = 'SectionAggregator' + compatible_materials: Literal['_'] = '_' + compatible_sections: Literal['_'] = '_' + select_section: Literal['1', '0'] = '1' + section_to_be_aggregated: str = 'Fiber' + activate_p: Literal['1', '0'] = '0' + axial_force_deformation: str = 'Elastic' + activate_mz: Literal['1', '0'] = '0' + moment_curvature_about_local_z_z: str = 'Elastic' + activate_vy: Literal['1', '0'] = '0' + shear_force_deformation_along_local_y_y: str = 'Elastic' + activate_my: Literal['1', '0'] = '0' + moment_curvature_about_local_y_y: str = 'Elastic' + activate_vz: Literal['1', '0'] = '0' + shear_force_deformation_along_local_z_z: str = 'Elastic' + activate_t: Literal['1', '0'] = '0' + torsion_force_deformation: str = 'Elastic' + diff --git a/src/otko/core/catalog/generated/series.py b/src/otko/core/catalog/generated/series.py index 47f00ee..6eb399c 100644 --- a/src/otko/core/catalog/generated/series.py +++ b/src/otko/core/catalog/generated/series.py @@ -17,134 +17,134 @@ from pydantic import BaseModel, ConfigDict, Field class SeriesSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Series", - "x-book": "Combined_Materials", - "dependencies": [ + 'x-gid-name': 'Series', + 'x-book': 'Combined_Materials', + 'dependencies': [ { - "field": "activate_1st_material", - "gid_name": "Activate_1st_material", - "rules": [ + 'field': 'activate_1st_material', + 'gid_name': 'Activate_1st_material', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Material_1", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Material_1', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Material_1", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_1', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_2nd_material", - "gid_name": "Activate_2nd_material", - "rules": [ + 'field': 'activate_2nd_material', + 'gid_name': 'Activate_2nd_material', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Material_2", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Material_2', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Material_2", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_2', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_3rd_material", - "gid_name": "Activate_3rd_material", - "rules": [ + 'field': 'activate_3rd_material', + 'gid_name': 'Activate_3rd_material', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Material_3", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Material_3', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Material_3", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_3', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_4th_material", - "gid_name": "Activate_4th_Material", - "rules": [ + 'field': 'activate_4th_material', + 'gid_name': 'Activate_4th_Material', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Material_4", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Material_4', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Material_4", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_4', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "activate_5th_material", - "gid_name": "Activate_5th_material", - "rules": [ + 'field': 'activate_5th_material', + 'gid_name': 'Activate_5th_material', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "SET", - "field": "Material_5", - "target": "#CURRENT#", + 'action': 'SET', + 'field': 'Material_5', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Material_5", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Material_5', + 'target': '#CURRENT#', }, ], }, @@ -154,14 +154,15 @@ class SeriesSpec(BaseModel): } ) - material: Literal["Series"] = "Series" - activate_1st_material: Literal["0", "1"] = "1" - material_1: str = "Elastic" - activate_2nd_material: Literal["0", "1"] = "1" - material_2: str = "Elastic" - activate_3rd_material: Literal["0", "1"] = "0" - material_3: str = "Elastic" - activate_4th_material: Literal["0", "1"] = "0" - material_4: str = "Elastic" - activate_5th_material: Literal["0", "1"] = "0" - material_5: str = "Elastic" + material: Literal['Series'] = 'Series' + activate_1st_material: Literal['0', '1'] = '1' + material_1: str = 'Elastic' + activate_2nd_material: Literal['0', '1'] = '1' + material_2: str = 'Elastic' + activate_3rd_material: Literal['0', '1'] = '0' + material_3: str = 'Elastic' + activate_4th_material: Literal['0', '1'] = '0' + material_4: str = 'Elastic' + activate_5th_material: Literal['0', '1'] = '0' + material_5: str = 'Elastic' + diff --git a/src/otko/core/catalog/generated/shell.py b/src/otko/core/catalog/generated/shell.py index e47d08a..1991283 100644 --- a/src/otko/core/catalog/generated/shell.py +++ b/src/otko/core/catalog/generated/shell.py @@ -17,17 +17,13 @@ from pydantic import BaseModel, ConfigDict, Field class ShellSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Shell", - "x-book": "Surface_Elements", + 'x-gid-name': 'Shell', + 'x-book': 'Surface_Elements', } ) - element_type: Literal["Shell"] = Field( - "Shell", - description="A Shel element object using a bilinear isoparametric formulation with modified shear interpolation", - ) - compatible_materials: Literal["PlateFiber/ElasticMembranePlate"] = ( - "PlateFiber/ElasticMembranePlate" - ) - type: str = "Elastic_Membrane_Plate" - weight_density: str = "25 kN/m^3" # TODO: unit-aware type + element_type: Literal['Shell'] = Field('Shell', description='A Shel element object using a bilinear isoparametric formulation with modified shear interpolation') + compatible_materials: Literal['PlateFiber/ElasticMembranePlate'] = 'PlateFiber/ElasticMembranePlate' + type: str = 'Elastic_Membrane_Plate' + weight_density: str = '25 kN/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/shelldkgq.py b/src/otko/core/catalog/generated/shelldkgq.py index 030a9ea..5fee640 100644 --- a/src/otko/core/catalog/generated/shelldkgq.py +++ b/src/otko/core/catalog/generated/shelldkgq.py @@ -17,15 +17,13 @@ from pydantic import BaseModel, ConfigDict, Field class ShelldkgqSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "ShellDKGQ", - "x-book": "Surface_Elements", + 'x-gid-name': 'ShellDKGQ', + 'x-book': 'Surface_Elements', } ) - element_type: Literal["ShellDKGQ"] = Field( - "ShellDKGQ", - description="A ShellDKGQ element object is a quadrilateral shell element based on the theory of generalized conforming element.", - ) - compatible_materials: Literal["_"] = "_" - type: str = "Elastic_Membrane_Plate" - weight_density: str = "25 kN/m^3" # TODO: unit-aware type + element_type: Literal['ShellDKGQ'] = Field('ShellDKGQ', description='A ShellDKGQ element object is a quadrilateral shell element based on the theory of generalized conforming element.') + compatible_materials: Literal['_'] = '_' + type: str = 'Elastic_Membrane_Plate' + weight_density: str = '25 kN/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/standard_brick_element.py b/src/otko/core/catalog/generated/standard_brick_element.py index 927ada6..8e91dd5 100644 --- a/src/otko/core/catalog/generated/standard_brick_element.py +++ b/src/otko/core/catalog/generated/standard_brick_element.py @@ -17,18 +17,16 @@ from pydantic import BaseModel, ConfigDict, Field class StandardBrickElementSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Standard_Brick_Element", - "x-book": "Solid_Elements", + 'x-gid-name': 'Standard_Brick_Element', + 'x-book': 'Solid_Elements', } ) - element_type: Literal["stdBrick"] = Field( - "stdBrick", - description="An eight-node brick element using a trilinear isoparametric formulation", - ) - compatible_materials: Literal["nD_Materials"] = "nD_Materials" - material_mat_multidimensional_nd_materials_user_materials: str = "Elastic_Isotropic" - applied_on_the_global_axes: Literal["_"] = "_" - x_direction: str = "0.0kN/m^3" # TODO: unit-aware type - y_direction: str = "0.0kN/m^3" # TODO: unit-aware type - z_direction: str = "0.0kN/m^3" # TODO: unit-aware type + element_type: Literal['stdBrick'] = Field('stdBrick', description='An eight-node brick element using a trilinear isoparametric formulation') + compatible_materials: Literal['nD_Materials'] = 'nD_Materials' + material_mat_multidimensional_nd_materials_user_materials: str = 'Elastic_Isotropic' + applied_on_the_global_axes: Literal['_'] = '_' + x_direction: str = '0.0kN/m^3' # TODO: unit-aware type + y_direction: str = '0.0kN/m^3' # TODO: unit-aware type + z_direction: str = '0.0kN/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/steel01.py b/src/otko/core/catalog/generated/steel01.py index 0f3c320..a928a79 100644 --- a/src/otko/core/catalog/generated/steel01.py +++ b/src/otko/core/catalog/generated/steel01.py @@ -23,156 +23,156 @@ class CyclicDataRow(BaseModel): class Steel01Spec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Steel01", - "x-book": "Uniaxial_Steel_Materials", - "dependencies": [ + 'x-gid-name': 'Steel01', + 'x-book': 'Uniaxial_Steel_Materials', + 'dependencies': [ { - "field": "formulation", - "gid_name": "Formulation", - "rules": [ + 'field': 'formulation', + 'gid_name': 'Formulation', + 'rules': [ { - "trigger": "Stress-Strain", - "actions": [ + 'trigger': 'Stress-Strain', + 'actions': [ { - "action": "RESTORE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Yield_Stress_Fy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Yield_Stress_Fy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Initial_elastic_tangent_E0", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Initial_elastic_tangent_E0', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_stiffness_K", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_stiffness_K', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_My", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_My', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Force-Deformation", - "actions": [ + 'trigger': 'Force-Deformation', + 'actions': [ { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Yield_Stress_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Yield_Stress_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_elastic_tangent_E0", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_elastic_tangent_E0', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Force_Fy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Force_Fy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Initial_stiffness_K", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Initial_stiffness_K', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_My", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_My', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Moment-Rotation", - "actions": [ + 'trigger': 'Moment-Rotation', + 'actions': [ { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Yield_Stress_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Yield_Stress_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_elastic_tangent_E0", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_elastic_tangent_E0', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_stiffness_K", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_stiffness_K', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_My", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_My', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -182,27 +182,23 @@ class Steel01Spec(BaseModel): } ) - material: Literal["Steel01"] = Field( - "Steel01", - description="Uniaxial bilinear steel material object with kinematic hardening and optional isotropic hardening described by a non-linear evolution equation.", - ) - formulation: Literal["Stress-Strain", "Force-Deformation", "Moment-Rotation"] = "Stress-Strain" - steel_grade: Literal["S235", "S275", "S355", "S450", "B500C", "Custom"] = "Custom" - yield_stress_fy: str = "500 MPa" # TODO: unit-aware type - force_fy: str = "500 kN" # TODO: unit-aware type - moment_my: str = "250 kNm" # TODO: unit-aware type - initial_elastic_tangent_e0: str = "200 GPa" # TODO: unit-aware type - initial_stiffness_k: str = "5000 kN/m" # TODO: unit-aware type - moment_per_rotation_unit: str = "10000 kNm" # TODO: unit-aware type - strain_hardening_ratio_b: float = Field( - 0.02, description="Ratio between post-yield tangent and initial elastic tangent." - ) - isotropic_hardening_parameters: Literal["_"] = "_" + material: Literal['Steel01'] = Field('Steel01', description='Uniaxial bilinear steel material object with kinematic hardening and optional isotropic hardening described by a non-linear evolution equation.') + formulation: Literal['Stress-Strain', 'Force-Deformation', 'Moment-Rotation'] = 'Stress-Strain' + steel_grade: Literal['S235', 'S275', 'S355', 'S450', 'B500C', 'Custom'] = 'Custom' + yield_stress_fy: str = '500 MPa' # TODO: unit-aware type + force_fy: str = '500 kN' # TODO: unit-aware type + moment_my: str = '250 kNm' # TODO: unit-aware type + initial_elastic_tangent_e0: str = '200 GPa' # TODO: unit-aware type + initial_stiffness_k: str = '5000 kN/m' # TODO: unit-aware type + moment_per_rotation_unit: str = '10000 kNm' # TODO: unit-aware type + strain_hardening_ratio_b: float = Field(0.02, description='Ratio between post-yield tangent and initial elastic tangent.') + isotropic_hardening_parameters: Literal['_'] = '_' isotropic_hardening_parameter_a1: int = 0 isotropic_hardening_parameter_a2: int = 1 isotropic_hardening_parameter_a3: int = 0 isotropic_hardening_parameter_a4: int = 1 - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/steel02.py b/src/otko/core/catalog/generated/steel02.py index 19b2c51..9fe1c55 100644 --- a/src/otko/core/catalog/generated/steel02.py +++ b/src/otko/core/catalog/generated/steel02.py @@ -23,201 +23,201 @@ class CyclicDataRow(BaseModel): class Steel02Spec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Steel02", - "x-book": "Uniaxial_Steel_Materials", - "dependencies": [ + 'x-gid-name': 'Steel02', + 'x-book': 'Uniaxial_Steel_Materials', + 'dependencies': [ { - "field": "formulation", - "gid_name": "Formulation", - "rules": [ + 'field': 'formulation', + 'gid_name': 'Formulation', + 'rules': [ { - "trigger": "Stress-Strain", - "actions": [ + 'trigger': 'Stress-Strain', + 'actions': [ { - "action": "RESTORE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Yield_Stress_Fy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Yield_Stress_Fy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Initial_elastic_tangent_E0", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Initial_elastic_tangent_E0', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_stiffness_K", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_stiffness_K', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_My", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_My', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Initial_stress", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Initial_stress', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_force", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_force', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_moment", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_moment', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Force-Deformation", - "actions": [ + 'trigger': 'Force-Deformation', + 'actions': [ { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Yield_Stress_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Yield_Stress_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_elastic_tangent_E0", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_elastic_tangent_E0', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Force_Fy", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Force_Fy', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Initial_stiffness_K", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Initial_stiffness_K', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_My", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_My', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_stress", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_stress', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Initial_force", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Initial_force', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_moment", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_moment', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Moment-Rotation", - "actions": [ + 'trigger': 'Moment-Rotation', + 'actions': [ { - "action": "HIDE", - "field": "Steel_grade", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Steel_grade', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Yield_Stress_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Yield_Stress_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_elastic_tangent_E0", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_elastic_tangent_E0', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Force_Fy", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Force_Fy', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_stiffness_K", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_stiffness_K', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_My", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_My', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Moment_per_rotation_unit", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Moment_per_rotation_unit', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_stress", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_stress', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Initial_force", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Initial_force', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Initial_moment", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Initial_moment', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -227,34 +227,30 @@ class Steel02Spec(BaseModel): } ) - material: Literal["Steel02"] = Field( - "Steel02", - description="uniaxial Giuffre-Menegotto-Pinto steel material object with isotropic strain hardening", - ) - formulation: Literal["Stress-Strain", "Force-Deformation", "Moment-Rotation"] = "Stress-Strain" - steel_grade: Literal["S235", "S275", "S355", "S450", "B500C", "Custom"] = "Custom" - yield_stress_fy: str = "500 MPa" # TODO: unit-aware type - force_fy: str = "500 kN" # TODO: unit-aware type - moment_my: str = "250 kNm" # TODO: unit-aware type - initial_elastic_tangent_e0: str = "200 GPa" # TODO: unit-aware type - initial_stiffness_k: str = "5000 kN/m" # TODO: unit-aware type - moment_per_rotation_unit: str = "10000 kNm" # TODO: unit-aware type - strain_hardening_ratio_b: float = Field( - 0.02, description="Ratio between post-yield tangent and initial elastic tangent." - ) - parameters_to_control_the_transition: Literal["_"] = "_" + material: Literal['Steel02'] = Field('Steel02', description='uniaxial Giuffre-Menegotto-Pinto steel material object with isotropic strain hardening') + formulation: Literal['Stress-Strain', 'Force-Deformation', 'Moment-Rotation'] = 'Stress-Strain' + steel_grade: Literal['S235', 'S275', 'S355', 'S450', 'B500C', 'Custom'] = 'Custom' + yield_stress_fy: str = '500 MPa' # TODO: unit-aware type + force_fy: str = '500 kN' # TODO: unit-aware type + moment_my: str = '250 kNm' # TODO: unit-aware type + initial_elastic_tangent_e0: str = '200 GPa' # TODO: unit-aware type + initial_stiffness_k: str = '5000 kN/m' # TODO: unit-aware type + moment_per_rotation_unit: str = '10000 kNm' # TODO: unit-aware type + strain_hardening_ratio_b: float = Field(0.02, description='Ratio between post-yield tangent and initial elastic tangent.') + parameters_to_control_the_transition: Literal['_'] = '_' parameter_r0: int = 20 parameter_cr1: float = 0.925 parameter_cr2: float = 0.15 - isotropic_hardening_parameters: Literal["_"] = "_" + isotropic_hardening_parameters: Literal['_'] = '_' isotropic_hardening_parameter_a1: int = 0 isotropic_hardening_parameter_a2: int = 1 isotropic_hardening_parameter_a3: int = 0 isotropic_hardening_parameter_a4: int = 1 - initial_stress: str = "0.0 MPa" # TODO: unit-aware type - initial_force: str = "0.0 kN" # TODO: unit-aware type - initial_moment: str = "0.0 kNm" # TODO: unit-aware type - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + initial_stress: str = '0.0 MPa' # TODO: unit-aware type + initial_force: str = '0.0 kN' # TODO: unit-aware type + initial_moment: str = '0.0 kNm' # TODO: unit-aware type + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/tri31.py b/src/otko/core/catalog/generated/tri31.py index 418d78b..ada28d8 100644 --- a/src/otko/core/catalog/generated/tri31.py +++ b/src/otko/core/catalog/generated/tri31.py @@ -17,20 +17,19 @@ from pydantic import BaseModel, ConfigDict, Field class Tri31Spec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Tri31", - "x-book": "Surface_Elements", + 'x-gid-name': 'Tri31', + 'x-book': 'Surface_Elements', } ) - element_type: Literal["Tri31"] = Field( - "Tri31", description="Constant strain triangular element" - ) - compatible_materials: Literal["_"] = "_" - material_mat_multidimensional_nd_materials_user_materials: str = "Elastic_Isotropic" - plane_behavior: Literal["PlaneStrain", "PlaneStress"] = "PlaneStrain" - thickness: str = "1.0 m" # TODO: unit-aware type - surface_pressure: str = "0.0 kPa" # TODO: unit-aware type - applied_on_the_global_axes: Literal["_"] = "_" - x_direction: str = "0.0 kN/m^3" # TODO: unit-aware type - y_direction: str = "0.0 kN/m^3" # TODO: unit-aware type - mass_density: str = "0.0ton/m^3" # TODO: unit-aware type + element_type: Literal['Tri31'] = Field('Tri31', description='Constant strain triangular element') + compatible_materials: Literal['_'] = '_' + material_mat_multidimensional_nd_materials_user_materials: str = 'Elastic_Isotropic' + plane_behavior: Literal['PlaneStrain', 'PlaneStress'] = 'PlaneStrain' + thickness: str = '1.0 m' # TODO: unit-aware type + surface_pressure: str = '0.0 kPa' # TODO: unit-aware type + applied_on_the_global_axes: Literal['_'] = '_' + x_direction: str = '0.0 kN/m^3' # TODO: unit-aware type + y_direction: str = '0.0 kN/m^3' # TODO: unit-aware type + mass_density: str = '0.0ton/m^3' # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/truss.py b/src/otko/core/catalog/generated/truss.py index a337c69..c789e73 100644 --- a/src/otko/core/catalog/generated/truss.py +++ b/src/otko/core/catalog/generated/truss.py @@ -17,196 +17,196 @@ from pydantic import BaseModel, ConfigDict, Field class TrussSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Truss", - "x-book": "Truss_Elements", - "dependencies": [ + 'x-gid-name': 'Truss', + 'x-book': 'Truss_Elements', + 'dependencies': [ { - "field": "cross_section", - "gid_name": "Cross_section", - "rules": [ + 'field': 'cross_section', + 'gid_name': 'Cross_section', + 'rules': [ { - "trigger": "Rectangular", - "actions": [ + 'trigger': 'Rectangular', + 'actions': [ { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_Bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_Bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Circular", - "actions": [ + 'trigger': 'Circular', + 'actions': [ { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_Bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_Bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Tee", - "actions": [ + 'trigger': 'Tee', + 'actions': [ { - "action": "RESTORE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Width_Bw", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Width_Bw', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, ], }, { - "trigger": "General", - "actions": [ + 'trigger': 'General', + 'actions': [ { - "action": "HIDE", - "field": "Height_h", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_h', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_B", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_B', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Diameter_d", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Diameter_d', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_bf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_bf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Height_hf", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Height_hf', + 'target': '#CURRENT#', }, { - "action": "HIDE", - "field": "Width_Bw", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Width_Bw', + 'target': '#CURRENT#', }, { - "action": "RESTORE", - "field": "Area_A", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Area_A', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "set_modification_factors", - "gid_name": "Set_modification_factors", - "rules": [ + 'field': 'set_modification_factors', + 'gid_name': 'Set_modification_factors', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "mod._A", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'mod._A', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "mod._A", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'mod._A', + 'target': '#CURRENT#', }, ], }, @@ -216,22 +216,21 @@ class TrussSpec(BaseModel): } ) - element_type: Literal["Truss"] = "Truss" - compatible_materials: Literal["_"] = "_" - material: str = Field( - "Elastic", description="Choose one existing material to use its parameters." - ) - mass_density: str = "0.0ton/m^3" # TODO: unit-aware type - consider_consistent_mass_matrix: Literal["1", "0"] = "0" - include_rayleigh_damping: Literal["1", "0"] = "0" - weight_density: str = "0.0 kN/m^3" # TODO: unit-aware type - cross_section: Literal["Rectangular", "Tee", "Circular", "General"] = "General" - height_h: str = "0.5m" # TODO: unit-aware type - width_b: str = "0.3m" # TODO: unit-aware type - diameter_d: str = "0.5m" # TODO: unit-aware type - width_bf: str = "0.4m" # TODO: unit-aware type - height_hf: str = "0.15m" # TODO: unit-aware type - width_bw: str = "0.25m" # TODO: unit-aware type - area_a: str = "1.0 m^2" # TODO: unit-aware type - set_modification_factors: Literal["0", "1"] = "0" + element_type: Literal['Truss'] = 'Truss' + compatible_materials: Literal['_'] = '_' + material: str = Field('Elastic', description='Choose one existing material to use its parameters.') + mass_density: str = '0.0ton/m^3' # TODO: unit-aware type + consider_consistent_mass_matrix: Literal['1', '0'] = '0' + include_rayleigh_damping: Literal['1', '0'] = '0' + weight_density: str = '0.0 kN/m^3' # TODO: unit-aware type + cross_section: Literal['Rectangular', 'Tee', 'Circular', 'General'] = 'General' + height_h: str = '0.5m' # TODO: unit-aware type + width_b: str = '0.3m' # TODO: unit-aware type + diameter_d: str = '0.5m' # TODO: unit-aware type + width_bf: str = '0.4m' # TODO: unit-aware type + height_hf: str = '0.15m' # TODO: unit-aware type + width_bw: str = '0.25m' # TODO: unit-aware type + area_a: str = '1.0 m^2' # TODO: unit-aware type + set_modification_factors: Literal['0', '1'] = '0' mod_a: int = 1 + diff --git a/src/otko/core/catalog/generated/tzsimple1.py b/src/otko/core/catalog/generated/tzsimple1.py index 39cb493..819052a 100644 --- a/src/otko/core/catalog/generated/tzsimple1.py +++ b/src/otko/core/catalog/generated/tzsimple1.py @@ -23,30 +23,30 @@ class CyclicDataRow(BaseModel): class Tzsimple1Spec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "TzSimple1", - "x-book": "Standard_Uniaxial_Materials", - "dependencies": [ + 'x-gid-name': 'TzSimple1', + 'x-book': 'Standard_Uniaxial_Materials', + 'dependencies': [ { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -56,17 +56,11 @@ class Tzsimple1Spec(BaseModel): } ) - material: Literal["_"] = "_" - t_z_backbone_curve: Literal["Reese_and_O'Neill_relation", "Mosher_relation"] = ( - "Reese_and_O'Neill_relation" - ) - ultimate_capacity_tult: str = "4000 kN" # TODO: unit-aware type - displacement_at_which_the_50_of_tult_is_mobilized_in_monotonic_loading: str = Field( - "0.02 m", - description="Displacement at which the 50% of tult is mobilized in monotonic loading", - ) # TODO: unit-aware type - viscous_damping_coefficient: str = Field( - "0.0 kNsec/m", description="Nonzero values are used to represent radiation damping effects" - ) # TODO: unit-aware type - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + material: Literal['_'] = '_' + t_z_backbone_curve: Literal["Reese_and_O'Neill_relation", 'Mosher_relation'] = "Reese_and_O'Neill_relation" + ultimate_capacity_tult: str = '4000 kN' # TODO: unit-aware type + displacement_at_which_the_50_of_tult_is_mobilized_in_monotonic_loading: str = Field('0.02 m', description='Displacement at which the 50% of tult is mobilized in monotonic loading') # TODO: unit-aware type + viscous_damping_coefficient: str = Field('0.0 kNsec/m', description='Nonzero values are used to represent radiation damping effects') # TODO: unit-aware type + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) + diff --git a/src/otko/core/catalog/generated/user_material.py b/src/otko/core/catalog/generated/user_material.py index 70adc8c..5f43e7d 100644 --- a/src/otko/core/catalog/generated/user_material.py +++ b/src/otko/core/catalog/generated/user_material.py @@ -17,11 +17,12 @@ from pydantic import BaseModel, ConfigDict, Field class UserMaterialSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "User_Material", - "x-book": "User_Materials", + 'x-gid-name': 'User_Material', + 'x-book': 'User_Materials', } ) - material: Literal["UserMaterial"] = "UserMaterial" - section: Literal["UserMaterial"] = "UserMaterial" - width: str = Field("0.1m", description="Used to calculate self weight") # TODO: unit-aware type + material: Literal['UserMaterial'] = 'UserMaterial' + section: Literal['UserMaterial'] = 'UserMaterial' + width: str = Field('0.1m', description='Used to calculate self weight') # TODO: unit-aware type + diff --git a/src/otko/core/catalog/generated/viscous.py b/src/otko/core/catalog/generated/viscous.py index fd9c380..e659a9a 100644 --- a/src/otko/core/catalog/generated/viscous.py +++ b/src/otko/core/catalog/generated/viscous.py @@ -23,30 +23,30 @@ class CyclicDataRow(BaseModel): class ViscousSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Viscous", - "x-book": "Standard_Uniaxial_Materials", - "dependencies": [ + 'x-gid-name': 'Viscous', + 'x-book': 'Standard_Uniaxial_Materials', + 'dependencies': [ { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -56,10 +56,11 @@ class ViscousSpec(BaseModel): } ) - material: Literal["Viscous"] = "Viscous" - damping_coefficient_c: str = "0.0 kNsec/m" # TODO: unit-aware type + material: Literal['Viscous'] = 'Viscous' + damping_coefficient_c: str = '0.0 kNsec/m' # TODO: unit-aware type power_factor_alpha: int = 1 - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/catalog/generated/viscous_damper.py b/src/otko/core/catalog/generated/viscous_damper.py index 1f53eac..c2c021d 100644 --- a/src/otko/core/catalog/generated/viscous_damper.py +++ b/src/otko/core/catalog/generated/viscous_damper.py @@ -23,56 +23,56 @@ class CyclicDataRow(BaseModel): class ViscousDamperSpec(BaseModel): model_config = ConfigDict( json_schema_extra={ - "x-gid-name": "Viscous_Damper", - "x-book": "Standard_Uniaxial_Materials", - "dependencies": [ + 'x-gid-name': 'Viscous_Damper', + 'x-book': 'Standard_Uniaxial_Materials', + 'dependencies': [ { - "field": "activate_gap_length", - "gid_name": "Activate_gap_length", - "rules": [ + 'field': 'activate_gap_length', + 'gid_name': 'Activate_gap_length', + 'rules': [ { - "trigger": "0", - "actions": [ + 'trigger': '0', + 'actions': [ { - "action": "HIDE", - "field": "Gap_length", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Gap_length', + 'target': '#CURRENT#', }, ], }, { - "trigger": "1", - "actions": [ + 'trigger': '1', + 'actions': [ { - "action": "RESTORE", - "field": "Gap_length", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Gap_length', + 'target': '#CURRENT#', }, ], }, ], }, { - "field": "analysis_type", - "gid_name": "Analysis_type", - "rules": [ + 'field': 'analysis_type', + 'gid_name': 'Analysis_type', + 'rules': [ { - "trigger": "Monotonic", - "actions": [ + 'trigger': 'Monotonic', + 'actions': [ { - "action": "HIDE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'HIDE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, { - "trigger": "Cyclic", - "actions": [ + 'trigger': 'Cyclic', + 'actions': [ { - "action": "RESTORE", - "field": "Cyclic_data", - "target": "#CURRENT#", + 'action': 'RESTORE', + 'field': 'Cyclic_data', + 'target': '#CURRENT#', }, ], }, @@ -82,13 +82,14 @@ class ViscousDamperSpec(BaseModel): } ) - material: Literal["ViscousDamper"] = "ViscousDamper" - elastic_stiffness: str = "300000 kN/m" # TODO: unit-aware type - damping_coefficient_cd: str = "280000 kNsec/m" # TODO: unit-aware type + material: Literal['ViscousDamper'] = 'ViscousDamper' + elastic_stiffness: str = '300000 kN/m' # TODO: unit-aware type + damping_coefficient_cd: str = '280000 kNsec/m' # TODO: unit-aware type velocity_exponent_alpha: float = 0.30 - activate_gap_length: Literal["1", "0"] = "0" - gap_length: str = "0.05 m" # TODO: unit-aware type - analysis_type: Literal["Monotonic", "Cyclic"] = "Monotonic" + activate_gap_length: Literal['1', '0'] = '0' + gap_length: str = '0.05 m' # TODO: unit-aware type + analysis_type: Literal['Monotonic', 'Cyclic'] = 'Monotonic' cyclic_data: list[CyclicDataRow] = Field(default_factory=list) - field: Literal["_"] = "_" - set_as_variable: Literal["0", "1"] = "0" + field: Literal['_'] = '_' + set_as_variable: Literal['0', '1'] = '0' + diff --git a/src/otko/core/defaults.py b/src/otko/core/defaults.py index 0980d6b..9742d21 100644 --- a/src/otko/core/defaults.py +++ b/src/otko/core/defaults.py @@ -159,8 +159,12 @@ def ensure_default_timeseries_and_pattern( if existing is not None: return existing pattern_name = name or DEFAULT_PATTERN_NAME - ts = make_default_time_series(project.next_time_series_id(), kind=ts_kind, name=pattern_name) + ts = make_default_time_series( + project.next_time_series_id(), kind=ts_kind, name=pattern_name + ) project.time_series.append(ts) - pattern = make_default_pattern(project.next_pattern_id(), ts.id, name=pattern_name) + pattern = make_default_pattern( + project.next_pattern_id(), ts.id, name=pattern_name + ) project.load_patterns.append(pattern) return pattern diff --git a/src/otko/core/geometry/__init__.py b/src/otko/core/geometry/__init__.py index 705c35d..7e1dbff 100644 --- a/src/otko/core/geometry/__init__.py +++ b/src/otko/core/geometry/__init__.py @@ -6,6 +6,7 @@ from otko.core.geometry.elements import ( DispBeamColumn, ElasticBeamColumn, Element, + ElementBase, ForceBeamColumn, QuadElement, TrussElement, @@ -25,6 +26,7 @@ from otko.core.geometry.node import Node __all__ = [ "Node", "Element", + "ElementBase", "TrussElement", "CorotTrussElement", "ElasticBeamColumn", diff --git a/src/otko/core/geometry/elements.py b/src/otko/core/geometry/elements.py index 35a2026..733f2bc 100644 --- a/src/otko/core/geometry/elements.py +++ b/src/otko/core/geometry/elements.py @@ -10,14 +10,28 @@ Conventions follow OpenSeesPy ``element ...`` commands. from __future__ import annotations -from typing import Annotated, Literal +from typing import Annotated, Literal, Union from pydantic import Field, PositiveFloat, PositiveInt from otko.core._base import Entity -class TrussElement(Entity): +class ElementBase(Entity): + """Shared base for all structural elements. + + Carries the display-only ``E`` fallback label so every element + type (including Quad) shows the same default name on the canvas + and in the table without persisting it into ``name``. + """ + + @property + def default_name(self) -> str: + """Fallback label for unnamed elements (``E``).""" + return f"E{self.id}" + + +class TrussElement(ElementBase): """Two-node truss with axial stiffness only — ``element truss``.""" type: Literal["Truss"] = "Truss" @@ -27,7 +41,7 @@ class TrussElement(Entity): rho: float = Field(default=0.0, ge=0.0, description="Mass per unit length.") -class CorotTrussElement(Entity): +class CorotTrussElement(ElementBase): """Co-rotational truss for large displacements — ``element corotTruss``.""" type: Literal["CorotTruss"] = "CorotTruss" @@ -37,7 +51,7 @@ class CorotTrussElement(Entity): rho: float = Field(default=0.0, ge=0.0) -class ElasticBeamColumn(Entity): +class ElasticBeamColumn(ElementBase): """Linear-elastic frame element — ``element elasticBeamColumn``. Pure-section style: provide section_id; OpenSees pulls EA, EI from @@ -60,7 +74,7 @@ class ElasticBeamColumn(Entity): ) -class ForceBeamColumn(Entity): +class ForceBeamColumn(ElementBase): """Force-based fibre frame element — ``element forceBeamColumn``.""" type: Literal["ForceBeamColumn"] = "ForceBeamColumn" @@ -72,7 +86,7 @@ class ForceBeamColumn(Entity): tolerance: float = Field(default=1e-12, gt=0.0) -class DispBeamColumn(Entity): +class DispBeamColumn(ElementBase): """Displacement-based fibre frame element — ``element dispBeamColumn``.""" type: Literal["DispBeamColumn"] = "DispBeamColumn" @@ -82,7 +96,7 @@ class DispBeamColumn(Entity): geom_transf: Literal["Linear", "PDelta", "Corotational"] = "Linear" -class ZeroLengthElement(Entity): +class ZeroLengthElement(ElementBase): """Two coincident nodes connected by uniaxial materials per DOF. Foundation building block for plastic hinges and isolators. @@ -106,7 +120,7 @@ class ZeroLengthElement(Entity): ) -class ZeroLengthSectionElement(Entity): +class ZeroLengthSectionElement(ElementBase): """Two coincident nodes connected by a full :class:`Section` — OpenSees ``element zeroLengthSection``. @@ -128,7 +142,7 @@ class ZeroLengthSectionElement(Entity): ) -class BeamWithHingesElement(Entity): +class BeamWithHingesElement(ElementBase): """Force-based beam with lumped plasticity at both ends — ``element beamWithHinges``. @@ -160,7 +174,7 @@ class BeamWithHingesElement(Entity): geom_transf: Literal["Linear", "PDelta", "Corotational"] = "Linear" -class QuadElement(Entity): +class QuadElement(ElementBase): """Four-node 2D quadrilateral (plane-stress / plane-strain) — ``element quad`` / ``bbarQuad`` / ``enhancedQuad``. @@ -198,14 +212,16 @@ class QuadElement(Entity): Element = Annotated[ - TrussElement - | CorotTrussElement - | ElasticBeamColumn - | ForceBeamColumn - | DispBeamColumn - | ZeroLengthElement - | ZeroLengthSectionElement - | BeamWithHingesElement - | QuadElement, + Union[ + TrussElement, + CorotTrussElement, + ElasticBeamColumn, + ForceBeamColumn, + DispBeamColumn, + ZeroLengthElement, + ZeroLengthSectionElement, + BeamWithHingesElement, + QuadElement, + ], Field(discriminator="type"), ] diff --git a/src/otko/core/geometry/grid.py b/src/otko/core/geometry/grid.py index f7d61ea..d726f68 100644 --- a/src/otko/core/geometry/grid.py +++ b/src/otko/core/geometry/grid.py @@ -87,17 +87,15 @@ class GridSystem(BaseModel): if isinstance(v, dict): records.append(v) else: - records.append( - { - "id": f"{prefix}{i + 1}", - "ordinate": float(v), - } - ) + records.append({ + "id": f"{prefix}{i + 1}", + "ordinate": float(v), + }) data[grid_key] = records return data @model_validator(mode="after") - def _sort_and_dedupe(self) -> GridSystem: + def _sort_and_dedupe(self) -> "GridSystem": for name in ("x_grid_lines", "y_grid_lines", "z_grid_lines"): lines: list[GridLine] = list(getattr(self, name)) lines.sort(key=lambda ln: ln.ordinate) @@ -123,21 +121,21 @@ class GridSystem(BaseModel): def bounds(self) -> tuple[tuple[float, float], tuple[float, float], tuple[float, float]]: """Return ((xmin, xmax), (ymin, ymax), (zmin, zmax)) spanning the grid.""" - def span(vs: list[float]) -> tuple[float, float]: if not vs: return (0.0, 0.0) return (vs[0], vs[-1]) - return span(self.x_lines), span(self.y_lines), span(self.z_lines) def make_grid_lines( - axis: Literal["X", "Y", "Z"], - ordinates: list[float], + axis: Literal["X", "Y", "Z"], ordinates: list[float], ) -> list[GridLine]: """Helper: build default-metadata GridLine records from flat ordinates.""" - return [GridLine(id=f"{axis}{i + 1}", ordinate=float(v)) for i, v in enumerate(ordinates)] + return [ + GridLine(id=f"{axis}{i + 1}", ordinate=float(v)) + for i, v in enumerate(ordinates) + ] class CoordinateSystem(BaseModel): @@ -162,9 +160,9 @@ class CoordinateSystem(BaseModel): cz, sz = math.cos(rz), math.sin(rz) # Rz · Ry · Rx (applied right-to-left: Rx first, then Ry, then Rz). return [ - [cy * cz, sx * sy * cz - cx * sz, cx * sy * cz + sx * sz], - [cy * sz, sx * sy * sz + cx * cz, cx * sy * sz - sx * cz], - [-sy, sx * cy, cx * cy], + [cy * cz, sx * sy * cz - cx * sz, cx * sy * cz + sx * sz], + [cy * sz, sx * sy * sz + cx * cz, cx * sy * sz - sx * cz], + [-sy, sx * cy, cx * cy], ] def local_to_world(self, p_local: tuple[float, float, float]) -> tuple[float, float, float]: diff --git a/src/otko/core/geometry/node.py b/src/otko/core/geometry/node.py index d37e3bf..e882b26 100644 --- a/src/otko/core/geometry/node.py +++ b/src/otko/core/geometry/node.py @@ -33,6 +33,11 @@ class Node(Entity): mass: Mass6 = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0) restraint: Restraint6 = (False, False, False, False, False, False) + @property + def default_name(self) -> str: + """Fallback label for unnamed nodes (``N``).""" + return f"N{self.id}" + @property def is_restrained(self) -> bool: """True if any DOF is fixed.""" diff --git a/src/otko/core/loads/__init__.py b/src/otko/core/loads/__init__.py index 68a1557..77429fc 100644 --- a/src/otko/core/loads/__init__.py +++ b/src/otko/core/loads/__init__.py @@ -12,9 +12,9 @@ pattern is deleted, its loads go with it. from __future__ import annotations -from typing import Annotated, Literal +from typing import Annotated, Literal, Union -from pydantic import BaseModel, ConfigDict, Field, PositiveInt, model_validator +from pydantic import BaseModel, ConfigDict, Field, PositiveInt from otko.core._base import Entity @@ -77,19 +77,15 @@ class ResponseSpectrum(Entity): type: Literal["ResponseSpectrum"] = "ResponseSpectrum" periods: list[float] = Field( - ..., - min_length=2, + ..., min_length=2, description="Periods (s), strictly increasing.", ) accelerations: list[float] = Field( - ..., - min_length=2, + ..., min_length=2, description="Spectral pseudo-accelerations (length must match `periods`).", ) damping_ratio: float = Field( - default=0.05, - ge=0.0, - le=1.0, + default=0.05, ge=0.0, le=1.0, description="Modal damping ratio the spectrum was built for.", ) @@ -110,7 +106,7 @@ class ResponseSpectrum(Entity): TimeSeries = Annotated[ - LinearTimeSeries | ConstantTimeSeries | PathTimeSeries, + Union[LinearTimeSeries, ConstantTimeSeries, PathTimeSeries], Field(discriminator="type"), ] @@ -189,67 +185,13 @@ class ImposedSupportMotionPattern(Entity): direction: int = Field(..., ge=1, le=6, description="DOF direction (1..6).") disp_series_id: PositiveInt node_ids: list[int] = Field( - ..., - min_length=1, + ..., min_length=1, description="Support nodes driven by the motion (each restrained in `direction`).", ) factor: float = 1.0 -# ──────────────────────────── Load combinations ──────────────────────────── -class LoadCombinationItem(BaseModel): - """One term of a case-result load combination: ``factor × results(case_id)``.""" - - model_config = ConfigDict(extra="forbid", validate_assignment=True) - - case_id: PositiveInt = Field(..., description="AnalysisCase whose results are scaled.") - factor: float = Field(default=1.0, description="Scale applied to that case's results.") - - -class LoadCombination(Entity): - """Named combination of *completed* analysis-case results (post-processing). - - A ``Linear`` combination is the weighted sum - ``Σ factor_i × results(case_i)`` over nodal displacements, reactions, - and element forces. Valid for linear-elastic static superposition - (e.g. ``1.2×Dead + 1.6×Live``) — combining nonlinear, transient, or - modal results is rejected at evaluation time, not here, so projects - stay loadable while the UI guides the user. - - An ``Envelope`` combination reports the per-DOF, per-step maximum and - minimum across the factored terms instead of their sum. - """ - - type: Literal["LoadCombination"] = "LoadCombination" - kind: Literal["Linear", "Envelope"] = Field( - default="Linear", description="Linear = weighted sum; Envelope = per-DOF max/min." - ) - items: list[LoadCombinationItem] = Field( - ..., min_length=1, description="Terms of the combination (≥1; ≥2 recommended)." - ) - - @model_validator(mode="after") - def _check_items(self) -> LoadCombination: - """Reject duplicate case ids and non-finite factors.""" - import math - - seen: set[int] = set() - dup: set[int] = set() - for item in self.items: - if item.case_id in seen: - dup.add(item.case_id) - seen.add(item.case_id) - if not math.isfinite(item.factor): - raise ValueError( - f"LoadCombination {self.id}: factor for case {item.case_id} " - f"must be finite, got {item.factor!r}." - ) - if dup: - raise ValueError(f"LoadCombination {self.id}: duplicate case ids: {sorted(dup)}.") - return self - - LoadPattern = Annotated[ - PlainLoadPattern | UniformExcitationPattern | ImposedSupportMotionPattern, + Union[PlainLoadPattern, UniformExcitationPattern, ImposedSupportMotionPattern], Field(discriminator="type"), ] diff --git a/src/otko/core/materials/__init__.py b/src/otko/core/materials/__init__.py index daa84f6..a12d75e 100644 --- a/src/otko/core/materials/__init__.py +++ b/src/otko/core/materials/__init__.py @@ -11,7 +11,7 @@ https://openseespydoc.readthedocs.io/en/latest/src/uniaxialMaterial.html from __future__ import annotations -from typing import Annotated, Literal +from typing import Annotated, Literal, Union from pydantic import Field, PositiveFloat, model_validator @@ -58,9 +58,7 @@ class Steel02(Entity): Fy: PositiveFloat E0: PositiveFloat b: float = Field(..., ge=0.0, le=1.0) - R0: float = Field( - default=18.0, description="Bauschinger curvature parameter (typically 10–20)." - ) + R0: float = Field(default=18.0, description="Bauschinger curvature parameter (typically 10–20).") cR1: float = Field(default=0.925) cR2: float = Field(default=0.15) @@ -85,10 +83,7 @@ class Concrete02(Entity): fpcu: float = Field(..., le=0.0) epsU: float = Field(..., lt=0.0) lambda_: float = Field( - ..., - alias="lambda", - ge=0.0, - le=1.0, + ..., alias="lambda", ge=0.0, le=1.0, description="Ratio between unloading slope at epscu and initial slope.", ) ft: PositiveFloat = Field(..., description="Tensile strength.") @@ -113,24 +108,20 @@ class Concrete04(Entity): epscu: float = Field(..., lt=0.0, description="Ultimate compressive strain (negative).") Ec: PositiveFloat = Field(..., description="Initial tangent modulus.") fct: float | None = Field( - default=None, - gt=0.0, + default=None, gt=0.0, description="Maximum tensile strength. Omit for no-tension model.", ) et: float | None = Field( - default=None, - gt=0.0, + default=None, gt=0.0, description="Ultimate tensile strain. Required when fct is given.", ) beta: float | None = Field( - default=None, - ge=0.0, - le=1.0, + default=None, ge=0.0, le=1.0, description="Cyclic degradation factor on unloading stiffness. Requires fct and et.", ) @model_validator(mode="after") - def _tensile_params_consistent(self) -> Concrete04: + def _tensile_params_consistent(self) -> "Concrete04": if self.fct is not None and self.et is None: raise ValueError("et is required when fct is given.") if self.et is not None and self.fct is None: @@ -147,8 +138,7 @@ class ElasticPP(Entity): E: PositiveFloat epsy_pos: PositiveFloat = Field(..., description="Yield strain in tension.") epsy_neg: float | None = Field( - default=None, - description="Yield strain in compression (negative); defaults to -epsy_pos.", + default=None, description="Yield strain in compression (negative); defaults to -epsy_pos.", ) eps0: float = Field(default=0.0, description="Initial strain.") @@ -186,8 +176,7 @@ class HystereticMaterial(Entity): d1: float = Field(default=0.0, ge=0.0, description="Ductility damage, linear portion.") d2: float = Field(default=0.0, ge=0.0, description="Ductility damage, cumulative portion.") beta: float = Field( - default=0.0, - ge=0.0, + default=0.0, ge=0.0, description="Unloading-stiffness degradation (0 = no degradation).", ) @@ -217,8 +206,7 @@ class HystereticSM(Entity): type: Literal["HystereticSM"] = "HystereticSM" pos_env: list[tuple[float, float]] = Field( - ..., - min_length=1, + ..., min_length=1, description="Positive envelope (force, deformation) pairs, force first.", ) neg_env: list[tuple[float, float]] = Field( @@ -229,16 +217,18 @@ class HystereticSM(Entity): # ──────────────────────────── Discriminated union ──────────────────────────── Material = Annotated[ - ElasticIsotropic - | ElasticUniaxial - | Steel01 - | Steel02 - | Concrete01 - | Concrete02 - | Concrete04 - | ElasticPP - | HystereticMaterial - | HystereticSM, + Union[ + ElasticIsotropic, + ElasticUniaxial, + Steel01, + Steel02, + Concrete01, + Concrete02, + Concrete04, + ElasticPP, + HystereticMaterial, + HystereticSM, + ], Field(discriminator="type"), ] """Tagged union of every material kind. Pydantic uses ``type`` to dispatch on JSON load.""" diff --git a/src/otko/core/project.py b/src/otko/core/project.py index f2d030a..20890a4 100644 --- a/src/otko/core/project.py +++ b/src/otko/core/project.py @@ -19,7 +19,15 @@ from __future__ import annotations from collections.abc import Iterable from typing import Any, cast -from pydantic import BaseModel, ConfigDict, Field, PositiveInt, PrivateAttr, model_validator +from pydantic import ( + BaseModel, + ConfigDict, + Field, + PositiveInt, + PrivateAttr, + field_validator, + model_validator, +) from otko.core.analysis import AnalysisCase from otko.core.constraints import EqualDOFConstraint @@ -30,7 +38,7 @@ from otko.core.geometry import ( Node, default_global_system, ) -from otko.core.loads import LoadCombination, LoadPattern, ResponseSpectrum, TimeSeries +from otko.core.loads import LoadPattern, ResponseSpectrum, TimeSeries from otko.core.materials import Material from otko.core.sections import Section from otko.core.units import UnitSystem @@ -110,7 +118,6 @@ _INDEX_KINDS: tuple[str, ...] = ( "load_patterns", "spectra", "analyses", - "combinations", ) @@ -122,7 +129,22 @@ class ProjectMeta(BaseModel): name: str = "Untitled" description: str = "" author: str = "" - units: UnitSystem = UnitSystem.SI_M_N + units: UnitSystem = UnitSystem.METRIC + + @field_validator("units", mode="before") + @classmethod + def _migrate_legacy_units(cls, value: object) -> object: + """Map retired 4-system value strings onto Metric/Imperial. + + Files saved under the dominant systems map exactly; files saved + under the retired detail systems (mm / ft) still load, but their + stored numbers keep the old scale — see ``LEGACY_UNIT_SYSTEMS``. + """ + from otko.core.units import LEGACY_UNIT_SYSTEMS + + if isinstance(value, str) and value in LEGACY_UNIT_SYSTEMS: + return LEGACY_UNIT_SYSTEMS[value] + return value class Project(BaseModel): @@ -166,7 +188,7 @@ class Project(BaseModel): return data @model_validator(mode="after") - def _ensure_global_system(self) -> Project: + def _ensure_global_system(self) -> "Project": """Guarantee that a 'Global' entry exists as the first coord system.""" has_global = any(cs.name == "Global" for cs in self.coord_systems) if not has_global: @@ -174,7 +196,7 @@ class Project(BaseModel): return self @model_validator(mode="after") - def _ensure_indexed(self) -> Project: + def _ensure_indexed(self) -> "Project": """Wrap every collection in an :class:`_IndexedList` and drop caches. Runs after each validation pass (construction, ``model_validate``, @@ -219,11 +241,10 @@ class Project(BaseModel): load_patterns: list[LoadPattern] = Field(default_factory=list) spectra: list[ResponseSpectrum] = Field(default_factory=list) analyses: list[AnalysisCase] = Field(default_factory=list) - combinations: list[LoadCombination] = Field(default_factory=list) # ─────────────────── invariants ─────────────────── @model_validator(mode="after") - def _check_ndm_ndf(self) -> Project: + def _check_ndm_ndf(self) -> "Project": valid = {(2, 2), (2, 3), (3, 3), (3, 6)} if (self.ndm, self.ndf) not in valid: raise ValueError( @@ -233,7 +254,7 @@ class Project(BaseModel): return self @model_validator(mode="after") - def _check_unique_ids(self) -> Project: + def _check_unique_ids(self) -> "Project": for label, items in ( ("node", self.nodes), ("material", self.materials), @@ -242,7 +263,6 @@ class Project(BaseModel): ("time series", self.time_series), ("load pattern", self.load_patterns), ("analysis", self.analyses), - ("combination", self.combinations), ): ids = [it.id for it in items] if len(ids) != len(set(ids)): @@ -277,10 +297,6 @@ class Project(BaseModel): def next_analysis_id(self) -> int: return self._next_id(self.analyses) - def next_combination_id(self) -> int: - """Return the next free load-combination id.""" - return self._next_id(self.combinations) - # ─────────────────── lookups ─────────────────── def node(self, node_id: PositiveInt) -> Node: return cast(Node, self._lookup("nodes", self.nodes, node_id, "node")) @@ -294,20 +310,6 @@ class Project(BaseModel): def element(self, element_id: PositiveInt) -> Element: return cast(Element, self._lookup("elements", self.elements, element_id, "element")) - def combination(self, combination_id: PositiveInt) -> LoadCombination: - """Return the load combination with ``combination_id``. - - Args: - combination_id: Combination tag to look up. - - Raises: - KeyError: if no combination carries that id. - """ - return cast( - LoadCombination, - self._lookup("combinations", self.combinations, combination_id, "combination"), - ) - def _lookup(self, kind: str, items: list[Any], target_id: int, label: str) -> Any: """Return the entity with ``target_id`` from ``items`` in O(1) time. @@ -420,14 +422,5 @@ class Project(BaseModel): f"uses invalid DOF {dof} for ndf={self.ndf}." ) - analysis_ids = {c.id for c in self.analyses} - for combo in self.combinations: - for item in combo.items: - if item.case_id not in analysis_ids: - problems.append( - f"Combination {combo.id} refers to missing analysis case " - f"{item.case_id}." - ) - if problems: raise ValueError("Reference validation failed:\n - " + "\n - ".join(problems)) diff --git a/src/otko/core/sections/__init__.py b/src/otko/core/sections/__init__.py index 530e0ca..5e8ce0e 100644 --- a/src/otko/core/sections/__init__.py +++ b/src/otko/core/sections/__init__.py @@ -16,7 +16,7 @@ exactly so the runner can emit them without further translation. from __future__ import annotations -from typing import Annotated, Literal +from typing import Annotated, Literal, Union from pydantic import BaseModel, ConfigDict, Field, PositiveFloat, PositiveInt @@ -61,7 +61,7 @@ class RectShape(BaseModel): SectionShape = Annotated[ - PipeShape | AngleShape | RectShape, + Union[PipeShape, AngleShape, RectShape], Field(discriminator="kind"), ] @@ -75,12 +75,8 @@ class ElasticSection(Entity): A: PositiveFloat Iz: PositiveFloat = Field(..., description="Moment of inertia about local z-axis.") Iy: PositiveFloat | None = Field(default=None, description="Required for 3D frames.") - G: PositiveFloat | None = Field( - default=None, description="Shear modulus; required for 3D frames." - ) - J: PositiveFloat | None = Field( - default=None, description="Torsional constant; required for 3D frames." - ) + G: PositiveFloat | None = Field(default=None, description="Shear modulus; required for 3D frames.") + J: PositiveFloat | None = Field(default=None, description="Torsional constant; required for 3D frames.") shape: SectionShape | None = Field( default=None, description=( @@ -165,12 +161,12 @@ class StraightLayer(BaseModel): Patch = Annotated[ - RectangularPatch | CircularPatch, + Union[RectangularPatch, CircularPatch], Field(discriminator="kind"), ] Layer = Annotated[ - StraightLayer, + Union[StraightLayer], Field(discriminator="kind"), ] @@ -202,8 +198,7 @@ class AggregatorDOF(BaseModel): material_id: PositiveInt dof: Literal["P", "Mz", "My", "Vy", "Vz", "T"] = Field( - ..., - description="Section DOF code (OpenSees section-deformation names).", + ..., description="Section DOF code (OpenSees section-deformation names).", ) @@ -225,6 +220,6 @@ class SectionAggregator(Entity): Section = Annotated[ - ElasticSection | FiberSection | SectionAggregator, + Union[ElasticSection, FiberSection, SectionAggregator], Field(discriminator="type"), ] diff --git a/src/otko/core/units.py b/src/otko/core/units.py index 63b40cc..df32b6d 100644 --- a/src/otko/core/units.py +++ b/src/otko/core/units.py @@ -10,30 +10,52 @@ The :func:`labels_for` helper returns an :class:`UnitLabels` bundle matching SAP2000's "Set Program Default Display Units" semantics: length / force / moment / stress / curvature / rotation labels, all driven by :class:`UnitSystem` enum. + +The ``DISPLAY`` table implements ``docs/consistent_units.md`` §4: pure +data mapping each quantity to a ``(display label, multiply-by-factor)`` +pair. Factors are applied in views/viewmodels on read only — converted +values never flow into commands, services, or persistence. Stored +values are always in the project's native system. """ from __future__ import annotations +import math from dataclasses import dataclass from enum import Enum class UnitSystem(str, Enum): - """Consistent unit systems supported by the application.""" + """Consistent unit systems supported by the application. - SI_M_N = "SI (m, N, kg, s, Pa)" + Two systems only: Metric (metre/newton) and Imperial (inch/kip). + OpenSees never converts, and neither do we — stored values are + always native to the project's system; views only relabel/rescale + for humans on read. + """ + + METRIC = "Metric (m, N, kg, s, Pa)" """Length: m, Force: N, Mass: kg, Time: s, Stress: Pa.""" - SI_MM_N = "SI (mm, N, t, s, MPa)" - """Length: mm, Force: N, Mass: t, Time: s, Stress: MPa.""" - - US_FT_KIP = "US (ft, kip, slug, s, ksf)" - """Length: ft, Force: kip, Mass: slug, Time: s, Stress: ksf.""" - - US_IN_KIP = "US (in, kip, kip·s²/in, s, ksi)" + IMPERIAL = "Imperial (in, kip, kip·s²/in, s, ksi)" """Length: in, Force: kip, Mass: kip·s²/in, Time: s, Stress: ksi.""" +#: Legacy value strings from the retired 4-system era, mapped to their +#: surviving system on load. Files saved under the two dominant systems +#: (``SI_M_N``/``US_IN_KIP``) map exactly. Files saved under the detail +#: systems (``SI_MM_N`` in mm, ``US_FT_KIP`` in ft) still load, but their +#: stored numbers keep their old scale — a mm model opened as Metric +#: will read 1000× too large until the engineer re-enters values in the +#: native m/in system. The scale caveat is logged by the loader path. +LEGACY_UNIT_SYSTEMS: dict[str, UnitSystem] = { + "SI (m, N, kg, s, Pa)": UnitSystem.METRIC, + "SI (mm, N, t, s, MPa)": UnitSystem.METRIC, + "US (ft, kip, slug, s, ksf)": UnitSystem.IMPERIAL, + "US (in, kip, kip·s²/in, s, ksi)": UnitSystem.IMPERIAL, +} + + @dataclass(frozen=True) class UnitLabels: """Display strings for each basic quantity in a unit system. @@ -51,7 +73,7 @@ class UnitLabels: _LABELS: dict[UnitSystem, UnitLabels] = { - UnitSystem.SI_M_N: UnitLabels( + UnitSystem.METRIC: UnitLabels( length="m", force="N", moment="N·m", @@ -59,23 +81,7 @@ _LABELS: dict[UnitSystem, UnitLabels] = { curvature="1/m", rotation="rad", ), - UnitSystem.SI_MM_N: UnitLabels( - length="mm", - force="N", - moment="N·mm", - stress="MPa", - curvature="1/mm", - rotation="rad", - ), - UnitSystem.US_FT_KIP: UnitLabels( - length="ft", - force="kip", - moment="kip·ft", - stress="ksf", - curvature="1/ft", - rotation="rad", - ), - UnitSystem.US_IN_KIP: UnitLabels( + UnitSystem.IMPERIAL: UnitLabels( length="in", force="kip", moment="kip·in", @@ -89,3 +95,255 @@ _LABELS: dict[UnitSystem, UnitLabels] = { def labels_for(units: UnitSystem) -> UnitLabels: """Return the label bundle for the given unit system.""" return _LABELS[units] + + +# ── Consistent-units plan (§5 quantity taxonomy + §4 display factors) ── + +#: Canonical quantity keys. Same strings as the future ``UnitTag`` so a +#: follow-up ADR can adopt them unchanged. ``section_dim`` is an extra +#: display-only key (geometry vs section-size prefix split in §3). +QUANTITIES: tuple[str, ...] = ( + "length", + "section_dim", + "displacement", + "rotation", + "force", + "moment", + "distributed_load", + "stress", + "area", + "inertia", + "mass", + "time", + "velocity", + "acceleration", + "frequency", + "curvature", + "temperature", +) + +#: Dimensionless quantities never take factors and are absent from +#: ``DISPLAY`` by design (strain, drift ratio, damping ratio, mass +#: participation, etc.). +DIMENSIONLESS: tuple[str, ...] = ( + "strain", + "drift", + "damping_ratio", + "mass_participation", +) + +#: Gravity constants for mass-from-weight derivations. Document the +#: constant next to every such computation; never hardcode it in a +#: system-agnostic path. +GRAVITY: dict[UnitSystem, float] = { + UnitSystem.METRIC: 9.81, # m/s² + UnitSystem.IMPERIAL: 386.4, # in/s² +} + +#: Degrees per radian — formatting option for post views only (§6). +#: Solver and storage stay in radians. +RAD_TO_DEG: float = 180.0 / math.pi + +#: Stored (native) unit labels per quantity — what the solver and the +#: ``.osmodel`` see. Views show native values everywhere (we never +#: auto-convert), so input hints, table headers, and axis labels must +#: use these, NOT the ``DISPLAY`` (human-prefix) labels. E.g. Metric +#: forces are stored in N ("Fx (N)"), even though post views may one +#: day render them as kN. +NATIVE: dict[UnitSystem, dict[str, str]] = { + UnitSystem.METRIC: { + "length": "m", + "section_dim": "m", + "displacement": "m", + "rotation": "rad", + "force": "N", + "moment": "N·m", + "distributed_load": "N/m", + "stress": "Pa", + "area": "m²", + "inertia": "m⁴", + "mass": "kg", + "time": "s", + "velocity": "m/s", + "acceleration": "m/s²", + "frequency": "Hz", + "curvature": "1/m", + "temperature": "°C", + }, + UnitSystem.IMPERIAL: { + "length": "in", + "section_dim": "in", + "displacement": "in", + "rotation": "rad", + "force": "kip", + "moment": "kip·in", + "distributed_load": "kip/in", + "stress": "ksi", + "area": "in²", + "inertia": "in⁴", + "mass": "kip·s²/in", + "time": "s", + "velocity": "in/s", + "acceleration": "in/s²", + "frequency": "Hz", + "curvature": "1/in", + "temperature": "°F", + }, +} + + +def native_label(units: UnitSystem, quantity: str) -> str: + """Return the stored-unit label for a quantity (e.g. ``"N"``). + + Use this for every label placed next to a native (unconverted) + value: input hints, table headers, axis titles. + """ + try: + return NATIVE[units][quantity] + except KeyError: + raise KeyError(f"No native unit for quantity '{quantity}' in {units.value}.") from None + + +#: View-layer display table: quantity → (display label, multiply factor). +#: Stored = native solver unit; display = what humans read. Factors are +#: applied on read; converted values are never written back. +DISPLAY: dict[UnitSystem, dict[str, tuple[str, float]]] = { + UnitSystem.METRIC: { + "length": ("m", 1.0), + "section_dim": ("mm", 1000.0), + "displacement": ("mm", 1000.0), + "rotation": ("rad", 1.0), + "force": ("kN", 1e-3), + "moment": ("kN·m", 1e-3), + "distributed_load": ("kN/m", 1e-3), + "stress": ("MPa", 1e-6), + "area": ("mm²", 1e6), + "inertia": ("mm⁴", 1e12), + "mass": ("kg", 1.0), + "time": ("s", 1.0), + "velocity": ("m/s", 1.0), + "acceleration": ("m/s²", 1.0), + "frequency": ("Hz", 1.0), + "curvature": ("1/m", 1.0), + "temperature": ("°C", 1.0), + }, + UnitSystem.IMPERIAL: { + "length": ("in", 1.0), + "section_dim": ("in", 1.0), + "displacement": ("in", 1.0), + "rotation": ("rad", 1.0), + "force": ("kip", 1.0), + "moment": ("kip·in", 1.0), + "distributed_load": ("kip/in", 1.0), + "stress": ("ksi", 1.0), + "area": ("in²", 1.0), + "inertia": ("in⁴", 1.0), + "mass": ("kip·s²/in", 1.0), + "time": ("s", 1.0), + "velocity": ("in/s", 1.0), + "acceleration": ("in/s²", 1.0), + "frequency": ("Hz", 1.0), + "curvature": ("1/in", 1.0), + "temperature": ("°F", 1.0), + }, +} + +#: Small-value display aliases (×1000 from kip units, view formatting +#: only — never separate systems). Applied when |display value| is +#: small enough that kip units would read as ~0. +_SMALL_ALIASES: dict[tuple[UnitSystem, str], tuple[str, float]] = { + (UnitSystem.IMPERIAL, "force"): ("lbf", 1000.0), + (UnitSystem.IMPERIAL, "distributed_load"): ("lbf/in", 1000.0), + (UnitSystem.IMPERIAL, "moment"): ("lb·in", 1000.0), +} + +#: Below this display magnitude (in base display units) the small-value +#: alias kicks in. Zero never aliases. +SMALL_ALIAS_THRESHOLD: float = 0.1 + + +def display_for(units: UnitSystem, quantity: str) -> tuple[str, float]: + """Return ``(display label, multiply-by-factor)`` for a quantity. + + Args: + units: Project's native stored system. + quantity: One of :data:`QUANTITIES`. + + Raises: + KeyError: For unknown or dimensionless quantities (dimensionless + values never take factors by design). + """ + try: + return DISPLAY[units][quantity] + except KeyError: + raise KeyError( + f"No display entry for quantity '{quantity}' in {units.value}. " + "Dimensionless quantities (strain, drift, damping, …) " + "never take factors." + ) from None + + +def display_label(units: UnitSystem, quantity: str) -> str: + """Return the display label for a quantity (e.g. ``"mm"``).""" + label, _factor = display_for(units, quantity) + return label + + +def display_factor(units: UnitSystem, quantity: str) -> float: + """Return the multiply-by-factor for a quantity (e.g. ``1000.0``).""" + _label, factor = display_for(units, quantity) + return factor + + +def to_display(value: float, units: UnitSystem, quantity: str) -> float: + """Convert a native stored value to its display value (× factor). + + View-layer only — the result must never be written back to the + model, commands, services, or persistence. + """ + return float(value) * display_factor(units, quantity) + + +def format_display( + value: float, + units: UnitSystem, + quantity: str, + *, + precision: str = ".4g", +) -> str: + """Format ``"12.3 mm"`` for a native value in display units.""" + label, factor = display_for(units, quantity) + return f"{float(value) * factor:{precision}} {label}" + + +def display_with_alias( + value: float, + units: UnitSystem, + quantity: str, +) -> tuple[float, str]: + """Return ``(display value, label)`` applying the lbf/plf alias rule. + + When the base display magnitude is non-zero but below + :data:`SMALL_ALIAS_THRESHOLD`, imperial force-like quantities are + scaled ×1000 into ``lbf``/``plf``/``lb·ft`` so ``0.004 kip`` + renders as ``4.0 lbf``. All other cases return the base display + pair unchanged. Pure formatting helper — no model impact. + """ + label, factor = display_for(units, quantity) + disp = float(value) * factor + alias = _SMALL_ALIASES.get((units, quantity)) + if alias is not None and disp != 0.0 and abs(disp) < SMALL_ALIAS_THRESHOLD: + alias_label, alias_scale = alias + return disp * alias_scale, alias_label + return disp, label + + +def format_rotation(value_rad: float, *, use_deg: bool = False) -> str: + """Format a rotation stored in radians. + + Solver and storage stay in radians; ``use_deg=True`` is a post-view + formatting option only (×180/π on read). + """ + if use_deg: + return f"{float(value_rad) * RAD_TO_DEG:.4g} °" + return f"{float(value_rad):.4g} rad" diff --git a/src/otko/resources/icons-dark/assign-distributed.svg b/src/otko/resources/icons-dark/assign-distributed.svg new file mode 100644 index 0000000..2f21560 --- /dev/null +++ b/src/otko/resources/icons-dark/assign-distributed.svg @@ -0,0 +1,11 @@ + + + Assign distributed load + + + + + + + + diff --git a/src/otko/resources/icons/assign-distributed.svg b/src/otko/resources/icons/assign-distributed.svg new file mode 100644 index 0000000..2f21560 --- /dev/null +++ b/src/otko/resources/icons/assign-distributed.svg @@ -0,0 +1,11 @@ + + + Assign distributed load + + + + + + + + diff --git a/src/otko/services/__init__.py b/src/otko/services/__init__.py index 0c14bb3..b14f725 100644 --- a/src/otko/services/__init__.py +++ b/src/otko/services/__init__.py @@ -6,12 +6,6 @@ worker lives in ``qt_workers`` and is the only module here that imports PySide6. """ -from otko.services.combinations import ( - EnvelopeResults, - combine_static_results, - envelope_static_results, - evaluate_combination, -) from otko.services.export import export_opspy, export_tcl from otko.services.material_tester import ( CyclicSegment, @@ -34,16 +28,12 @@ from otko.services.results import ( __all__ = [ "PROJECT_FILE_SUFFIX", "CyclicSegment", - "EnvelopeResults", "LoadProtocol", "MaterialTestResult", "ModalResults", "OpenSeesRunner", "StaticResults", "TransientResults", - "combine_static_results", - "envelope_static_results", - "evaluate_combination", "export_opspy", "export_tcl", "load_project", diff --git a/src/otko/services/_emit.py b/src/otko/services/_emit.py index ea6507a..61e7530 100644 --- a/src/otko/services/_emit.py +++ b/src/otko/services/_emit.py @@ -409,10 +409,7 @@ class OpenSeesEmitter: if el.consistent_mass: ebc_args.append("-cMass") ops.element( - "elasticBeamColumn", - el.id, - *el.nodes, - *ebc_args, + "elasticBeamColumn", el.id, *el.nodes, *ebc_args, ) case ForceBeamColumn(): tag = self._element_geom_transf_tag[el.id] @@ -433,13 +430,9 @@ class OpenSeesEmitter: ops.element("dispBeamColumn", el.id, *el.nodes, tag, el.id) case ZeroLengthElement(): zl_args: list[Any] = [ - "zeroLength", - el.id, - *el.nodes, - "-mat", - *el.material_ids, - "-dir", - *el.dofs, + "zeroLength", el.id, *el.nodes, + "-mat", *el.material_ids, + "-dir", *el.dofs, ] # ``-doRayleigh 1`` only when requested (default off, matching # OpenSees' zeroLength default) — so an isolator can opt its @@ -654,27 +647,15 @@ class OpenSeesEmitter: ) vel = np.gradient(np.asarray(ts.values, dtype=float), ts.dt) ops.timeSeries( - "Path", - vel_tag, - "-dt", - ts.dt, - "-values", - *vel.tolist(), - "-factor", - ts.factor, + "Path", vel_tag, "-dt", ts.dt, "-values", *vel.tolist(), + "-factor", ts.factor, ) for nid in pat.node_ids: ops.remove("sp", nid, pat.direction) ops.pattern("MultipleSupport", pat.id) ops.groundMotion( - pat.id, - "Plain", - "-disp", - pat.disp_series_id, - "-vel", - vel_tag, - "-fact", - pat.factor, + pat.id, "Plain", "-disp", pat.disp_series_id, "-vel", vel_tag, + "-fact", pat.factor, ) for nid in pat.node_ids: ops.imposedMotion(nid, pat.direction, pat.id) diff --git a/src/otko/services/animation_export.py b/src/otko/services/animation_export.py index fa80790..c23f5ea 100644 --- a/src/otko/services/animation_export.py +++ b/src/otko/services/animation_export.py @@ -12,9 +12,8 @@ the main thread. from __future__ import annotations import math -from collections.abc import Callable from pathlib import Path -from typing import Any +from typing import Any, Callable import numpy as np @@ -56,7 +55,8 @@ def export_mode_shape_video( else: # MP4 / WebM use FFmpeg backend. macro_block_size=1 disables # the auto-resize-for-h264 pass (still works on modern players). - iio.imwrite(output_path, frames, fps=fps, codec="libx264", macro_block_size=1) + iio.imwrite(output_path, frames, fps=fps, codec="libx264", + macro_block_size=1) def export_time_history_video( @@ -94,4 +94,5 @@ def export_time_history_video( if suffix == ".gif": iio.imwrite(output_path, frames, duration=1.0 / fps, loop=0) else: - iio.imwrite(output_path, frames, fps=fps, codec="libx264", macro_block_size=1) + iio.imwrite(output_path, frames, fps=fps, codec="libx264", + macro_block_size=1) diff --git a/src/otko/services/combinations.py b/src/otko/services/combinations.py deleted file mode 100644 index 20a0d3a..0000000 --- a/src/otko/services/combinations.py +++ /dev/null @@ -1,235 +0,0 @@ -"""Case-result load combinations — post-processing superposition. - -A :class:`LoadCombination` names a weighted set of *completed* analysis -results (e.g. ``1.2×Dead + 1.6×Live``). Evaluation is pure NumPy over -:class:`StaticResults` — no solver re-run — and is only valid for -linear-elastic static superposition. Combining any other result kind -(transient, modal, pushover, response-spectrum) raises :class:`TypeError`. - -Two combination kinds: - -- ``Linear`` → :func:`combine_static_results`: weighted sum, returned as - :class:`StaticResults` (``case_id``/``case_name`` taken from the combo). -- ``Envelope`` → :func:`envelope_static_results`: per-DOF, per-step - maximum/minimum across the factored terms, returned as - :class:`EnvelopeResults`. -""" - -from __future__ import annotations - -from collections.abc import Mapping, Sequence -from dataclasses import dataclass, field -from typing import Any - -import numpy as np - -from otko.core.loads import LoadCombination -from otko.services.results import StaticResults - - -@dataclass -class EnvelopeResults: - """Per-DOF, per-step max/min across a combination's factored terms.""" - - case_id: int - case_name: str - n_steps: int - node_disp_max: dict[int, np.ndarray] = field(default_factory=dict) - """node_id → array of shape (n_steps, ndf), per-DOF maxima.""" - node_disp_min: dict[int, np.ndarray] = field(default_factory=dict) - """node_id → array of shape (n_steps, ndf), per-DOF minima.""" - node_reaction_max: dict[int, np.ndarray] = field(default_factory=dict) - """node_id → array of shape (n_steps, ndf), per-DOF maxima.""" - node_reaction_min: dict[int, np.ndarray] = field(default_factory=dict) - """node_id → array of shape (n_steps, ndf), per-DOF minima.""" - element_forces_max: dict[int, np.ndarray] = field(default_factory=dict) - """element_id → array of shape (n_steps, n_components), maxima.""" - element_forces_min: dict[int, np.ndarray] = field(default_factory=dict) - """element_id → array of shape (n_steps, n_components), minima.""" - - -def _check_compatible(results: Sequence[StaticResults]) -> int: - """Validate equal steps, nodes, elements and shapes; return ``n_steps``. - - Raises: - ValueError: on any incompatibility, with the offending detail. - """ - if not results: - raise ValueError("Need at least one result to combine.") - first = results[0] - n_steps = first.n_steps - node_ids = set(first.node_disp) - elem_ids = set(first.element_forces) - reaction_ids = set(first.node_reaction) - for res in results[1:]: - if res.n_steps != n_steps: - raise ValueError( - f"Step-count mismatch: case {first.case_id} has {n_steps} steps, " - f"case {res.case_id} has {res.n_steps}." - ) - if set(res.node_disp) != node_ids: - raise ValueError(f"Node set mismatch: case {first.case_id} vs case {res.case_id}.") - if set(res.node_reaction) != reaction_ids: - raise ValueError(f"Reaction set mismatch: case {first.case_id} vs case {res.case_id}.") - if set(res.element_forces) != elem_ids: - raise ValueError(f"Element set mismatch: case {first.case_id} vs case {res.case_id}.") - for nid in node_ids: - if res.node_disp[nid].shape != first.node_disp[nid].shape: - raise ValueError( - f"Displacement shape mismatch at node {nid}: " - f"case {first.case_id} vs case {res.case_id}." - ) - if res.node_reaction[nid].shape != first.node_reaction[nid].shape: - raise ValueError( - f"Reaction shape mismatch at node {nid}: " - f"case {first.case_id} vs case {res.case_id}." - ) - for eid in elem_ids: - if res.element_forces[eid].shape != first.element_forces[eid].shape: - raise ValueError( - f"Element-force shape mismatch at element {eid}: " - f"case {first.case_id} vs case {res.case_id}." - ) - return n_steps - - -def combine_static_results( - results: Sequence[StaticResults], - factors: Sequence[float], - *, - case_id: int, - case_name: str, -) -> StaticResults: - """Return the weighted sum ``Σ factor_i × results_i``. - - Args: - results: Source static results, one per combination term. - factors: Scale per term (same order as ``results``). - case_id: Tag for the returned results (the combination's id). - case_name: Label for the returned results (the combination's name). - - Raises: - ValueError: on length mismatch, non-finite factor, or - incompatible result shapes. - """ - if len(results) != len(factors): - raise ValueError(f"Results/factors length mismatch: {len(results)} vs {len(factors)}.") - for factor in factors: - if not np.isfinite(factor): - raise ValueError(f"Combination factor must be finite, got {factor!r}.") - n_steps = _check_compatible(results) - - node_disp: dict[int, np.ndarray] = {} - node_reaction: dict[int, np.ndarray] = {} - for nid in results[0].node_disp: - acc_d = np.zeros_like(results[0].node_disp[nid], dtype=float) - acc_r = np.zeros_like(results[0].node_reaction[nid], dtype=float) - for r, f in zip(results, factors, strict=False): - acc_d = acc_d + float(f) * r.node_disp[nid] - acc_r = acc_r + float(f) * r.node_reaction[nid] - node_disp[nid] = acc_d - node_reaction[nid] = acc_r - element_forces: dict[int, np.ndarray] = {} - for eid in results[0].element_forces: - acc_e = np.zeros_like(results[0].element_forces[eid], dtype=float) - for r, f in zip(results, factors, strict=False): - acc_e = acc_e + float(f) * r.element_forces[eid] - element_forces[eid] = acc_e - return StaticResults( - case_id=case_id, - case_name=case_name, - n_steps=n_steps, - node_disp=node_disp, - node_reaction=node_reaction, - element_forces=element_forces, - ) - - -def envelope_static_results( - results: Sequence[StaticResults], - factors: Sequence[float], - *, - case_id: int, - case_name: str, -) -> EnvelopeResults: - """Return per-DOF, per-step max/min across the factored terms. - - Each term is ``factor_i × results_i``; the envelope takes the - element-wise maximum (and minimum) across terms at every step. - """ - if len(results) != len(factors): - raise ValueError(f"Results/factors length mismatch: {len(results)} vs {len(factors)}.") - for factor in factors: - if not np.isfinite(factor): - raise ValueError(f"Combination factor must be finite, got {factor!r}.") - n_steps = _check_compatible(results) - - def _stack(getter: Any) -> dict[int, np.ndarray]: - keys = list(getter(results[0]).keys()) - return { - k: np.stack([float(f) * getter(r)[k] for r, f in zip(results, factors, strict=False)]) - for k in keys - } - - disp = _stack(lambda r: r.node_disp) - reac = _stack(lambda r: r.node_reaction) - elem = _stack(lambda r: r.element_forces) - return EnvelopeResults( - case_id=case_id, - case_name=case_name, - n_steps=n_steps, - node_disp_max={k: v.max(axis=0) for k, v in disp.items()}, - node_disp_min={k: v.min(axis=0) for k, v in disp.items()}, - node_reaction_max={k: v.max(axis=0) for k, v in reac.items()}, - node_reaction_min={k: v.min(axis=0) for k, v in reac.items()}, - element_forces_max={k: v.max(axis=0) for k, v in elem.items()}, - element_forces_min={k: v.min(axis=0) for k, v in elem.items()}, - ) - - -def evaluate_combination( - results_by_case_id: Mapping[int, Any], - combination: LoadCombination, -) -> StaticResults | EnvelopeResults: - """Evaluate ``combination`` against already-computed results. - - Args: - results_by_case_id: ``{analysis_case_id: results}`` — every - referenced case must be present as :class:`StaticResults`. - combination: The named combination to evaluate. - - Returns: - :class:`StaticResults` for ``Linear`` combos, - :class:`EnvelopeResults` for ``Envelope`` combos. - - Raises: - KeyError: if a referenced case has no entry in the map. - TypeError: if any referenced result is not :class:`StaticResults` - (superposition is only meaningful for static results). - ValueError: propagated from the combine/envelope helpers on - shape incompatibility. - """ - ordered: list[StaticResults] = [] - factors: list[float] = [] - for item in combination.items: - if item.case_id not in results_by_case_id: - raise KeyError( - f"Combination {combination.id}: no results for case {item.case_id} — " - "run that case first." - ) - res = results_by_case_id[item.case_id] - if not isinstance(res, StaticResults): - raise TypeError( - f"Combination {combination.id}: case {item.case_id} produced " - f"{type(res).__name__}, not StaticResults — case-result " - "combinations only support (linear) static cases." - ) - ordered.append(res) - factors.append(item.factor) - if combination.kind == "Envelope": - return envelope_static_results( - ordered, factors, case_id=combination.id, case_name=combination.name - ) - return combine_static_results( - ordered, factors, case_id=combination.id, case_name=combination.name - ) diff --git a/src/otko/services/deformation.py b/src/otko/services/deformation.py index 8c60f7b..a93340f 100644 --- a/src/otko/services/deformation.py +++ b/src/otko/services/deformation.py @@ -24,7 +24,7 @@ class DeformationSource: displacements to PyVista point arrays. """ - displacements: np.ndarray # shape (n_nodes, 3) — x, y, z components + displacements: np.ndarray # shape (n_nodes, 3) — x, y, z components node_id_to_row: dict[int, int] scale: float = 1.0 @@ -38,11 +38,8 @@ class DeformationSource: def static_to_deformation( - project: Project, - results: StaticResults, - *, - step: int = -1, - scale: float = 1.0, + project: Project, results: StaticResults, *, + step: int = -1, scale: float = 1.0, ) -> DeformationSource: """Build a DeformationSource from a static analysis's nodal displacements. @@ -60,16 +57,13 @@ def static_to_deformation( # Take only translation DOFs (first 2 in 2D, first 3 in 3D). n_take = min(3, snapshot.shape[0]) disp[node_id_to_row[nid], :n_take] = snapshot[:n_take] - return DeformationSource(displacements=disp, node_id_to_row=node_id_to_row, scale=scale) + return DeformationSource(displacements=disp, + node_id_to_row=node_id_to_row, scale=scale) def modal_to_deformation( - project: Project, - results: ModalResults, - *, - mode: int = 0, - scale: float = 1.0, - phase: float = 1.0, + project: Project, results: ModalResults, *, + mode: int = 0, scale: float = 1.0, phase: float = 1.0, ) -> DeformationSource: """Build a DeformationSource from a modal analysis's mode shape. @@ -84,9 +78,10 @@ def modal_to_deformation( mode_number = mode + 1 # mode_shapes is 1-indexed if mode_number not in results.mode_shapes: - return DeformationSource(displacements=disp, node_id_to_row=node_id_to_row, scale=scale) + return DeformationSource(displacements=disp, + node_id_to_row=node_id_to_row, scale=scale) - eigvec = results.mode_shapes[mode_number] # dict: nid → np.ndarray of DOF values + eigvec = results.mode_shapes[mode_number] # dict: nid → np.ndarray of DOF values for nid, vec in eigvec.items(): if nid not in node_id_to_row: continue @@ -106,15 +101,12 @@ def modal_to_deformation( norm_factor = (bbox * 0.05) / max_amp disp *= norm_factor - return DeformationSource(displacements=disp, node_id_to_row=node_id_to_row, scale=scale) + return DeformationSource(displacements=disp, + node_id_to_row=node_id_to_row, scale=scale) def transient_to_deformation_at_step( - project: Project, - results, - *, - step: int = 0, - scale: float = 1.0, + project: Project, results, *, step: int = 0, scale: float = 1.0, ) -> DeformationSource: """Build a DeformationSource from a transient analysis at one step. @@ -147,7 +139,8 @@ def transient_to_deformation_at_step( norm_factor = (bbox * 0.10) / max_amp disp *= norm_factor - return DeformationSource(displacements=disp, node_id_to_row=node_id_to_row, scale=scale) + return DeformationSource(displacements=disp, + node_id_to_row=node_id_to_row, scale=scale) def linear_static_auto_scale(project: Project, results: StaticResults) -> float: diff --git a/src/otko/services/element_forces.py b/src/otko/services/element_forces.py index d802579..36de591 100644 --- a/src/otko/services/element_forces.py +++ b/src/otko/services/element_forces.py @@ -41,36 +41,27 @@ from otko.services.results import StaticResults class ForceComponent(Enum): """Which force component to plot.""" - N = "N" # axial - V2 = "V2" # shear in local y (in-plane shear for 2D) - V3 = "V3" # shear in local z - T = "T" # torsion - M2 = "M2" # moment about local y - M3 = "M3" # moment about local z (in-plane moment for 2D) + N = "N" # axial + V2 = "V2" # shear in local y (in-plane shear for 2D) + V3 = "V3" # shear in local z + T = "T" # torsion + M2 = "M2" # moment about local y + M3 = "M3" # moment about local z (in-plane moment for 2D) # Map (component, end) → index into the local-force vector for 3D and 2D. _INDEX_3D = { - (ForceComponent.N, "i"): 0, - (ForceComponent.N, "j"): 6, - (ForceComponent.V2, "i"): 1, - (ForceComponent.V2, "j"): 7, - (ForceComponent.V3, "i"): 2, - (ForceComponent.V3, "j"): 8, - (ForceComponent.T, "i"): 3, - (ForceComponent.T, "j"): 9, - (ForceComponent.M2, "i"): 4, - (ForceComponent.M2, "j"): 10, - (ForceComponent.M3, "i"): 5, - (ForceComponent.M3, "j"): 11, + (ForceComponent.N, "i"): 0, (ForceComponent.N, "j"): 6, + (ForceComponent.V2, "i"): 1, (ForceComponent.V2, "j"): 7, + (ForceComponent.V3, "i"): 2, (ForceComponent.V3, "j"): 8, + (ForceComponent.T, "i"): 3, (ForceComponent.T, "j"): 9, + (ForceComponent.M2, "i"): 4, (ForceComponent.M2, "j"): 10, + (ForceComponent.M3, "i"): 5, (ForceComponent.M3, "j"): 11, } _INDEX_2D = { - (ForceComponent.N, "i"): 0, - (ForceComponent.N, "j"): 3, - (ForceComponent.V2, "i"): 1, - (ForceComponent.V2, "j"): 4, - (ForceComponent.M3, "i"): 2, - (ForceComponent.M3, "j"): 5, + (ForceComponent.N, "i"): 0, (ForceComponent.N, "j"): 3, + (ForceComponent.V2, "i"): 1, (ForceComponent.V2, "j"): 4, + (ForceComponent.M3, "i"): 2, (ForceComponent.M3, "j"): 5, } # Truss elements expose a different localForce layout than frames: @@ -78,12 +69,10 @@ _INDEX_2D = { # 3D truss → 6-vector [N_i, 0, 0, N_j, 0, 0] # So we map only the N component; other components return None. _INDEX_TRUSS_2D = { - (ForceComponent.N, "i"): 0, - (ForceComponent.N, "j"): 2, + (ForceComponent.N, "i"): 0, (ForceComponent.N, "j"): 2, } _INDEX_TRUSS_3D = { - (ForceComponent.N, "i"): 0, - (ForceComponent.N, "j"): 3, + (ForceComponent.N, "i"): 0, (ForceComponent.N, "j"): 3, } @@ -133,7 +122,6 @@ def extract_diagram_data( # Local import to avoid a cycle (element classes live in core.geometry). from otko.core import CorotTrussElement, TrussElement - truss_types = (TrussElement, CorotTrussElement) for el in project.elements: diff --git a/src/otko/services/export.py b/src/otko/services/export.py index 5886360..f3ca54d 100644 --- a/src/otko/services/export.py +++ b/src/otko/services/export.py @@ -35,7 +35,7 @@ from __future__ import annotations from collections.abc import Callable from typing import Any -from otko._const import OPENSEESPY_VERSION, __version__ +from otko import __version__ from otko.core import ( ModalCase, Project, @@ -49,7 +49,7 @@ from otko.services.opensees_runner import OpenSeesRunner __all__ = ["RecordingOps", "export_opspy", "export_tcl"] #: Pinned solver version written into every script header. -PINNED_OPENSEESPY = f"openseespy=={OPENSEESPY_VERSION}" +PINNED_OPENSEESPY = "openseespy==3.5.1.12" #: Recorded call triple: (ops method name, positional args, keyword args). RecordedCall = tuple[str, tuple[Any, ...], dict[str, Any]] @@ -238,7 +238,7 @@ def _record_calls(project: Project, case_id: int | None) -> list[RecordedCall]: runner.build() else: case = _find_case(project, case_id) - if isinstance(case, StaticCase | ModalCase | PushoverCase): + if isinstance(case, (StaticCase, ModalCase, PushoverCase)): # Safe to run end-to-end: dummy query results let the # harvesting code finish; queries are filtered at render. runner.run(case) @@ -362,7 +362,7 @@ def export_opspy(project: Project, case_id: int | None = None) -> str: are appended after the model. Returns: - The script source. The header pins the solver version, + The script source. The header pins ``openseespy==3.5.1.12``, the OTKO version and the display units. Raises: diff --git a/src/otko/services/material_tester.py b/src/otko/services/material_tester.py index e494521..fe70fbc 100644 --- a/src/otko/services/material_tester.py +++ b/src/otko/services/material_tester.py @@ -63,9 +63,7 @@ class CyclicSegment(BaseModel): current -> *compressive_peak* -> *tensile_peak* -> 0. """ - compressive_peak: float = Field( - ..., lt=0.0, description="Target compressive strain (negative)." - ) + compressive_peak: float = Field(..., lt=0.0, description="Target compressive strain (negative).") tensile_peak: float = Field(..., gt=0.0, description="Target tensile strain (positive).") n_cycles: PositiveInt = Field(1, description="Number of complete excursions to run.") @@ -162,35 +160,17 @@ def _emit_uniaxial(ops: Any, mat: Any) -> None: ops.uniaxialMaterial("Steel01", mat.id, *args) case Steel02(): ops.uniaxialMaterial( - "Steel02", - mat.id, - mat.Fy, - mat.E0, - mat.b, - mat.R0, - mat.cR1, - mat.cR2, + "Steel02", mat.id, mat.Fy, mat.E0, mat.b, mat.R0, mat.cR1, mat.cR2, ) case Concrete01(): ops.uniaxialMaterial( - "Concrete01", - mat.id, - mat.fpc, - mat.epsc0, - mat.fpcu, - mat.epsU, + "Concrete01", mat.id, mat.fpc, mat.epsc0, mat.fpcu, mat.epsU, ) case Concrete02(): ops.uniaxialMaterial( - "Concrete02", - mat.id, - mat.fpc, - mat.epsc0, - mat.fpcu, - mat.epsU, - mat.lambda_, - mat.ft, - mat.Ets, + "Concrete02", mat.id, + mat.fpc, mat.epsc0, mat.fpcu, mat.epsU, + mat.lambda_, mat.ft, mat.Ets, ) case Concrete04(): args = [mat.fpc, mat.epsc0, mat.epscu, mat.Ec] @@ -207,25 +187,10 @@ def _emit_uniaxial(ops: Any, mat: Any) -> None: ops.uniaxialMaterial("ElasticPP", mat.id, *args) case HystereticMaterial(): ops.uniaxialMaterial( - "Hysteretic", - mat.id, - mat.s1p, - mat.e1p, - mat.s2p, - mat.e2p, - mat.s3p, - mat.e3p, - mat.s1n, - mat.e1n, - mat.s2n, - mat.e2n, - mat.s3n, - mat.e3n, - mat.px, - mat.py, - mat.d1, - mat.d2, - mat.beta, + "Hysteretic", mat.id, + mat.s1p, mat.e1p, mat.s2p, mat.e2p, mat.s3p, mat.e3p, + mat.s1n, mat.e1n, mat.s2n, mat.e2n, mat.s3n, mat.e3n, + mat.px, mat.py, mat.d1, mat.d2, mat.beta, ) case _: raise TypeError(f"Unsupported material type: {type(mat).__name__}") @@ -274,13 +239,10 @@ def test_uniaxial_material( """ if ops_module is None: import openseespy.opensees as _ops_default - ops_module = _ops_default ops = ops_module - mat_name = getattr(material, "name", None) or str( - getattr(material, "type", type(material).__name__) - ) + mat_name = getattr(material, "name", None) or str(getattr(material, "type", type(material).__name__)) try: # Clear any leftover OpenSees state from a prior call or runner. diff --git a/src/otko/services/peer_record.py b/src/otko/services/peer_record.py index ce99a54..618c2e5 100644 --- a/src/otko/services/peer_record.py +++ b/src/otko/services/peer_record.py @@ -11,7 +11,6 @@ building the PathTimeSeries. from __future__ import annotations -import contextlib import re from pathlib import Path @@ -47,8 +46,7 @@ def parse_peer_record(path: str | Path) -> tuple[float, int, list[float]]: break # New NGA format: "3930 0.00500 NPTS, DT" m_new = re.match( - r"^(\d+)\s+([0-9.eE+\-]+)\s+NPTS\s*,\s*DT", - stripped, + r"^(\d+)\s+([0-9.eE+\-]+)\s+NPTS\s*,\s*DT", stripped, ) if m_new: npts = int(m_new.group(1)) @@ -65,9 +63,10 @@ def parse_peer_record(path: str | Path) -> tuple[float, int, list[float]]: values: list[float] = [] for line in lines[data_start:]: for tok in line.split(): - # Skip stray non-numeric tokens. - with contextlib.suppress(ValueError): + try: values.append(float(tok)) + except ValueError: + pass # skip stray tokens if not values: raise ValueError("Header parsed but no numeric data lines found.") return dt, npts, values @@ -83,8 +82,10 @@ def parse_plain_values(path: str | Path) -> list[float]: vals: list[float] = [] for line in text.splitlines(): for tok in line.split(): - with contextlib.suppress(ValueError): + try: vals.append(float(tok)) + except ValueError: + pass if not vals: raise ValueError(f"{path} contains no numeric values.") return vals diff --git a/src/otko/services/qt_workers.py b/src/otko/services/qt_workers.py index aeaf199..4419a1b 100644 --- a/src/otko/services/qt_workers.py +++ b/src/otko/services/qt_workers.py @@ -32,8 +32,8 @@ class AnalysisWorker(QObject): started = Signal() log = Signal(str) - finished = Signal(object) # emits StaticResults / ModalResults / TransientResults - failed = Signal(str) # human-readable error message + traceback + finished = Signal(object) # emits StaticResults / ModalResults / TransientResults + failed = Signal(str) # human-readable error message + traceback def __init__( self, @@ -52,10 +52,8 @@ class AnalysisWorker(QObject): """Slot to be invoked by ``QThread.started``.""" self.started.emit() try: - self.log.emit( - f"Building model: {len(self._project.nodes)} nodes, " - f"{len(self._project.elements)} elements." - ) + self.log.emit(f"Building model: {len(self._project.nodes)} nodes, " + f"{len(self._project.elements)} elements.") runner = OpenSeesRunner(self._project) self.log.emit(f"Running case '{self._case.name}' ({type(self._case).__name__}) ...") results = runner.run(self._case, results_dir=self._results_dir) diff --git a/src/otko/services/results.py b/src/otko/services/results.py index 3163ed5..43b608f 100644 --- a/src/otko/services/results.py +++ b/src/otko/services/results.py @@ -56,8 +56,8 @@ class PushoverResults: n_steps: int control_node: int control_dof: int - control_disp: np.ndarray # shape (n_steps + 1,), includes t=0 - base_shear: np.ndarray # shape (n_steps + 1,), signed + control_disp: np.ndarray # shape (n_steps + 1,), includes t=0 + base_shear: np.ndarray # shape (n_steps + 1,), signed node_disp: dict[int, np.ndarray] = field(default_factory=dict) """node_id → shape (n_steps + 1, ndf) displacement history.""" element_forces: dict[int, np.ndarray] = field(default_factory=dict) @@ -158,7 +158,7 @@ class ResponseSpectrumResults: case_id: int case_name: str direction: int - combination: str # "SRSS" or "CQC" + combination: str # "SRSS" or "CQC" combined_disp: dict[int, np.ndarray] = field(default_factory=dict) """node_id → 3-vector of peak combined translational displacements.""" modes: list = field(default_factory=list) diff --git a/src/otko/services/section_bbox.py b/src/otko/services/section_bbox.py index 18f5a20..edabcc6 100644 --- a/src/otko/services/section_bbox.py +++ b/src/otko/services/section_bbox.py @@ -52,10 +52,8 @@ def _angle_in_arc(angle: float, start: float, span: float) -> bool: return ((angle - start) % 360.0) <= span + 1e-9 -def bbox_for_section( - section: object, - project: Project | None = None, -) -> tuple[float, float] | None: +def bbox_for_section(section: object, project: "Project | None" = None, + ) -> tuple[float, float] | None: """Return ``(width_y, height_z)`` of the section's local bounding box. ``None`` means "size could not be inferred" — the caller should skip @@ -71,7 +69,6 @@ def bbox_for_section( SectionAggregator, StraightLayer, ) - # ── ElasticSection: prefer explicit display dims; else back-solve. if isinstance(section, ElasticSection): explicit = _elastic_explicit_dims(section) diff --git a/src/otko/services/section_properties.py b/src/otko/services/section_properties.py index a637428..a041721 100644 --- a/src/otko/services/section_properties.py +++ b/src/otko/services/section_properties.py @@ -21,6 +21,7 @@ import numpy as np from otko.core.sections import ( CircularPatch, FiberSection, + Fibre, RectangularPatch, StraightLayer, ) @@ -34,9 +35,9 @@ class SectionProps: area: float centroid_y: float centroid_z: float - Iy: float # about centroid - Iz: float # about centroid - fibre_yz: np.ndarray # (n, 3): y, z, area + Iy: float # about centroid + Iz: float # about centroid + fibre_yz: np.ndarray # (n, 3): y, z, area def expand_fibres(sec: FiberSection) -> np.ndarray: @@ -64,11 +65,9 @@ def expand_fibres(sec: FiberSection) -> np.ndarray: yc = p.y_center + r_mid * math.cos(theta) zc = p.z_center + r_mid * math.sin(theta) # Annular sector area: (r_outer² - r_inner²) * dθ / (2·n_rad) - a = ( - ((p.r_inner + (ir + 1) * d_r) ** 2 - (p.r_inner + ir * d_r) ** 2) - * math.radians(d_theta) - / 2.0 - ) + a = ((p.r_inner + (ir + 1) * d_r) ** 2 + - (p.r_inner + ir * d_r) ** 2) \ + * math.radians(d_theta) / 2.0 rows.append((yc, zc, a)) for lay in sec.layers: @@ -103,8 +102,8 @@ def compute_section_props(sec: FiberSection) -> SectionProps: zc = float(np.sum(a * z) / total_a) # Second moments of area about centroid (parallel axis from each fibre). - Iz = float(np.sum(a * (y - yc) ** 2)) # about z-axis - Iy = float(np.sum(a * (z - zc) ** 2)) # about y-axis + Iz = float(np.sum(a * (y - yc) ** 2)) # about z-axis + Iy = float(np.sum(a * (z - zc) ** 2)) # about y-axis return SectionProps( n_fibres=len(fibres), diff --git a/src/otko/services/spectrum.py b/src/otko/services/spectrum.py index 9cde026..a242dbf 100644 --- a/src/otko/services/spectrum.py +++ b/src/otko/services/spectrum.py @@ -24,22 +24,20 @@ from otko.services.results import ModalResults class ModeContribution: """Per-mode metadata for a response-spectrum analysis.""" - mode_number: int # 1-indexed - period: float # s - frequency: float # Hz - angular_frequency: float # rad/s - participation_factor: float # Γ_i for the chosen direction - effective_mass: float # M_eff,i = Γ_i² · M_i - mass_ratio: float # M_eff,i / Σ m - sa_at_period: float # Sa(T_i) from spectrum + mode_number: int # 1-indexed + period: float # s + frequency: float # Hz + angular_frequency: float # rad/s + participation_factor: float # Γ_i for the chosen direction + effective_mass: float # M_eff,i = Γ_i² · M_i + mass_ratio: float # M_eff,i / Σ m + sa_at_period: float # Sa(T_i) from spectrum modal_peak_disp: dict[int, np.ndarray] = field(default_factory=dict) """node_id → peak modal displacement vector (3D translations).""" def mass_participation( - project: Project, - modal: ModalResults, - direction: int, + project: Project, modal: ModalResults, direction: int, ) -> list[ModeContribution]: """Compute Γ_i, M_eff,i and frequency for every mode. @@ -81,23 +79,21 @@ def mass_participation( m_eff = 0.0 else: gamma = numerator / denominator - m_eff = gamma**2 * denominator + m_eff = gamma ** 2 * denominator omega = float(np.sqrt(abs(modal.eigenvalues[mode_number - 1]))) period = (2.0 * np.pi / omega) if omega > 0.0 else float("inf") ratio = (m_eff / total_mass) if total_mass > 0.0 else 0.0 - out.append( - ModeContribution( - mode_number=mode_number, - period=period, - frequency=omega / (2.0 * np.pi) if omega > 0.0 else 0.0, - angular_frequency=omega, - participation_factor=gamma, - effective_mass=m_eff, - mass_ratio=ratio, - sa_at_period=0.0, # filled in by combine_spectrum - ) - ) + out.append(ModeContribution( + mode_number=mode_number, + period=period, + frequency=omega / (2.0 * np.pi) if omega > 0.0 else 0.0, + angular_frequency=omega, + participation_factor=gamma, + effective_mass=m_eff, + mass_ratio=ratio, + sa_at_period=0.0, # filled in by combine_spectrum + )) return out @@ -141,7 +137,8 @@ def combine_modal_response( m.sa_at_period = 0.0 continue m.sa_at_period = interp_sa(spectrum, m.period) - scale = m.participation_factor * m.sa_at_period / (m.angular_frequency**2) + scale = (m.participation_factor * m.sa_at_period + / (m.angular_frequency ** 2)) shape = modal.mode_shapes[m.mode_number] for nid, vec in shape.items(): n_take = min(3, vec.size) @@ -159,7 +156,7 @@ def combine_modal_response( for m in modes: u = m.modal_peak_disp.get(nid) if u is not None: - sq_sum += u**2 + sq_sum += u ** 2 combined[nid] = np.sqrt(sq_sum) elif method.upper() == "CQC": zeta = damping if damping is not None else spectrum.damping_ratio @@ -172,8 +169,8 @@ def combine_modal_response( if wi <= 0.0 or wj <= 0.0: continue r = wj / wi - num = 8.0 * zeta**2 * (1.0 + r) * r**1.5 - denom = (1.0 - r**2) ** 2 + 4.0 * zeta**2 * r * (1.0 + r) ** 2 + num = 8.0 * zeta ** 2 * (1.0 + r) * r ** 1.5 + denom = (1.0 - r ** 2) ** 2 + 4.0 * zeta ** 2 * r * (1.0 + r) ** 2 rho[i, j] = num / denom if denom > 0.0 else 0.0 for nid in node_ids: sq_sum = np.zeros(3) diff --git a/src/otko/viewmodels/analysis_runner.py b/src/otko/viewmodels/analysis_runner.py index 5773307..7046e0c 100644 --- a/src/otko/viewmodels/analysis_runner.py +++ b/src/otko/viewmodels/analysis_runner.py @@ -20,7 +20,7 @@ class AnalysisRunner(QObject): started = Signal() log = Signal(str) - finished = Signal(object) # StaticResults / ModalResults / TransientResults + finished = Signal(object) # StaticResults / ModalResults / TransientResults failed = Signal(str) runningChanged = Signal(bool) diff --git a/src/otko/viewmodels/project_vm.py b/src/otko/viewmodels/project_vm.py index 9ea06d1..148371d 100644 --- a/src/otko/viewmodels/project_vm.py +++ b/src/otko/viewmodels/project_vm.py @@ -11,7 +11,6 @@ a re-paint without losing camera/selection state. from __future__ import annotations -import contextlib from pathlib import Path from PySide6.QtCore import QObject, Signal, Slot @@ -24,8 +23,8 @@ from otko.services import load_project, save_project class ProjectViewModel(QObject): """Holds the current Project, its file path, dirty state, and undo stack.""" - projectChanged = Signal(object) # emits Project | None - modelMutated = Signal() # same project, mutated by a command + projectChanged = Signal(object) # emits Project | None + modelMutated = Signal() # same project, mutated by a command dirtyChanged = Signal(bool) def __init__(self, parent: QObject | None = None) -> None: @@ -41,8 +40,10 @@ class ProjectViewModel(QObject): @Slot(bool) def _on_stack_clean_changed(self, clean: bool) -> None: # Guard against the late-fire that Qt sends during destruction. - with contextlib.suppress(RuntimeError): + try: self._set_dirty(not clean) + except RuntimeError: + pass # ── read ───────────────────────────────────────────────────────── @property diff --git a/src/otko/views/action_handlers.py b/src/otko/views/action_handlers.py index 4352b00..2608a6a 100644 --- a/src/otko/views/action_handlers.py +++ b/src/otko/views/action_handlers.py @@ -34,6 +34,7 @@ from otko.commands import ( SetRestraintCommand, SetUnitsCommand, ) +from otko.core.units import UnitSystem from otko.services import PROJECT_FILE_SUFFIX from otko.views.dialogs import ( AddNodeDialog, @@ -49,7 +50,6 @@ from otko.views.dialogs import ( AssignZeroLengthSectionDialog, CoordinateGridSystemsDialog, LinearTimeSeriesDialog, - LoadCombinationManagerDialog, MaterialLibraryDialog, MaterialTesterDialog, MirrorDialog, @@ -254,7 +254,12 @@ class ActionHandlers: if not sel_nodes: QMessageBox.information(self, "Move", "Select one or more nodes first.") return - dlg = MoveDialog(len(sel_nodes), self) + proj = self._vm.project + dlg = MoveDialog( + len(sel_nodes), + self, + units=proj.meta.units if proj is not None else UnitSystem.METRIC, + ) if dlg.exec() != QDialog.DialogCode.Accepted: return try: @@ -276,7 +281,14 @@ class ActionHandlers: for el in self._vm.project.elements if all(nid in sel_nodes for nid in el.nodes) } - dlg = ReplicateDialog(len(sel_nodes), len(sel_elements), self) + dlg = ReplicateDialog( + len(sel_nodes), + len(sel_elements), + self, + units=self._vm.project.meta.units + if self._vm.project is not None + else UnitSystem.METRIC, + ) if dlg.exec() != QDialog.DialogCode.Accepted: return try: @@ -370,6 +382,7 @@ class ActionHandlers: grid=proj.grid_system, ndm=proj.ndm, parent=self, + units=proj.meta.units, ) if dlg.exec() != QDialog.DialogCode.Accepted: return @@ -553,7 +566,12 @@ class ActionHandlers: for p in self._vm.project.load_patterns if isinstance(p, PlainLoadPattern) ] - dlg = AssignLoadDialog(len(sel_nodes), existing_patterns=existing, parent=self) + dlg = AssignLoadDialog( + len(sel_nodes), + existing_patterns=existing, + parent=self, + units=self._vm.project.meta.units, + ) if dlg.exec() != QDialog.DialogCode.Accepted: return try: @@ -719,7 +737,12 @@ class ActionHandlers: "Select one or more nodes first.", ) return - dlg = AssignMassesDialog(len(sel_nodes), ndf=self._vm.project.ndf, parent=self) + dlg = AssignMassesDialog( + len(sel_nodes), + ndf=self._vm.project.ndf, + parent=self, + units=self._vm.project.meta.units, + ) if dlg.exec() != QDialog.DialogCode.Accepted: return try: @@ -744,7 +767,10 @@ class ActionHandlers: (p.id, p.name) for p in project.load_patterns if isinstance(p, PlainLoadPattern) ] dlg = AssignDistributedLoadDialog( - len(sel_elements), existing_patterns=existing, parent=self + len(sel_elements), + existing_patterns=existing, + parent=self, + units=project.meta.units, ) if dlg.exec() != QDialog.DialogCode.Accepted: return @@ -918,11 +944,6 @@ class ActionHandlers: self._on_new() AnalysisCaseManagerDialog(self._vm, self).exec() - def _on_combo_manager(self) -> None: - if self._vm.project is None: - self._on_new() - LoadCombinationManagerDialog(self._vm, self).exec() - def _on_run_analysis(self) -> None: if self._vm.project is None: QMessageBox.information(self, "Run Analysis", "Open or create a project first.") @@ -944,6 +965,10 @@ class ActionHandlers: self._latest_results = results project = self._vm.project if project is not None: + try: + self._results_panel.set_units(project.meta.units, project.ndf) + except Exception: + pass self._results_panel.show_results(results, ndm=project.ndm, ndf=project.ndf) else: self._results_panel.show_results(results) @@ -967,7 +992,7 @@ class ActionHandlers: def _on_about(self) -> None: box = QMessageBox(self) box.setWindowTitle("About OTKO") - box.setText(f"

OTKO {__version__}

" "

A modern desktop GUI for OpenSeesPy.

") + box.setText(f"

OTKO {__version__}

A modern desktop GUI for OpenSeesPy.

") app_svg = Path(__file__).resolve().parent.parent / "resources" / "icons" / "app.svg" if app_svg.is_file(): box.setIconPixmap(QIcon(str(app_svg)).pixmap(64, 64)) @@ -1037,6 +1062,18 @@ class ActionHandlers: window itself); call them when present and never fail when absent. """ + try: + project = self._vm.project + if project is not None and hasattr(self, "_results_panel"): + try: + self._results_panel.set_units( # type: ignore[attr-defined] + project.meta.units, + project.ndf, + ) + except Exception: + pass + except Exception: + pass hosts = [getattr(self, "_dock_manager", None), self] seen: set[int] = set() for host in hosts: diff --git a/src/otko/views/canvas3d/diagram_renderer.py b/src/otko/views/canvas3d/diagram_renderer.py index 57c0a70..835536c 100644 --- a/src/otko/views/canvas3d/diagram_renderer.py +++ b/src/otko/views/canvas3d/diagram_renderer.py @@ -22,7 +22,6 @@ Convention: from __future__ import annotations -import contextlib import logging from typing import Any @@ -31,7 +30,6 @@ import pyvista as pv from otko.core import Project from otko.services.element_forces import DiagramData, ForceComponent -from otko.views.canvas3d.style import RenderStyle _LOG = logging.getLogger("otko.diagram") @@ -63,15 +61,27 @@ class DiagramRenderer: - ``clear()`` removes the overlay. """ - def __init__(self, plotter: Any, style: RenderStyle | None = None) -> None: + def __init__(self, plotter: Any) -> None: self._plotter = plotter - self._style = style or RenderStyle() self._actor: Any = None self._label_actor: Any = None + self._units_label: str = "" # ── public ────────────────────────────────────────────────────── - def render(self, project: Project, data: DiagramData, scale: float) -> None: - """Build and display the diagram for ``data`` at ``scale``.""" + def render( + self, + project: Project, + data: DiagramData, + scale: float, + *, + units_label: str = "", + ) -> None: + """Build and display the diagram for ``data`` at ``scale``. + + ``units_label`` (e.g. ``"kip"``) is appended to the min/max + value annotations; values stay native. + """ + self._units_label = units_label or "" self.clear() if data.element_ids.size == 0: return @@ -80,7 +90,7 @@ class DiagramRenderer: # e.g. asking for "torsion" on a planar bending model. Log # a hint so the user understands the empty viewport. _LOG.info( - "All '%s' values are zero for this analysis step " "— nothing to draw.", + "All '%s' values are zero for this analysis step — nothing to draw.", data.component.name, ) return @@ -160,9 +170,7 @@ class DiagramRenderer: self._actor = self._plotter.add_mesh( mesh, scalars="value", - # Shared response palette (opstool's default diverging scale), so - # the PyVista and Plotly backends stay visually consistent. - cmap=list(self._style.response_scale_colors), + cmap="coolwarm", clim=(-vmax, vmax), show_scalar_bar=True, scalar_bar_args={"title": comp_label, "n_labels": 5}, @@ -218,7 +226,8 @@ class DiagramRenderer: # If max == min (all equal) just one label; skip the dup. unique_indices = [idx_max] if idx_max == idx_min else [idx_max, idx_min] positions = np.vstack([candidates[i][1] for i in unique_indices]) - labels = [self._format_value(candidates[i][0]) for i in unique_indices] + unit_suffix = f" {self._units_label}" if getattr(self, "_units_label", "") else "" + labels = [f"{self._format_value(candidates[i][0])}{unit_suffix}" for i in unique_indices] try: return self._plotter.add_point_labels( @@ -259,8 +268,10 @@ class DiagramRenderer: pass self._actor = None if self._label_actor is not None: - with contextlib.suppress(Exception): + try: self._plotter.remove_actor(self._label_actor, render=False) + except Exception: + pass self._label_actor = None # ── helpers ───────────────────────────────────────────────────── diff --git a/src/otko/views/canvas3d/model_canvas.py b/src/otko/views/canvas3d/model_canvas.py index 3e00425..b761d62 100644 --- a/src/otko/views/canvas3d/model_canvas.py +++ b/src/otko/views/canvas3d/model_canvas.py @@ -12,6 +12,7 @@ needing a reference to the renderer. from __future__ import annotations +import os from typing import Any import numpy as np @@ -24,17 +25,13 @@ from otko.core import Project from otko.views.canvas3d.model_renderer import ModelRenderer from otko.views.canvas3d.selection import SelectionState from otko.views.canvas3d.style import RenderStyle -from otko.views.canvas_base import CanvasCapabilities -PICK_DEBUG = False +_PICK_DEBUG = os.environ.get("OSS_PICK_DEBUG") == "1" class ModelCanvas(QtInteractor): # type: ignore[misc] """The central 3D viewport widget.""" - #: VTK supports every optional overlay layer. - capabilities = CanvasCapabilities() - # Convenience signals re-emitted from SelectionState. nodePicked = Signal(int) elementPicked = Signal(int) @@ -130,7 +127,7 @@ class ModelCanvas(QtInteractor): # type: ignore[misc] cx = qt_x * dpr cy = (h_logical - qt_y) * dpr - if PICK_DEBUG: + if _PICK_DEBUG: print( f"[pick] click qt=({qt_x:.0f},{qt_y:.0f}) → vtk=({cx:.0f},{cy:.0f}) " f"viewport_logical=({self.width()}x{h_logical}) dpr={dpr}" @@ -151,7 +148,7 @@ class ModelCanvas(QtInteractor): # type: ignore[misc] if node_screen is not None and len(node_screen): d2 = (node_screen[:, 0] - cx) ** 2 + (node_screen[:, 1] - cy) ** 2 idx = int(np.argmin(d2)) - if PICK_DEBUG: + if _PICK_DEBUG: print( f"[pick] nearest node id={node_ids[idx]} " f"screen={node_screen[idx]} d={np.sqrt(d2[idx]):.1f}px " @@ -184,7 +181,7 @@ class ModelCanvas(QtInteractor): # type: ignore[misc] closest = a_screen + t[:, None] * ab d2 = ((p - closest) ** 2).sum(axis=1) idx = int(np.argmin(d2)) - if PICK_DEBUG: + if _PICK_DEBUG: print( f"[pick] nearest frame id={frame_ids[idx]} " f"d={float(np.sqrt(d2[idx])):.1f}px " @@ -194,7 +191,7 @@ class ModelCanvas(QtInteractor): # type: ignore[misc] self._dispatch_pick("element", int(frame_ids[idx])) return - if PICK_DEBUG: + if _PICK_DEBUG: print("[pick] no hit within tolerance") # ── Empty-click fallback: pixel-space snap to grid intersections. ── @@ -205,7 +202,7 @@ class ModelCanvas(QtInteractor): # type: ignore[misc] snapped = self._nearest_grid_intersection_px(cx, cy, grid_tol_px) if snapped is not None: self.emptyClicked.emit(float(snapped[0]), float(snapped[1]), float(snapped[2])) - elif PICK_DEBUG: + elif _PICK_DEBUG: print("[pick] off-grid click — no snap target within tolerance") def _grid_intersections_world(self) -> np.ndarray | None: @@ -263,7 +260,7 @@ class ModelCanvas(QtInteractor): # type: ignore[misc] return None d2 = (screen[:, 0] - cx) ** 2 + (screen[:, 1] - cy) ** 2 idx = int(np.argmin(d2)) - if PICK_DEBUG: + if _PICK_DEBUG: print( f"[grid-snap] nearest intersection " f"world={world_points[idx]} d={float(np.sqrt(d2[idx])):.1f}px " @@ -342,17 +339,6 @@ class ModelCanvas(QtInteractor): # type: ignore[misc] self._renderer.set_show_local_axes(enabled) self.render() - def set_style(self, style: RenderStyle) -> None: - """Swap the visual style and repaint the scene. - - Applies the background immediately (it lives outside the renderer) - and rebuilds the actors whose colours come from the style LUTs. - """ - self._style = style - self.set_background(style.background_bottom, top=style.background_top) - self._renderer.set_style(style) - self.render() - def set_display_options(self, *, show_node_labels: bool, show_element_labels: bool) -> None: """Toggle viewport labels such as node and element names.""" self._renderer.set_display_options( diff --git a/src/otko/views/canvas3d/model_renderer.py b/src/otko/views/canvas3d/model_renderer.py index 12c88cb..2be41c4 100644 --- a/src/otko/views/canvas3d/model_renderer.py +++ b/src/otko/views/canvas3d/model_renderer.py @@ -10,7 +10,6 @@ Mode-aware: MODEL / DEFORMED / MODAL change only the points array. from __future__ import annotations -import contextlib import enum from dataclasses import dataclass from typing import Any @@ -25,20 +24,15 @@ from otko.core import ( ElasticBeamColumn, ForceBeamColumn, NodalLoad, + QuadElement, + UniformElementLoad, PlainLoadPattern, Project, - QuadElement, TrussElement, - UniformElementLoad, ZeroLengthElement, ZeroLengthSectionElement, ) -from otko.views.canvas3d.style import ( - SELECTED_STATE, - RenderStyle, - element_family_index, - family_palette, -) +from otko.views.canvas3d.style import RenderStyle class RendererMode(enum.Enum): @@ -190,6 +184,9 @@ class ModelRenderer: r, g, b = (int(round(x * 255)) for x in rgb) return f"#{r:02x}{g:02x}{b:02x}" + _NODE_LUT = ["#d9d9d9", "#00ffff"] # gray normal, cyan selected + _FRAME_LUT = ["#338cd9", "#00ffff"] # blue normal, cyan selected + def __init__(self, plotter: Any, style: RenderStyle | None = None) -> None: self._plotter = plotter self._style = style or RenderStyle() @@ -208,8 +205,6 @@ class ModelRenderer: self._frame_actor: Any = None self._frame_ids_ordered: list[int] = [] self._frame_id_to_row: dict[int, int] = {} - #: Per-frame palette index (family), parallel to ``_frame_ids_ordered``. - self._frame_family: list[int] = [] self._node_label_actor: Any = None self._element_label_actor: Any = None self._show_node_labels: bool = False @@ -224,8 +219,10 @@ class ModelRenderer: # plan view at Z=3 doesn't see the Z=0 grid cluttering the view. self._working_plane: tuple[str, float] | None = None - with contextlib.suppress(Exception): + try: self._plotter.enable_anti_aliasing("ssaa") + except Exception: + pass # ── public API ─────────────────────────────────────────────────── def render(self, project: Project | None) -> None: @@ -282,16 +279,6 @@ class ModelRenderer: if self._project is not None: self.render(self._project) - def set_style(self, style: RenderStyle) -> None: - """Swap the visual style and rebuild the scene it colours. - - Colours are baked into actor LUTs at build time, so a style change - needs a full re-render rather than a scalar-array tweak. - """ - self._style = style - if self._project is not None: - self.render(self._project) - def set_working_plane(self, plane: tuple[str, float] | None) -> None: """Update the working-plane filter and rebuild the grid overlay. @@ -320,12 +307,11 @@ class ModelRenderer: self._reglyph_nodes() if self._frame_pd is not None and self._frame_ids_ordered: - # Restore each cell to its family slot, then flag the selected ones. - states = np.array(self._frame_family, dtype=np.int8) + states = np.zeros(len(self._frame_ids_ordered), dtype=np.int8) for eid in element_ids: row = self._frame_id_to_row.get(eid) if row is not None: - states[row] = SELECTED_STATE + states[row] = 1 self._frame_pd.cell_data["_oss_state"] = states self._frame_pd.Modified() @@ -343,8 +329,10 @@ class ModelRenderer: """ # Remove any previous marker. if self._hover_actor is not None: - with contextlib.suppress(Exception): + try: self._plotter.remove_actor(self._hover_actor, render=False) + except Exception: + pass self._hover_actor = None if world_point is None: @@ -388,13 +376,15 @@ class ModelRenderer: sphere = pv.Sphere(radius=radius, theta_resolution=8, phi_resolution=8) glyph = self._node_pd.glyph(geom=sphere, scale=False, orient=False) if self._node_actor is not None: - with contextlib.suppress(Exception): + try: self._plotter.remove_actor(self._node_actor, render=False) + except Exception: + pass self._node_glyph = glyph self._node_actor = self._plotter.add_mesh( glyph, scalars="_oss_state", - cmap=[self._style.node_color, self._style.node_selected_color], + cmap=self._NODE_LUT, clim=[0, 1], show_scalar_bar=False, pickable=True, @@ -407,7 +397,6 @@ class ModelRenderer: return cells: list[int] = [] ids: list[int] = [] - families: list[int] = [] for el in frames: try: i = self._node_id_to_row[el.nodes[0]] @@ -416,7 +405,6 @@ class ModelRenderer: continue cells.extend([2, i, j]) ids.append(el.id) - families.append(element_family_index(el)) if not ids: return pd = pv.PolyData() @@ -424,18 +412,16 @@ class ModelRenderer: pd.lines = np.array(cells, dtype=np.int64) pd.cell_data["_oss_id"] = np.array(ids, dtype=np.int64) pd.cell_data["_oss_kind"] = np.array(["element"] * len(ids), dtype=object) - # Cell scalar carries the palette slot (family, or SELECTED_STATE). - pd.cell_data["_oss_state"] = np.array(families, dtype=np.int8) + pd.cell_data["_oss_state"] = np.zeros(len(ids), dtype=np.int8) self._frame_pd = pd self._frame_ids_ordered = ids self._frame_id_to_row = {eid: i for i, eid in enumerate(ids)} - self._frame_family = families self._frame_actor = self._plotter.add_mesh( pd, scalars="_oss_state", - cmap=family_palette(self._style), - clim=[0, SELECTED_STATE], + cmap=self._FRAME_LUT, + clim=[0, 1], show_scalar_bar=False, line_width=3.0, pickable=True, @@ -498,17 +484,10 @@ class ModelRenderer: plane_axis = axis_idx plane_offset_local = plane_off - cs_shift - # Bind the per-iteration plane state as defaults so the closure - # cannot read a later iteration's values (B023); the helper is - # only ever called from within this loop body. - def _on_active_plane( - local_pt: tuple[float, float, float], - _axis: int | None = plane_axis, - _offset: float | None = plane_offset_local, - ) -> bool: - if _axis is None or _offset is None: + def _on_active_plane(local_pt: tuple[float, float, float]) -> bool: + if plane_axis is None: return True - return abs(local_pt[_axis] - _offset) < 1e-6 + return abs(local_pt[plane_axis] - plane_offset_local) < 1e-6 # type: ignore[operator] # Collect active + dim segments separately so they get their # own polydata + actor (different opacity / color). @@ -517,21 +496,13 @@ class ModelRenderer: dim_pts: list[tuple[float, float, float]] = [] dim_cells: list[int] = [] - def add_seg( - p1: tuple[float, float, float], - p2: tuple[float, float, float], - _active_pts: list[tuple[float, float, float]] = active_pts, - _active_cells: list[int] = active_cells, - _dim_pts: list[tuple[float, float, float]] = dim_pts, - _dim_cells: list[int] = dim_cells, - _cs: Any = cs, - ) -> None: + def add_seg(p1: tuple[float, float, float], p2: tuple[float, float, float]) -> None: on_active = _on_active_plane(p1) and _on_active_plane(p2) - bucket_pts = _active_pts if on_active else _dim_pts - bucket_cells = _active_cells if on_active else _dim_cells + bucket_pts = active_pts if on_active else dim_pts + bucket_cells = active_cells if on_active else dim_cells i = len(bucket_pts) - bucket_pts.append(_cs.coord.local_to_world(p1)) - bucket_pts.append(_cs.coord.local_to_world(p2)) + bucket_pts.append(cs.coord.local_to_world(p1)) + bucket_pts.append(cs.coord.local_to_world(p2)) bucket_cells.extend([2, i, i + 1]) z_planes = zs if zs else [0.0] @@ -643,7 +614,7 @@ class ModelRenderer: continue # By design: area-only / thickness-only / material-only # elements carry no section. - if isinstance(el, TrussElement | CorotTrussElement | QuadElement | ZeroLengthElement): + if isinstance(el, (TrussElement, CorotTrussElement, QuadElement, ZeroLengthElement)): continue node_i = node_by_id.get(el.nodes[0]) node_j = node_by_id.get(el.nodes[1]) @@ -694,16 +665,18 @@ class ModelRenderer: diag = self._diag_of_points(self._node_original_points) cap = max(diag * 0.08, 1e-6) for el in project.elements: - if isinstance(el, QuadElement | ZeroLengthElement | ZeroLengthSectionElement): + if isinstance(el, (QuadElement, ZeroLengthElement, ZeroLengthSectionElement)): continue if not isinstance( el, - ElasticBeamColumn - | DispBeamColumn - | ForceBeamColumn - | BeamWithHingesElement - | TrussElement - | CorotTrussElement, + ( + ElasticBeamColumn, + DispBeamColumn, + ForceBeamColumn, + BeamWithHingesElement, + TrussElement, + CorotTrussElement, + ), ): continue node_i = node_by_id.get(el.nodes[0]) @@ -780,8 +753,8 @@ class ModelRenderer: """Add one semi-transparent steel-blue non-pickable aux actor.""" actor = self._plotter.add_mesh( mesh, - color=self._style.extrusion_color, - opacity=self._style.extrusion_opacity, + color=(0.35, 0.60, 0.85), # cool steel-blue + opacity=0.22, show_edges=True, edge_color=(0.15, 0.25, 0.45), line_width=1.0, @@ -1001,7 +974,7 @@ class ModelRenderer: ndf = project.ndf dof_idx = _dof_indices(ndf) size = max(self._diag_of_points(self._node_original_points) * 0.015, 1e-6) - support_color = self._style.support_color + support_color = (1.0, 0.5, 0.1) for node in project.nodes: if not any(node.restraint[i] for i in dof_idx): continue @@ -1015,7 +988,7 @@ class ModelRenderer: if not project.load_patterns or self._node_original_points is None: return scale = max(self._diag_of_points(self._node_original_points) * 0.05, 1e-6) - load_color = self._style.nodal_load_color + load_color = (0.2, 0.85, 0.2) node_by_id = {n.id: n for n in project.nodes} elem_by_id = {e.id: e for e in project.elements} for pattern in project.load_patterns: @@ -1046,7 +1019,7 @@ class ModelRenderer: # Draw N arrows along the element span, each perpendicular # to the axis in the direction of the load. Uses the same # orange-green palette as nodal loads but with shorter arrows. - elem_load_color = self._style.element_load_color # distributed loads + elem_load_color = (1.0, 0.55, 0.2) # orange n_arrows_per_elem = 5 for eload in pattern.element_loads: if not isinstance(eload, UniformElementLoad): @@ -1121,11 +1094,15 @@ class ModelRenderer: self._clear_label_actors() for a in (self._node_actor, self._frame_actor): if a is not None: - with contextlib.suppress(Exception): + try: self._plotter.remove_actor(a, render=False) + except Exception: + pass for a in self._aux_actors: - with contextlib.suppress(Exception): + try: self._plotter.remove_actor(a, render=False) + except Exception: + pass self._node_actor = None self._frame_actor = None self._aux_actors.clear() @@ -1135,7 +1112,6 @@ class ModelRenderer: self._node_ids_ordered = [] self._node_id_to_row = {} self._frame_ids_ordered = [] - self._frame_family = [] self._frame_id_to_row = {} self._node_original_points = None self._deformation = None @@ -1144,8 +1120,10 @@ class ModelRenderer: def _clear_label_actors(self) -> None: for actor in (self._node_label_actor, self._element_label_actor): if actor is not None: - with contextlib.suppress(Exception): + try: self._plotter.remove_actor(actor, render=False) + except Exception: + pass self._node_label_actor = None self._element_label_actor = None @@ -1162,13 +1140,11 @@ class ModelRenderer: if self._node_pd is None or len(project.nodes) == 0: return None pts = np.asarray(self._node_pd.points) - labels = [ - (node.name.strip() if node.name.strip() else f"N{node.id}") for node in project.nodes - ] + labels = [node.display_name for node in project.nodes] return self._plotter.add_point_labels( pts, labels, - font_size=self._style.label_font_size, + font_size=12, shape_opacity=0.15, text_color="black", point_color="white", @@ -1192,13 +1168,13 @@ class ModelRenderer: if i is None or j is None: continue centers.append((pts[i] + pts[j]) / 2.0) - labels.append(el.name.strip() if el.name.strip() else f"E{el.id}") + labels.append(el.display_name) if not centers: return None return self._plotter.add_point_labels( np.asarray(centers), labels, - font_size=self._style.label_font_size, + font_size=12, shape_opacity=0.15, text_color="black", point_color="white", diff --git a/src/otko/views/canvas3d/style.py b/src/otko/views/canvas3d/style.py index a55d43b..21b7a34 100644 --- a/src/otko/views/canvas3d/style.py +++ b/src/otko/views/canvas3d/style.py @@ -4,64 +4,11 @@ All colors, sizes, and glyph parameters live here. Changes apply globally through the renderer; downstream code never hard-codes a color or radius. A future "theme" feature can simply swap a different RenderStyle instance. - -The subset in :attr:`RenderStyle.EDITABLE` is surfaced to the user through -Options → Plot Properties… and persisted in ``QSettings`` (``plot/props``). -Palette values marked "opstool" are adapted from opstool v1.0.26 (GPL-3.0); -see ``NOTICE``. """ from __future__ import annotations -from dataclasses import dataclass, replace -from typing import Any, ClassVar - -from otko.core import CorotTrussElement, TrussElement, ZeroLengthElement - -#: Selected elements / nodes occupy the last slot of the palette LUT. -SELECTED_STATE: int = 3 - - -def element_family_index(element: object) -> int: - """Palette slot for a frame element: 0 beam, 1 truss, 2 link/zero-length.""" - if isinstance(element, TrussElement | CorotTrussElement): - return 1 - if isinstance(element, ZeroLengthElement): - return 2 - return 0 - - -def family_palette(style: RenderStyle) -> list[str]: - """LUT shared by both backends: [beam, truss, link, selected].""" - return [ - style.element_beam_color, - style.element_truss_color, - style.element_link_color, - style.selected_color, - ] - - -#: Colours / sizes the Plot Properties dialog exposes, in display order. -STYLE_FIELDS: tuple[tuple[str, str, str], ...] = ( - ("element_beam_color", "Beam elements", "color"), - ("element_truss_color", "Truss elements", "color"), - ("element_link_color", "Link / zero-length", "color"), - ("selected_color", "Selection highlight", "color"), - ("node_color", "Nodes", "color"), - ("node_selected_color", "Selected nodes", "color"), - ("support_color", "Supports", "color"), - ("nodal_load_color", "Nodal loads", "color"), - ("element_load_color", "Element loads", "color"), - ("extrusion_color", "Section extrusions", "color"), - ("extrusion_opacity", "Extrusion opacity", "float"), - ("background_top", "Background (top)", "color"), - ("background_bottom", "Background (bottom)", "color"), - ("show_axis_outline", "Axis outline (grid + ticks)", "bool"), - ("label_font_size", "Label font size", "int"), -) - -#: Field names the dialog edits and that are persisted between sessions. -EDITABLE_FIELDS: tuple[str, ...] = tuple(name for name, _label, _kind in STYLE_FIELDS) +from dataclasses import dataclass @dataclass(frozen=True, slots=True) @@ -72,58 +19,20 @@ class RenderStyle: background_top: str = "#dbe2ef" background_bottom: str = "#f5f7fb" - node_color: str = "#d9d9d9" # normal node sphere / marker - node_selected_color: str = "#00ffff" # selected node + node_color: str = "#f0a500" # warm gold + node_selected_color: str = "#00d4ff" # bright cyan frame_color: str = "#1f1f1f" - truss_color: str = "#2e5cb8" # also the Z-axis colour of the view triad + truss_color: str = "#2e5cb8" zerolength_color: str = "#a020f0" - selected_color: str = "#00ffff" # selected element highlight + selected_color: str = "#00d4ff" - # ── element palette (adapted from opstool v1.0.26, GPL-3.0) ─────── - # opstool colours wireframe elements by *family* rather than painting - # every frame the same colour, which makes mixed beam/truss/link models - # readable at a glance. Values come from ``PLOT_ARGS_DEFAULT`` in - # ``opstool/vis/{pyvista,plotly}/plot_utils.py``; see ``NOTICE``. - element_beam_color: str = "#0652ff" # beam-column family - element_truss_color: str = "#FF8C00" # truss family - element_link_color: str = "#39FF14" # zero-length / link family - - # ── overlay colours ───────────────────────────────────────────── - support_color: str = "#ff8019" - nodal_load_color: str = "#33d933" - element_load_color: str = "#ff8c33" - extrusion_color: str = "#598cff" - extrusion_opacity: float = 0.22 - label_font_size: int = 12 - - #: Draw the plotly scene grid + tick marks around the model (the "outline" - #: of opstool's scene recipe). Off by default to keep the SAP2000-like - #: clean viewport; the coloured X/Y/Z axis lines always stay visible. - show_axis_outline: bool = False - - #: Diverging scale for scalar response overlays (force diagrams today, - #: nodal / element response plots later): blue → red, evenly spaced. - #: This is opstool's ``default_cmap`` (RdYlBu reversed). - response_scale_colors: tuple[str, ...] = ( - "#313695", - "#4575b4", - "#74add1", - "#abd9e9", - "#e0f3f8", - "#fee090", - "#fdae61", - "#f46d43", - "#d73027", - "#a50026", - ) - - fix_color: str = "#c0392b" # firebrick + fix_color: str = "#c0392b" # firebrick pin_color: str = "#c0392b" roller_color: str = "#e67e22" custom_support_color: str = "#7f8c8d" - load_color: str = "#27ae60" # Y-axis colour of the view triad + load_color: str = "#27ae60" mass_color: str = "#9b59b6" # ── sizes (relative to bbox diagonal unless absolute) ──────────── @@ -136,28 +45,7 @@ class RenderStyle: support_relative_size: float = 0.025 support_min_size: float = 0.10 - load_relative_length: float = 0.10 # arrow length / bbox_diag + load_relative_length: float = 0.10 # arrow length / bbox_diag load_min_length: float = 0.5 selection_thickness_factor: float = 1.6 # multiplier for selected actors - - #: Names of the fields the Plot Properties dialog edits. - EDITABLE: ClassVar[tuple[str, ...]] = EDITABLE_FIELDS - - # ── helpers ────────────────────────────────────────────────────── - def response_colorscale(self) -> list[tuple[float, str]]: - """Plotly ``colorscale`` form of :attr:`response_scale_colors`.""" - count = len(self.response_scale_colors) - if count < 2: - return [(0.0, self.response_scale_colors[0])] - return [ - (index / (count - 1), color) for index, color in enumerate(self.response_scale_colors) - ] - - def with_updates(self, **changes: Any) -> RenderStyle: - """Return a copy with ``changes`` applied (frozen-dataclass safe).""" - return replace(self, **changes) - - def editable_values(self) -> dict[str, Any]: - """The dialog-managed subset of this style, as a plain dict.""" - return {name: getattr(self, name) for name in self.EDITABLE} diff --git a/src/otko/views/canvas_base.py b/src/otko/views/canvas_base.py deleted file mode 100644 index 0352961..0000000 --- a/src/otko/views/canvas_base.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Backend-agnostic contract for the central 3D canvas. - -Two backends implement it: - -- :class:`otko.views.canvas3d.ModelCanvas` — PyVista/VTK (native OpenGL, default) -- :class:`otko.views.canvas_plotly.PlotlyCanvas` — plotly.js in a QWebEngineView - -The ``MainWindow`` swaps between them at runtime, so both must expose the same -public surface (signals, selection, working plane, view presets, display -toggles) and declare what optional overlay layers they support via -:class:`CanvasCapabilities`. -""" - -from __future__ import annotations - -import importlib.util -from dataclasses import dataclass -from typing import Any, Protocol - - -def plotly_available() -> bool: - """True when the optional ``plotly`` package can be imported. - - The Plotly backend needs plotly.js (shipped inside the ``plotly`` wheel) - to render; without it the backend menu entry is disabled. - """ - return importlib.util.find_spec("plotly") is not None - - -@dataclass(frozen=True) -class CanvasCapabilities: - """Optional overlay layers a backend may or may not implement. - - Consumers query these (see ``RenderControls._refresh_action_enablement``) - instead of assuming a feature works on every backend. - """ - - #: Force-diagram ribbon overlay (N/V/M diagrams). - diagrams: bool = True - #: SAP2000-style section extrusion overlay. - extrusions: bool = True - #: Per-element local-axis triad overlay. - local_axes: bool = True - #: Node / element text labels. - labels: bool = True - #: Off-screen frame capture (mode-shape / time-history video export). - animation_export: bool = True - - -class CanvasBackend(Protocol): - """Structural contract implemented by every canvas backend. - - This mirrors :class:`ModelCanvas`; it exists so the swap path and the - consumers have one written-down interface to code against. - """ - - capabilities: CanvasCapabilities - selection: Any - - def show_project(self, project: Any) -> None: ... - - def clear_model(self) -> None: ... - - def render(self) -> None: ... - - def reset_camera(self) -> None: ... - - def view_isometric(self) -> None: ... - - def view_xy(self) -> None: ... - - def view_xz(self) -> None: ... - - def view_yz(self) -> None: ... - - def set_working_plane(self, plane: str, offset: float) -> None: ... - - def clear_working_plane(self) -> None: ... - - def working_plane_type(self) -> str | None: ... - - def working_plane_offset(self) -> float | None: ... - - def set_snap_preview_enabled(self, enabled: bool) -> None: ... - - def set_show_section_extrusions(self, enabled: bool) -> None: ... - - def set_show_local_axes(self, enabled: bool) -> None: ... - - def set_display_options(self, *, show_node_labels: bool, show_element_labels: bool) -> None: ... - - def set_default_selection_enabled(self, enabled: bool) -> None: ... diff --git a/src/otko/views/canvas_plotly/__init__.py b/src/otko/views/canvas_plotly/__init__.py deleted file mode 100644 index 7fabb6d..0000000 --- a/src/otko/views/canvas_plotly/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Plotly-backed canvas package (optional GUI backend). - -``PlotlyCanvas`` is exported lazily (PEP 562): the pure -:mod:`~otko.views.canvas_plotly.trace_builder` must stay importable in the -headless test job, which has no Qt at all. -""" - -from __future__ import annotations - -from typing import Any - -from otko.views.canvas_plotly.trace_builder import ( - PlotlyTraceBuilder, - Scene, - SceneOptions, -) - -__all__ = ["PlotlyCanvas", "PlotlyTraceBuilder", "Scene", "SceneOptions"] - - -def __getattr__(name: str) -> Any: - if name == "PlotlyCanvas": - from otko.views.canvas_plotly.plotly_canvas import PlotlyCanvas - - return PlotlyCanvas - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/otko/views/canvas_plotly/bridge.py b/src/otko/views/canvas_plotly/bridge.py deleted file mode 100644 index 9dafbce..0000000 --- a/src/otko/views/canvas_plotly/bridge.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Qt ↔ plotly.js bridge exposed through ``QWebChannel``. - -The page calls :meth:`_Bridge.picked` / :meth:`_Bridge.snapClicked`; every -call is re-emitted here and wired to the canvas' own Qt signals. -""" - -from __future__ import annotations - -from PySide6.QtCore import QObject, Signal, Slot - - -class _Bridge(QObject): - """Slot surface the JavaScript side addresses as ``otkoBridge``.""" - - #: (kind, entity id, additive modifier held) where kind is node|element. - picked = Signal(str, int, bool) - #: World-space coordinates of a clicked grid intersection. - snapClicked = Signal(float, float, float) - - @Slot(str, int, bool) - def onPicked(self, kind: str, entity_id: int, additive: bool) -> None: - """JS entry point: a model entity (or snap target) was clicked.""" - self.picked.emit(kind, int(entity_id), bool(additive)) - - @Slot(float, float, float) - def onSnapClicked(self, x: float, y: float, z: float) -> None: - """JS entry point: a grid-intersection target was clicked.""" - self.snapClicked.emit(float(x), float(y), float(z)) diff --git a/src/otko/views/canvas_plotly/html.py b/src/otko/views/canvas_plotly/html.py deleted file mode 100644 index 46e145e..0000000 --- a/src/otko/views/canvas_plotly/html.py +++ /dev/null @@ -1,276 +0,0 @@ -"""HTML/JS runtime for the Plotly canvas. - -The page is materialised once per process into a temp directory and loaded -from ``file://``: the plotly.js bundle is ~5 MB, which is past -``QWebEngineView.setHtml``'s data-URL limit, and writing it to disk also lets -the browser cache it across figure updates. - -The JS side exposes three entry points to Python (called via -``QWebEnginePage.runJavaScript``): - -- ``otkoUpdate(payloadJson)`` — replace data + layout with ``Plotly.react``, - which diffs client-side and leaves the interactive camera untouched. -- ``otkoSetCamera(cameraJson)`` — apply a camera alone (view presets, - parallel-projection toggle). -- ``otkoSetSnapEnabled(bool)`` — arm/disarm the hover snap-target preview. - -Clicks travel the other way through the ``otkoBridge`` QWebChannel object: -entity picks carry the trace ``meta.kind`` and the point ``customdata``. -""" - -from __future__ import annotations - -import atexit -import shutil -import tempfile -from pathlib import Path - -#: Materialised runtime directory (plotly.min.js + index.html + qwebchannel.js). -_ASSETS_DIR: Path | None = None - -# ``qrc:///qtwebchannel/qwebchannel.js`` is served by QtWebEngine's internal -# resource scheme; it is not reachable through QFile. -_PAGE = """ - - - - - - - -
- - - - -""" - - -def runtime_url() -> str: - """Ensure the JS runtime is on disk and return the page's file path. - - Called once per :class:`~otko.views.canvas_plotly.PlotlyCanvas`; the - directory is reused and removed at interpreter exit. - """ - global _ASSETS_DIR - directory = _ensure_dir() - index = directory / "index.html" - if not index.exists(): - (directory / "index.html").write_text(_PAGE, encoding="utf-8") - (directory / "plotly.min.js").write_text(_plotly_js(), encoding="utf-8") - return str(index) - - -def _ensure_dir() -> Path: - global _ASSETS_DIR - if _ASSETS_DIR is None: - _ASSETS_DIR = Path(tempfile.mkdtemp(prefix="otko-plotly-")) - atexit.register(shutil.rmtree, _ASSETS_DIR, True) - return _ASSETS_DIR - - -def _plotly_js() -> str: - """The offline plotly.js bundle shipped inside the ``plotly`` package.""" - from plotly.offline import get_plotlyjs - - return get_plotlyjs() diff --git a/src/otko/views/canvas_plotly/plotly_canvas.py b/src/otko/views/canvas_plotly/plotly_canvas.py deleted file mode 100644 index ddecb15..0000000 --- a/src/otko/views/canvas_plotly/plotly_canvas.py +++ /dev/null @@ -1,417 +0,0 @@ -"""Plotly-backed 3D canvas. - -A ``QWebEngineView`` hosting plotly.js, driven over ``QWebChannel``. It -implements the same public surface as :class:`otko.views.canvas3d.ModelCanvas` -(signals, selection, working plane, view presets, display toggles) so -``MainWindow`` can swap the two at runtime. - -Update strategy: figures are pushed with ``Plotly.react``. Because react -resets any scene attribute the incoming layout omits, a non-framing push is -flagged ``preserveView`` and the JS side carries the live camera and axis -ranges forward — so re-rendering on a model edit or selection change never -yanks the camera the user is orbiting. Only an explicit re-frame (new -project, view preset, ``reset_camera``, style change) sends the computed -camera plus opstool-style padded axis ranges. -""" - -from __future__ import annotations - -import json -import math -from collections.abc import Callable -from dataclasses import replace -from typing import Any - -from PySide6.QtCore import QObject, QUrl, Signal -from PySide6.QtWebChannel import QWebChannel -from PySide6.QtWebEngineCore import QWebEnginePage -from PySide6.QtWebEngineWidgets import QWebEngineView -from PySide6.QtWidgets import QVBoxLayout, QWidget - -from otko.views.canvas3d.model_renderer import RendererMode -from otko.views.canvas3d.selection import SelectionState -from otko.views.canvas3d.style import RenderStyle -from otko.views.canvas_base import CanvasCapabilities -from otko.views.canvas_plotly import html as _html -from otko.views.canvas_plotly.bridge import _Bridge -from otko.views.canvas_plotly.trace_builder import ( - PlotlyTraceBuilder, - Scene, - SceneOptions, -) - -#: View preset directions (unit-ish vectors from the scene centre to the eye). -_VIEW_DIRECTIONS = { - "iso": (1.0, 1.0, 0.8), - "xy": (0.0, 0.0, 1.0), - "xz": (0.0, -1.0, 0.0), - "yz": (1.0, 0.0, 0.0), -} - - -class _LoggingPage(QWebEnginePage): - """A web page that forwards the JS console to the app's console dock. - - A plotly failure inside the page is otherwise invisible from Python (the - ``react`` promise just rejects), which makes a frozen canvas very hard to - diagnose. - """ - - def __init__(self, log: Callable[[str, str], None], parent: QObject | None = None) -> None: - super().__init__(parent) - self._log = log - - def javaScriptConsoleMessage( - self, - level: QWebEnginePage.JavaScriptConsoleMessageLevel, - message: str, - line_number: int, - source_id: str, - ) -> None: - names = { - QWebEnginePage.JavaScriptConsoleMessageLevel.ErrorMessageLevel: "error", - QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel: "warning", - } - severity = names.get(level, "info") - source = source_id.rsplit("/", 1)[-1] or "plotly" - self._log(f"[web] {source}:{line_number} {message}", severity) - - -class _CameraShim: - """Mimics ``canvas.camera.parallel_projection`` as consumed elsewhere.""" - - def __init__(self, canvas: PlotlyCanvas) -> None: - self._canvas = canvas - - @property - def parallel_projection(self) -> bool: - return self._canvas._parallel - - @parallel_projection.setter - def parallel_projection(self, value: bool) -> None: - self._canvas.set_parallel_projection(bool(value)) - - -class _PlotlyRendererFacade: - """Stand-in for ``ModelRenderer`` covering the calls made on ``_renderer``. - - ``RenderControls`` and ``DockManager`` reach into ``canvas._renderer`` for - ``render`` / ``set_mode`` / ``_mode``; this keeps those code paths - backend-agnostic. - """ - - def __init__(self, canvas: PlotlyCanvas) -> None: - self._canvas = canvas - - @property - def _mode(self) -> RendererMode: - return self._canvas._mode - - @property - def _project(self) -> Any: - return self._canvas._project - - @property - def _working_plane(self) -> tuple[str, float] | None: - return self._canvas._working_plane - - def render(self, project: Any) -> None: - self._canvas.set_project(project) - - def set_mode(self, mode: RendererMode, deformation: Any = None) -> None: - self._canvas.set_mode(mode, deformation) - - def set_working_plane(self, plane: tuple[str, float] | None) -> None: - if plane is None: - self._canvas.set_working_plane(None, 0.0) - else: - self._canvas.set_working_plane(plane[0], plane[1]) - - def set_show_section_extrusions(self, on: bool) -> None: - self._canvas.set_show_section_extrusions(on) - - def set_show_local_axes(self, on: bool) -> None: - self._canvas.set_show_local_axes(on) - - def set_display_options(self, *, show_node_labels: bool, show_element_labels: bool) -> None: - self._canvas.set_display_options( - show_node_labels=show_node_labels, - show_element_labels=show_element_labels, - ) - - -class PlotlyCanvas(QWidget): - """The central 3D viewport, rendered by plotly.js in a web view.""" - - nodePicked = Signal(int) - elementPicked = Signal(int) - emptyClicked = Signal(float, float, float) - - #: Force diagrams and off-screen video capture are not implemented on - #: this backend yet (both are PyVista-specific today). - capabilities = CanvasCapabilities(diagrams=False, animation_export=False) - - def __init__( - self, - parent: QWidget | None = None, - style: RenderStyle | None = None, - selection: SelectionState | None = None, - log: Callable[[str, str], None] | None = None, - ) -> None: - super().__init__(parent) - self._log = log - self._style = style or RenderStyle() - self.selection = selection or SelectionState(self) - self._builder = PlotlyTraceBuilder(self._style) - self._project: Any = None - self._scene = Scene(data=[], layout={}) - self._options = SceneOptions() - self._mode = RendererMode.MODEL - self._parallel = False - self._view_preset = "iso" - self._snap_enabled = False - self._default_selection_enabled = True - self._working_plane: tuple[str, float] | None = None - self._camera = _CameraShim(self) - self._framing_dirty = True - self._renderer = _PlotlyRendererFacade(self) - self._ready = False - - self._build_ui() - self.selection.selectionChanged.connect(self._on_selection_changed) - - # ── construction ───────────────────────────────────────────────── - def _build_ui(self) -> None: - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - self._web = QWebEngineView(self) - if self._log is not None: - self._web.setPage(_LoggingPage(self._log, self._web)) - self._channel = QWebChannel(self._web.page()) - self._bridge = _Bridge(self) - self._channel.registerObject("otkoBridge", self._bridge) - self._web.page().setWebChannel(self._channel) - self._web.loadFinished.connect(self._on_loaded) - self._bridge.picked.connect(self._on_picked) - self._bridge.snapClicked.connect(self._on_snap_clicked) - layout.addWidget(self._web) - self._web.load(QUrl.fromLocalFile(_html.runtime_url())) - - def _on_loaded(self, ok: bool) -> None: - if not ok: - return - self._ready = True - self._push_scene() - self._eval(f"window.otkoSetSnapEnabled({_js_bool(self._snap_enabled)})") - - # ── public API (mirrors ModelCanvas) ───────────────────────────── - def show_project(self, project: Any) -> None: - """Render (or clear) a project; frame the camera when nodes exist.""" - self.set_project(project) - if project is not None and project.nodes: - self._view_preset = "iso" - self._framing_dirty = True - self.render() - - def clear_model(self) -> None: - """Remove the model but keep the view/selection machinery.""" - self.selection.clear() - self.set_project(None) - self.render() - - def render(self, *args: Any, **kwargs: Any) -> None: - """Rebuild the figure and hand it to plotly.js. - - ``QWidget.render`` is overloaded for painting into a target; those - calls are forwarded untouched so the widget stays well-behaved, and - only the no-argument canvas idiom (shared with ``ModelCanvas``) - triggers a figure push. - """ - if args or kwargs: - super().render(*args, **kwargs) - return - self._push_scene() - - def set_project(self, project: Any) -> None: - """Bind a project without rendering (used by the ``_renderer`` facade).""" - self._project = project - - def set_mode(self, mode: RendererMode, deformation: Any = None) -> None: - """Set MODEL / DEFORMED / MODAL plus the displacement source to apply.""" - self._mode = mode - self._options = replace(self._options, deformation=deformation) - - def set_parallel_projection(self, on: bool) -> None: - self._parallel = bool(on) - self._framing_dirty = True - - def reset_camera(self) -> None: - self._view_preset = "iso" - self._framing_dirty = True - self.render() - - def view_isometric(self) -> None: - self._view_preset = "iso" - self._framing_dirty = True - self.render() - - def view_xy(self) -> None: - """Top view: the eye sits on +Z looking down.""" - self._view_preset = "xy" - self._framing_dirty = True - self.render() - - def view_xz(self) -> None: - """Front view: the eye sits on -Y.""" - self._view_preset = "xz" - self._framing_dirty = True - self.render() - - def view_yz(self) -> None: - """Right view: the eye sits on +X.""" - self._view_preset = "yz" - self._framing_dirty = True - self.render() - - # ── working plane ──────────────────────────────────────────────── - def set_working_plane(self, plane: str | None, offset: float) -> None: - """Filter the grid overlay to the active plan / elevation level.""" - if plane is None: - self._working_plane = None - else: - if plane not in ("XY", "XZ", "YZ"): - raise ValueError(f"Unsupported working plane: {plane!r}") - self._working_plane = (plane, float(offset)) - self._options = replace(self._options, working_plane=self._working_plane) - self.render() - - def clear_working_plane(self) -> None: - self.set_working_plane(None, 0.0) - - def working_plane_type(self) -> str | None: - return self._working_plane[0] if self._working_plane is not None else None - - def working_plane_offset(self) -> float | None: - return self._working_plane[1] if self._working_plane is not None else None - - # ── display toggles ────────────────────────────────────────────── - def set_snap_preview_enabled(self, enabled: bool) -> None: - """Toggle the hover snap-target preview (draw tools turn it on).""" - self._snap_enabled = bool(enabled) - self._eval(f"window.otkoSetSnapEnabled({_js_bool(self._snap_enabled)})") - - def set_show_section_extrusions(self, enabled: bool) -> None: - self._options = replace(self._options, show_extrusions=bool(enabled)) - self.render() - - def set_show_local_axes(self, enabled: bool) -> None: - self._options = replace(self._options, show_local_axes=bool(enabled)) - self.render() - - def set_style(self, style: RenderStyle) -> None: - """Swap the visual style and repaint the figure. - - Deliberately *not* a re-frame: colour/opacity are trace props and the - background/outline ride the layout, so a style change (including the - live preview while dragging a colour) updates without snapping the - camera the user has moved. - """ - self._style = style - self._builder.set_style(style) - self.render() - - def set_display_options(self, *, show_node_labels: bool, show_element_labels: bool) -> None: - self._options = replace( - self._options, - show_node_labels=bool(show_node_labels), - show_element_labels=bool(show_element_labels), - ) - self.render() - - def set_default_selection_enabled(self, enabled: bool) -> None: - """When False, picks fire signals but do not touch :attr:`selection`.""" - self._default_selection_enabled = bool(enabled) - - # ── internals ─────────────────────────────────────────────────── - def _push_scene(self) -> None: - """Send the figure to plotly.js. - - ``Plotly.react`` resets any scene attribute the incoming layout omits, - which used to snap the camera *and* the padded ranges back on every - selection change. So a non-framing push marks itself - ``preserveView`` and the JS side carries the live camera/ranges - forward; only an explicit re-frame (new project, view preset, - ``reset_camera``) sends the computed framing. Style changes are - non-framing: they re-colour in place. - """ - self._scene = self._builder.build(self._project, self._options) - if not self._ready: - # Nothing to push yet; crucially this must come *before* the - # framing flag is consumed, or the framing scheduled before - # loadFinished would be dropped on the floor. - return - layout = dict(self._scene.layout) - re_framed = self._framing_dirty - if re_framed: - scene_layout = dict(layout["scene"]) - scene_layout["camera"] = self._camera_dict(self._scene) - for axis, override in self._scene.axis_overrides().items(): - scene_layout[axis] = {**scene_layout.get(axis, {}), **override} - layout["scene"] = scene_layout - self._framing_dirty = False - payload = json.dumps({"data": self._scene.data, "layout": layout}) - self._eval(f"window.otkoUpdate({json.dumps(payload)}, {_js_bool(not re_framed)})") - - def _camera_dict(self, scene: Scene) -> dict[str, Any]: - cx, cy, cz = scene.center - distance = max(scene.diagonal, 1e-6) * 1.6 - direction = _VIEW_DIRECTIONS.get(self._view_preset, _VIEW_DIRECTIONS["iso"]) - norm = math.sqrt(sum(component * component for component in direction)) or 1.0 - eye = ( - cx + direction[0] / norm * distance, - cy + direction[1] / norm * distance, - cz + direction[2] / norm * distance, - ) - # Looking straight down the Z axis needs a non-degenerate up vector. - up = (0.0, 1.0, 0.0) if self._view_preset == "xy" else (0.0, 0.0, 1.0) - return { - "eye": {"x": eye[0], "y": eye[1], "z": eye[2]}, - "center": {"x": cx, "y": cy, "z": cz}, - "up": {"x": up[0], "y": up[1], "z": up[2]}, - "projection": {"type": "orthographic" if self._parallel else "perspective"}, - } - - def _eval(self, js: str) -> None: - if self._ready: - self._web.page().runJavaScript(js) - - def _on_selection_changed(self, nodes: frozenset[int], elements: frozenset[int]) -> None: - self._options = replace( - self._options, - selection_nodes=frozenset(nodes), - selection_elements=frozenset(elements), - ) - self.render() - - def _on_picked(self, kind: str, entity_id: int, additive: bool) -> None: - if kind == "node": - if self._default_selection_enabled: - if additive: - self.selection.toggle_node(entity_id) - else: - self.selection.select_node(entity_id) - self.nodePicked.emit(entity_id) - elif kind == "element": - if self._default_selection_enabled: - if additive: - self.selection.toggle_element(entity_id) - else: - self.selection.select_element(entity_id) - self.elementPicked.emit(entity_id) - - def _on_snap_clicked(self, x: float, y: float, z: float) -> None: - self.emptyClicked.emit(float(x), float(y), float(z)) - - -def _js_bool(value: bool) -> str: - return "true" if value else "false" diff --git a/src/otko/views/canvas_plotly/trace_builder.py b/src/otko/views/canvas_plotly/trace_builder.py deleted file mode 100644 index fa5fc94..0000000 --- a/src/otko/views/canvas_plotly/trace_builder.py +++ /dev/null @@ -1,1028 +0,0 @@ -"""Project → Plotly figure dictionaries. - -Pure data transformation: no Qt, no pyvista, no plotly import. The -:class:`PlotlyCanvas` serialises the returned :class:`Scene` to JSON and -hands it to plotly.js; the unit tests exercise it headless. - -Geometry conventions mirror :mod:`otko.views.canvas3d.model_renderer` so the -two backends draw the same model, but the output medium differs: - -- Nodes / supports / labels are ``scatter3d`` markers, whose size is in - *pixels* (plotly.js does not scale markers with world units). -- Frames are ``scatter3d`` line traces. Plotly cannot colour individual - segments of one line trace, so selection is expressed as two traces - (normal + selected) whose segment lists are regrouped on every update. -- Loads and local axes are ``cone`` traces (plotly.js has no 3D arrow glyph). -- Section extrusions are ``mesh3d`` box sweeps from - :func:`otko.services.section_bbox.bbox_for_section` — the same bbox - fallback the PyVista backend uses when a shape hint is not an exact fit. - -Picking rides on ``customdata`` + trace ``meta``: every pickable trace is -tagged ``meta={"kind": "node" | "element" | "snap"}`` and the JS side reads -``point.data.meta.kind`` to decide what the click meant. -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any - -import numpy as np - -from otko.core import ( - BeamWithHingesElement, - CorotTrussElement, - DispBeamColumn, - ElasticBeamColumn, - ForceBeamColumn, - NodalLoad, - PlainLoadPattern, - Project, - QuadElement, - TrussElement, - UniformElementLoad, - ZeroLengthElement, - ZeroLengthSectionElement, -) -from otko.views.canvas3d.style import ( - RenderStyle, - element_family_index, - family_palette, -) - -_FRAME_CLASSES = ( - ElasticBeamColumn, - DispBeamColumn, - ForceBeamColumn, - BeamWithHingesElement, - TrussElement, - CorotTrussElement, - ZeroLengthElement, -) - -#: Triangle indices for the 8-corner box sweep built by :meth:`_SceneBuilder._box_corners`. -_BOX_TRIS = ( - (0, 1, 2), - (0, 2, 3), - (4, 5, 6), - (4, 6, 7), - (0, 1, 5), - (0, 5, 4), - (3, 2, 6), - (3, 6, 7), - (0, 3, 7), - (0, 7, 4), - (1, 2, 6), - (1, 6, 5), -) - -#: Support kind → plotly 3D marker symbol. -_SUPPORT_SYMBOLS = { - "fix": "square", - "pin": "triangle-up", - "roller": "circle", - "custom": "diamond", -} - -_NODE_MARKER_SIZE = 7.0 -_SUPPORT_MARKER_SIZE = 11.0 -_SNAP_MARKER_SIZE = 8.0 - - -@dataclass(frozen=True) -class SceneOptions: - """Everything the builder needs beyond the project itself.""" - - selection_nodes: frozenset[int] = frozenset() - selection_elements: frozenset[int] = frozenset() - #: Anything exposing ``shifted(points, node_ids)`` (e.g. ``DeformationSource``). - deformation: Any = None - working_plane: tuple[str, float] | None = None - show_node_labels: bool = False - show_element_labels: bool = False - show_extrusions: bool = False - show_local_axes: bool = False - - -@dataclass -class Scene: - """A transport-ready plotly figure plus framing metadata.""" - - data: list[dict[str, Any]] - layout: dict[str, Any] - center: tuple[float, float, float] = (0.0, 0.0, 0.0) - diagonal: float = 1.0 - #: Trace index (in ``data``) of the empty hover-snap marker, or -1. - hover_trace: int = -1 - #: Padded ``(min, max)`` per axis, used to frame the camera deterministically. - axis_bounds: dict[str, tuple[float, float]] = field(default_factory=dict) - - def to_payload(self) -> dict[str, Any]: - """Figure dict without the camera — camera is owned by the widget.""" - return {"data": self.data, "layout": self.layout} - - def axis_overrides(self) -> dict[str, Any]: - """Nested ``scene`` range pins for the framed data window. - - Only pushed together with the camera (view presets / new project), so - the user's own zoom and pan survive ordinary data updates. The padding - comes from opstool's scene recipe (``pad_ratio`` 0.15). - """ - return { - f"{name}axis": {"range": [low, high], "autorange": False} - for name, (low, high) in self.axis_bounds.items() - } - - -@dataclass -class _Mesh: - """Accumulates a triangle soup for a single ``mesh3d`` trace.""" - - x: list[float] = field(default_factory=list) - y: list[float] = field(default_factory=list) - z: list[float] = field(default_factory=list) - i: list[int] = field(default_factory=list) - j: list[int] = field(default_factory=list) - k: list[int] = field(default_factory=list) - - def add_box(self, corners: np.ndarray) -> None: - base = len(self.x) - for cx, cy, cz in corners: - self.x.append(float(cx)) - self.y.append(float(cy)) - self.z.append(float(cz)) - for a, b, c in _BOX_TRIS: - self.i.append(base + a) - self.j.append(base + b) - self.k.append(base + c) - - @property - def is_empty(self) -> bool: - return not self.x - - def as_trace(self, *, color: str, opacity: float, name: str) -> dict[str, Any]: - return { - "type": "mesh3d", - "x": self.x, - "y": self.y, - "z": self.z, - "i": self.i, - "j": self.j, - "k": self.k, - "color": color, - "opacity": opacity, - "flatshading": True, - "hoverinfo": "skip", - "name": name, - "showscale": False, - } - - -# ── small geometry helpers ──────────────────────────────────────────────── -def _diag_of_points(pts: np.ndarray | None) -> float: - if pts is None or len(pts) == 0: - return 1.0 - mn, mx = pts.min(axis=0), pts.max(axis=0) - d = float(np.linalg.norm(mx - mn)) - return d if d > 0 else 1.0 - - -#: Padding fraction added around the model when framing the view. -#: From opstool's scene recipe (``pad_ratio=0.15`` for model views). -_PAD_RATIO = 0.15 - - -def _padded_axis_bounds(pts: np.ndarray | None) -> dict[str, tuple[float, float]]: - """Padded ``(min, max)`` per axis; degenerate axes get unit slack.""" - if pts is None or len(pts) == 0: - return {name: (-1.0, 1.0) for name in ("x", "y", "z")} - lower = np.asarray(pts, dtype=float).min(axis=0) - upper = np.asarray(pts, dtype=float).max(axis=0) - pad = (upper - lower) * _PAD_RATIO - bounds: dict[str, tuple[float, float]] = {} - for index, name in enumerate(("x", "y", "z")): - low = float(lower[index] - pad[index]) - high = float(upper[index] + pad[index]) - if high - low < 1e-9: # planar / single-point model - low, high = low - 1.0, high + 1.0 - bounds[name] = (low, high) - return bounds - - -def _diag_of_bounds(bounds: dict[str, tuple[float, float]]) -> float: - """Padded bounding-box diagonal — the framing distance yardstick.""" - diagonal = float(np.linalg.norm([high - low for low, high in bounds.values()])) - return diagonal if diagonal > 0 else 1.0 - - -def _frame_basis(el: Any, x_local: np.ndarray) -> tuple[np.ndarray, np.ndarray]: - """Local (y, z) basis — mirrors ``ModelRenderer._frame_basis``.""" - x = x_local / float(np.linalg.norm(x_local)) - vecxz = getattr(el, "vecxz", None) - if vecxz is not None: - try: - v = np.asarray(vecxz, dtype=float) - z_local = v - float(np.dot(v, x)) * x - n = float(np.linalg.norm(z_local)) - if n > 1e-9: - z_local /= n - y_local = np.cross(z_local, x) - m = float(np.linalg.norm(y_local)) - if m > 1e-9: - return y_local / m, z_local - except (TypeError, ValueError): - pass - z_global = np.array([0.0, 0.0, 1.0]) - y_local = np.cross(z_global, x) - if float(np.linalg.norm(y_local)) < 1e-6: - y_local = np.cross(np.array([1.0, 0.0, 0.0]), x) - y_local /= float(np.linalg.norm(y_local)) - z_local = np.cross(x, y_local) - return y_local, z_local - - -def _dof_indices(ndf: int) -> tuple[int, ...]: - if ndf == 6: - return (0, 1, 2, 3, 4, 5) - if ndf == 3: - return (0, 1, 5) - if ndf == 2: - return (0, 1) - return tuple(range(ndf)) - - -def _classify_support(restraint: tuple[bool, ...], dof_idx: tuple[int, ...]) -> str: - flags = [restraint[i] for i in dof_idx] - if all(flags): - return "fix" - trans_flags = [flags[k] for k, idx in enumerate(dof_idx) if idx < 3] - rot_flags = [flags[k] for k, idx in enumerate(dof_idx) if idx >= 3] - if trans_flags and all(trans_flags) and not any(rot_flags): - return "pin" - if sum(flags) == 1: - return "roller" - return "custom" - - -def _line_trace( - segments: list[tuple[tuple[float, float, float], tuple[float, float, float]]], - *, - color: str, - width: float, - name: str, - opacity: float = 1.0, - meta: dict[str, Any] | None = None, - customdata: list[Any] | None = None, -) -> dict[str, Any]: - """One ``scatter3d`` line trace from None-separated segment endpoints.""" - x: list[float | None] = [] - y: list[float | None] = [] - z: list[float | None] = [] - for a, b in segments: - x.extend([float(a[0]), float(b[0]), None]) - y.extend([float(a[1]), float(b[1]), None]) - z.extend([float(a[2]), float(b[2]), None]) - trace: dict[str, Any] = { - "type": "scatter3d", - "mode": "lines", - "x": x, - "y": y, - "z": z, - "line": {"color": color, "width": width}, - "opacity": opacity, - "hoverinfo": "skip", - "name": name, - "showlegend": False, - } - if meta is not None: - trace["meta"] = meta - if customdata is not None: - trace["customdata"] = customdata - return trace - - -class PlotlyTraceBuilder: - """Builds the full figure for a project.""" - - def __init__(self, style: RenderStyle | None = None) -> None: - self._style = style or RenderStyle() - - def set_style(self, style: RenderStyle) -> None: - """Swap the visual style; the next :meth:`build` picks it up.""" - self._style = style - - # ── public ─────────────────────────────────────────────────────── - def build(self, project: Project | None, options: SceneOptions | None = None) -> Scene: - opts = options or SceneOptions() - if project is None: - return Scene(data=[], layout=self._layout()) - - nodes = list(project.nodes) - node_ids = [n.id for n in nodes] - points = np.array([n.coords for n in nodes], dtype=float) if nodes else np.empty((0, 3)) - if opts.deformation is not None and len(points): - points = np.asarray(opts.deformation.shifted(points, node_ids), dtype=float) - node_row = {nid: i for i, nid in enumerate(node_ids)} - - data: list[dict[str, Any]] = [] - grid_pts = self._build_grid(project, data, opts) - self._build_extrusions(project, data, opts) - self._build_local_axes(project, data, opts) - self._build_loads(project, data, opts) - self._build_supports(project, data, opts) - self._build_frames(project, data, opts, points, node_row) - self._build_nodes(data, opts, points, node_ids) - self._build_labels(project, data, opts, points, node_row) - hover_trace = self._build_hover_marker(data) - - candidates = [points] if len(points) else [] - if grid_pts is not None: - candidates.append(grid_pts) - all_pts = np.vstack(candidates) if candidates else np.empty((0, 3)) - center = tuple(np.mean(all_pts, axis=0)) if len(all_pts) else (0.0, 0.0, 0.0) - bounds = _padded_axis_bounds(all_pts) - return Scene( - data=data, - layout=self._layout(), - center=(float(center[0]), float(center[1]), float(center[2])), - diagonal=_diag_of_bounds(bounds), - hover_trace=hover_trace, - axis_bounds=bounds, - ) - - # ── layout ─────────────────────────────────────────────────────── - def _layout(self) -> dict[str, Any]: - style = self._style - outline = style.show_axis_outline - - def axis(color: str, title: str) -> dict[str, Any]: - # Coloured axis lines + titles stay visible as the orientation cue - # (plotly has no corner triad); the grid/ticks are the optional - # "outline" from opstool's scene recipe. - return { - "title": {"text": title, "font": {"color": color, "size": 12}}, - "showgrid": outline, - "showbackground": False, - "zeroline": outline, - "showticklabels": outline, - "showspikes": False, - "visible": True, - "linecolor": color, - "linewidth": 2, - } - - return { - "paper_bgcolor": style.background_bottom, - "plot_bgcolor": style.background_bottom, - "showlegend": False, - "margin": {"l": 0, "r": 0, "t": 0, "b": 0}, - "uirevision": "otko", - "scene": { - "bgcolor": style.background_bottom, - "aspectmode": "data", - "dragmode": "orbit", - "xaxis": axis(style.fix_color, "X"), - "yaxis": axis(style.load_color, "Y"), - "zaxis": axis(style.truss_color, "Z"), - }, - } - - # ── grid ───────────────────────────────────────────────────────── - def _build_grid( - self, project: Project, data: list[dict[str, Any]], opts: SceneOptions - ) -> np.ndarray | None: - coord_systems = getattr(project, "coord_systems", None) or [] - palette = [ - ((0.08, 0.08, 0.08), (0.85, 0.55, 0.00)), - ((0.20, 0.35, 0.55), (0.85, 0.55, 0.00)), - ((0.20, 0.55, 0.30), (0.85, 0.55, 0.00)), - ((0.55, 0.20, 0.40), (0.85, 0.55, 0.00)), - ((0.35, 0.20, 0.55), (0.85, 0.55, 0.00)), - ] - all_dots: list[np.ndarray] = [] - - for idx, cs in enumerate(coord_systems): - grid = cs.grid - if not grid.visible or getattr(grid, "hide_all", False): - continue - xs = list(grid.x_lines) - ys = list(grid.y_lines) - zs = list(grid.z_lines) - if not (xs or ys or zs): - continue - palette_idx = 0 if cs.is_global() else (idx % (len(palette) - 1)) + 1 - grid_color, dot_color = palette[palette_idx] - grid_hex = _rgb_to_hex(grid_color) - dot_hex = _rgb_to_hex(dot_color) - - xmin, xmax = (min(xs), max(xs)) if xs else (-1.0, 1.0) - ymin, ymax = (min(ys), max(ys)) if ys else (-1.0, 1.0) - if xmin == xmax: - xmin, xmax = xmin - 1.0, xmax + 1.0 - if ymin == ymax: - ymin, ymax = ymin - 1.0, ymax + 1.0 - - plane_axis: int | None = None - plane_offset: float | None = None - if opts.working_plane is not None: - name, off = opts.working_plane - axis_idx = {"XY": 2, "XZ": 1, "YZ": 0}[name] - plane_axis = axis_idx - plane_offset = off - cs.coord.origin[axis_idx] - - def on_plane( - local: tuple[float, float, float], - _axis: int | None = plane_axis, - _offset: float | None = plane_offset, - ) -> bool: - if _axis is None or _offset is None: - return True - return abs(local[_axis] - _offset) < 1e-6 - - active: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = [] - dim: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = [] - - def add_seg( - p1: tuple[float, float, float], - p2: tuple[float, float, float], - _active: list = active, - _dim: list = dim, - _on_plane: Any = on_plane, - _cs: Any = cs, - ) -> None: - on = _on_plane(p1) and _on_plane(p2) - bucket = _active if on else _dim - bucket.append((_cs.coord.local_to_world(p1), _cs.coord.local_to_world(p2))) - - z_planes = zs if zs else [0.0] - for z in z_planes: - for x in xs: - add_seg((x, ymin, z), (x, ymax, z)) - for y in ys: - add_seg((xmin, y, z), (xmax, y, z)) - if opts.working_plane is None and zs and xs and ys: - for x in xs: - for y in ys: - add_seg((x, y, zs[0]), (x, y, zs[-1])) - - if dim: - data.append(_line_trace(dim, color=grid_hex, width=1, name="grid", opacity=0.18)) - if active: - data.append(_line_trace(active, color=grid_hex, width=2, name="grid-active")) - - active_dots: list[tuple[float, float, float]] = [] - dim_dots: list[tuple[float, float, float]] = [] - for z in z_planes: - for x in xs or [0.0]: - for y in ys or [0.0]: - pt = cs.coord.local_to_world((x, y, z)) - (active_dots if on_plane((x, y, z)) else dim_dots).append(pt) - if dim_dots: - data.append(self._dot_trace(dim_dots, color="#999999", opacity=0.35)) - if active_dots: - data.append( - self._dot_trace( - active_dots, - color=dot_hex, - opacity=1.0, - meta={"kind": "snap"}, - ) - ) - all_dots.extend(np.asarray(active_dots, dtype=float)) - - return np.vstack(all_dots) if all_dots else None - - @staticmethod - def _dot_trace( - points: list[tuple[float, float, float]], - *, - color: str, - opacity: float, - meta: dict[str, Any] | None = None, - ) -> dict[str, Any]: - customdata = ( - [[float(p[0]), float(p[1]), float(p[2])] for p in points] if meta is not None else None - ) - trace: dict[str, Any] = { - "type": "scatter3d", - "mode": "markers", - "x": [float(p[0]) for p in points], - "y": [float(p[1]) for p in points], - "z": [float(p[2]) for p in points], - "marker": {"color": color, "size": _SNAP_MARKER_SIZE, "line": {"width": 0}}, - "opacity": opacity, - "hoverinfo": "skip", - "name": "snap", - "showlegend": False, - } - if meta is not None: - trace["meta"] = meta - trace["customdata"] = customdata - return trace - - # ── model entities ─────────────────────────────────────────────── - def _build_nodes( - self, - data: list[dict[str, Any]], - opts: SceneOptions, - points: np.ndarray, - node_ids: list[int], - ) -> None: - if not len(points): - return - colors = [ - self._style.node_selected_color - if nid in opts.selection_nodes - else self._style.node_color - for nid in node_ids - ] - data.append( - { - "type": "scatter3d", - "mode": "markers", - "x": [float(p[0]) for p in points], - "y": [float(p[1]) for p in points], - "z": [float(p[2]) for p in points], - "marker": { - "color": colors, - "size": _NODE_MARKER_SIZE, - "line": {"color": "#4d4d4d", "width": 1}, - }, - "customdata": list(node_ids), - "meta": {"kind": "node"}, - # Engineering-notation hover (opstool's trace recipe) so a - # hover identifies the entity instead of showing the raw id. - "hovertemplate": "Node #%{customdata}", - "name": "nodes", - "showlegend": False, - } - ) - - def _build_frames( - self, - project: Project, - data: list[dict[str, Any]], - opts: SceneOptions, - points: np.ndarray, - node_row: dict[int, int], - ) -> None: - """One line trace whose per-point ``line.color`` carries the palette. - - plotly accepts an *array* for ``Scatter3d.line.color`` mapped through - a ``colorscale``, so a single trace can colour every element - individually — by family and by selection — instead of one trace per - colour. That also leaves room to colour by response value later. - Each element's two endpoints (and its ``None`` separator) share one - colour index, so each drawn segment is a solid colour. - """ - if not len(points): - return - scale_colors = family_palette(self._style) - selected_index = len(scale_colors) - 1 - - x: list[float | None] = [] - y: list[float | None] = [] - z: list[float | None] = [] - color_index: list[float] = [] - customdata: list[Any] = [] - for el in project.elements: - if not isinstance(el, _FRAME_CLASSES): - continue - i = node_row.get(el.nodes[0]) - j = node_row.get(el.nodes[1]) - if i is None or j is None: - continue - index = float( - selected_index if el.id in opts.selection_elements else element_family_index(el) - ) - x.extend([float(points[i][0]), float(points[j][0]), None]) - y.extend([float(points[i][1]), float(points[j][1]), None]) - z.extend([float(points[i][2]), float(points[j][2]), None]) - color_index.extend([index, index, index]) - customdata.extend([el.id, el.id, None]) - if not x: - return - - count = len(scale_colors) - data.append( - { - "type": "scatter3d", - "mode": "lines", - "x": x, - "y": y, - "z": z, - "line": { - "color": color_index, - "colorscale": [ - (index / (count - 1), color) for index, color in enumerate(scale_colors) - ], - "cmin": 0, - "cmax": count - 1, - "width": 4, - }, - "customdata": customdata, - "meta": {"kind": "element"}, - "hovertemplate": "Element #%{customdata}", - "name": "elements", - "showlegend": False, - } - ) - - def _build_supports( - self, project: Project, data: list[dict[str, Any]], opts: SceneOptions - ) -> None: - if not project.nodes: - return - dof_idx = _dof_indices(project.ndf) - groups: dict[str, list[Any]] = {} - for node in project.nodes: - if not any(node.restraint[i] for i in dof_idx): - continue - kind = _classify_support(node.restraint, dof_idx) - groups.setdefault(kind, []).append(node) - for kind, nodes in groups.items(): - data.append( - { - "type": "scatter3d", - "mode": "markers", - "x": [float(n.coords[0]) for n in nodes], - "y": [float(n.coords[1]) for n in nodes], - "z": [float(n.coords[2]) for n in nodes], - "marker": { - "color": self._style.support_color, - "size": _SUPPORT_MARKER_SIZE, - "symbol": _SUPPORT_SYMBOLS[kind], - "line": {"color": "#7f3f00", "width": 1}, - }, - "hoverinfo": "skip", - "name": f"support-{kind}", - "showlegend": False, - } - ) - - # ── loads / axes / extrusions ──────────────────────────────────── - def _build_loads( - self, project: Project, data: list[dict[str, Any]], opts: SceneOptions - ) -> None: - if not project.load_patterns or not project.nodes: - return - node_by_id = {n.id: n for n in project.nodes} - elem_by_id = {e.id: e for e in project.elements} - pts = np.array([n.coords for n in project.nodes], dtype=float) - scale = max(_diag_of_points(pts) * 0.05, 1e-6) - - nodal_x: list[float] = [] - nodal_y: list[float] = [] - nodal_z: list[float] = [] - nodal_u: list[float] = [] - nodal_v: list[float] = [] - nodal_w: list[float] = [] - dist_x: list[float] = [] - dist_y: list[float] = [] - dist_z: list[float] = [] - dist_u: list[float] = [] - dist_v: list[float] = [] - dist_w: list[float] = [] - - for pattern in project.load_patterns: - if not isinstance(pattern, PlainLoadPattern): - continue - for nload in pattern.nodal_loads: - if not isinstance(nload, NodalLoad): - continue - node = node_by_id.get(nload.node_id) - if node is None: - continue - f = np.asarray(nload.forces[:3], dtype=float) - mag = float(np.linalg.norm(f)) - if mag < 1e-12: - continue - direction = f / mag - tail = np.asarray(node.coords, dtype=float) - direction * scale - nodal_x.append(float(tail[0])) - nodal_y.append(float(tail[1])) - nodal_z.append(float(tail[2])) - nodal_u.append(float(direction[0])) - nodal_v.append(float(direction[1])) - nodal_w.append(float(direction[2])) - - for eload in pattern.element_loads: - if not isinstance(eload, UniformElementLoad): - continue - elem = elem_by_id.get(eload.element_id) - if elem is None: - continue - node_i = node_by_id.get(elem.nodes[0]) - node_j = node_by_id.get(elem.nodes[1]) - if node_i is None or node_j is None: - continue - pi = np.asarray(node_i.coords, dtype=float) - pj = np.asarray(node_j.coords, dtype=float) - axis = pj - pi - length = float(np.linalg.norm(axis)) - if length < 1e-9: - continue - x_local = axis / length - z_global = np.array([0.0, 0.0, 1.0]) - y_local = np.cross(z_global, x_local) - if float(np.linalg.norm(y_local)) < 1e-6: - y_local = np.cross(np.array([0.0, 1.0, 0.0]), x_local) - y_local /= float(np.linalg.norm(y_local)) - z_local = np.cross(x_local, y_local) - load_vec = eload.wx * x_local + eload.wy * y_local + eload.wz * z_local - mag = float(np.linalg.norm(load_vec)) - if mag < 1e-12: - continue - direction = load_vec / mag - n_arrows = 5 - for k in range(n_arrows): - t = (k + 0.5) / n_arrows - tail = pi + t * axis - direction * (0.4 * scale) - dist_x.append(float(tail[0])) - dist_y.append(float(tail[1])) - dist_z.append(float(tail[2])) - dist_u.append(float(direction[0])) - dist_v.append(float(direction[1])) - dist_w.append(float(direction[2])) - - if nodal_x: - data.append( - _cone_trace( - nodal_x, - nodal_y, - nodal_z, - nodal_u, - nodal_v, - nodal_w, - color=self._style.nodal_load_color, - name="nodal-loads", - size=scale, - ) - ) - if dist_x: - data.append( - _cone_trace( - dist_x, - dist_y, - dist_z, - dist_u, - dist_v, - dist_w, - color=self._style.element_load_color, - name="element-loads", - size=0.6 * scale, - ) - ) - - def _build_local_axes( - self, project: Project, data: list[dict[str, Any]], opts: SceneOptions - ) -> None: - if not opts.show_local_axes or not project.nodes: - return - node_by_id = {n.id: n for n in project.nodes} - pts = np.array([n.coords for n in project.nodes], dtype=float) - cap = max(_diag_of_points(pts) * 0.08, 1e-6) - axes: dict[str, dict[str, list[float]]] = { - "x": {"x": [], "y": [], "z": [], "u": [], "v": [], "w": []}, - "y": {"x": [], "y": [], "z": [], "u": [], "v": [], "w": []}, - "z": {"x": [], "y": [], "z": [], "u": [], "v": [], "w": []}, - } - for el in project.elements: - if isinstance(el, QuadElement | ZeroLengthElement | ZeroLengthSectionElement): - continue - if not isinstance( - el, - ElasticBeamColumn - | DispBeamColumn - | ForceBeamColumn - | BeamWithHingesElement - | TrussElement - | CorotTrussElement, - ): - continue - node_i = node_by_id.get(el.nodes[0]) - node_j = node_by_id.get(el.nodes[1]) - if node_i is None or node_j is None: - continue - pi = np.asarray(node_i.coords, dtype=float) - pj = np.asarray(node_j.coords, dtype=float) - axis = pj - pi - length = float(np.linalg.norm(axis)) - if length < 1e-9: - continue - x_local = axis / length - y_local, z_local = _frame_basis(el, x_local) - mid = (pi + pj) / 2.0 - for key, direction in (("x", x_local), ("y", y_local), ("z", z_local)): - bucket = axes[key] - bucket["x"].append(float(mid[0])) - bucket["y"].append(float(mid[1])) - bucket["z"].append(float(mid[2])) - bucket["u"].append(float(direction[0])) - bucket["v"].append(float(direction[1])) - bucket["w"].append(float(direction[2])) - for key, color in (("x", "#ff0000"), ("y", "#00bf00"), ("z", "#3366ff")): - bucket = axes[key] - if bucket["x"]: - data.append( - _cone_trace( - bucket["x"], - bucket["y"], - bucket["z"], - bucket["u"], - bucket["v"], - bucket["w"], - color=color, - name=f"local-{key}", - size=cap, - ) - ) - - def _build_extrusions( - self, project: Project, data: list[dict[str, Any]], opts: SceneOptions - ) -> None: - if not opts.show_extrusions: - return - from otko.services.section_bbox import bbox_for_section - - node_by_id = {n.id: n for n in project.nodes} - section_by_id = {s.id: s for s in project.sections} - mesh = _Mesh() - for el in project.elements: - if not isinstance(el, _FRAME_CLASSES): - continue - if isinstance(el, TrussElement | CorotTrussElement | QuadElement | ZeroLengthElement): - continue - section_id = getattr(el, "section_id", None) - if section_id is None: - continue - section = section_by_id.get(section_id) - if section is None: - continue - dims = bbox_for_section(section, project) - if dims is None: - continue - w_y, h_z = dims - if w_y <= 0 or h_z <= 0: - continue - node_i = node_by_id.get(el.nodes[0]) - node_j = node_by_id.get(el.nodes[1]) - if node_i is None or node_j is None: - continue - pi = np.asarray(node_i.coords, dtype=float) - pj = np.asarray(node_j.coords, dtype=float) - axis = pj - pi - length = float(np.linalg.norm(axis)) - if length < 1e-9: - continue - x_local = axis / length - y_local, z_local = _frame_basis(el, x_local) - mesh.add_box(self._box_corners(pi, x_local, y_local, z_local, length, w_y, h_z)) - if not mesh.is_empty: - data.append( - mesh.as_trace( - color=self._style.extrusion_color, - opacity=self._style.extrusion_opacity, - name="extrusions", - ) - ) - - @staticmethod - def _box_corners( - pi: np.ndarray, - x_local: np.ndarray, - y_local: np.ndarray, - z_local: np.ndarray, - length: float, - w_y: float, - h_z: float, - ) -> np.ndarray: - hy, hz = w_y / 2.0, h_z / 2.0 - offsets = np.array( - [ - [0.0, -hy, -hz], - [length, -hy, -hz], - [length, +hy, -hz], - [0.0, +hy, -hz], - [0.0, -hy, +hz], - [length, -hy, +hz], - [length, +hy, +hz], - [0.0, +hy, +hz], - ] - ) - basis = np.column_stack([x_local, y_local, z_local]) - return pi + offsets @ basis.T - - # ── labels / hover marker ──────────────────────────────────────── - def _build_labels( - self, - project: Project, - data: list[dict[str, Any]], - opts: SceneOptions, - points: np.ndarray, - node_row: dict[int, int], - ) -> None: - if opts.show_node_labels and len(points): - labels = [(n.name.strip() if n.name.strip() else f"N{n.id}") for n in project.nodes] - data.append( - _text_trace( - points, labels, name="node-labels", font_size=self._style.label_font_size - ) - ) - if opts.show_element_labels and len(points): - centers: list[np.ndarray] = [] - labels: list[str] = [] - for el in project.elements: - if len(el.nodes) != 2: - continue - i = node_row.get(el.nodes[0]) - j = node_row.get(el.nodes[1]) - if i is None or j is None: - continue - centers.append((points[i] + points[j]) / 2.0) - labels.append(el.name.strip() if el.name.strip() else f"E{el.id}") - if centers: - data.append( - _text_trace( - np.asarray(centers), - labels, - name="element-labels", - font_size=self._style.label_font_size, - ) - ) - - @staticmethod - def _build_hover_marker(data: list[dict[str, Any]]) -> int: - data.append( - { - "type": "scatter3d", - "mode": "markers", - "x": [], - "y": [], - "z": [], - "marker": { - "color": "#ffd900", - "size": 13, - "line": {"color": "#8a6d00", "width": 1}, - }, - "hoverinfo": "skip", - "name": "snap-hover", - "showlegend": False, - "meta": {"kind": "hover"}, - } - ) - return len(data) - 1 - - -def _cone_trace( - x: list[float], - y: list[float], - z: list[float], - u: list[float], - v: list[float], - w: list[float], - *, - color: str, - name: str, - size: float, -) -> dict[str, Any]: - return { - "type": "cone", - "x": x, - "y": y, - "z": z, - "u": u, - "v": v, - "w": w, - "anchor": "tail", - "sizemode": "absolute", - "sizeref": float(size), - "colorscale": [[0, color], [1, color]], - "showscale": False, - "hoverinfo": "skip", - "name": name, - "showlegend": False, - } - - -def _text_trace( - points: np.ndarray, labels: list[str], *, name: str, font_size: int -) -> dict[str, Any]: - return { - "type": "scatter3d", - "mode": "text", - "x": [float(p[0]) for p in points], - "y": [float(p[1]) for p in points], - "z": [float(p[2]) for p in points], - "text": labels, - "textposition": "top center", - "textfont": {"size": font_size, "color": "#111111"}, - "hoverinfo": "skip", - "name": name, - "showlegend": False, - } - - -def _rgb_to_hex(rgb: tuple[float, float, float]) -> str: - r, g, b = (int(round(v * 255)) for v in rgb) - return f"#{r:02x}{g:02x}{b:02x}" diff --git a/src/otko/views/dialogs/__init__.py b/src/otko/views/dialogs/__init__.py index e4dff02..73305a1 100644 --- a/src/otko/views/dialogs/__init__.py +++ b/src/otko/views/dialogs/__init__.py @@ -12,7 +12,6 @@ from otko.views.dialogs.assign_property import ( from otko.views.dialogs.assign_support import PRESETS, AssignSupportDialog from otko.views.dialogs.assign_zls import AssignZeroLengthSectionDialog from otko.views.dialogs.case_manager import AnalysisCaseManagerDialog -from otko.views.dialogs.combination_dialog import LoadCombinationManagerDialog from otko.views.dialogs.coord_grid_systems import ( CoordinateGridSystemsDialog, CoordSystemDataDialog, @@ -34,7 +33,6 @@ from otko.views.dialogs.move import MoveDialog from otko.views.dialogs.path_time_series import PathTimeSeriesDialog from otko.views.dialogs.pattern_loads import PatternLoadsDialog from otko.views.dialogs.plain_pattern import PlainPatternDialog -from otko.views.dialogs.plot_properties import PlotPropertiesDialog from otko.views.dialogs.quick_grid_lines import QuickGridLinesDialog from otko.views.dialogs.quick_guide import QuickGuideDialog from otko.views.dialogs.replicate import ReplicateDialog @@ -62,7 +60,6 @@ __all__ = [ "DisplayOptionsDialog", "GridSystemDialog", "LinearTimeSeriesDialog", - "LoadCombinationManagerDialog", "MaterialLibraryDialog", "MaterialTesterDialog", "MirrorDialog", @@ -70,7 +67,6 @@ __all__ = [ "PathTimeSeriesDialog", "PatternLoadsDialog", "PlainPatternDialog", - "PlotPropertiesDialog", "QuickGridLinesDialog", "QuickGuideDialog", "ReplicateDialog", diff --git a/src/otko/views/dialogs/add_node.py b/src/otko/views/dialogs/add_node.py index 1c5ddc5..ff13e53 100644 --- a/src/otko/views/dialogs/add_node.py +++ b/src/otko/views/dialogs/add_node.py @@ -22,6 +22,7 @@ from PySide6.QtWidgets import ( from otko.core import Node from otko.core.geometry import GridSystem +from otko.core.units import UnitSystem, native_label def _snap(value: float, lines: list[float]) -> float: @@ -48,6 +49,7 @@ class AddNodeDialog(QDialog): parent: QWidget | None = None, *, plane_elevation: float | None = None, + units: UnitSystem = UnitSystem.METRIC, ) -> None: super().__init__(parent) self.setWindowTitle("Add Node") @@ -55,6 +57,7 @@ class AddNodeDialog(QDialog): self._grid = grid self._ndm = ndm self._plane_elevation = plane_elevation + self._units = units self._build_ui() def _build_ui(self) -> None: @@ -65,19 +68,22 @@ class AddNodeDialog(QDialog): self._x = self._spin() self._y = self._spin() self._z = self._spin() + try: + length_u = native_label(self._units, "length") + except KeyError: + length_u = "" if self._plane_elevation is not None and self._ndm == 3: self._z.setValue(float(self._plane_elevation)) - form.addRow("X:", self._x) - form.addRow("Y:", self._y) + form.addRow(f"X ({length_u}):" if length_u else "X:", self._x) + form.addRow(f"Y ({length_u}):" if length_u else "Y:", self._y) if self._ndm == 3: - form.addRow("Z:", self._z) + form.addRow(f"Z ({length_u}):" if length_u else "Z:", self._z) else: self._z.setVisible(False) if self._plane_elevation is not None and self._ndm == 3: form.addRow( QLabel( - f"Suggested Z = active working-plane level " - f"({self._plane_elevation:g}).", + f"Suggested Z = active working-plane level ({self._plane_elevation:g}).", ) ) root.addLayout(form) diff --git a/src/otko/views/dialogs/assign_equal_dof.py b/src/otko/views/dialogs/assign_equal_dof.py index 9ebf0ec..38f5094 100644 --- a/src/otko/views/dialogs/assign_equal_dof.py +++ b/src/otko/views/dialogs/assign_equal_dof.py @@ -42,12 +42,10 @@ class AssignEqualDOFDialog(QDialog): def _build_ui(self) -> None: layout = QVBoxLayout(self) - layout.addWidget( - QLabel( - "Tie selected nodes together in chosen DOFs using OpenSees " - "equalDOF.", - ) - ) + layout.addWidget(QLabel( + "Tie selected nodes together in chosen DOFs using OpenSees " + "equalDOF.", + )) form = QFormLayout() @@ -66,7 +64,7 @@ class AssignEqualDOFDialog(QDialog): dof_form = QFormLayout() self._dof_boxes: list[QCheckBox] = [] labels = ("Ux", "Uy", "Uz", "Rx", "Ry", "Rz") - active_labels = labels[:2] + (labels[5:6] if self._ndf == 3 else labels[2 : self._ndf]) + active_labels = labels[:2] + (labels[5:6] if self._ndf == 3 else labels[2:self._ndf]) if self._ndf == 2: active_labels = labels[:2] elif self._ndf == 3: @@ -81,10 +79,7 @@ class AssignEqualDOFDialog(QDialog): if self._suggested_dofs is not None and dof_id in self._suggested_dofs: cb.setChecked(True) self._dof_boxes.append(cb) - dof_form.addRow( - f"DOF {i if self._ndf != 3 else (1 if label=='Ux' else 2 if label=='Uy' else 3)}:", - cb, - ) + dof_form.addRow(f"DOF {i if self._ndf != 3 else (1 if label=='Ux' else 2 if label=='Uy' else 3)}:", cb) layout.addLayout(dof_form) buttons = QDialogButtonBox( diff --git a/src/otko/views/dialogs/assign_hinge.py b/src/otko/views/dialogs/assign_hinge.py index 7e5f4e3..0c98753 100644 --- a/src/otko/views/dialogs/assign_hinge.py +++ b/src/otko/views/dialogs/assign_hinge.py @@ -24,16 +24,14 @@ from PySide6.QtWidgets import ( ) from otko.core import Project -from otko.core.units import UnitSystem +from otko.core.units import UnitSystem, native_label # Nominal elastic-interior defaults per unit system (stiffness scale only; # geometric A/Iz/Iy/J defaults are nominal either way). Stored project # values always win when the selection provides them. _UNIT_DEFAULTS: dict[str, dict[str, object]] = { - UnitSystem.SI_M_N.value: {"E": 200e9, "G": 80e9, "stress": "Pa"}, - UnitSystem.SI_MM_N.value: {"E": 200e3, "G": 80e3, "stress": "MPa"}, - UnitSystem.US_IN_KIP.value: {"E": 29000.0, "G": 11200.0, "stress": "ksi"}, - UnitSystem.US_FT_KIP.value: {"E": 29000.0 * 144.0, "G": 11200.0 * 144.0, "stress": "ksf"}, + UnitSystem.METRIC.value: {"E": 200e9, "G": 80e9, "stress": "Pa"}, + UnitSystem.IMPERIAL.value: {"E": 29000.0, "G": 11200.0, "stress": "ksi"}, } _FALLBACK_DEFAULTS: dict[str, object] = {"E": 200e9, "G": 80e9, "stress": "Pa"} @@ -77,6 +75,24 @@ class AssignHingeDialog(QDialog): g_default = float(units["G"]) # type: ignore[arg-type] stress = str(units["stress"]) + # Unit hints only — values are entered in native stored units. + try: + length_u = native_label(self._project.meta.units, "length") + except KeyError: + length_u = "" + try: + stress_u = native_label(self._project.meta.units, "stress") + except KeyError: + stress_u = stress + try: + area_u = native_label(self._project.meta.units, "area") + except KeyError: + area_u = "" + try: + inertia_u = native_label(self._project.meta.units, "inertia") + except KeyError: + inertia_u = "" + form = QFormLayout() # Hinge section pickers self._sec_i = QComboBox() @@ -97,29 +113,35 @@ class AssignHingeDialog(QDialog): self._lp_i.setDecimals(4) self._lp_i.setValue(float(prefill.get("lp_i", 0.1)) if prefill else 0.1) self._lp_i.setSingleStep(0.01) - form.addRow("Lp_i (hinge length i):", self._lp_i) + form.addRow( + f"Lp_i (hinge length i) ({length_u}):" if length_u else "Lp_i (hinge length i):", + self._lp_i, + ) self._lp_j = QDoubleSpinBox() self._lp_j.setRange(1e-6, 100.0) self._lp_j.setDecimals(4) self._lp_j.setValue(float(prefill.get("lp_j", 0.1)) if prefill else 0.1) self._lp_j.setSingleStep(0.01) - form.addRow("Lp_j (hinge length j):", self._lp_j) + form.addRow( + f"Lp_j (hinge length j) ({length_u}):" if length_u else "Lp_j (hinge length j):", + self._lp_j, + ) # Elastic interior properties (defaults follow project.meta.units). - form.addRow(QLabel(f"Elastic interior ({stress} / project length units):")) + form.addRow(QLabel(f"Elastic interior ({stress_u} / {length_u}):")) self._E = self._dspin(float(prefill.get("E", e_default)) if prefill else e_default, 1e6) self._A = self._dspin(float(prefill.get("A", 0.01)) if prefill else 0.01, 0.001) self._Iz = self._dspin(float(prefill.get("Iz", 1e-4)) if prefill else 1e-4, 1e-6) self._Iy = self._dspin(float(prefill.get("Iy", 1e-4)) if prefill else 1e-4, 1e-6) self._G = self._dspin(float(prefill.get("G", g_default)) if prefill else g_default, 1e6) self._J = self._dspin(float(prefill.get("J", 1e-5)) if prefill else 1e-5, 1e-7) - form.addRow(f"E ({stress}):", self._E) - form.addRow("A:", self._A) - form.addRow("Iz:", self._Iz) - form.addRow("Iy:", self._Iy) - form.addRow(f"G ({stress}):", self._G) - form.addRow("J:", self._J) + form.addRow(f"E ({stress_u}):", self._E) + form.addRow(f"A ({area_u}):" if area_u else "A:", self._A) + form.addRow(f"Iz ({inertia_u}):" if inertia_u else "Iz:", self._Iz) + form.addRow(f"Iy ({inertia_u}):" if inertia_u else "Iy:", self._Iy) + form.addRow(f"G ({stress_u}):", self._G) + form.addRow(f"J ({inertia_u}):" if inertia_u else "J:", self._J) layout.addLayout(form) diff --git a/src/otko/views/dialogs/assign_load.py b/src/otko/views/dialogs/assign_load.py index 534ba69..7b8e7f9 100644 --- a/src/otko/views/dialogs/assign_load.py +++ b/src/otko/views/dialogs/assign_load.py @@ -8,12 +8,15 @@ from PySide6.QtWidgets import ( QDialogButtonBox, QDoubleSpinBox, QFormLayout, + QInputDialog, QLabel, QLineEdit, QVBoxLayout, QWidget, ) +from otko.core.units import UnitSystem, native_label + # Sentinel the pattern combo's userData holds to mean "create a new one". _NEW_PATTERN_SENTINEL = "__new__" @@ -37,6 +40,7 @@ class AssignLoadDialog(QDialog): current_forces: tuple[float, float, float, float, float, float] | None = None, active_pattern_id: int | None = None, mixed: bool = False, + units: UnitSystem = UnitSystem.METRIC, ) -> None: super().__init__(parent) self.setWindowTitle("Assign Nodal Load") @@ -44,6 +48,7 @@ class AssignLoadDialog(QDialog): self._current_forces = current_forces self._active_pattern_id = active_pattern_id self._mixed = mixed + self._units = units self._build_ui(n_selected) def _build_ui(self, n_selected: int) -> None: @@ -95,6 +100,15 @@ class AssignLoadDialog(QDialog): self._on_pattern_changed(self._pattern_cb.currentIndex()) # ── Force vector ── + # Unit hints only — values are entered in native stored units. + try: + force_u = native_label(self._units, "force") + except KeyError: + force_u = "" + try: + moment_u = native_label(self._units, "moment") + except KeyError: + moment_u = "" form = QFormLayout() self._spinboxes: dict[str, QDoubleSpinBox] = {} prefill = self._current_forces or (0.0, 0.0, 0.0, 0.0, 0.0, 0.0) @@ -105,7 +119,9 @@ class AssignLoadDialog(QDialog): sb.setSingleStep(1.0) sb.setValue(float(val)) self._spinboxes[label] = sb - form.addRow(f"{label}:", sb) + unit = force_u if label.startswith("F") else moment_u + row_label = f"{label} ({unit}):" if unit else f"{label}:" + form.addRow(row_label, sb) layout.addLayout(form) self._refresh_active_label() diff --git a/src/otko/views/dialogs/assign_masses.py b/src/otko/views/dialogs/assign_masses.py index f87a710..f21a863 100644 --- a/src/otko/views/dialogs/assign_masses.py +++ b/src/otko/views/dialogs/assign_masses.py @@ -8,8 +8,6 @@ it maps cleanly onto our existing :class:`SetMassCommand`. from __future__ import annotations -import contextlib - from PySide6.QtWidgets import ( QCheckBox, QDialog, @@ -21,6 +19,8 @@ from PySide6.QtWidgets import ( QWidget, ) +from otko.core.units import UnitSystem, native_label + class AssignMassesDialog(QDialog): """Modal dialog: enter translational + rotational mass components. @@ -29,6 +29,8 @@ class AssignMassesDialog(QDialog): current: ``(Mx, My, Mz, Mxx, Myy, Mzz)`` values already on the selection, if known — prefilled instead of zeros. mixed: True when the selection carries differing masses. + units: Project's native unit system — label hints only; values + are entered in native stored units, never converted. """ def __init__( @@ -39,12 +41,14 @@ class AssignMassesDialog(QDialog): *, current: tuple[float, float, float, float, float, float] | None = None, mixed: bool = False, + units: UnitSystem = UnitSystem.METRIC, ) -> None: super().__init__(parent) self.setWindowTitle("Assign Masses") self._ndf = ndf self._current = current self._mixed = mixed + self._units = units self._build_ui(n_selected) def _build_ui(self, n_selected: int) -> None: @@ -56,24 +60,43 @@ class AssignMassesDialog(QDialog): header += " Fields prefilled with the selection's current values." root.addWidget(QLabel(header)) + # Unit hints only — values are entered in native stored units. + try: + mass_u = native_label(self._units, "mass") + except KeyError: + mass_u = "" + try: + rot_u = f"{mass_u}·{native_label(self._units, 'length')}²" if mass_u else "" + except KeyError: + rot_u = "" + cur = self._current or (0.0, 0.0, 0.0, 0.0, 0.0, 0.0) form = QFormLayout() self._mx = self._spin(cur[0]) - form.addRow("Translation X:", self._mx) + form.addRow(f"Translation X ({mass_u}):" if mass_u else "Translation X:", self._mx) self._my = self._spin(cur[1]) - form.addRow("Translation Y:", self._my) + form.addRow(f"Translation Y ({mass_u}):" if mass_u else "Translation Y:", self._my) if self._ndf >= 3: self._mz = self._spin(cur[2]) - form.addRow("Translation Z:", self._mz) + form.addRow(f"Translation Z ({mass_u}):" if mass_u else "Translation Z:", self._mz) else: self._mz = self._spin(cur[2]) if self._ndf == 6: self._mxx = self._spin(cur[3]) - form.addRow("Rotation X (Ixx):", self._mxx) + form.addRow( + f"Rotation X [Ixx] ({rot_u}):" if rot_u else "Rotation X [Ixx]:", + self._mxx, + ) self._myy = self._spin(cur[4]) - form.addRow("Rotation Y (Iyy):", self._myy) + form.addRow( + f"Rotation Y [Iyy] ({rot_u}):" if rot_u else "Rotation Y [Iyy]:", + self._myy, + ) self._mzz = self._spin(cur[5]) - form.addRow("Rotation Z (Izz):", self._mzz) + form.addRow( + f"Rotation Z [Izz] ({rot_u}):" if rot_u else "Rotation Z [Izz]:", + self._mzz, + ) else: self._mxx = self._spin(cur[3]) self._myy = self._spin(cur[4]) @@ -105,8 +128,10 @@ class AssignMassesDialog(QDialog): self._my.setValue(self._mx.value()) self._mx.valueChanged.connect(self._my.setValue) else: - with contextlib.suppress(RuntimeError, TypeError): + try: self._mx.valueChanged.disconnect(self._my.setValue) + except (RuntimeError, TypeError): + pass def mass_vector(self) -> tuple[float, float, float, float, float, float]: """Return the 6-tuple (Mx, My, Mz, Mxx, Myy, Mzz).""" diff --git a/src/otko/views/dialogs/assign_property.py b/src/otko/views/dialogs/assign_property.py index 5d62f4a..6127e99 100644 --- a/src/otko/views/dialogs/assign_property.py +++ b/src/otko/views/dialogs/assign_property.py @@ -41,9 +41,9 @@ class AssignSectionDialog(QDialog): layout.addWidget(QLabel(self._current_note())) if not self._sections: - layout.addWidget( - QLabel("No sections defined. Open Define → Section Library first.") - ) + layout.addWidget(QLabel( + "No sections defined. Open Define → Section Library first." + )) self._combo = QComboBox() for s in self._sections: @@ -115,15 +115,15 @@ class AssignMaterialDialog(QDialog): def _build_ui(self, n_elements: int) -> None: layout = QVBoxLayout(self) - layout.addWidget( - QLabel(f"Apply material to {n_elements} selected truss/zero-length element(s).") - ) + layout.addWidget(QLabel( + f"Apply material to {n_elements} selected truss/zero-length element(s)." + )) layout.addWidget(QLabel(self._current_note())) if not self._materials: - layout.addWidget( - QLabel("No materials defined. Open Define → Material Library first.") - ) + layout.addWidget(QLabel( + "No materials defined. Open Define → Material Library first." + )) self._combo = QComboBox() for m in self._materials: diff --git a/src/otko/views/dialogs/assign_support.py b/src/otko/views/dialogs/assign_support.py index 8b3a5bc..61f4872 100644 --- a/src/otko/views/dialogs/assign_support.py +++ b/src/otko/views/dialogs/assign_support.py @@ -8,6 +8,7 @@ model's actual ``ndf`` at translation time. from __future__ import annotations +from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QButtonGroup, QCheckBox, @@ -21,12 +22,13 @@ from PySide6.QtWidgets import ( QWidget, ) + # Preset → 6-tuple (Ux, Uy, Uz, Rx, Ry, Rz) PRESETS: dict[str, tuple[bool, bool, bool, bool, bool, bool]] = { - "Free": (False, False, False, False, False, False), - "Roller (Z)": (False, False, True, False, False, False), - "Pin": (True, True, True, False, False, False), - "Fix": (True, True, True, True, True, True), + "Free": (False, False, False, False, False, False), + "Roller (Z)": (False, False, True, False, False, False), + "Pin": (True, True, True, False, False, False), + "Fix": (True, True, True, True, True, True), } diff --git a/src/otko/views/dialogs/assign_zls.py b/src/otko/views/dialogs/assign_zls.py index b2c580c..c1d23f0 100644 --- a/src/otko/views/dialogs/assign_zls.py +++ b/src/otko/views/dialogs/assign_zls.py @@ -25,9 +25,8 @@ from otko.core import Project class AssignZeroLengthSectionDialog(QDialog): """Modal dialog: pick a section, confirm creation between 2 nodes.""" - def __init__( - self, project: Project, node_ids: tuple[int, int], parent: QWidget | None = None - ) -> None: + def __init__(self, project: Project, node_ids: tuple[int, int], + parent: QWidget | None = None) -> None: super().__init__(parent) self.setWindowTitle("Assign Zero-Length Section") self._project = project @@ -36,14 +35,12 @@ class AssignZeroLengthSectionDialog(QDialog): def _build_ui(self) -> None: root = QVBoxLayout(self) - root.addWidget( - QLabel( - f"Connect node {self._node_ids[0]} and " - f"{self._node_ids[1]} through a zero-length " - "section element. The two nodes must share the same " - "coordinates." - ) - ) + root.addWidget(QLabel( + f"Connect node {self._node_ids[0]} and " + f"{self._node_ids[1]} through a zero-length " + "section element. The two nodes must share the same " + "coordinates." + )) form = QFormLayout() self._section_cb = QComboBox() @@ -56,7 +53,8 @@ class AssignZeroLengthSectionDialog(QDialog): root.addLayout(form) buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, + QDialogButtonBox.StandardButton.Ok + | QDialogButtonBox.StandardButton.Cancel, ) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) diff --git a/src/otko/views/dialogs/case_forms.py b/src/otko/views/dialogs/case_forms.py index d608992..66080a8 100644 --- a/src/otko/views/dialogs/case_forms.py +++ b/src/otko/views/dialogs/case_forms.py @@ -21,6 +21,7 @@ from PySide6.QtWidgets import ( QListWidget, QListWidgetItem, QSpinBox, + QVBoxLayout, QWidget, ) @@ -33,6 +34,15 @@ from otko.core import ( StaticCase, TransientCase, ) +from otko.core.units import UnitSystem, native_label + + +def _u(units: UnitSystem, quantity: str) -> str: + """Display label for ``quantity`` ("" when dimensionless/unknown).""" + try: + return native_label(units, quantity) + except KeyError: + return "" # ─────────────────────────── helpers ─────────────────────────── @@ -204,11 +214,14 @@ class CaseFormBase(QWidget): patterns: list[LoadPattern], analyses: list[AnalysisCase], parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, ) -> None: super().__init__(parent) self._case_id: int | None = None self._patterns = patterns self._analyses = analyses + self._units = units self._layout = QFormLayout(self) self._name_edit = QLineEdit() self._layout.addRow("Name:", self._name_edit) @@ -253,8 +266,10 @@ class StaticCaseForm(CaseFormBase): patterns: list[LoadPattern], analyses: list[AnalysisCase], parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, ) -> None: - super().__init__(patterns, analyses, parent) + super().__init__(patterns, analyses, parent, units=units) self._patterns_picker, self._pattern_factor_spins = _make_pattern_picker_with_factors( patterns ) @@ -327,8 +342,10 @@ class ModalCaseForm(CaseFormBase): patterns: list[LoadPattern], analyses: list[AnalysisCase], parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, ) -> None: - super().__init__(patterns, analyses, parent) + super().__init__(patterns, analyses, parent, units=units) self._n_modes = _int_spin(3) self._solver = QComboBox() self._solver.addItems(["genBandArpack", "fullGenLapack", "symmBandLapack"]) @@ -363,8 +380,10 @@ class TransientCaseForm(CaseFormBase): patterns: list[LoadPattern], analyses: list[AnalysisCase], parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, ) -> None: - super().__init__(patterns, analyses, parent) + super().__init__(patterns, analyses, parent, units=units) self._patterns_picker, self._pattern_factor_spins = _make_pattern_picker_with_factors( patterns ) @@ -403,7 +422,8 @@ class TransientCaseForm(CaseFormBase): ) self._layout.addRow(QLabel("Patterns to remove after preload (optional):")) self._layout.addRow(self._remove_patterns_picker) - self._layout.addRow("dt:", self._dt) + time_u = _u(self._units, "time") + self._layout.addRow(f"dt ({time_u}):" if time_u else "dt:", self._dt) self._layout.addRow("Number of steps:", self._n_steps) self._layout.addRow("System:", self._system) self._layout.addRow("Constraints:", self._constraints) @@ -477,8 +497,10 @@ class PushoverCaseForm(CaseFormBase): patterns: list[LoadPattern], analyses: list[AnalysisCase], parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, ) -> None: - super().__init__(patterns, analyses, parent) + super().__init__(patterns, analyses, parent, units=units) self._patterns_picker, self._pattern_factor_spins = _make_pattern_picker_with_factors( patterns ) @@ -515,8 +537,12 @@ class PushoverCaseForm(CaseFormBase): ) self._layout.addRow("Control node:", self._control_node) self._layout.addRow("Control DOF:", self._control_dof) - self._layout.addRow("Target displacement:", self._target) - self._layout.addRow("Step size:", self._step) + disp_u = _u(self._units, "displacement") + self._layout.addRow( + f"Target displacement ({disp_u}):" if disp_u else "Target displacement:", + self._target, + ) + self._layout.addRow(f"Step size ({disp_u}):" if disp_u else "Step size:", self._step) self._layout.addRow("Base nodes:", self._base_nodes) self._layout.addRow("System:", self._system) self._layout.addRow("Constraints:", self._constraints) @@ -572,8 +598,10 @@ class ResponseSpectrumCaseForm(CaseFormBase): patterns: list[LoadPattern], analyses: list[AnalysisCase], parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, ) -> None: - super().__init__(patterns, analyses, parent) + super().__init__(patterns, analyses, parent, units=units) # Patterns aren't used by RS case but base class wants the param. self._modal_case = _int_spin(1, minimum=1) self._spectrum_id = _int_spin(1, minimum=1) @@ -629,8 +657,10 @@ def form_for( case: AnalysisCase, patterns: list[LoadPattern], analyses: list[AnalysisCase], + *, + units: UnitSystem = UnitSystem.METRIC, ) -> CaseFormBase: cls = FORM_REGISTRY[case.type] - form = cls(patterns, analyses) + form = cls(patterns, analyses, units=units) form.populate(case) return form diff --git a/src/otko/views/dialogs/case_manager.py b/src/otko/views/dialogs/case_manager.py index 3ba5f29..fa03b7a 100644 --- a/src/otko/views/dialogs/case_manager.py +++ b/src/otko/views/dialogs/case_manager.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from pydantic import ValidationError from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QDialog, @@ -20,6 +19,7 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) +from pydantic import ValidationError from otko.commands import ( AddAnalysisCasesCommand, @@ -33,8 +33,9 @@ from otko.core import ( StaticCase, TransientCase, ) +from otko.core.units import UnitSystem from otko.viewmodels import ProjectViewModel -from otko.views.dialogs.case_forms import form_for +from otko.views.dialogs.case_forms import FORM_REGISTRY, form_for if TYPE_CHECKING: from otko.core import Project @@ -84,7 +85,7 @@ def _default_case(kind: str, cid: int, project: Project): # type: ignore[no-unt raise ValueError("Add a Modal case before adding a ResponseSpectrum case.") if not project.spectra: raise ValueError( - "Define at least one response spectrum before adding " "a ResponseSpectrum case." + "Define at least one response spectrum before adding a ResponseSpectrum case." ) return ResponseSpectrumCase( id=cid, @@ -183,6 +184,11 @@ class AnalysisCaseManagerDialog(QDialog): cid = item.data(Qt.ItemDataRole.UserRole) return next((c for c in self._vm.project.analyses if c.id == cid), None) + def _project_units(self) -> UnitSystem: + """Project display units (Metric default when no project is open).""" + proj = self._vm.project + return proj.meta.units if proj is not None else UnitSystem.METRIC + def _on_row_changed(self, _row: int) -> None: case = self._selected_case() if case is None or self._vm.project is None: @@ -191,7 +197,12 @@ class AnalysisCaseManagerDialog(QDialog): old = self._stack.widget(0) self._stack.removeWidget(old) old.deleteLater() - form = form_for(case, self._vm.project.load_patterns, self._vm.project.analyses) + form = form_for( + case, + self._vm.project.load_patterns, + self._vm.project.analyses, + units=self._project_units(), + ) self._stack.addWidget(form) self._stack.setCurrentWidget(form) self._type_label.setText(form.type_label) diff --git a/src/otko/views/dialogs/combination_dialog.py b/src/otko/views/dialogs/combination_dialog.py deleted file mode 100644 index 882736d..0000000 --- a/src/otko/views/dialogs/combination_dialog.py +++ /dev/null @@ -1,258 +0,0 @@ -"""Load Combination manager — library-style CRUD over case-result combos. - -A combination names a weighted set of *completed* static-case results -(e.g. ``1.2×Dead + 1.6×Live``). Evaluation is post-processing -superposition in :mod:`otko.services.combinations` — the dialog only -edits the definition; running happens from File → Run → Combinations. -""" - -from __future__ import annotations - -from pydantic import ValidationError -from PySide6.QtCore import Qt -from PySide6.QtWidgets import ( - QComboBox, - QDialog, - QDialogButtonBox, - QDoubleSpinBox, - QFormLayout, - QHBoxLayout, - QLabel, - QLineEdit, - QListWidget, - QListWidgetItem, - QMessageBox, - QPushButton, - QVBoxLayout, - QWidget, -) - -from otko.commands import ( - AddCombinationsCommand, - DeleteCombinationsCommand, - UpdateCombinationCommand, -) -from otko.core import LoadCombination, LoadCombinationItem -from otko.viewmodels import ProjectViewModel - - -def _spin(default: float = 1.0) -> QDoubleSpinBox: - """Factor editor: wide range, 3 decimals, auto-selects its row on edit.""" - sb = QDoubleSpinBox() - sb.setRange(-1e9, 1e9) - sb.setDecimals(3) - sb.setSingleStep(0.1) - sb.setValue(default) - sb.setMaximumWidth(100) - sb.setToolTip("Scale applied to this case's results (1.0 = full).") - return sb - - -class CombinationForm(QWidget): - """Edit one combination: name, kind, and per-case factors.""" - - def __init__(self, vm: ProjectViewModel, parent: QWidget | None = None) -> None: - super().__init__(parent) - self._vm = vm - self._combo_id: int | None = None - self._spins: dict[int, QDoubleSpinBox] = {} - layout = QFormLayout(self) - self._name_edit = QLineEdit() - layout.addRow("Name:", self._name_edit) - self._kind = QComboBox() - self._kind.addItems(["Linear", "Envelope"]) - self._kind.setToolTip( - "Linear = weighted sum Σ factor×results. " - "Envelope = per-DOF max/min across factored terms." - ) - layout.addRow("Kind:", self._kind) - layout.addRow(QLabel("Cases (tick to include; factor beside each):")) - self._cases = QListWidget() - self._cases.setMaximumHeight(220) - layout.addRow(self._cases) - layout.addRow( - QLabel( - "Only linear-static cases combine meaningfully. " - "Transient / modal / pushover results are rejected at evaluation." - ) - ) - - # ── populate / read ────────────────────────────────────────── - def populate(self, combo: LoadCombination) -> None: - """Fill the form from ``combo`` (case list rebuilt from the project).""" - self._combo_id = combo.id - self._name_edit.setText(combo.name) - self._kind.setCurrentText(combo.kind) - self._rebuild_case_list(combo) - - def _rebuild_case_list(self, combo: LoadCombination | None = None) -> None: - factors = {i.case_id: i.factor for i in combo.items} if combo else {} - self._cases.clear() - self._spins.clear() - project = self._vm.project - if project is None: - return - for case in project.analyses: - item = QListWidgetItem() - item.setData(Qt.ItemDataRole.UserRole, case.id) - self._cases.addItem(item) - cell = QWidget() - row = QHBoxLayout(cell) - row.setContentsMargins(2, 0, 2, 0) - row.addWidget(QLabel(f"#{case.id} {case.name or '(unnamed)'} [{case.type}]"), 1) - spin = _spin(float(factors.get(case.id, 1.0))) - spin.valueChanged.connect(lambda _v, _it=item: _it.setSelected(True)) - row.addWidget(spin) - self._cases.setItemWidget(item, cell) - self._spins[case.id] = spin - if case.id in factors: - item.setSelected(True) - - def read(self) -> LoadCombination: - """Build a validated combination from the form (raises on bad input).""" - if self._combo_id is None: - raise ValueError("No combination selected.") - selected = [i.data(Qt.ItemDataRole.UserRole) for i in self._cases.selectedItems()] - if not selected: - raise ValueError("Select at least one analysis case for this combination.") - items = [ - LoadCombinationItem(case_id=int(cid), factor=float(self._spins[int(cid)].value())) - for cid in selected - ] - return LoadCombination( - id=self._combo_id, - name=self._name_edit.text(), - kind=self._kind.currentText(), # type: ignore[arg-type] - items=items, - ) - - -class LoadCombinationManagerDialog(QDialog): - """CRUD dialog for load combinations.""" - - def __init__(self, vm: ProjectViewModel, parent: QWidget | None = None) -> None: - super().__init__(parent) - self.setWindowTitle("Load Combinations") - self.resize(720, 520) - self._vm = vm - self._build_ui() - self._refresh_list() - self._vm.modelMutated.connect(self._refresh_list) - - def _build_ui(self) -> None: - outer = QVBoxLayout(self) - outer.addWidget( - QLabel( - "Load Combinations — weighted sets of completed static-case " - "results (e.g. 1.2×Dead + 1.6×Live). Evaluate from File → Run." - ) - ) - body = QHBoxLayout() - outer.addLayout(body, stretch=1) - - left = QVBoxLayout() - self._list = QListWidget() - self._list.currentRowChanged.connect(self._on_row_changed) - left.addWidget(self._list, stretch=1) - btn_row = QHBoxLayout() - self._add_btn = QPushButton("Add…") - self._delete_btn = QPushButton("Delete") - self._add_btn.clicked.connect(self._on_add) - self._delete_btn.clicked.connect(self._on_delete) - btn_row.addWidget(self._add_btn) - btn_row.addWidget(self._delete_btn) - left.addLayout(btn_row) - body.addLayout(left, stretch=1) - - right = QVBoxLayout() - self._form = CombinationForm(self._vm) - right.addWidget(self._form, stretch=1) - self._apply_btn = QPushButton("Apply changes") - self._apply_btn.clicked.connect(self._on_apply) - right.addWidget(self._apply_btn, alignment=Qt.AlignmentFlag.AlignRight) - body.addLayout(right, stretch=2) - - buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, parent=self) - buttons.rejected.connect(self.reject) - buttons.accepted.connect(self.accept) - outer.addWidget(buttons) - - # ── list ───────────────────────────────────────────────────── - def _refresh_list(self) -> None: - selected_id = None - if self._list.currentItem() is not None: - selected_id = self._list.currentItem().data(Qt.ItemDataRole.UserRole) - self._list.clear() - for c in self._vm.project.combinations if self._vm.project else []: - label = f"#{c.id} {c.name or '(unnamed)'} [{c.kind}]" - item = QListWidgetItem(label) - item.setData(Qt.ItemDataRole.UserRole, c.id) - self._list.addItem(item) - if selected_id is not None: - self._select_by_id(selected_id) - if self._list.currentRow() < 0 and self._list.count(): - self._list.setCurrentRow(0) - if not self._list.count(): - self._form._combo_id = None - - def _selected(self) -> LoadCombination | None: - item = self._list.currentItem() - if item is None or self._vm.project is None: - return None - cid = item.data(Qt.ItemDataRole.UserRole) - return next((c for c in self._vm.project.combinations if c.id == cid), None) - - def _on_row_changed(self, _row: int) -> None: - combo = self._selected() - if combo is None: - return - self._form.populate(combo) - - def _select_by_id(self, target_id: int) -> None: - for i in range(self._list.count()): - if self._list.item(i).data(Qt.ItemDataRole.UserRole) == target_id: - self._list.setCurrentRow(i) - return - - # ── mutations ──────────────────────────────────────────────── - def _on_apply(self) -> None: - try: - new_combo = self._form.read() - self._vm.apply_command(UpdateCombinationCommand(self._vm, new_combo)) - except (ValidationError, ValueError) as exc: - QMessageBox.critical(self, "Validation error", str(exc)) - - def _on_add(self) -> None: - if self._vm.project is None: - return - if not self._vm.project.analyses: - QMessageBox.information( - self, - "No cases", - "Define at least one analysis case first (Analyze → Cases…).", - ) - return - new_id = self._vm.project.next_combination_id() - first = self._vm.project.analyses[0] - try: - combo = LoadCombination( - id=new_id, - name="New", - items=[LoadCombinationItem(case_id=first.id)], - ) - except (ValidationError, ValueError) as exc: - QMessageBox.warning(self, "Could not create combination", str(exc)) - return - self._vm.apply_command(AddCombinationsCommand(self._vm, [combo])) - self._select_by_id(new_id) - - def _on_delete(self) -> None: - combo = self._selected() - if combo is None: - return - reply = QMessageBox.question( - self, "Delete combination", f"Delete load combination #{combo.id}?" - ) - if reply != QMessageBox.StandardButton.Yes: - return - self._vm.apply_command(DeleteCombinationsCommand(self._vm, {combo.id})) diff --git a/src/otko/views/dialogs/coord_grid_systems.py b/src/otko/views/dialogs/coord_grid_systems.py index 595464e..8e86b1a 100644 --- a/src/otko/views/dialogs/coord_grid_systems.py +++ b/src/otko/views/dialogs/coord_grid_systems.py @@ -36,6 +36,7 @@ from otko.core.geometry import ( CoordinateSystem, GridSystem, ) +from otko.core.units import UnitSystem, native_label from otko.views.dialogs.grid_system import ( _coords_from_spacings, _parse_spacings, @@ -58,6 +59,8 @@ class CoordSystemDataDialog(QDialog): existing: CoordinateGridSystem | None = None, is_global: bool = False, parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, ) -> None: super().__init__(parent) self.setWindowTitle( @@ -65,6 +68,7 @@ class CoordSystemDataDialog(QDialog): ) self._is_global = is_global self._existing = existing + self._units = units self._build_ui() if existing is not None: self._load_existing(existing) @@ -90,9 +94,13 @@ class CoordSystemDataDialog(QDialog): self._ox = self._spin() self._oy = self._spin() self._oz = self._spin() + try: + length_u = native_label(self._units, "length") + except KeyError: + length_u = "" origin_row = QHBoxLayout() for label, w in (("X", self._ox), ("Y", self._oy), ("Z", self._oz)): - origin_row.addWidget(QLabel(f"{label}:")) + origin_row.addWidget(QLabel(f"{label} ({length_u}):" if length_u else f"{label}:")) origin_row.addWidget(w) origin_wrap = QWidget() origin_wrap.setLayout(origin_row) @@ -113,7 +121,7 @@ class CoordSystemDataDialog(QDialog): for w in (self._ox, self._oy, self._oz, self._rx, self._ry, self._rz): w.setEnabled(False) loc_box.setToolTip( - "Global system is anchored at the world origin " "with identity orientation.", + "Global system is anchored at the world origin with identity orientation.", ) layout.addWidget(loc_box) @@ -135,9 +143,9 @@ class CoordSystemDataDialog(QDialog): self._x_edit = QLineEdit("5") self._y_edit = QLineEdit("5") self._z_edit = QLineEdit("") - grid_form.addRow("X spacings:", self._x_edit) - grid_form.addRow("Y spacings:", self._y_edit) - grid_form.addRow("Z spacings:", self._z_edit) + grid_form.addRow(f"X spacings ({length_u}):" if length_u else "X spacings:", self._x_edit) + grid_form.addRow(f"Y spacings ({length_u}):" if length_u else "Y spacings:", self._y_edit) + grid_form.addRow(f"Z spacings ({length_u}):" if length_u else "Z spacings:", self._z_edit) self._visible_cb = QCheckBox("Visible in 3D view") self._visible_cb.setChecked(True) diff --git a/src/otko/views/dialogs/define_grid_data.py b/src/otko/views/dialogs/define_grid_data.py index 7440b04..6ed4b1d 100644 --- a/src/otko/views/dialogs/define_grid_data.py +++ b/src/otko/views/dialogs/define_grid_data.py @@ -18,8 +18,6 @@ Top-level controls: from __future__ import annotations -from typing import ClassVar - from PySide6.QtCore import Qt from PySide6.QtGui import QColor from PySide6.QtWidgets import ( @@ -29,6 +27,7 @@ from PySide6.QtWidgets import ( QComboBox, QDialog, QDialogButtonBox, + QDoubleSpinBox, QFormLayout, QGroupBox, QHBoxLayout, @@ -52,6 +51,16 @@ from otko.core.geometry import ( GridLine, GridSystem, ) +from otko.core.units import UnitSystem, native_label + + +def _length_unit(units: UnitSystem) -> str: + """Display label for ordinate/spacing values ("" when unknown).""" + try: + return native_label(units, "length") + except KeyError: + return "" + _LINE_TYPE_CHOICES = ["Primary", "Secondary"] _BUBBLE_LOC_CHOICES = ["Start", "End"] @@ -60,27 +69,35 @@ _BUBBLE_LOC_CHOICES = ["Start", "End"] class _AxisGridTable(QWidget): """A single X/Y/Z spreadsheet — one row per GridLine.""" - COLUMNS: ClassVar[list[str]] = [ - "Grid ID", - "Ordinate", - "Line Type", - "Visibility", - "Bubble Loc", - "Color", - ] + COLUMNS = ["Grid ID", "Ordinate", "Line Type", "Visibility", "Bubble Loc", "Color"] - def __init__(self, axis: str, parent: QWidget | None = None) -> None: + def __init__( + self, + axis: str, + parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, + ) -> None: super().__init__(parent) self.axis = axis self._show_spacing = False + self._units = units self._build_ui() + def _columns(self) -> list[str]: + """Column headers with the length unit on Ordinate/Spacing.""" + cols = self.COLUMNS.copy() + length_u = _length_unit(self._units) + base = "Spacing" if self._show_spacing else "Ordinate" + cols[1] = f"{base} ({length_u})" if length_u else base + return cols + def _build_ui(self) -> None: root = QVBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) self._table = QTableWidget(0, len(self.COLUMNS)) - self._table.setHorizontalHeaderLabels(self.COLUMNS) + self._table.setHorizontalHeaderLabels(self._columns()) self._table.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeMode.Interactive, ) @@ -119,9 +136,7 @@ class _AxisGridTable(QWidget): # Read the current ordinates, then rewrite as spacings (or back). ords = self._collect_ordinates() self._show_spacing = as_spacing - header = self.COLUMNS.copy() - header[1] = "Spacing" if as_spacing else "Ordinate" - self._table.setHorizontalHeaderLabels(header) + self._table.setHorizontalHeaderLabels(self._columns()) for row, v in enumerate(self._iter_display_values(ords)): item = self._table.item(row, 1) if item is not None: @@ -264,7 +279,7 @@ class _AxisGridTable(QWidget): lines.sort(key=lambda ln: ln.ordinate) self._table.setRowCount(0) self._show_spacing = False # reset to ordinate view after sort - self._table.setHorizontalHeaderLabels(self.COLUMNS) + self._table.setHorizontalHeaderLabels(self._columns()) for ln in lines: self._append_row(ln) @@ -288,6 +303,8 @@ class DefineGridSystemDataDialog(QDialog): existing: CoordinateGridSystem | None = None, is_global: bool = False, parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, ) -> None: super().__init__(parent) self.setWindowTitle( @@ -295,6 +312,7 @@ class DefineGridSystemDataDialog(QDialog): ) self.resize(780, 620) self._is_global = is_global + self._units = units # Working copy: keep origin/rotation for round-tripping without # re-opening the Coord System Location form. self._origin: tuple[float, float, float] = ( @@ -352,9 +370,9 @@ class DefineGridSystemDataDialog(QDialog): # ── Tabs: X / Y / Z grid data spreadsheets ── self._tabs = QTabWidget() - self._tab_x = _AxisGridTable("X") - self._tab_y = _AxisGridTable("Y") - self._tab_z = _AxisGridTable("Z") + self._tab_x = _AxisGridTable("X", units=self._units) + self._tab_y = _AxisGridTable("Y", units=self._units) + self._tab_z = _AxisGridTable("Z", units=self._units) self._tabs.addTab(self._tab_x, "X Grid Data") self._tabs.addTab(self._tab_y, "Y Grid Data") self._tabs.addTab(self._tab_z, "Z Grid Data") @@ -396,8 +414,10 @@ class DefineGridSystemDataDialog(QDialog): def _update_origin_label(self) -> None: ox, oy, oz = self._origin rx, ry, rz = self._rotation_deg + length_u = _length_unit(self._units) + origin_txt = f"Origin ({length_u}): " if length_u else "Origin: " self._origin_label.setText( - f"Origin: ({ox:g}, {oy:g}, {oz:g}) " f"Rotation: ({rx:g}°, {ry:g}°, {rz:g}°)" + f"{origin_txt}({ox:g}, {oy:g}, {oz:g}) Rotation: ({rx:g}°, {ry:g}°, {rz:g}°)" ) def _load_existing(self, cs: CoordinateGridSystem) -> None: @@ -428,6 +448,7 @@ class DefineGridSystemDataDialog(QDialog): origin=self._origin, rotation_deg=self._rotation_deg, parent=self, + units=self._units, ) if dlg.exec() != QDialog.DialogCode.Accepted: return @@ -439,7 +460,7 @@ class DefineGridSystemDataDialog(QDialog): QuickGridLinesDialog, ) - dlg = QuickGridLinesDialog(parent=self) + dlg = QuickGridLinesDialog(parent=self, units=self._units) if dlg.exec() != QDialog.DialogCode.Accepted: return xs, ys, zs = dlg.ordinates() diff --git a/src/otko/views/dialogs/distributed_load.py b/src/otko/views/dialogs/distributed_load.py index 306a678..b5d3ef2 100644 --- a/src/otko/views/dialogs/distributed_load.py +++ b/src/otko/views/dialogs/distributed_load.py @@ -14,6 +14,8 @@ from PySide6.QtWidgets import ( QWidget, ) +from otko.core.units import UnitSystem, native_label + # Sentinel the pattern combo's userData holds to mean "create a new one". _NEW_PATTERN_SENTINEL = "__new__" @@ -49,6 +51,7 @@ class AssignDistributedLoadDialog(QDialog): current: tuple[float, float, float] | None = None, active_pattern: str | None = None, mixed: bool = False, + units: UnitSystem = UnitSystem.METRIC, ) -> None: super().__init__(parent) self.setWindowTitle("Assign Distributed Load") @@ -56,6 +59,7 @@ class AssignDistributedLoadDialog(QDialog): self._current = current self._active_pattern = active_pattern self._mixed = mixed + self._units = units self._build_ui(n_selected) def _build_ui(self, n_selected: int) -> None: @@ -96,6 +100,10 @@ class AssignDistributedLoadDialog(QDialog): # Show / hide the new-name field in line with the combo. self._on_pattern_changed(self._pattern_cb.currentIndex()) + try: + dist_u = native_label(self._units, "distributed_load") + except KeyError: + dist_u = "" form = QFormLayout() self._spinboxes: dict[str, QDoubleSpinBox] = {} prefill = {"wx": 0.0, "wy": 0.0, "wz": 0.0} @@ -111,9 +119,10 @@ class AssignDistributedLoadDialog(QDialog): sb.setDecimals(4) sb.setSingleStep(1.0) sb.setValue(float(prefill[label])) - sb.setToolTip(tip) + sb.setToolTip(f"{tip} [{dist_u}]" if dist_u else tip) self._spinboxes[label] = sb - form.addRow(f"{label}:", sb) + row_label = f"{label} ({dist_u}):" if dist_u else f"{label}:" + form.addRow(row_label, sb) layout.addLayout(form) layout.addWidget( diff --git a/src/otko/views/dialogs/grid_system.py b/src/otko/views/dialogs/grid_system.py index c1ac05c..3fb7ec1 100644 --- a/src/otko/views/dialogs/grid_system.py +++ b/src/otko/views/dialogs/grid_system.py @@ -17,14 +17,15 @@ from PySide6.QtWidgets import ( QDialog, QDialogButtonBox, QFormLayout, - QLabel, QLineEdit, + QLabel, QVBoxLayout, QWidget, ) from otko.core import Node from otko.core.geometry import GridSystem +from otko.core.units import UnitSystem, native_label def _parse_spacings(text: str) -> list[float]: @@ -64,12 +65,18 @@ class GridSystemDialog(QDialog): """Dialog for entering X/Y/Z grid-line coordinates.""" def __init__( - self, next_node_id: int, existing: GridSystem | None = None, parent: QWidget | None = None + self, + next_node_id: int, + existing: GridSystem | None = None, + parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, ) -> None: super().__init__(parent) self.setWindowTitle("Grid System") self._next_id = next_node_id self._existing = existing + self._units = units self._build_ui() if existing is not None: self._load_existing(existing) @@ -95,9 +102,16 @@ class GridSystemDialog(QDialog): self._x_edit = QLineEdit("5") self._y_edit = QLineEdit("5") self._z_edit = QLineEdit("5") - form.addRow("X spacings:", self._x_edit) - form.addRow("Y spacings:", self._y_edit) - form.addRow("Z spacings:", self._z_edit) + try: + length_u = native_label(self._units, "length") + except KeyError: + length_u = "" + form.addRow(f"X spacings ({length_u}):" if length_u else "X spacings:", self._x_edit) + form.addRow(f"Y spacings ({length_u}):" if length_u else "Y spacings:", self._y_edit) + form.addRow(f"Z spacings ({length_u}):" if length_u else "Z spacings:", self._z_edit) + for edit in (self._x_edit, self._y_edit, self._z_edit): + if length_u: + edit.setPlaceholderText(f"spacings in {length_u}") layout.addLayout(form) self._visible_cb = QCheckBox("Show grid in 3D view") @@ -152,7 +166,7 @@ class GridSystemDialog(QDialog): else: extra = "
Nodes will NOT be created automatically." self._preview.setText( - f"Grid: {len(xs)} × {len(ys)} × {len(zs)} lines." f"{extra}" + f"Grid: {len(xs)} × {len(ys)} × {len(zs)} lines.{extra}" ) except (ValueError, IndexError) as exc: self._preview.setText(f"Parse error: {exc}") diff --git a/src/otko/views/dialogs/locate_origin.py b/src/otko/views/dialogs/locate_origin.py index 28a8205..2729292 100644 --- a/src/otko/views/dialogs/locate_origin.py +++ b/src/otko/views/dialogs/locate_origin.py @@ -13,11 +13,14 @@ from PySide6.QtWidgets import ( QDoubleSpinBox, QFormLayout, QGroupBox, + QHBoxLayout, QLabel, QVBoxLayout, QWidget, ) +from otko.core.units import UnitSystem, native_label + class CoordSystemLocationOrientationDialog(QDialog): """Just origin + XYZ Euler rotation in degrees.""" @@ -27,9 +30,12 @@ class CoordSystemLocationOrientationDialog(QDialog): origin: tuple[float, float, float] = (0.0, 0.0, 0.0), rotation_deg: tuple[float, float, float] = (0.0, 0.0, 0.0), parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, ) -> None: super().__init__(parent) self.setWindowTitle("Coord System Location and Orientation") + self._units = units self._build_ui() self._set_values(origin, rotation_deg) @@ -42,14 +48,18 @@ class CoordSystemLocationOrientationDialog(QDialog): ) ) - origin_box = QGroupBox("Origin (world units)") + try: + length_u = native_label(self._units, "length") + except KeyError: + length_u = "" + origin_box = QGroupBox(f"Origin ({length_u})" if length_u else "Origin (world units)") of = QFormLayout(origin_box) self._ox = self._spin() - of.addRow("X:", self._ox) + of.addRow(f"X ({length_u}):" if length_u else "X:", self._ox) self._oy = self._spin() - of.addRow("Y:", self._oy) + of.addRow(f"Y ({length_u}):" if length_u else "Y:", self._oy) self._oz = self._spin() - of.addRow("Z:", self._oz) + of.addRow(f"Z ({length_u}):" if length_u else "Z:", self._oz) root.addWidget(origin_box) rot_box = QGroupBox("Rotation about axes (degrees)") diff --git a/src/otko/views/dialogs/material_forms.py b/src/otko/views/dialogs/material_forms.py index e151f59..1afb5f3 100644 --- a/src/otko/views/dialogs/material_forms.py +++ b/src/otko/views/dialogs/material_forms.py @@ -30,6 +30,15 @@ from otko.core import ( Steel01, Steel02, ) +from otko.core.units import UnitSystem, native_label + + +def _u(units: UnitSystem, quantity: str) -> str: + """Display label for ``quantity`` ("" when dimensionless/unknown).""" + try: + return native_label(units, quantity) + except KeyError: + return "" # ─────────────────────────── helpers ─────────────────────────── @@ -67,8 +76,11 @@ class MaterialFormBase(QWidget): type_label: str = "Material" - def __init__(self, parent: QWidget | None = None) -> None: + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: super().__init__(parent) + self._units = units self._material_id: int | None = None self._layout = QFormLayout(self) self._name_edit = QLineEdit() @@ -97,16 +109,21 @@ class MaterialFormBase(QWidget): class Steel01Form(MaterialFormBase): type_label = "Steel01 — bilinear with kinematic hardening" - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: + super().__init__(parent, units=units) # Unit-agnostic: type any positive value directly. Arrow keys # step by 1.0 so kip-in users (Fy ≈ 60) get useful increments; # SI-Pa users (Fy ≈ 4e8) still type directly. self._fy = _spin(420e6, step=1.0, minimum=1e-9) self._e0 = _spin(200e9, step=1.0, minimum=1e-9) self._b = _spin(0.01, decimals=4, minimum=0.0, maximum=1.0, step=0.01) - self._layout.addRow("Fy:", self._fy) - self._layout.addRow("E0:", self._e0) + stress_u = _u(self._units, "stress") + fy_label = f"Fy ({stress_u}):" if stress_u else "Fy:" + e0_label = f"E0 ({stress_u}):" if stress_u else "E0:" + self._layout.addRow(fy_label, self._fy) + self._layout.addRow(e0_label, self._e0) self._layout.addRow("b (hardening ratio):", self._b) def _populate_specific(self, m: Steel01) -> None: @@ -128,17 +145,22 @@ class Steel01Form(MaterialFormBase): class Steel02Form(MaterialFormBase): type_label = "Steel02 — Giuffré-Menegotto-Pinto" - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: + super().__init__(parent, units=units) self._fy = _spin(355e6, step=1.0, minimum=1e-9) self._e0 = _spin(200e9, step=1.0, minimum=1e-9) self._b = _spin(0.005, decimals=4, minimum=0.0, maximum=1.0, step=0.001) self._r0 = _spin(18.0, decimals=2, minimum=10.0, maximum=20.0, step=0.5) self._cR1 = _spin(0.925, decimals=4, step=0.01) self._cR2 = _spin(0.15, decimals=4, step=0.01) + stress_u = _u(self._units, "stress") + fy_label = f"Fy ({stress_u}):" if stress_u else "Fy:" + e0_label = f"E0 ({stress_u}):" if stress_u else "E0:" for label, w in ( - ("Fy:", self._fy), - ("E0:", self._e0), + (fy_label, self._fy), + (e0_label, self._e0), ("b:", self._b), ("R0:", self._r0), ("cR1:", self._cR1), @@ -171,8 +193,10 @@ class Steel02Form(MaterialFormBase): class Concrete01Form(MaterialFormBase): type_label = "Concrete01 — Kent-Scott-Park, no tension" - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: + super().__init__(parent, units=units) # f'c, f'cu: any non-zero negative value. The old -1e3 max # assumed SI-Pa and locked out kip-in users who wanted # values like -6 ksi; -1e-6 leaves the full unit range open. @@ -180,11 +204,14 @@ class Concrete01Form(MaterialFormBase): self._epsc0 = _spin(-0.002, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4) self._fpcu = _spin(-15e6, step=1.0, maximum=0.0) self._epsU = _spin(-0.005, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4) + stress_u = _u(self._units, "stress") + fpc_label = f"f'c ({stress_u}):" if stress_u else "f'c (-):" + fpcu_label = f"f'cu ({stress_u}):" if stress_u else "f'cu (-):" for label, w in ( - ("f'c (-):", self._fpc), - ("ε_c0 (-):", self._epsc0), - ("f'cu (-):", self._fpcu), - ("ε_U (-):", self._epsU), + (fpc_label, self._fpc), + ("ε_c0:", self._epsc0), + (fpcu_label, self._fpcu), + ("ε_U:", self._epsU), ): self._layout.addRow(label, w) @@ -209,8 +236,10 @@ class Concrete01Form(MaterialFormBase): class Concrete02Form(MaterialFormBase): type_label = "Concrete02 — Kent-Scott-Park with linear tension" - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: + super().__init__(parent, units=units) # Same range fix as Concrete01 — permissive so kip-in values # like -6 aren't clamped by a hard-coded SI-Pa maximum. self._fpc = _spin(-30e6, step=1.0, maximum=0.0) @@ -220,14 +249,19 @@ class Concrete02Form(MaterialFormBase): self._lambda = _spin(0.1, decimals=4, minimum=0.0, maximum=1.0, step=0.01) self._ft = _spin(3e6, step=1.0, minimum=1e-9) self._ets = _spin(2e9, step=1.0, minimum=1e-9) + stress_u = _u(self._units, "stress") + fpc_label = f"f'c ({stress_u}):" if stress_u else "f'c (-):" + fpcu_label = f"f'cu ({stress_u}):" if stress_u else "f'cu (-):" + ft_label = f"ft (tensile, {stress_u}):" if stress_u else "ft (tensile):" + ets_label = f"Ets (soften, {stress_u}):" if stress_u else "Ets (soften):" for label, w in ( - ("f'c (-):", self._fpc), - ("ε_c0 (-):", self._epsc0), - ("f'cu (-):", self._fpcu), - ("ε_U (-):", self._epsU), + (fpc_label, self._fpc), + ("ε_c0:", self._epsc0), + (fpcu_label, self._fpcu), + ("ε_U:", self._epsU), ("λ (unload ratio):", self._lambda), - ("ft (tensile):", self._ft), - ("Ets (soften):", self._ets), + (ft_label, self._ft), + (ets_label, self._ets), ): self._layout.addRow(label, w) @@ -258,8 +292,10 @@ class Concrete02Form(MaterialFormBase): class Concrete04Form(MaterialFormBase): type_label = "Concrete04 — Popovics concrete (optional tension)" - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: + super().__init__(parent, units=units) self._fpc = _spin(-30e6, step=1.0, maximum=0.0) self._epsc0 = _spin(-0.002, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4) self._epscu = _spin(-0.005, decimals=6, minimum=-1.0, maximum=0.0, step=1e-4) @@ -267,12 +303,16 @@ class Concrete04Form(MaterialFormBase): # fct=0 means no-tension model; positive value activates tensile branch. self._fct = _spin(0.0, step=1.0, minimum=0.0) self._et = _spin(0.0, decimals=6, minimum=0.0, step=1e-5) + stress_u = _u(self._units, "stress") + fpc_label = f"f'c ({stress_u}):" if stress_u else "f'c (-):" + ec_label = f"Ec ({stress_u}):" if stress_u else "Ec:" + fct_label = f"fct (0 = no tension, {stress_u}):" if stress_u else "fct (0 = no tension):" for label, w in ( - ("f'c (-):", self._fpc), - ("ε_c0 (-):", self._epsc0), - ("ε_cu (-):", self._epscu), - ("Ec:", self._Ec), - ("fct (0 = no tension):", self._fct), + (fpc_label, self._fpc), + ("ε_c0:", self._epsc0), + ("ε_cu:", self._epscu), + (ec_label, self._Ec), + (fct_label, self._fct), ("et (if fct > 0):", self._et), ): self._layout.addRow(label, w) @@ -304,11 +344,15 @@ class Concrete04Form(MaterialFormBase): class ElasticUniaxialForm(MaterialFormBase): type_label = "Elastic (uniaxial)" - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: + super().__init__(parent, units=units) self._e = _spin(200e9, step=1.0, minimum=1e-9) self._eta = _spin(0.0, decimals=4) - self._layout.addRow("E:", self._e) + stress_u = _u(self._units, "stress") + e_label = f"E ({stress_u}):" if stress_u else "E:" + self._layout.addRow(e_label, self._e) self._layout.addRow("η (damping):", self._eta) def _populate_specific(self, m: ElasticUniaxial) -> None: @@ -328,14 +372,27 @@ class ElasticUniaxialForm(MaterialFormBase): class ElasticIsotropicForm(MaterialFormBase): type_label = "Elastic Isotropic (3D)" - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: + super().__init__(parent, units=units) self._e = _spin(200e9, step=1.0, minimum=1e-9) self._nu = _spin(0.3, decimals=4, minimum=-1.0, maximum=0.5, step=0.01) self._rho = _spin(7850.0, decimals=2, minimum=0.0, step=100.0) - self._layout.addRow("E:", self._e) + stress_u = _u(self._units, "stress") + mass_u = _u(self._units, "mass") + length_u = _u(self._units, "length") + e_label = f"E ({stress_u}):" if stress_u else "E:" + if mass_u and length_u: + rho_label = f"ρ (density, {mass_u}/{length_u}³):" # noqa: RUF001 + rho_tip = f"Mass density in {mass_u}/{length_u}³ (native units, no conversion)." + else: + rho_label = "ρ (density):" + rho_tip = "Mass density in native project units (no conversion)." + self._layout.addRow(e_label, self._e) self._layout.addRow("ν (Poisson):", self._nu) - self._layout.addRow("ρ (density):", self._rho) + self._layout.addRow(rho_label, self._rho) + self._rho.setToolTip(rho_tip) def _populate_specific(self, m: ElasticIsotropic) -> None: self._e.setValue(m.E) @@ -356,11 +413,15 @@ class ElasticIsotropicForm(MaterialFormBase): class ElasticPPForm(MaterialFormBase): type_label = "Elastic-Perfectly-Plastic" - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: + super().__init__(parent, units=units) self._e = _spin(200e9, step=1.0, minimum=1e-9) self._epsy = _spin(0.002, decimals=6, minimum=1e-6, step=1e-4) - self._layout.addRow("E:", self._e) + stress_u = _u(self._units, "stress") + e_label = f"E ({stress_u}):" if stress_u else "E:" + self._layout.addRow(e_label, self._e) self._layout.addRow("ε_y (yield strain):", self._epsy) def _populate_specific(self, m: ElasticPP) -> None: @@ -390,9 +451,9 @@ FORM_REGISTRY: dict[str, type[MaterialFormBase]] = { } -def form_for(material: Any) -> MaterialFormBase: +def form_for(material: Any, *, units: UnitSystem = UnitSystem.METRIC) -> MaterialFormBase: """Return a form instance pre-populated for ``material``.""" cls = FORM_REGISTRY[material.type] - form = cls() + form = cls(units=units) form.populate(material) return form diff --git a/src/otko/views/dialogs/material_library.py b/src/otko/views/dialogs/material_library.py index 371a507..5b1de6c 100644 --- a/src/otko/views/dialogs/material_library.py +++ b/src/otko/views/dialogs/material_library.py @@ -7,9 +7,9 @@ committed. Each Add/Edit/Delete is its own undoable step. from __future__ import annotations -from pydantic import ValidationError from PySide6.QtCore import Qt from PySide6.QtWidgets import ( + QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, @@ -23,12 +23,14 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) +from pydantic import ValidationError from otko.commands import ( AddMaterialsCommand, DeleteMaterialsCommand, UpdateMaterialCommand, ) +from otko.core.units import UnitSystem from otko.viewmodels import ProjectViewModel from otko.views.dialogs.material_forms import FORM_REGISTRY, form_for @@ -120,6 +122,11 @@ class MaterialLibraryDialog(QDialog): return next((m for m in self._vm.project.materials if m.id == mid), None) # ── slots ──────────────────────────────────────────────────────── + def _project_units(self) -> UnitSystem: + """Project display units (Metric default when no project is open).""" + proj = self._vm.project + return proj.meta.units if proj is not None else UnitSystem.METRIC + def _on_row_changed(self, _row: int) -> None: material = self._selected_material() if material is None: @@ -129,7 +136,7 @@ class MaterialLibraryDialog(QDialog): old = self._stack.widget(0) self._stack.removeWidget(old) old.deleteLater() - form = form_for(material) + form = form_for(material, units=self._project_units()) self._stack.addWidget(form) self._stack.setCurrentWidget(form) self._type_label.setText(form.type_label) @@ -162,7 +169,7 @@ class MaterialLibraryDialog(QDialog): # Build a default-valued instance using the form: instantiate, populate # nothing (defaults shown in the form), then read with the new id. form_cls = FORM_REGISTRY[kind] - form = form_cls() + form = form_cls(units=self._project_units()) form._material_id = new_id # type: ignore[attr-defined] try: new_material = form.read() diff --git a/src/otko/views/dialogs/material_tester.py b/src/otko/views/dialogs/material_tester.py index 325fba7..8718ed0 100644 --- a/src/otko/views/dialogs/material_tester.py +++ b/src/otko/views/dialogs/material_tester.py @@ -232,7 +232,7 @@ class MaterialTesterDialog(QDialog): """Stress label for the current project units (strain is unitless).""" if self._vm.project is not None: return labels_for(self._vm.project.meta.units).stress - return labels_for(UnitSystem.SI_M_N).stress + return labels_for(UnitSystem.METRIC).stress def _refresh_materials(self) -> None: """Repopulate the picker from the project, keeping the selection.""" diff --git a/src/otko/views/dialogs/mirror.py b/src/otko/views/dialogs/mirror.py index 7be07f6..d5131e7 100644 --- a/src/otko/views/dialogs/mirror.py +++ b/src/otko/views/dialogs/mirror.py @@ -24,12 +24,12 @@ class MirrorDialog(QDialog): def _build_ui(self, n_nodes: int, n_elements: int) -> None: layout = QVBoxLayout(self) - layout.addWidget( - QLabel(f"Mirror {n_nodes} node(s) and {n_elements} element(s).") - ) - layout.addWidget( - QLabel("Only elements whose endpoints are both in the selection are copied.") - ) + layout.addWidget(QLabel( + f"Mirror {n_nodes} node(s) and {n_elements} element(s)." + )) + layout.addWidget(QLabel( + "Only elements whose endpoints are both in the selection are copied." + )) box = QGroupBox("Reflection plane (passes through origin)") box_layout = QVBoxLayout(box) diff --git a/src/otko/views/dialogs/move.py b/src/otko/views/dialogs/move.py index 2d415b2..9754c98 100644 --- a/src/otko/views/dialogs/move.py +++ b/src/otko/views/dialogs/move.py @@ -12,13 +12,22 @@ from PySide6.QtWidgets import ( QWidget, ) +from otko.core.units import UnitSystem, native_label + class MoveDialog(QDialog): """Dialog for an (dx, dy, dz) translation.""" - def __init__(self, n_nodes: int, parent: QWidget | None = None) -> None: + def __init__( + self, + n_nodes: int, + parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, + ) -> None: super().__init__(parent) self.setWindowTitle("Move") + self._units = units self._build_ui(n_nodes) def _build_ui(self, n_nodes: int) -> None: @@ -36,9 +45,13 @@ class MoveDialog(QDialog): return sb self._dx, self._dy, self._dz = _spin(), _spin(), _spin() - form.addRow("dX:", self._dx) - form.addRow("dY:", self._dy) - form.addRow("dZ:", self._dz) + try: + length_u = native_label(self._units, "length") + except KeyError: + length_u = "" + form.addRow(f"dX ({length_u}):" if length_u else "dX:", self._dx) + form.addRow(f"dY ({length_u}):" if length_u else "dY:", self._dy) + form.addRow(f"dZ ({length_u}):" if length_u else "dZ:", self._dz) layout.addLayout(form) buttons = QDialogButtonBox( diff --git a/src/otko/views/dialogs/path_time_series.py b/src/otko/views/dialogs/path_time_series.py index 66a6b34..b76cc7f 100644 --- a/src/otko/views/dialogs/path_time_series.py +++ b/src/otko/views/dialogs/path_time_series.py @@ -44,13 +44,11 @@ class PathTimeSeriesDialog(QDialog): def _build_ui(self) -> None: root = QVBoxLayout(self) - root.addWidget( - QLabel( - "Path TimeSeries — tabulated values sampled at a " - "uniform time step. Used by UniformExcitation (ground " - "motion) and by PlainLoadPattern scaled forces." - ) - ) + root.addWidget(QLabel( + "Path TimeSeries — tabulated values sampled at a " + "uniform time step. Used by UniformExcitation (ground " + "motion) and by PlainLoadPattern scaled forces." + )) form = QFormLayout() self._name_edit = QLineEdit("GroundMotion") @@ -67,11 +65,11 @@ class PathTimeSeriesDialog(QDialog): self._factor_spin.setRange(-1e12, 1e12) self._factor_spin.setDecimals(6) self._factor_spin.setSingleStep(1.0) - self._factor_spin.setValue(386.4) # default: convert g → in/s² + self._factor_spin.setValue(386.4) # default: convert g → in/s² self._factor_spin.setToolTip( "Multiplier applied to every value at runtime. Typical use: " - "386.4 for ground motion in g → in/s² (US_IN_KIP), 9.81 for " - "SI, 1.0 for data already in the project's unit system." + "386.4 for ground motion in g → in/s² (Imperial), 9.81 for " + "Metric, 1.0 for data already in the project's unit system." ) form.addRow("Factor:", self._factor_spin) root.addLayout(form) @@ -86,7 +84,9 @@ class PathTimeSeriesDialog(QDialog): btn_row.addWidget(self._btn_plain) root.addLayout(btn_row) - self._status = QLabel("No data loaded — use one of the import buttons.") + self._status = QLabel( + "No data loaded — use one of the import buttons." + ) self._status.setWordWrap(True) self._status.setStyleSheet("color: #666;") root.addWidget(self._status) @@ -102,7 +102,8 @@ class PathTimeSeriesDialog(QDialog): root.addWidget(self._preview, 1) buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, + QDialogButtonBox.StandardButton.Ok + | QDialogButtonBox.StandardButton.Cancel, ) buttons.accepted.connect(self._on_accept) buttons.rejected.connect(self.reject) @@ -111,16 +112,14 @@ class PathTimeSeriesDialog(QDialog): # ── file-import slots ─────────────────────────────────────────── def _on_import_peer(self) -> None: fname, _ = QFileDialog.getOpenFileName( - self, - "Import PEER record", - "", - "PEER records (*.at2 *.AT2);;All files (*)", + self, "Import PEER record", + "", "PEER records (*.at2 *.AT2);;All files (*)", ) if not fname: return try: dt, npts, vals = parse_peer_record(fname) - except Exception as exc: + except Exception as exc: # noqa: BLE001 QMessageBox.critical(self, "PEER import failed", str(exc)) return self._values = vals @@ -133,21 +132,20 @@ class PathTimeSeriesDialog(QDialog): def _on_import_plain(self) -> None: fname, _ = QFileDialog.getOpenFileName( - self, - "Import plain values", - "", - "Text files (*.txt *.csv *.dat);;All files (*)", + self, "Import plain values", + "", "Text files (*.txt *.csv *.dat);;All files (*)", ) if not fname: return try: vals = parse_plain_values(fname) - except Exception as exc: + except Exception as exc: # noqa: BLE001 QMessageBox.critical(self, "Import failed", str(exc)) return self._values = vals self._status.setText( - f"Loaded {len(vals)} values from plain-text file. " "Set Δt manually above." + f"Loaded {len(vals)} values from plain-text file. " + "Set Δt manually above." ) self._refresh_preview() @@ -169,9 +167,7 @@ class PathTimeSeriesDialog(QDialog): def _on_accept(self) -> None: if not self._values: QMessageBox.warning( - self, - "No data", - "Import a record first.", + self, "No data", "Import a record first.", ) return self.accept() diff --git a/src/otko/views/dialogs/pattern_loads.py b/src/otko/views/dialogs/pattern_loads.py index 3d76214..c48b3e2 100644 --- a/src/otko/views/dialogs/pattern_loads.py +++ b/src/otko/views/dialogs/pattern_loads.py @@ -36,6 +36,7 @@ from otko.core import ( UniformElementLoad, UniformExcitationPattern, ) +from otko.core.units import UnitSystem, native_label # Column headers for the two tables. Order matches how the Assign # dialogs expose the values, so reading one row is a direct mapping @@ -62,11 +63,19 @@ class PatternLoadsDialog(QDialog): parent: QWidget | None = None, *, vm: Any | None = None, + units: UnitSystem | None = None, ) -> None: super().__init__(parent) self._pattern = pattern self._pattern_id = pattern.id self._vm = vm + if units is not None: + self._units = units + else: + try: + self._units = vm.project.meta.units # type: ignore[union-attr] + except Exception: + self._units = UnitSystem.METRIC self._rebuilding = False self._info = QLabel("") # Keep references for tests + future programmatic inspection. @@ -134,6 +143,34 @@ class PatternLoadsDialog(QDialog): f"is applied directly to the model DOFs.", ) + def _unit(self, quantity: str) -> str: + try: + return native_label(self._units, quantity) + except KeyError: + return "" + + def _nodal_headers(self) -> tuple[str, ...]: + f = self._unit("force") + m = self._unit("moment") + return ( + "Node", + f"Fx ({f})" if f else "Fx", + f"Fy ({f})" if f else "Fy", + f"Fz ({f})" if f else "Fz", + f"Mx ({m})" if m else "Mx", + f"My ({m})" if m else "My", + f"Mz ({m})" if m else "Mz", + ) + + def _element_headers(self) -> tuple[str, ...]: + d = self._unit("distributed_load") + return ( + "Element", + f"wx ({d})" if d else "wx", + f"wy ({d})" if d else "wy", + f"wz ({d})" if d else "wz", + ) + def _make_nodal_section(self) -> QWidget: assert isinstance(self._pattern, PlainLoadPattern) container = QWidget(self) @@ -144,7 +181,7 @@ class PatternLoadsDialog(QDialog): if not loads: v.addWidget(QLabel("(none)")) return container - table = self._new_table(len(loads), _NODAL_HEADERS) + table = self._new_table(len(loads), self._nodal_headers()) self._fill_nodal_table(table, loads) table.itemChanged.connect(self._on_nodal_edited) self._nodal_table = table @@ -161,7 +198,7 @@ class PatternLoadsDialog(QDialog): if not loads: v.addWidget(QLabel("(none)")) return container - table = self._new_table(len(loads), _ELEMENT_HEADERS) + table = self._new_table(len(loads), self._element_headers()) self._fill_element_table(table, loads) table.itemChanged.connect(self._on_element_edited) self._element_table = table diff --git a/src/otko/views/dialogs/plot_properties.py b/src/otko/views/dialogs/plot_properties.py deleted file mode 100644 index ae249ef..0000000 --- a/src/otko/views/dialogs/plot_properties.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Plot Properties dialog — the user-facing subset of :class:`RenderStyle`. - -The field table lives in :mod:`otko.views.canvas3d.style` (``STYLE_FIELDS``), -so adding an editable property is a one-line change there plus a default on -the dataclass. Edits are emitted through :attr:`PlotPropertiesDialog.changed` -so the canvas can preview them live; ``style()`` returns the composed result. -""" - -from __future__ import annotations - -from PySide6.QtCore import Signal -from PySide6.QtGui import QColor -from PySide6.QtWidgets import ( - QCheckBox, - QColorDialog, - QDialog, - QDialogButtonBox, - QDoubleSpinBox, - QFormLayout, - QLabel, - QPushButton, - QSpinBox, - QVBoxLayout, - QWidget, -) - -from otko.views.canvas3d.style import STYLE_FIELDS, RenderStyle - - -class _ColorButton(QPushButton): - """A swatch that opens :class:`QColorDialog` and reports the hex value.""" - - colorChanged = Signal(str) - - def __init__(self, color: str, parent: QWidget | None = None) -> None: - super().__init__(parent) - self._color = color - self.setFixedWidth(96) - self._refresh() - self.clicked.connect(self._pick) - - def color(self) -> str: - """Current colour as a ``#rrggbb`` string.""" - return self._color - - def set_color(self, color: str) -> None: - """Set the swatch colour (does not emit :attr:`colorChanged`).""" - self._color = color - self._refresh() - - def _refresh(self) -> None: - self.setText(self._color.upper()) - self.setStyleSheet(f"background-color: {self._color}; color: {self._text_color()};") - - def _text_color(self) -> str: - color = QColor(self._color) - luminance = 0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue() - return "#000000" if luminance > 140 else "#ffffff" - - def _pick(self) -> None: - chosen = QColorDialog.getColor(QColor(self._color), self, "Select colour") - if chosen.isValid(): - self.set_color(chosen.name()) - self.colorChanged.emit(self._color) - - -class PlotPropertiesDialog(QDialog): - """Modeless-free editor for the persisted plot style.""" - - #: Emitted with the composed :class:`RenderStyle` on every edit. - changed = Signal(object) - - def __init__(self, style: RenderStyle, parent: QWidget | None = None) -> None: - super().__init__(parent) - self.setWindowTitle("Plot Properties") - self._base = style - self._colors: dict[str, _ColorButton] = {} - self._numbers: dict[str, QDoubleSpinBox | QSpinBox] = {} - self._checks: dict[str, QCheckBox] = {} - - layout = QVBoxLayout(self) - form = QFormLayout() - for name, label, kind in STYLE_FIELDS: - widget = self._build_widget(name, getattr(style, name), kind) - form.addRow(QLabel(f"{label}:"), widget) - layout.addLayout(form) - - buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel - ) - reset = buttons.addButton("Reset to Defaults", QDialogButtonBox.ButtonRole.ResetRole) - assert reset is not None - reset.clicked.connect(self._reset) - buttons.accepted.connect(self.accept) - buttons.rejected.connect(self.reject) - layout.addWidget(buttons) - - # ── public ────────────────────────────────────────────────────── - def plot_style(self) -> RenderStyle: - """The composed style for the current widget values. - - Named ``plot_style`` (not ``style``) because :meth:`QWidget.style` - already exists and must keep its Qt meaning. - """ - updates: dict[str, object] = {name: w.color() for name, w in self._colors.items()} - updates.update({name: w.value() for name, w in self._numbers.items()}) - updates.update({name: w.isChecked() for name, w in self._checks.items()}) - return self._base.with_updates(**updates) - - # ── internals ─────────────────────────────────────────────────── - def _build_widget(self, name: str, value: object, kind: str) -> QWidget: - if kind == "color": - button = _ColorButton(str(value)) - button.colorChanged.connect(self._emit_changed) - self._colors[name] = button - return button - if kind == "float": - spin = QDoubleSpinBox() - spin.setRange(0.0, 1.0) - spin.setSingleStep(0.05) - spin.setDecimals(2) - spin.setValue(float(value)) # type: ignore[arg-type] - spin.valueChanged.connect(self._emit_changed) - self._numbers[name] = spin - return spin - if kind == "bool": - check = QCheckBox() - check.setChecked(bool(value)) - check.toggled.connect(self._emit_changed) - self._checks[name] = check - return check - int_spin = QSpinBox() - int_spin.setRange(6, 32) - int_spin.setValue(int(value)) # type: ignore[call-overload] - int_spin.valueChanged.connect(self._emit_changed) - self._numbers[name] = int_spin - return int_spin - - def _emit_changed(self, *_args: object) -> None: - self.changed.emit(self.plot_style()) - - def _reset(self) -> None: - defaults = RenderStyle() - for name, button in self._colors.items(): - button.set_color(getattr(defaults, name)) - for name, spin in self._numbers.items(): - spin.setValue(getattr(defaults, name)) - for name, check in self._checks.items(): - check.setChecked(getattr(defaults, name)) - self._emit_changed() diff --git a/src/otko/views/dialogs/quick_grid_lines.py b/src/otko/views/dialogs/quick_grid_lines.py index 7928d60..b0589d5 100644 --- a/src/otko/views/dialogs/quick_grid_lines.py +++ b/src/otko/views/dialogs/quick_grid_lines.py @@ -11,6 +11,7 @@ from PySide6.QtWidgets import ( QDialog, QDialogButtonBox, QDoubleSpinBox, + QFormLayout, QGroupBox, QHBoxLayout, QLabel, @@ -19,13 +20,21 @@ from PySide6.QtWidgets import ( QWidget, ) +from otko.core.units import UnitSystem, native_label + class QuickGridLinesDialog(QDialog): """Three parallel (n, spacing, first_coord) inputs — one per axis.""" - def __init__(self, parent: QWidget | None = None) -> None: + def __init__( + self, + parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, + ) -> None: super().__init__(parent) self.setWindowTitle("Quick Grid Lines") + self._units = units self._build_ui() def _build_ui(self) -> None: @@ -83,9 +92,13 @@ class QuickGridLinesDialog(QDialog): row.addWidget(QLabel("Number of lines:")) row.addWidget(n) - row.addWidget(QLabel("Spacing:")) + try: + length_u = native_label(self._units, "length") + except KeyError: + length_u = "" + row.addWidget(QLabel(f"Spacing ({length_u}):" if length_u else "Spacing:")) row.addWidget(s) - row.addWidget(QLabel("First coord:")) + row.addWidget(QLabel(f"First coord ({length_u}):" if length_u else "First coord:")) row.addWidget(f) parent_layout.addWidget(box) return n, s, f diff --git a/src/otko/views/dialogs/quick_guide.py b/src/otko/views/dialogs/quick_guide.py index 9b0c424..85daf78 100644 --- a/src/otko/views/dialogs/quick_guide.py +++ b/src/otko/views/dialogs/quick_guide.py @@ -29,7 +29,7 @@ with Define → Add Plain Load Pattern… or Define → Add Uniform Excitation… for seismic input.

4 — Draw the model

-

Tools toolbar (top): Select, Draw Node (F1), +

Tools toolbar (left): Select, Draw Node (F1), Draw Frame (F2), Draw Truss (F3, icon-less by design). Define → Coordinate System/Grids… (Ctrl+G) and Add Node… (Ctrl+N) for precise layout; use the top-toolbar Level combo in Top/Front/Right views.

diff --git a/src/otko/views/dialogs/replicate.py b/src/otko/views/dialogs/replicate.py index f627ec3..a9f774e 100644 --- a/src/otko/views/dialogs/replicate.py +++ b/src/otko/views/dialogs/replicate.py @@ -17,6 +17,8 @@ from PySide6.QtWidgets import ( QWidget, ) +from otko.core.units import UnitSystem, native_label + class ReplicateDialog(QDialog): """Dialog for offset (dx, dy, dz) and number of copies. @@ -34,10 +36,12 @@ class ReplicateDialog(QDialog): parent: QWidget | None = None, *, ndm: int = 3, + units: UnitSystem = UnitSystem.METRIC, ) -> None: super().__init__(parent) self.setWindowTitle("Replicate") self._ndm = ndm + self._units = units self._build_ui(n_nodes, n_elements) def _build_ui(self, n_nodes: int, n_elements: int) -> None: @@ -66,9 +70,13 @@ class ReplicateDialog(QDialog): self._n.setRange(1, 200) self._n.setValue(1) - form.addRow("dX:", self._dx) - form.addRow("dY:", self._dy) - form.addRow("dZ:", self._dz) + try: + length_u = native_label(self._units, "length") + except KeyError: + length_u = "" + form.addRow(f"dX ({length_u}):" if length_u else "dX:", self._dx) + form.addRow(f"dY ({length_u}):" if length_u else "dY:", self._dy) + form.addRow(f"dZ ({length_u}):" if length_u else "dZ:", self._dz) form.addRow("Number of copies:", self._n) layout.addLayout(form) diff --git a/src/otko/views/dialogs/run_analysis.py b/src/otko/views/dialogs/run_analysis.py index b1beb12..8ae03e7 100644 --- a/src/otko/views/dialogs/run_analysis.py +++ b/src/otko/views/dialogs/run_analysis.py @@ -57,18 +57,6 @@ class RunAnalysisDialog(QDialog): row.addWidget(self._case_combo, stretch=1) layout.addLayout(row) - combo_row = QHBoxLayout() - combo_row.addWidget(QLabel("Combination:")) - self._combo_combo = QComboBox() - combo_row.addWidget(self._combo_combo, stretch=1) - self._eval_btn = QPushButton("Evaluate") - self._eval_btn.setToolTip( - "Run each referenced static case, then combine results " - "in post-processing (linear superposition, no extra solver input)." - ) - combo_row.addWidget(self._eval_btn) - layout.addLayout(combo_row) - # Run-time damping overrides for transient cases only. self._damping_box = QGroupBox("Rayleigh damping (transient only)") form = QFormLayout(self._damping_box) @@ -77,12 +65,14 @@ class RunAnalysisDialog(QDialog): self._alpha_m.setRange(0.0, 1e6) self._alpha_m.setDecimals(6) self._alpha_m.setSingleStep(0.01) + self._alpha_m.setToolTip("Mass-proportional damping (1/s) — native units, no conversion.") form.addRow("alphaM (mass-prop.):", self._alpha_m) self._beta_k = QDoubleSpinBox() self._beta_k.setRange(0.0, 1e6) self._beta_k.setDecimals(6) self._beta_k.setSingleStep(1e-4) + self._beta_k.setToolTip("Stiffness-proportional damping (s) — native units, no conversion.") form.addRow("betaK (stiffness-prop.):", self._beta_k) self._mode1_damping = QDoubleSpinBox() @@ -121,7 +111,6 @@ class RunAnalysisDialog(QDialog): def _wire(self) -> None: self._run_btn.clicked.connect(self._on_run) - self._eval_btn.clicked.connect(self._on_evaluate_combo) self._buttons.rejected.connect(self.reject) self._case_combo.currentIndexChanged.connect(self._on_case_changed) @@ -149,21 +138,15 @@ class RunAnalysisDialog(QDialog): def _populate_cases(self) -> None: self._case_combo.clear() - self._combo_combo.clear() if self._vm.project is None: self._run_btn.setEnabled(False) - self._eval_btn.setEnabled(False) self._damping_box.setVisible(False) return for case in self._vm.project.analyses: label = f"#{case.id} {case.name or '(unnamed)'} [{case.type}]" self._case_combo.addItem(label, userData=case.id) - for combo in self._vm.project.combinations: - label = f"#{combo.id} {combo.name or '(unnamed)'} [{combo.kind}]" - self._combo_combo.addItem(label, userData=combo.id) has_cases = self._case_combo.count() > 0 self._run_btn.setEnabled(has_cases) - self._eval_btn.setEnabled(self._combo_combo.count() > 0) if not has_cases: # No transient case can be selected, so the run-only damping # overrides are meaningless — hide them alongside the disabled Run. @@ -193,52 +176,9 @@ class RunAnalysisDialog(QDialog): results_dir = self._vm.path.parent / f"{self._vm.path.stem}_results" try: self._runner.run(self._vm.project, case, results_dir=results_dir) - except Exception as exc: + except Exception as exc: # noqa: BLE001 self._log.appendPlainText(f"Could not start: {exc}") - def _on_evaluate_combo(self) -> None: - """Run each referenced static case, then combine in post-processing. - - Synchronous (linear statics are fast): each case runs through - :class:`OpenSeesRunner` on the GUI thread, then - :func:`evaluate_combination` superposes the results. The combined - output is forwarded through ``runner.finished`` so the main - window displays it like any other result. - """ - from otko.services.combinations import evaluate_combination - from otko.services.opensees_runner import OpenSeesRunner - - if self._runner.is_running or self._vm.project is None: - return - combo_id = self._combo_combo.currentData() - project = self._vm.project - combo = next((c for c in project.combinations if c.id == combo_id), None) - if combo is None: - return - self._results = None - self._log.clear() - self._log.appendPlainText(f"Evaluating combination '{combo.name}' [{combo.kind}] ...") - try: - case_by_id = {c.id: c for c in project.analyses} - collected: dict[int, Any] = {} - for item in combo.items: - case = case_by_id.get(item.case_id) - if case is None: - raise ValueError(f"Combination references missing case id={item.case_id}.") - self._log.appendPlainText( - f" running case #{case.id} '{case.name}' × {item.factor:g} ..." - ) - collected[item.case_id] = OpenSeesRunner(project).run(case) - combined = evaluate_combination(collected, combo) - self._results = combined - self._log.appendPlainText(f"--- Done. Returned {type(combined).__name__}. ---") - self._runner.finished.emit(combined) - except Exception as exc: - import traceback - - self._log.appendPlainText("--- FAILED ---") - self._log.appendPlainText(f"{exc}\n{traceback.format_exc()}") - def _on_started(self) -> None: self._log.appendPlainText("--- Analysis started ---") @@ -258,6 +198,4 @@ class RunAnalysisDialog(QDialog): self._progress.setVisible(running) # With zero cases Run must stay disabled even when idle. self._run_btn.setEnabled((not running) and self._case_combo.count() > 0) - self._eval_btn.setEnabled((not running) and self._combo_combo.count() > 0) self._case_combo.setEnabled(not running) - self._combo_combo.setEnabled(not running) diff --git a/src/otko/views/dialogs/section_editor.py b/src/otko/views/dialogs/section_editor.py index fdb9031..020a39e 100644 --- a/src/otko/views/dialogs/section_editor.py +++ b/src/otko/views/dialogs/section_editor.py @@ -12,6 +12,7 @@ from __future__ import annotations from typing import Any +import numpy as np import pyqtgraph as pg from PySide6.QtCore import Qt from PySide6.QtWidgets import ( @@ -38,6 +39,7 @@ from otko.core.sections import ( RectangularPatch, StraightLayer, ) +from otko.core.units import UnitSystem, native_label from otko.services.section_properties import ( compute_section_props, expand_fibres, @@ -63,16 +65,22 @@ class FiberSectionEditor(QDialog): material_ids: list[int], existing: FiberSection | None = None, parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, ) -> None: super().__init__(parent) self.setWindowTitle("Fiber Section Editor") self.resize(1000, 650) + self._units = units self._material_ids = material_ids self._patches: list[Any] = [] # RectangularPatch | CircularPatch self._layers: list[StraightLayer] = [] self._section_id: int = existing.id if existing else 1 self._section_name: str = existing.name if existing else "" self._gj: float | None = existing.GJ if existing else None + # (form, field widget, base name, quantity) for rows whose label + # carries a unit hint — refreshed by set_units(). + self._unit_rows: list[tuple[QFormLayout, QWidget, str, str]] = [] self._build_ui() if existing is not None: self._load_existing(existing) @@ -120,6 +128,13 @@ class FiberSectionEditor(QDialog): self._rect_zj = self._spin(0.15) self._rect_ny = self._ispin(8) self._rect_nz = self._ispin(8) + # Unit hints only — values are entered in native stored units. + self._add_unit_row(pf, "y_i", "length", self._rect_yi) + self._add_unit_row(pf, "z_i", "length", self._rect_zi) + self._add_unit_row(pf, "y_j", "length", self._rect_yj) + self._add_unit_row(pf, "z_j", "length", self._rect_zj) + pf.addRow("n_fib_y:", self._rect_ny) + pf.addRow("n_fib_z:", self._rect_nz) self._rect_rows = [ ("y_i:", self._rect_yi), ("z_i:", self._rect_zi), @@ -128,8 +143,6 @@ class FiberSectionEditor(QDialog): ("n_fib_y:", self._rect_ny), ("n_fib_z:", self._rect_nz), ] - for label, widget in self._rect_rows: - pf.addRow(label, widget) # Circ fields (initially hidden) self._circ_yc = self._spin(0.0) @@ -138,6 +151,12 @@ class FiberSectionEditor(QDialog): self._circ_ro = self._spin(0.15) self._circ_nc = self._ispin(16) self._circ_nr = self._ispin(4) + self._add_unit_row(pf, "y_center", "length", self._circ_yc) + self._add_unit_row(pf, "z_center", "length", self._circ_zc) + self._add_unit_row(pf, "r_inner", "length", self._circ_ri) + self._add_unit_row(pf, "r_outer", "length", self._circ_ro) + pf.addRow("n_circ:", self._circ_nc) + pf.addRow("n_rad:", self._circ_nr) self._circ_rows = [ ("y_center:", self._circ_yc), ("z_center:", self._circ_zc), @@ -146,8 +165,6 @@ class FiberSectionEditor(QDialog): ("n_circ:", self._circ_nc), ("n_rad:", self._circ_nr), ] - for label, widget in self._circ_rows: - pf.addRow(label, widget) self._add_patch_btn = QPushButton("Add patch") self._add_patch_btn.clicked.connect(self._on_add_patch) @@ -170,11 +187,11 @@ class FiberSectionEditor(QDialog): self._layer_ye = self._spin(0.12) self._layer_ze = self._spin(-0.12) lf.addRow("n_bars:", self._layer_nbars) - lf.addRow("bar_area:", self._layer_area) - lf.addRow("y_start:", self._layer_ys) - lf.addRow("z_start:", self._layer_zs) - lf.addRow("y_end:", self._layer_ye) - lf.addRow("z_end:", self._layer_ze) + self._add_unit_row(lf, "bar_area", "area", self._layer_area) + self._add_unit_row(lf, "y_start", "length", self._layer_ys) + self._add_unit_row(lf, "z_start", "length", self._layer_zs) + self._add_unit_row(lf, "y_end", "length", self._layer_ye) + self._add_unit_row(lf, "z_end", "length", self._layer_ze) self._add_layer_btn = QPushButton("Add layer") self._add_layer_btn.clicked.connect(self._on_add_layer) lf.addRow(self._add_layer_btn) @@ -205,8 +222,8 @@ class FiberSectionEditor(QDialog): self._preview = pg.PlotWidget() self._preview.setBackground("#1e1e1e") self._preview.setAspectLocked(True) - self._preview.setLabel("bottom", "y (m)") - self._preview.setLabel("left", "z (m)") + self._apply_preview_labels() + self._preview.showGrid(x=True, y=True, alpha=0.3) self._preview.showGrid(x=True, y=True, alpha=0.3) right_layout.addWidget(self._preview, 1) @@ -219,6 +236,76 @@ class FiberSectionEditor(QDialog): self._on_patch_type_changed(0) + def set_units(self, units: UnitSystem) -> None: + """Re-apply the project's stored unit system to labels. + + Values are always entered in native units; only the row and axis + labels change. Never throws. + """ + try: + self._units = units + self._apply_preview_labels() + self._apply_row_labels() + self._refresh_preview() + except Exception: + pass + + def _apply_preview_labels(self) -> None: + """Set preview axis labels from the stored length unit.""" + try: + length = native_label(self._units, "length") + except KeyError: + length = "m" + self._preview.setLabel("bottom", f"y ({length})") + self._preview.setLabel("left", f"z ({length})") + + def _unit_suffix(self, quantity: str) -> str: + """Return ``" (unit)"`` for a quantity, or ``""`` when unknown. + + Unit hints only — values are entered in native stored units, + never converted. + """ + try: + return f" ({native_label(self._units, quantity)})" + except KeyError: + return "" + + def _add_unit_row(self, form: QFormLayout, base: str, quantity: str, widget: QWidget) -> None: + """Add a ``" ():"`` row and register it for set_units.""" + form.addRow(f"{base}{self._unit_suffix(quantity)}:", widget) + self._unit_rows.append((form, widget, base, quantity)) + + def _apply_row_labels(self) -> None: + """Refresh registered unit-hint row labels for the stored units.""" + for form, widget, base, quantity in self._unit_rows: + try: + pos = form.getWidgetPosition(widget) + except RuntimeError: + continue + if not isinstance(pos, tuple) or not pos: + continue + row = pos[0] + item = form.itemAt(row, QFormLayout.ItemRole.LabelRole) + label_widget = item.widget() if item is not None else None + if isinstance(label_widget, QLabel): + label_widget.setText(f"{base}{self._unit_suffix(quantity)}:") + + def _gj_label(self) -> str: + """Return the GJ unit hint (moment·length² composite).""" + try: + moment_u = native_label(self._units, "moment") + length_u = native_label(self._units, "length") + except KeyError: + return "" + return f"{moment_u}·{length_u}²" + + def _gj_props_suffix(self) -> str: + """Return ``" GJ = … "`` when GJ is set, else ``""``.""" + if self._gj is None: + return "" + gj_u = self._gj_label() + return f" GJ = {self._gj:.4e} {gj_u}" if gj_u else f" GJ = {self._gj:.4e}" + # ── helpers ───────────────────────────────────────────────────── @staticmethod def _spin(default: float = 0.0, *, step: float = 0.01, minimum: float = -1e6) -> QDoubleSpinBox: @@ -323,6 +410,7 @@ class FiberSectionEditor(QDialog): self._props_label.setText("Add patches or layers to see the preview.") return + f = props.fibre_yz # Color by material: assign a palette index per unique material_id. mat_ids = [] for p in self._patches: @@ -382,7 +470,8 @@ class FiberSectionEditor(QDialog): f"Fibres: {props.n_fibres} " f"A = {props.area:.4e} m² " f"Centroid = ({props.centroid_y:.4e}, {props.centroid_z:.4e}) " - f"Iy = {props.Iy:.4e} m⁴ Iz = {props.Iz:.4e} m⁴", + f"Iy = {props.Iy:.4e} m⁴ Iz = {props.Iz:.4e} m⁴" + f"{self._gj_props_suffix()}", ) def _load_existing(self, sec: FiberSection) -> None: diff --git a/src/otko/views/dialogs/section_forms.py b/src/otko/views/dialogs/section_forms.py index ec386e4..de35cf7 100644 --- a/src/otko/views/dialogs/section_forms.py +++ b/src/otko/views/dialogs/section_forms.py @@ -19,13 +19,19 @@ from PySide6.QtWidgets import ( ) from otko.core import ( - AngleShape, ElasticSection, FiberSection, - PipeShape, - RectShape, SectionAggregator, ) +from otko.core.units import UnitSystem, native_label + + +def _u(units: UnitSystem, quantity: str) -> str: + """Display label for ``quantity`` ("" when dimensionless/unknown).""" + try: + return native_label(units, quantity) + except KeyError: + return "" def _spin( @@ -70,8 +76,11 @@ def _opt_value(sb: QDoubleSpinBox) -> float | None: class SectionFormBase(QWidget): type_label: str = "Section" - def __init__(self, parent: QWidget | None = None) -> None: + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: super().__init__(parent) + self._units = units self._section_id: int | None = None self._layout = QFormLayout(self) self._name_edit = QLineEdit() @@ -97,30 +106,42 @@ class SectionFormBase(QWidget): class ElasticSectionForm(SectionFormBase): type_label = "Elastic Section" - #: Shape kind → visible display-only dimensions (see the core - #: ``SectionShape`` union). ``None`` hides them all. + #: Shape → visible display-only dimensions. ``None`` hides them all. _SHAPE_DIMS: ClassVar[dict[str | None, tuple[str, ...]]] = { None: (), - "rect": ("b", "d"), - "angle": ("b", "d", "t"), - "pipe": ("od", "t"), + "rect": ("b", "h"), + "I": ("b", "d", "tw", "tf"), + "H": ("b", "d", "tw", "tf"), + "pipe": ("diameter",), + "circle": ("diameter",), } - def __init__(self, parent: QWidget | None = None) -> None: + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: super().__init__(parent) + self._units = units self._e = _spin(200e9, step=1e9) self._a = _spin(0.01, step=1e-4) self._iz = _spin(8.33e-6, step=1e-7) self._iy = _spin(8.33e-6, step=1e-7) self._g = _spin(80e9, step=1e9) self._j = _spin(1e-6, step=1e-7) + # Unit hints only — values are entered in native stored units. + stress_u = _u(self._units, "stress") + area_u = _u(self._units, "area") + inertia_u = _u(self._units, "inertia") + + def _lbl(name: str, unit: str) -> str: + return f"{name} ({unit}):" if unit else f"{name}:" + for label, w in ( - ("E:", self._e), - ("A:", self._a), - ("Iz:", self._iz), - ("Iy:", self._iy), - ("G:", self._g), - ("J:", self._j), + (_lbl("E", stress_u), self._e), + (_lbl("A", area_u), self._a), + (_lbl("Iz", inertia_u), self._iz), + (_lbl("Iy", inertia_u), self._iy), + (_lbl("G", stress_u), self._g), + (_lbl("J", inertia_u), self._j), ): self._layout.addRow(label, w) self._layout.addRow(QLabel("Iy, G, J required for 3D models.")) @@ -129,16 +150,17 @@ class ElasticSectionForm(SectionFormBase): self._layout.addRow(QLabel("Extruded-view shape (display only).")) self._shape = QComboBox() self._shape.addItem("(none)", None) - for key in ("rect", "angle", "pipe"): + for key in ("rect", "I", "H", "pipe", "circle"): self._shape.addItem(key, key) self._shape.currentIndexChanged.connect(self._update_dim_visibility) self._layout.addRow("Shape:", self._shape) self._dim_spins: dict[str, QDoubleSpinBox] = {} self._dim_labels: dict[str, QLabel] = {} - for key in ("b", "d", "t", "od"): + sec_u = _u(self._units, "section_dim") + for key in ("b", "h", "d", "tw", "tf", "diameter"): spin = _opt_spin() - lbl = QLabel(f"{key}:") + lbl = QLabel(f"{key} ({sec_u}):" if sec_u else f"{key}:") self._dim_spins[key] = spin self._dim_labels[key] = lbl self._layout.addRow(lbl, spin) @@ -162,15 +184,20 @@ class ElasticSectionForm(SectionFormBase): self._g.setValue(s.G) if s.J is not None: self._j.setValue(s.J) - kind = getattr(s.shape, "kind", None) if s.shape is not None else None - idx = self._shape.findData(kind) + idx = self._shape.findData(s.shape) self._shape.setCurrentIndex(idx if idx >= 0 else 0) for key, spin in self._dim_spins.items(): - v = getattr(s.shape, key, None) if s.shape is not None else None + v = getattr(s, key, None) spin.setValue(v if v is not None else _UNSET_SENTINEL) self._update_dim_visibility() def _read_specific(self, sid: int) -> ElasticSection: + shape = self._shape.currentData() + visible = set(self._SHAPE_DIMS.get(shape, ())) + dims = { + key: (_opt_value(spin) if key in visible else None) + for key, spin in self._dim_spins.items() + } return ElasticSection( id=sid, name=self._name_edit.text(), @@ -180,38 +207,10 @@ class ElasticSectionForm(SectionFormBase): Iy=self._iy.value(), G=self._g.value(), J=self._j.value(), - shape=self._read_shape(), + shape=shape, + **dims, # type: ignore[arg-type] ) - def _read_shape(self) -> PipeShape | AngleShape | RectShape | None: - """Build the display-only shape hint from the form fields. - - Dimensions are optional in the UI, so an incomplete shape degrades - to ``None`` instead of raising — the hint is never emitted to - OpenSees and cannot change an analysis result. - """ - - def dim(key: str) -> float | None: - return _opt_value(self._dim_spins[key]) - - kind = self._shape.currentData() - if kind == "rect": - b, d = dim("b"), dim("d") - if b is None or d is None: - return None - return RectShape(b=b, d=d) - if kind == "angle": - b, d, t = dim("b"), dim("d"), dim("t") - if b is None or d is None or t is None: - return None - return AngleShape(b=b, d=d, t=t) - if kind == "pipe": - od, t = dim("od"), dim("t") - if od is None or t is None: - return None - return PipeShape(od=od, t=t) - return None - class FiberSectionSummaryForm(SectionFormBase): """Read-only overview of a :class:`FiberSection`. @@ -224,8 +223,10 @@ class FiberSectionSummaryForm(SectionFormBase): type_label = "Fiber Section" - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: + super().__init__(parent, units=units) self._name_edit.setEnabled(False) self._summary = QLabel("") self._summary.setWordWrap(True) @@ -266,8 +267,10 @@ class SectionAggregatorSummaryForm(SectionFormBase): type_label = "Section Aggregator" - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent) + def __init__( + self, parent: QWidget | None = None, *, units: UnitSystem = UnitSystem.METRIC + ) -> None: + super().__init__(parent, units=units) self._name_edit.setEnabled(False) self._summary = QLabel("") self._summary.setWordWrap(True) @@ -299,19 +302,19 @@ FORM_REGISTRY: dict[str, type[SectionFormBase]] = { } -def form_for(section: Any) -> SectionFormBase: +def form_for(section: Any, *, units: UnitSystem = UnitSystem.METRIC) -> SectionFormBase: cls = FORM_REGISTRY.get(section.type) if cls is None: # Graceful fallback — unknown section types display a minimal # placeholder instead of crashing the entire dialog. - form = SectionFormBase() + form = SectionFormBase(units=units) form._layout.addRow( - QLabel(f"No form registered for section type " f"{section.type} yet.") + QLabel(f"No form registered for section type {section.type} yet.") ) form._section_id = section.id form._name_edit.setText(getattr(section, "name", "") or "") form._name_edit.setEnabled(False) return form - form = cls() + form = cls(units=units) form.populate(section) return form diff --git a/src/otko/views/dialogs/section_library.py b/src/otko/views/dialogs/section_library.py index 198c821..723b0bf 100644 --- a/src/otko/views/dialogs/section_library.py +++ b/src/otko/views/dialogs/section_library.py @@ -2,7 +2,6 @@ from __future__ import annotations -from pydantic import ValidationError from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QDialog, @@ -18,12 +17,14 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) +from pydantic import ValidationError from otko.commands import ( AddSectionsCommand, DeleteSectionsCommand, UpdateSectionCommand, ) +from otko.core.units import UnitSystem from otko.viewmodels import ProjectViewModel from otko.views.dialogs.section_forms import FORM_REGISTRY, form_for @@ -118,13 +119,23 @@ class SectionLibraryDialog(QDialog): if not isinstance(sec, FiberSection) or self._vm.project is None: return mat_ids = [m.id for m in self._vm.project.materials] - dlg = FiberSectionEditor(mat_ids, existing=sec, parent=self) + dlg = FiberSectionEditor( + mat_ids, + existing=sec, + parent=self, + units=self._vm.project.meta.units, + ) if dlg.exec() != QDialog.DialogCode.Accepted: return updated = dlg.result_section().model_copy(update={"id": sec.id}) self._vm.apply_command(UpdateSectionCommand(self._vm, updated)) self._select_by_id(sec.id) + def _project_units(self) -> UnitSystem: + """Project display units (Metric default when no project is open).""" + proj = self._vm.project + return proj.meta.units if proj is not None else UnitSystem.METRIC + def _on_row_changed(self, _row: int) -> None: section = self._selected_section() if section is None: @@ -133,7 +144,7 @@ class SectionLibraryDialog(QDialog): old = self._stack.widget(0) self._stack.removeWidget(old) old.deleteLater() - form = form_for(section) + form = form_for(section, units=self._project_units()) self._stack.addWidget(form) self._stack.setCurrentWidget(form) self._type_label.setText(form.type_label) @@ -167,7 +178,7 @@ class SectionLibraryDialog(QDialog): if not ok: return new_id = self._vm.project.next_section_id() - form = FORM_REGISTRY[kind]() + form = FORM_REGISTRY[kind](units=self._project_units()) form._section_id = new_id # type: ignore[attr-defined] try: new_section = form.read() @@ -185,7 +196,11 @@ class SectionLibraryDialog(QDialog): mat_ids = [m.id for m in self._vm.project.materials] new_id = self._vm.project.next_section_id() - dlg = FiberSectionEditor(mat_ids, parent=self) + dlg = FiberSectionEditor( + mat_ids, + parent=self, + units=self._vm.project.meta.units, + ) if dlg.exec() != QDialog.DialogCode.Accepted: return sec = dlg.result_section().model_copy(update={"id": new_id}) diff --git a/src/otko/views/dialogs/uniform_excitation.py b/src/otko/views/dialogs/uniform_excitation.py index b7cc471..f5c7cd7 100644 --- a/src/otko/views/dialogs/uniform_excitation.py +++ b/src/otko/views/dialogs/uniform_excitation.py @@ -23,6 +23,7 @@ from PySide6.QtWidgets import ( from otko.core import Project, UniformExcitationPattern + _DIRECTION_CHOICES: list[tuple[int, str]] = [ (1, "1 — X (horizontal)"), (2, "2 — Y (vertical for ndm=2, lateral for ndm=3)"), @@ -50,12 +51,10 @@ class UniformExcitationDialog(QDialog): def _build_ui(self) -> None: root = QVBoxLayout(self) - root.addWidget( - QLabel( - "UniformExcitation — apply a base ground motion " - "to every free node in the chosen DOF direction." - ) - ) + root.addWidget(QLabel( + "UniformExcitation — apply a base ground motion " + "to every free node in the chosen DOF direction." + )) form = QFormLayout() self._name_edit = QLineEdit("GroundMotion") @@ -85,13 +84,16 @@ class UniformExcitationDialog(QDialog): self._factor_spin.setDecimals(6) self._factor_spin.setSingleStep(0.1) self._factor_spin.setValue(1.0) - self._factor_spin.setToolTip("Extra scale applied on top of the TimeSeries' own factor.") + self._factor_spin.setToolTip( + "Extra scale applied on top of the TimeSeries' own factor." + ) form.addRow("Factor:", self._factor_spin) root.addLayout(form) buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, + QDialogButtonBox.StandardButton.Ok + | QDialogButtonBox.StandardButton.Cancel, ) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) diff --git a/src/otko/views/dock_manager.py b/src/otko/views/dock_manager.py index cd34449..f54db51 100644 --- a/src/otko/views/dock_manager.py +++ b/src/otko/views/dock_manager.py @@ -11,7 +11,7 @@ from __future__ import annotations from contextlib import suppress from PySide6.QtCore import Qt -from PySide6.QtWidgets import QDockWidget, QMessageBox, QTreeWidget, QTreeWidgetItem +from PySide6.QtWidgets import QDockWidget, QMessageBox, QPlainTextEdit, QTreeWidget, QTreeWidgetItem from otko.services.deformation import ( linear_static_auto_scale, @@ -34,7 +34,6 @@ from otko.services.results import ( ) from otko.views.canvas3d.model_renderer import RendererMode from otko.views.docks import ( - ConsoleDock, DeformedShapeView, ForceDiagramView, HysteresisView, @@ -47,6 +46,28 @@ from otko.views.docks import ( TimeHistoryView, ) +#: Force-diagram component → display quantity (mirrors the dock's map). +_DIAGRAM_QUANTITY: dict[str, str] = { + "N": "force", + "V2": "force", + "V3": "force", + "M2": "moment", + "M3": "moment", + "T": "moment", +} + + +def _diagram_units_label(project: object | None, component: ForceComponent) -> str: + """Display unit for a force-diagram component (``""`` when unknown).""" + try: + from otko.core.units import native_label + + units = project.meta.units # type: ignore[attr-defined] + quantity = _DIAGRAM_QUANTITY.get(component.value, "force") + return native_label(units, quantity) + except Exception: + return "" + class DockManager: """Owns dock construction and post-processing result views.""" @@ -104,9 +125,13 @@ class DockManager: self.tabifyDockWidget(props_dock, tree_dock) tree_dock.raise_() - self._console_dock = ConsoleDock(self) - self._console = self._console_dock.view - self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self._console_dock) + self._console = QPlainTextEdit() + self._console.setReadOnly(True) + self._console.setPlaceholderText("Logs and OpenSeesPy output will appear here.") + console_dock = QDockWidget("Console", self) + console_dock.setWidget(self._console) + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, console_dock) + self._console_dock = console_dock self._table = TableDock() table_dock = QDockWidget("Table", self) @@ -119,7 +144,7 @@ class DockManager: results_dock.setWidget(self._results_panel) self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, results_dock) self._results_dock = results_dock - self.tabifyDockWidget(self._console_dock, table_dock) + self.tabifyDockWidget(console_dock, table_dock) self.tabifyDockWidget(table_dock, results_dock) table_dock.raise_() @@ -210,14 +235,6 @@ class DockManager: because the off-screen renderer must be touched from the GUI thread (VTK's Qt-backed render window isn't thread-safe). """ - if not self._canvas.capabilities.animation_export: - QMessageBox.information( - self, - "Export Mode Shape Animation", - "Animation export needs the PyVista backend " - "(Options → Canvas Backend → PyVista (Native)).", - ) - return from PySide6.QtWidgets import QFileDialog path, _sel = QFileDialog.getSaveFileName( @@ -268,14 +285,6 @@ class DockManager: "Run a Static analysis first; force diagrams visualise its element-force output.", ) return - if not self._canvas.capabilities.diagrams: - QMessageBox.information( - self, - "Force Diagram", - "Force diagrams are not available on the Plotly backend yet.\n\n" - "Switch back via Options → Canvas Backend → PyVista (Native).", - ) - return self._tear_down_post_dock() # Pick the component with the largest abs_max as the initial choice @@ -284,7 +293,10 @@ class DockManager: best_comp, best_data = self._best_initial_component() suggested = force_diagram_auto_scale(self._vm.project, best_data) - view = ForceDiagramView(suggested_scale=suggested) + view = ForceDiagramView( + suggested_scale=suggested, + units=self._vm.project.meta.units, + ) # Sync the picker to the auto-chosen best component WITHOUT firing # signals (we'll emit a single render below). view._component.blockSignals(True) @@ -307,7 +319,15 @@ class DockManager: component, ) if self._diagram_renderer is not None: - self._diagram_renderer.render(self._vm.project, data, scale) + self._diagram_renderer.render( + self._vm.project, + data, + scale, + units_label=_diagram_units_label( + self._vm.project, + component, + ), + ) self._canvas.render() def _on_component_changed(component: ForceComponent) -> None: @@ -360,7 +380,7 @@ class DockManager: ) return self._tear_down_post_dock() - view = TimeHistoryView() + view = TimeHistoryView(units=self._vm.project.meta.units) view.set_results(self._latest_results, ndm=self._vm.project.ndm, ndf=self._vm.project.ndf) view.set_available_nodes([n.id for n in self._vm.project.nodes]) dock = QDockWidget("Time-History Plot", self) @@ -380,14 +400,6 @@ class DockManager: "Run a Transient (time-history) analysis first.", ) return - if not self._canvas.capabilities.animation_export: - QMessageBox.information( - self, - "Export Time-History Animation", - "Animation export needs the PyVista backend " - "(Options → Canvas Backend → PyVista (Native)).", - ) - return from pathlib import Path from PySide6.QtWidgets import QFileDialog, QInputDialog @@ -492,7 +504,7 @@ class DockManager: # (or "1/in") rather than hard-coded SI m / N. from otko.core import UnitSystem - units = self._vm.project.meta.units if self._vm.project is not None else UnitSystem.SI_M_N + units = self._vm.project.meta.units if self._vm.project is not None else UnitSystem.METRIC ndf = self._vm.project.ndf if self._vm.project is not None else 6 view = PushoverCurveView(units=units, ndf=ndf) view.set_results(self._latest_results) @@ -522,7 +534,7 @@ class DockManager: spectrum = next((s for s in self._vm.project.spectra if s.id == case.spectrum_id), None) self._tear_down_post_dock() - view = ResponseSpectrumView() + view = ResponseSpectrumView(units=self._vm.project.meta.units) view.set_results(self._latest_results, spectrum) dock = QDockWidget("Response Spectrum", self) dock.setWidget(view) diff --git a/src/otko/views/docks/__init__.py b/src/otko/views/docks/__init__.py index c479a97..5b292c3 100644 --- a/src/otko/views/docks/__init__.py +++ b/src/otko/views/docks/__init__.py @@ -1,6 +1,5 @@ """Reusable dock widget contents.""" -from otko.views.docks.console import ConsoleDock from otko.views.docks.deformed_shape import DeformedShapeView from otko.views.docks.force_diagram import ForceDiagramView from otko.views.docks.hysteresis import HysteresisView @@ -13,15 +12,8 @@ from otko.views.docks.table_dock import TableDock from otko.views.docks.time_history import TimeHistoryView __all__ = [ - "PropertyEditorDock", - "ResultsPanel", - "TableDock", - "DeformedShapeView", - "ModeShapeAnimator", - "ForceDiagramView", - "TimeHistoryView", - "HysteresisView", - "PushoverCurveView", - "ResponseSpectrumView", - "ConsoleDock", + "PropertyEditorDock", "ResultsPanel", "TableDock", + "DeformedShapeView", "ModeShapeAnimator", + "ForceDiagramView", "TimeHistoryView", "HysteresisView", + "PushoverCurveView", "ResponseSpectrumView", ] diff --git a/src/otko/views/docks/console.py b/src/otko/views/docks/console.py deleted file mode 100644 index e00308c..0000000 --- a/src/otko/views/docks/console.py +++ /dev/null @@ -1,108 +0,0 @@ -# Ported from otko-development (MIT), (c) 2026 OTKO contributors -"""The console dock: the timestamped analysis log.""" - -from __future__ import annotations - -from datetime import datetime - -from PySide6.QtGui import QFont, QFontDatabase -from PySide6.QtWidgets import ( - QApplication, - QDockWidget, - QHBoxLayout, - QPlainTextEdit, - QPushButton, - QVBoxLayout, - QWidget, -) - -__all__ = ["ConsoleDock"] - -#: Recognised severities; anything else is treated as information. -_SEVERITIES = ("info", "warning", "error", "success") - - -class ConsoleDock(QDockWidget): - """Show analysis log lines with a timestamp and severity label. - - The dock never decides anything about the model: it only renders text the - viewmodel or worker emits. Lines are appended, so the log is a faithful - running record of the session. - """ - - def __init__(self, parent: QWidget | None = None) -> None: - """Create the console dock with a read-only, monospaced log view.""" - super().__init__("Console", parent) - self.setObjectName("consoleDock") - - self._view = QPlainTextEdit(self) - self._view.setReadOnly(True) - self._view.setObjectName("consoleView") - self._view.setMaximumBlockCount(5000) - self._view.setPlaceholderText("Logs and OpenSeesPy output will appear here.") - self._view.setFont(self._monospace_font()) - - clear_button = QPushButton("Clear", self) - clear_button.setToolTip("Clear the console log") - clear_button.setStatusTip("Clear the console log") - clear_button.clicked.connect(self.clear) - copy_button = QPushButton("Copy All", self) - copy_button.setToolTip("Copy the whole log to the clipboard") - copy_button.setStatusTip("Copy the whole log to the clipboard") - copy_button.clicked.connect(self._copy_all) - - buttons = QHBoxLayout() - buttons.addStretch(1) - buttons.addWidget(copy_button) - buttons.addWidget(clear_button) - - layout = QVBoxLayout() - layout.setContentsMargins(4, 4, 4, 4) - layout.setSpacing(4) - layout.addWidget(self._view, 1) - layout.addLayout(buttons) - - container = QWidget(self) - container.setLayout(layout) - self.setWidget(container) - - # -- API ----------------------------------------------------------- - - def log(self, message: str, severity: str = "info") -> None: - """Append ``message`` with a timestamp and ``severity`` tag. - - Every line carries a wall-clock time and a severity so a failure is - easy to spot after a run. - """ - tag = severity if severity in _SEVERITIES else "info" - stamp = datetime.now().strftime("%H:%M:%S") - self._view.appendPlainText(f"{stamp} [{tag.upper():<7}] {message}") - - def clear(self) -> None: - """Remove every line from the log.""" - self._view.clear() - - @property - def text(self) -> str: - """Return the full log text (used by tests and copy-all).""" - return self._view.toPlainText() - - @property - def view(self) -> QPlainTextEdit: - """Return the inner text view (for legacy append-only call sites).""" - return self._view - - # -- Internals ----------------------------------------------------- - - def _copy_all(self) -> None: - """Copy the whole log to the clipboard.""" - clipboard = QApplication.clipboard() - if clipboard is not None: - clipboard.setText(self.text) - - @staticmethod - def _monospace_font() -> QFont: - """Return a fixed-width font so timestamps align.""" - font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) - font.setPointSize(10) - return font diff --git a/src/otko/views/docks/deformed_shape.py b/src/otko/views/docks/deformed_shape.py index de89664..3745c64 100644 --- a/src/otko/views/docks/deformed_shape.py +++ b/src/otko/views/docks/deformed_shape.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( @@ -31,7 +31,8 @@ class DeformedShapeView(QWidget): scaleChanged = Signal(float) closed = Signal() - def __init__(self, suggested_scale: float = 1.0, parent: QWidget | None = None) -> None: + def __init__(self, suggested_scale: float = 1.0, + parent: QWidget | None = None) -> None: super().__init__(parent) self._suggested = max(suggested_scale, 1e-6) self._build_ui() @@ -40,9 +41,9 @@ class DeformedShapeView(QWidget): layout = QVBoxLayout(self) layout.setContentsMargins(8, 8, 8, 8) layout.addWidget(QLabel("Deformed Shape")) - layout.addWidget( - QLabel("The slider scales displacement around the suggested factor.") - ) + layout.addWidget(QLabel( + "The slider scales displacement around the suggested factor." + )) form = QFormLayout() @@ -52,8 +53,8 @@ class DeformedShapeView(QWidget): # Multiplier ranging 0.1× — 10× the suggested scale. self._slider = QSlider(Qt.Orientation.Horizontal) - self._slider.setRange(1, 1000) # represents 0.01 — 10.00 - self._slider.setValue(100) # 1.00 × + self._slider.setRange(1, 1000) # represents 0.01 — 10.00 + self._slider.setValue(100) # 1.00 × self._slider.valueChanged.connect(self._on_slider) self._spin = QDoubleSpinBox() diff --git a/src/otko/views/docks/force_diagram.py b/src/otko/views/docks/force_diagram.py index a19e99d..f2e1f26 100644 --- a/src/otko/views/docks/force_diagram.py +++ b/src/otko/views/docks/force_diagram.py @@ -10,6 +10,8 @@ Exposes two signals: from __future__ import annotations +import math + from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( QComboBox, @@ -23,8 +25,19 @@ from PySide6.QtWidgets import ( QWidget, ) +from otko.core.units import UnitSystem, native_label from otko.services.element_forces import ForceComponent +#: ForceDiagram component → display quantity (plan §5 taxonomy keys). +_COMPONENT_QUANTITY: dict[str, str] = { + "N": "force", + "V2": "force", + "V3": "force", + "M2": "moment", + "M3": "moment", + "T": "moment", +} + class ForceDiagramView(QWidget): """Compact controls for live-updating an element-force diagram.""" @@ -33,12 +46,30 @@ class ForceDiagramView(QWidget): changed = Signal(object, float) # (ForceComponent, scale) closed = Signal() - def __init__(self, suggested_scale: float = 1.0, parent: QWidget | None = None) -> None: + def __init__( + self, + suggested_scale: float = 1.0, + parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, + ) -> None: super().__init__(parent) + self._units = units self._scale_base = max(suggested_scale, 1e-12) self._pending_component_change = False self._build_ui(suggested_scale) + def set_units(self, units: UnitSystem, ndf: int | None = None) -> None: + """Re-apply the project's stored units to component labels. + + Values stay native; only human-facing labels change. Never throws. + """ + try: + self._units = units + self._refresh_component_labels() + except Exception: + pass + # ── public API ────────────────────────────────────────────────── def set_scale_base(self, suggested: float) -> None: """Replace the slider's reference scale (called when the host @@ -61,6 +92,34 @@ class ForceDiagramView(QWidget): def current_component(self) -> ForceComponent: return self._component.currentData() + def _component_text(self, comp: ForceComponent) -> str: + """Component name with its display unit, e.g. ``"M3 (kN·m)"``.""" + try: + quantity = _COMPONENT_QUANTITY.get(comp.value, "force") + unit = native_label(self._units, quantity) + return f"{comp.value} ({unit})" + except Exception: + return comp.value + + def _refresh_component_labels(self) -> None: + """Rewrite combo texts for the current units, keeping selection.""" + try: + current = self._component.currentData() + except Exception: + return + self._component.blockSignals(True) + try: + for i in range(self._component.count()): + comp = self._component.itemData(i) + if isinstance(comp, ForceComponent): + self._component.setItemText(i, self._component_text(comp)) + if current is not None: + idx = self._component.findData(current) + if idx >= 0: + self._component.setCurrentIndex(idx) + finally: + self._component.blockSignals(False) + # ── UI build ──────────────────────────────────────────────────── def _build_ui(self, suggested_scale: float) -> None: root = QVBoxLayout(self) @@ -70,7 +129,7 @@ class ForceDiagramView(QWidget): self._component = QComboBox() for comp in ForceComponent: - self._component.addItem(comp.value, comp) + self._component.addItem(self._component_text(comp), comp) form.addRow("Component:", self._component) self._scale_spin = QDoubleSpinBox() diff --git a/src/otko/views/docks/hysteresis.py b/src/otko/views/docks/hysteresis.py index c8f0a79..6204f88 100644 --- a/src/otko/views/docks/hysteresis.py +++ b/src/otko/views/docks/hysteresis.py @@ -15,7 +15,6 @@ For 2D it's [N1, Vy1, Mz1, N2, Vy2, Mz2]. from __future__ import annotations -import contextlib from typing import Any import pyqtgraph as pg @@ -31,6 +30,7 @@ from PySide6.QtWidgets import ( QWidget, ) +from otko.core.units import UnitSystem, native_label from otko.services.results import TransientResults from otko.views.docks._labels import dof_symbol @@ -48,14 +48,51 @@ class HysteresisView(QWidget): closed = Signal() - def __init__(self, parent: QWidget | None = None) -> None: + def __init__( + self, + parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, + ) -> None: super().__init__(parent) + self._units = units self._results: TransientResults | None = None self._ndm: int | None = None self._ndf: int | None = None self._curve: Any | None = None self._build_ui() + def set_units(self, units: UnitSystem, ndf: int | None = None) -> None: + """Re-apply stored units to axis labels. Values stay native.""" + try: + self._units = units + if ndf is not None: + self._ndf = int(ndf) + except Exception: + pass + + def _axis_unit(self, kind: str) -> str: + """Display unit for an axis kind.""" + key = { + "node_disp": "displacement", + "node_vel": "velocity", + "node_accel": "acceleration", + "element_force": "force", + "disp": "displacement", + }.get(kind, "displacement") + # Moment components (M/T) carry moment units. + if kind == "element_force": + try: + comp = self._y_component.currentData() or "" + if str(comp).startswith(("M", "T")): + return native_label(self._units, "moment") + except Exception: + pass + try: + return native_label(self._units, key) + except KeyError: + return "" + # ── public API ────────────────────────────────────────────────── def set_results( self, results: TransientResults | None, *, ndm: int | None = None, ndf: int | None = None @@ -252,9 +289,14 @@ class HysteresisView(QWidget): y_vals[:n], pen=pg.mkPen("#1f77b4", width=2), ) + x_unit = self._axis_unit("disp") self._plot.setLabel( "bottom", - f"N{self._x_node.currentData()} {self._dof_symbol(self._x_dof.value(), x_dofs)} disp", + f"N{self._x_node.currentData()} {self._dof_symbol(self._x_dof.value(), x_dofs)} " + f"disp ({x_unit})" + if x_unit + else f"N{self._x_node.currentData()} " + f"{self._dof_symbol(self._x_dof.value(), x_dofs)} disp", ) self._plot.setLabel("left", self._y_label(y_dofs)) @@ -328,16 +370,20 @@ class HysteresisView(QWidget): def _y_label(self, n_dofs: int) -> str: kind = self._y_kind.currentData() + unit = self._axis_unit(kind or "") + suffix = f" ({unit})" if unit else "" if kind == "element_force": - return f"E{self._y_element.currentData()} {self._y_component.currentData()}" + return f"E{self._y_element.currentData()} {self._y_component.currentData()}{suffix}" kind_label = {"node_disp": "disp", "node_vel": "vel", "node_accel": "accel"}.get( kind, "value" ) symbol = self._dof_symbol(self._y_dof.value(), n_dofs) - return f"N{self._y_node.currentData()} {symbol} {kind_label}" + return f"N{self._y_node.currentData()} {symbol} {kind_label}{suffix}" def _clear_curve(self) -> None: if self._curve is not None: - with contextlib.suppress(Exception): + try: self._plot.removeItem(self._curve) + except Exception: + pass self._curve = None diff --git a/src/otko/views/docks/property_editor.py b/src/otko/views/docks/property_editor.py index b6c5ed0..db5f6f9 100644 --- a/src/otko/views/docks/property_editor.py +++ b/src/otko/views/docks/property_editor.py @@ -8,8 +8,9 @@ dumb: no direct Project mutation, no Qt ↔ OpenSees coupling. from __future__ import annotations -from collections.abc import Callable +from typing import Callable +from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QComboBox, QDoubleSpinBox, @@ -24,6 +25,8 @@ from PySide6.QtWidgets import ( ) from otko.core import Project +from otko.core.units import UnitSystem, native_label + # Element types the Properties dock lets the user switch between. # "ElasticBeamColumn" needs a section; "Truss" / "CorotTruss" need a @@ -115,6 +118,26 @@ class PropertyEditorDock(QScrollArea): self._layout.addWidget(lbl) self._layout.addStretch(1) + def _units(self) -> UnitSystem: + """Return the bound project's unit system (live, per refresh).""" + try: + if self._project is not None: + return self._project.meta.units + except AttributeError: + pass + return UnitSystem.METRIC + + def _unit_suffix(self, quantity: str) -> str: + """Return ``" (unit)"`` for a quantity, or ``""`` when unknown. + + Unit hints only — values are shown in native stored units, + never converted. + """ + try: + return f" ({native_label(self._units(), quantity)})" + except KeyError: + return "" + def _show_node(self, node_id: int) -> None: try: node = self._project.node(node_id) # type: ignore[union-attr] @@ -125,7 +148,7 @@ class PropertyEditorDock(QScrollArea): form = QFormLayout() form.addRow("Name:", QLabel(node.name or "—")) form.addRow( - "X, Y, Z:", + f"X, Y, Z{self._unit_suffix('length')}:", QLabel( f"{node.coords[0]:.4f}, {node.coords[1]:.4f}, {node.coords[2]:.4f}", ), @@ -133,19 +156,33 @@ class PropertyEditorDock(QScrollArea): form.addRow("Restraint:", QLabel(self._fmt_restraint(node.restraint))) self._layout.addLayout(form) - # ── Mass editor ── + # ── Mass editor (unit hints only — native stored values) ── group = QGroupBox("Lumped mass") gform = QFormLayout(group) self._mass_spins: list[QDoubleSpinBox] = [] - labels = ("mx", "my", "mz", "Ixx", "Iyy", "Izz") - for i, lbl in enumerate(labels): + mass_suffix = self._unit_suffix("mass") + try: + mass_u = native_label(self._units(), "mass") + length_u = native_label(self._units(), "length") + rot_suffix = f" ({mass_u}·{length_u}²)" if mass_u and length_u else "" + except KeyError: + rot_suffix = "" + labels = ( + ("mx", mass_suffix), + ("my", mass_suffix), + ("mz", mass_suffix), + ("Ixx", rot_suffix), + ("Iyy", rot_suffix), + ("Izz", rot_suffix), + ) + for i, (lbl, suffix) in enumerate(labels): sb = QDoubleSpinBox() sb.setRange(0.0, 1e12) sb.setDecimals(4) sb.setSingleStep(100.0) sb.setValue(float(node.mass[i])) self._mass_spins.append(sb) - gform.addRow(f"{lbl}:", sb) + gform.addRow(f"{lbl}{suffix}:", sb) btn_row = QHBoxLayout() btn_row.addStretch(1) @@ -274,9 +311,12 @@ class PropertyEditorDock(QScrollArea): self.on_change_element_fields(_eid, {"area": new_val}) area_spin.editingFinished.connect(_on_area_edited) - form.addRow("Area:", area_spin) + form.addRow(f"Area{self._unit_suffix('area')}:", area_spin) elif hasattr(el, "area"): - form.addRow("Area:", QLabel(f"{el.area:g}")) + form.addRow( + f"Area{self._unit_suffix('area')}:", + QLabel(f"{el.area:g}"), + ) if hasattr(el, "geom_transf"): form.addRow("Geom transf:", QLabel(el.geom_transf)) diff --git a/src/otko/views/docks/pushover_curve.py b/src/otko/views/docks/pushover_curve.py index 86beff2..4bc5c6f 100644 --- a/src/otko/views/docks/pushover_curve.py +++ b/src/otko/views/docks/pushover_curve.py @@ -26,7 +26,7 @@ from PySide6.QtWidgets import ( QWidget, ) -from otko.core import UnitSystem, labels_for +from otko.core import UnitLabels, UnitSystem, labels_for from otko.services.results import PushoverResults if TYPE_CHECKING: @@ -67,7 +67,7 @@ class PushoverCurveView(QWidget): def __init__( self, - units: UnitSystem = UnitSystem.SI_M_N, + units: UnitSystem = UnitSystem.METRIC, ndf: int = 6, parent: QWidget | None = None, ) -> None: diff --git a/src/otko/views/docks/response_spectrum.py b/src/otko/views/docks/response_spectrum.py index 7664171..2e0a995 100644 --- a/src/otko/views/docks/response_spectrum.py +++ b/src/otko/views/docks/response_spectrum.py @@ -8,7 +8,7 @@ Two panels: from __future__ import annotations import pyqtgraph as pg -from PySide6.QtCore import Qt, Signal +from PySide6.QtCore import Signal from PySide6.QtWidgets import ( QHBoxLayout, QHeaderView, @@ -20,8 +20,10 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) +from PySide6.QtCore import Qt from otko.core import ResponseSpectrum +from otko.core.units import UnitSystem, native_label from otko.services.results import ResponseSpectrumResults @@ -30,15 +32,71 @@ class ResponseSpectrumView(QWidget): closed = Signal() - def __init__(self, parent: QWidget | None = None) -> None: + def __init__( + self, + parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, + ) -> None: super().__init__(parent) + self._units = units + self._results: ResponseSpectrumResults | None = None + self._spectrum: ResponseSpectrum | None = None self._build_ui() + def set_units(self, units: UnitSystem, ndf: int | None = None) -> None: + """Re-apply stored units to the Sa axis. Values stay native.""" + try: + self._units = units + self._apply_axis_labels() + self._refresh_table_headers() + if self._results is not None: + self.set_results(self._results, self._spectrum) + except Exception: + pass + + def _accel_unit(self) -> str: + try: + return native_label(self._units, "acceleration") + except KeyError: + return "" + + def _refresh_table_headers(self) -> None: + """Set mass-table headers with the Sa display unit.""" + try: + unit = self._accel_unit() + sa_col = f"Sa(T) ({unit})" if unit else "Sa(T)" + self._table.setHorizontalHeaderLabels( + [ + "Mode", + "T (s)", + "f (Hz)", + "Γ", + "M_eff", + "Mass %", + sa_col, + ] + ) + except Exception: + pass + + def _apply_axis_labels(self) -> None: + try: + unit = self._accel_unit() + label = f"Sa ({unit})" if unit else "Sa" + self._plot.setLabel("left", label) + self._plot.setLabel("bottom", "Period", units="s") + except Exception: + pass + def set_results( self, results: ResponseSpectrumResults | None, spectrum: ResponseSpectrum | None, ) -> None: + self._results = results + self._spectrum = spectrum + self._apply_axis_labels() self._plot.clear() self._table.setRowCount(0) if results is None: @@ -142,17 +200,7 @@ class ResponseSpectrumView(QWidget): splitter.addWidget(self._plot) self._table = QTableWidget(0, 7) - self._table.setHorizontalHeaderLabels( - [ - "Mode", - "T (s)", - "f (Hz)", - "Γ", - "M_eff", - "Mass %", - "Sa(T)", - ] - ) + self._refresh_table_headers() self._table.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeMode.Stretch, ) diff --git a/src/otko/views/docks/results_panel.py b/src/otko/views/docks/results_panel.py index 9384e9a..27ff18d 100644 --- a/src/otko/views/docks/results_panel.py +++ b/src/otko/views/docks/results_panel.py @@ -20,7 +20,7 @@ from PySide6.QtWidgets import ( QWidget, ) -from otko.services.combinations import EnvelopeResults +from otko.core.units import UnitSystem, native_label from otko.services.results import ( ModalResults, StaticResults, @@ -32,12 +32,46 @@ from otko.views.docks._labels import dof_labels class ResultsPanel(QWidget): """Tabbed view that swaps based on the type of results received.""" - def __init__(self, parent: QWidget | None = None) -> None: + def __init__( + self, + parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, + ) -> None: super().__init__(parent) + self._units = units self._ndm: int | None = None self._ndf: int | None = None + self._last_results: Any | None = None self._build_ui() + def set_units(self, units: UnitSystem, ndf: int | None = None) -> None: + """Re-apply stored units to table headers. Values stay native.""" + try: + self._units = units + if ndf is not None: + self._ndf = int(ndf) + if self._last_results is not None: + self.show_results( + self._last_results, + ndm=self._ndm, + ndf=self._ndf, + ) + except Exception: + pass + + def _disp_unit(self) -> str: + try: + return native_label(self._units, "displacement") + except KeyError: + return "" + + def _force_unit(self) -> str: + try: + return native_label(self._units, "force") + except KeyError: + return "" + def _build_ui(self) -> None: layout = QVBoxLayout(self) layout.setContentsMargins(4, 4, 4, 4) @@ -59,19 +93,9 @@ class ResultsPanel(QWidget): (see :mod:`otko.views.docks._labels`). """ self._ndm, self._ndf = ndm, ndf + self._last_results = results self._tabs.clear() - if isinstance(results, EnvelopeResults): - self._title.setText( - f"Combination (Envelope) — #{results.case_id} " - f"'{results.case_name}' ({results.n_steps} step(s))" - ) - self._tabs.addTab( - self._build_envelope_table(results, "disp"), "Displacements (max/min)" - ) - self._tabs.addTab( - self._build_envelope_table(results, "reaction"), "Reactions (max/min)" - ) - elif isinstance(results, StaticResults): + if isinstance(results, StaticResults): self._title.setText( f"Static — case #{results.case_id} '{results.case_name}' " f"({results.n_steps} step(s))" @@ -96,49 +120,35 @@ class ResultsPanel(QWidget): if not rows: return self._empty_table_widget() ndf = r.node_disp[rows[0]].shape[1] - headers = ["Node", *dof_labels(ndf, self._ndm, self._ndf, kind="disp")] + base = dof_labels(ndf, self._ndm, self._ndf, kind="disp") + unit = self._disp_unit() + headers = ["Node", *[f"{h} ({unit})" if unit else h for h in base]] table = self._make_table(headers, len(rows)) for i, nid in enumerate(rows): self._set_cell(table, i, 0, str(nid)) last_step = r.node_disp[nid][-1] for j, val in enumerate(last_step): self._set_cell(table, i, j + 1, f"{val:.6g}") - return self._wrap(table, "Final-step displacements") + unit = self._disp_unit() + caption = f"Final-step displacements ({unit})" if unit else "Final-step displacements" + return self._wrap(table, caption) def _build_static_reaction_table(self, r: StaticResults) -> QWidget: rows = sorted(r.node_reaction.keys()) if not rows: return self._empty_table_widget() ndf = r.node_reaction[rows[0]].shape[1] - headers = ["Node", *dof_labels(ndf, self._ndm, self._ndf, kind="force")] + base = dof_labels(ndf, self._ndm, self._ndf, kind="force") + unit = self._force_unit() + headers = ["Node", *[f"{h} ({unit})" if unit else h for h in base]] table = self._make_table(headers, len(rows)) for i, nid in enumerate(rows): self._set_cell(table, i, 0, str(nid)) last_step = r.node_reaction[nid][-1] for j, val in enumerate(last_step): self._set_cell(table, i, j + 1, f"{val:.6g}") - return self._wrap(table, "Final-step reactions") - - def _build_envelope_table(self, r: EnvelopeResults, kind: str) -> QWidget: - """Max/min table for envelope ``kind`` (``"disp"`` or ``"reaction"``).""" - max_map = r.node_disp_max if kind == "disp" else r.node_reaction_max - min_map = r.node_disp_min if kind == "disp" else r.node_reaction_min - rows = sorted(max_map.keys()) - if not rows: - return self._empty_table_widget() - ndf = max_map[rows[0]].shape[1] - dofs = dof_labels(ndf, self._ndm, self._ndf, kind="disp" if kind == "disp" else "force") - headers = ["Node", *[f"{d} max" for d in dofs], *[f"{d} min" for d in dofs]] - table = self._make_table(headers, len(rows)) - for i, nid in enumerate(rows): - self._set_cell(table, i, 0, str(nid)) - for j, val in enumerate(max_map[nid][-1]): - self._set_cell(table, i, j + 1, f"{val:.6g}") - for j, val in enumerate(min_map[nid][-1]): - self._set_cell(table, i, j + 1 + ndf, f"{val:.6g}") - caption = ( - "Final-step displacement envelope" if kind == "disp" else "Final-step reaction envelope" - ) + unit = self._force_unit() + caption = f"Final-step reactions ({unit})" if unit else "Final-step reactions" return self._wrap(table, caption) def _build_modal_table(self, r: ModalResults) -> QWidget: diff --git a/src/otko/views/docks/table_dock.py b/src/otko/views/docks/table_dock.py index 3af8383..dee7152 100644 --- a/src/otko/views/docks/table_dock.py +++ b/src/otko/views/docks/table_dock.py @@ -35,7 +35,6 @@ from PySide6.QtWidgets import ( from otko.commands import ( AddAnalysisCasesCommand, - AddCombinationsCommand, AddElementLoadsCommand, AddElementsCommand, AddEqualDOFConstraintCommand, @@ -48,7 +47,6 @@ from otko.commands import ( AssignMaterialCommand, AssignSectionCommand, DeleteAnalysisCasesCommand, - DeleteCombinationsCommand, DeleteElementLoadsCommand, DeleteElementsCommand, DeleteEqualDOFConstraintCommand, @@ -61,7 +59,6 @@ from otko.commands import ( SetMassCommand, SetRestraintCommand, UpdateAnalysisCaseCommand, - UpdateCombinationCommand, UpdateElementFieldsCommand, UpdateElementLoadCommand, UpdateEqualDOFConstraintCommand, @@ -78,8 +75,6 @@ from otko.core import ( ElasticUniaxial, EqualDOFConstraint, LinearTimeSeries, - LoadCombination, - LoadCombinationItem, NodalLoad, Node, PlainLoadPattern, @@ -87,6 +82,7 @@ from otko.core import ( TrussElement, UniformElementLoad, ) +from otko.core.units import UnitSystem, native_label from otko.views.docks._labels import dof_labels _MASS_LABELS = ("mx", "my", "mz", "Ixx", "Iyy", "Izz") @@ -174,7 +170,6 @@ _ANALYSIS_ATTRS = [ "tolerance", "max_iter", ] -_COMBO_HEADERS = ["id", "name", "kind", "cases", "factors"] _TAB_ORDER = ( "Nodes", @@ -187,7 +182,6 @@ _TAB_ORDER = ( "Element Loads", "Constraints", "Analyses", - "Combinations", ) @@ -197,7 +191,7 @@ def _fmt(value: Any) -> str: return "—" if isinstance(value, float): return f"{value:g}" - if isinstance(value, list | tuple): + if isinstance(value, (list, tuple)): return ", ".join(_fmt(v) for v in value) return str(value) @@ -277,7 +271,6 @@ class TableDock(QWidget): "Element Loads": self._load_element_loads, "Constraints": self._load_constraints, "Analyses": self._load_analyses, - "Combinations": self._load_combinations, } for name in _TAB_ORDER: page = QWidget() @@ -328,6 +321,36 @@ class TableDock(QWidget): self._project = project self.refresh() + def set_units(self, units: UnitSystem, ndf: int | None = None) -> None: + """Refresh headers for a units change. Values stay native.""" + try: + self.refresh() + except Exception: + pass + + def _units(self) -> UnitSystem: + """Project's stored units, defaulting to Metric.""" + try: + if self._project is not None: + return self._project.meta.units # type: ignore[no-any-return] + except Exception: + pass + return UnitSystem.METRIC + + def _u(self, quantity: str) -> str: + """Display label for a quantity, or ``""`` when dimensionless.""" + try: + return native_label(self._units(), quantity) + except KeyError: + return "" + + def _suffixed(self, name: str, quantity: str | None) -> str: + """Column name with its display-unit suffix (bare when None).""" + if quantity is None: + return name + unit = self._u(quantity) + return f"{name} ({unit})" if unit else name + def refresh(self) -> None: """Repaint every tab from the current project (``modelMutated``). @@ -425,7 +448,6 @@ class TableDock(QWidget): "Element Loads": self._commit_element_load, "Constraints": self._commit_constraint, "Analyses": self._commit_analysis, - "Combinations": self._commit_combination, }[tab] handler(self._tables[tab], item) except (ValueError, KeyError, IndexError) as exc: @@ -496,12 +518,20 @@ class TableDock(QWidget): # ── Nodes ────────────────────────────────────────────────────── def _load_nodes(self, table: QTableWidget) -> None: nodes = list(self._project.nodes) if self._project is not None else [] - table.setColumnCount(len(_NODE_HEADERS)) - table.setHorizontalHeaderLabels(_NODE_HEADERS) + headers = [ + self._suffixed(h, {"x": "length", "y": "length", "z": "length"}.get(h)) + if h in ("x", "y", "z") + else self._suffixed(h, "mass") + if h in ("mx", "my", "mz", "Ixx", "Iyy", "Izz") + else h + for h in _NODE_HEADERS + ] + table.setColumnCount(len(headers)) + table.setHorizontalHeaderLabels(headers) table.setRowCount(len(nodes)) for r, n in enumerate(nodes): self._put(table, r, 0, str(n.id), editable=False, data=n.id) - self._put(table, r, 1, n.name, data=n.id) + self._put(table, r, 1, n.display_name, data=n.id) for c, v in enumerate(n.coords, start=2): self._put(table, r, c, f"{v:g}", data=n.id) for c, v in enumerate(n.mass, start=5): @@ -515,6 +545,12 @@ class TableDock(QWidget): nid = self._row_key(table, row) node = project.node(nid) if col == 1: + if not node.name.strip() and item.text().strip() == node.display_name: + # Confirmed the displayed fallback unchanged — not a real + # rename, so leave the stored name blank instead of + # persisting "N" (and keep it off the undo stack). + self.refresh() + return updated = self._dry_run(node, {"name": item.text()}) self._dispatch(UpdateNodeFieldsCommand(vm, nid, {"name": updated.name})) elif 2 <= col <= 4: @@ -546,7 +582,7 @@ class TableDock(QWidget): table.setRowCount(len(elements)) for r, el in enumerate(elements): self._put(table, r, 0, str(el.id), editable=False, data=el.id) - self._put(table, r, 1, el.name, data=el.id) + self._put(table, r, 1, el.display_name, data=el.id) self._put(table, r, 2, el.type, editable=False, data=el.id) self._put(table, r, 3, ", ".join(str(n) for n in el.nodes), data=el.id) self._put( @@ -592,6 +628,12 @@ class TableDock(QWidget): eid = self._row_key(table, row) el = project.element(eid) if col == 1: + if not el.name.strip() and item.text().strip() == el.display_name: + # Confirmed the displayed fallback unchanged — not a real + # rename, so leave the stored name blank instead of + # persisting "E" (and keep it off the undo stack). + self.refresh() + return updated = self._dry_run(el, {"name": item.text()}) self._dispatch(UpdateElementFieldsCommand(vm, eid, {"name": updated.name})) elif col == 3: @@ -625,8 +667,12 @@ class TableDock(QWidget): # ── Materials ────────────────────────────────────────────────── def _load_materials(self, table: QTableWidget) -> None: materials = list(self._project.materials) if self._project is not None else [] - table.setColumnCount(len(_MATERIAL_HEADERS)) - table.setHorizontalHeaderLabels(_MATERIAL_HEADERS) + headers_m = [ + self._suffixed(h, "stress") if h in ("E", "Fy", "E0", "fpc", "fpcu", "ft") else h + for h in _MATERIAL_HEADERS + ] + table.setColumnCount(len(headers_m)) + table.setHorizontalHeaderLabels(headers_m) table.setRowCount(len(materials)) for r, m in enumerate(materials): self._put(table, r, 0, str(m.id), editable=False, data=m.id) @@ -659,8 +705,20 @@ class TableDock(QWidget): # ── Sections ─────────────────────────────────────────────────── def _load_sections(self, table: QTableWidget) -> None: sections = list(self._project.sections) if self._project is not None else [] - table.setColumnCount(len(_SECTION_HEADERS)) - table.setHorizontalHeaderLabels(_SECTION_HEADERS) + headers_s = [ + self._suffixed(h, "stress") + if h in ("E", "G") + else self._suffixed(h, "area") + if h == "A" + else self._suffixed(h, "inertia") + if h in ("Iz", "Iy", "J") + else self._suffixed(h, "section_dim") + if h in ("b", "h", "d", "tw", "tf", "diameter") + else h + for h in _SECTION_HEADERS + ] + table.setColumnCount(len(headers_s)) + table.setHorizontalHeaderLabels(headers_s) table.setRowCount(len(sections)) for r, s in enumerate(sections): self._put(table, r, 0, str(s.id), editable=False, data=s.id) @@ -863,8 +921,16 @@ class TableDock(QWidget): continue for i, load in enumerate(p.nodal_loads): rows.append((p.id, i, load)) - table.setColumnCount(len(_NODAL_LOAD_HEADERS)) - table.setHorizontalHeaderLabels(_NODAL_LOAD_HEADERS) + headers = [ + self._suffixed(h, "force") + if h.startswith("F") + else self._suffixed(h, "moment") + if h.startswith("M") + else h + for h in _NODAL_LOAD_HEADERS + ] + table.setColumnCount(len(headers)) + table.setHorizontalHeaderLabels(headers) table.setRowCount(len(rows)) for r, (pid, i, load) in enumerate(rows): self._put(table, r, 0, str(pid), editable=False, data=(pid, i)) @@ -884,8 +950,12 @@ class TableDock(QWidget): continue for i, load in enumerate(p.element_loads): rows.append((p.id, i, load)) - table.setColumnCount(len(_ELEMENT_LOAD_HEADERS)) - table.setHorizontalHeaderLabels(_ELEMENT_LOAD_HEADERS) + headers_el = [ + self._suffixed(h, "distributed_load") if h in ("wx", "wy", "wz") else h + for h in _ELEMENT_LOAD_HEADERS + ] + table.setColumnCount(len(headers_el)) + table.setHorizontalHeaderLabels(headers_el) table.setRowCount(len(rows)) for r, (pid, i, load) in enumerate(rows): self._put(table, r, 0, str(pid), editable=False, data=(pid, i)) @@ -1035,7 +1105,7 @@ class TableDock(QWidget): try: key = int(pid) label = name_of.get(key, str(key)) or str(key) - parts.append(f"{label}×{float(factor):g}") + parts.append(f"{label}×{float(factor):g}") # noqa: RUF001 except (TypeError, ValueError): continue return ", ".join(parts) if parts else "—" @@ -1104,49 +1174,6 @@ class TableDock(QWidget): updated = self._dry_run(case, {attr: value}) self._dispatch(UpdateAnalysisCaseCommand(vm, updated)) - # ── Combinations ─────────────────────────────────────────────── - def _load_combinations(self, table: QTableWidget) -> None: - combos = list(self._project.combinations) if self._project is not None else [] - table.setColumnCount(len(_COMBO_HEADERS)) - table.setHorizontalHeaderLabels(_COMBO_HEADERS) - table.setRowCount(len(combos)) - for r, combo in enumerate(combos): - self._put(table, r, 0, str(combo.id), editable=False, data=combo.id) - self._put(table, r, 1, combo.name, data=combo.id) - self._put(table, r, 2, combo.kind, data=combo.id) - self._put( - table, - r, - 3, - ", ".join(str(i.case_id) for i in combo.items), - editable=False, - data=combo.id, - ) - self._put( - table, - r, - 4, - ", ".join(f"{i.factor:g}" for i in combo.items), - editable=False, - data=combo.id, - ) - - def _commit_combination(self, table: QTableWidget, item: QTableWidgetItem) -> None: - vm, project = self._bound() - row, col = item.row(), item.column() - cid = self._row_key(table, row) - combo = next(c for c in project.combinations if c.id == cid) - if col == 1: - updated = self._dry_run(combo, {"name": item.text()}) - elif col == 2: - raw = item.text().strip() - if raw not in ("Linear", "Envelope"): - raise ValueError("kind: expected 'Linear' or 'Envelope'.") - updated = self._dry_run(combo, {"kind": raw}) - else: - return - self._dispatch(UpdateCombinationCommand(vm, updated)) - # ── Add / Delete rows ────────────────────────────────────────── def _require_project(self, tab: str) -> Any | None: if self._vm is None or self._project is None: @@ -1244,16 +1271,6 @@ class TableDock(QWidget): pattern_ids=[project.load_patterns[0].id], ) self._dispatch(AddAnalysisCasesCommand(vm, [case])) - elif tab == "Combinations": - if not project.analyses: - self._say("Combinations: define an analysis case first.") - return - combo = LoadCombination( - id=project.next_combination_id(), - name="New", - items=[LoadCombinationItem(case_id=project.analyses[0].id)], - ) - self._dispatch(AddCombinationsCommand(vm, [combo])) except Exception as exc: label = tab[:-1] if tab.endswith("s") else tab self._say(f"Add {label} failed: {exc}") @@ -1326,8 +1343,6 @@ class TableDock(QWidget): self._dispatch(DeleteLoadPatternCommand(vm, ids)) elif tab == "Analyses": self._dispatch(DeleteAnalysisCasesCommand(vm, ids)) - elif tab == "Combinations": - self._dispatch(DeleteCombinationsCommand(vm, ids)) except Exception as exc: self._say(f"Delete {tab} failed: {exc}") self.refresh() diff --git a/src/otko/views/docks/time_history.py b/src/otko/views/docks/time_history.py index 560dba0..f0a3f88 100644 --- a/src/otko/views/docks/time_history.py +++ b/src/otko/views/docks/time_history.py @@ -10,9 +10,9 @@ runner to record additional series (planned for a later phase). from __future__ import annotations -import contextlib from typing import Any +import numpy as np import pyqtgraph as pg from PySide6.QtCore import Signal from PySide6.QtWidgets import ( @@ -27,9 +27,17 @@ from PySide6.QtWidgets import ( QWidget, ) +from otko.core.units import UnitSystem, native_label from otko.services.results import TransientResults from otko.views.docks._labels import dof_symbol +#: Trace quantity → display quantity key (plan §5 taxonomy). +_QUANTITY_KEY: dict[str, str] = { + "disp": "displacement", + "vel": "velocity", + "accel": "acceleration", +} + # A small palette that reads well on dark + light themes. _COLORS = [ "#1f77b4", @@ -55,14 +63,46 @@ class TimeHistoryView(QWidget): closed = Signal() - def __init__(self, parent: QWidget | None = None) -> None: + def __init__( + self, + parent: QWidget | None = None, + *, + units: UnitSystem = UnitSystem.METRIC, + ) -> None: super().__init__(parent) + self._units = units self._results: TransientResults | None = None self._ndm: int | None = None self._ndf: int | None = None self._traces: list[tuple[int, int, Any]] = [] # (node_id, dof, plot_item) self._build_ui() + def set_units(self, units: UnitSystem, ndf: int | None = None) -> None: + """Re-apply stored units to axis labels. Values stay native.""" + try: + self._units = units + if ndf is not None: + self._ndf = int(ndf) + self._refresh_axis_label() + except Exception: + pass + + def _y_unit(self, quantity: str = "disp") -> str: + """Display unit for a trace quantity (x-axis always ``s``).""" + try: + return native_label(self._units, _QUANTITY_KEY.get(quantity, "displacement")) + except KeyError: + return "" + + def _refresh_axis_label(self) -> None: + """Refresh the y-axis label for the current quantity selection.""" + try: + quantity = self._quantity.currentData() or "disp" + base = {"disp": "Displacement", "vel": "Velocity", "accel": "Acceleration"}[quantity] + self._plot.setLabel("left", f"{base} ({self._y_unit(quantity)})") + except Exception: + pass + # ── public ────────────────────────────────────────────────────── def set_results( self, results: TransientResults | None, *, ndm: int | None = None, ndf: int | None = None @@ -155,7 +195,7 @@ class TimeHistoryView(QWidget): pg.setConfigOptions(antialias=True) self._plot = pg.PlotWidget() self._plot.setBackground("#1e1e1e") - self._plot.setLabel("left", "Displacement") + self._plot.setLabel("left", f"Displacement ({self._y_unit('disp')})") self._plot.setLabel("bottom", "Time", units="s") self._plot.showGrid(x=True, y=True, alpha=0.3) self._plot.addLegend(offset=(8, 8)) @@ -204,11 +244,13 @@ class TimeHistoryView(QWidget): self._trace_list.addItem(QListWidgetItem(label)) # Update y-axis label to reflect what's plotted (last-write-wins). y_label = {"disp": "Displacement", "vel": "Velocity", "accel": "Acceleration"}[quantity] - self._plot.setLabel("left", y_label) + self._plot.setLabel("left", f"{y_label} ({self._y_unit(quantity)})") def _clear_traces(self) -> None: for _, _, item in self._traces: - with contextlib.suppress(Exception): + try: self._plot.removeItem(item) + except Exception: + pass self._traces.clear() self._trace_list.clear() diff --git a/src/otko/views/formatting.py b/src/otko/views/formatting.py deleted file mode 100644 index 1123a08..0000000 --- a/src/otko/views/formatting.py +++ /dev/null @@ -1,97 +0,0 @@ -# Ported from otko-development (MIT), (c) 2026 OTKO contributors -"""Small presentation helpers shared by docks and dialogs. - -Keeps labels, unit hints and numeric parsing in one place so every surface -names a field the same way and rejects bad input with the same message shape. -Pure helpers only — no Qt imports live here. -""" - -from __future__ import annotations - -from otko.core import UnitLabels, UnitSystem, labels_for - -__all__ = [ - "DOF_LABELS", - "MASS_LABELS", - "RESTRAINT_LABELS", - "format_number", - "parse_float", - "parse_int", - "unit_label", -] - -#: Six translational/rotational DOF labels, indexed 0-5. -DOF_LABELS: tuple[str, ...] = ("Ux", "Uy", "Uz", "Rx", "Ry", "Rz") - -#: Six mass-component labels, indexed 0-5. -MASS_LABELS: tuple[str, ...] = ("Mx", "My", "Mz", "Ixx", "Iyy", "Izz") - -#: Human-readable restraint component names (restraint + rotation). -RESTRAINT_LABELS: tuple[str, ...] = ( - "Ux", - "Uy", - "Uz", - "Rx", - "Ry", - "Rz", -) - -#: Map a public quantity name to the :class:`UnitLabels` field that holds it. -_QUANTITY_FIELDS: dict[str, str] = { - "length": "length", - "force": "force", - "moment": "moment", - "stress": "stress", - "curvature": "curvature", - "rotation": "rotation", -} - - -def format_number(value: float) -> str: - """Return a compact, round-trippable string for ``value``. - - Integral floats lose the decimal point; everything else keeps up to six - significant digits so a round-trip through a table cell is exact enough - for engineering values. - """ - if value == int(value): - return str(int(value)) - return f"{value:.6g}" - - -def parse_float(text: str, field: str, *, allow_empty: bool = False) -> float: - """Parse ``text`` as a float or raise ``ValueError`` naming ``field``. - - An empty string is accepted when ``allow_empty`` is set and yields ``0.0`` - so a blank optional cell is not treated as an error. - """ - stripped = text.strip() - if not stripped: - if allow_empty: - return 0.0 - raise ValueError(f"{field} is required") - try: - return float(stripped) - except ValueError: - raise ValueError(f"{field} must be a number, got {stripped!r}") from None - - -def parse_int(text: str, field: str, *, allow_empty: bool = False) -> int: - """Parse ``text`` as an int or raise ``ValueError`` naming ``field``.""" - value = parse_float(text, field, allow_empty=allow_empty) - if value != int(value): - raise ValueError(f"{field} must be a whole number, got {text.strip()!r}") - return int(value) - - -def unit_label(quantity: str, system: UnitSystem) -> str: - """Return the display label for ``quantity`` in ``system``. - - ``quantity`` is one of ``length``, ``force``, ``moment``, ``stress``, - ``curvature`` or ``rotation``; anything else raises ``ValueError``. - """ - field = _QUANTITY_FIELDS.get(quantity) - if field is None: - raise ValueError(f"unknown unit quantity: {quantity!r}") - labels: UnitLabels = labels_for(system) - return getattr(labels, field) diff --git a/src/otko/views/main_window.py b/src/otko/views/main_window.py index 5241869..5577d8b 100644 --- a/src/otko/views/main_window.py +++ b/src/otko/views/main_window.py @@ -13,21 +13,15 @@ The ``projectChanged`` / ``modelMutated`` signals live on the from __future__ import annotations -import json from pathlib import Path -from typing import Any -from PySide6.QtCore import QSettings -from PySide6.QtGui import QCloseEvent, QIcon -from PySide6.QtWidgets import QMainWindow, QMessageBox, QStackedWidget +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import QMainWindow from otko.viewmodels import AnalysisRunner, ProjectViewModel from otko.views.action_handlers import ActionHandlers from otko.views.canvas3d import ModelCanvas from otko.views.canvas3d.diagram_renderer import DiagramRenderer -from otko.views.canvas3d.selection import SelectionState -from otko.views.canvas3d.style import RenderStyle -from otko.views.canvas_base import plotly_available from otko.views.dock_manager import DockManager from otko.views.menu_builder import MenuBuilder from otko.views.render_controls import RenderControls @@ -39,9 +33,6 @@ from otko.views.tools import ( ToolController, ) -#: Canvas backends, in menu order. -CANVAS_BACKENDS = ("pyvista", "plotly") - class MainWindow( QMainWindow, @@ -52,7 +43,7 @@ class MainWindow( ): """Top-level application shell.""" - def __init__(self, settings: QSettings | None = None) -> None: + def __init__(self) -> None: super().__init__() self.setWindowTitle("OTKO") self.setWindowIcon( @@ -68,22 +59,12 @@ class MainWindow( self._show_node_labels = False self._show_element_labels = False - # One selection state shared by every canvas backend so a live swap - # keeps the current selection (canvas, tree, table, properties dock). - self._selection = SelectionState(self) - - self._settings = settings if settings is not None else QSettings("OTKO", "OTKO") - self._canvas_backend = self._stored_canvas_backend() - #: Shared, user-editable visual style (both backends read it). - self._style = self._load_plot_style() - self._canvases: dict[str, Any] = {} - self._diagram_renderers: dict[str, DiagramRenderer] = {} - # A stack holds every backend's canvas: swapping is just - # setCurrentWidget, so no backend widget is ever destroyed mid-session. - self._canvas_stack = QStackedWidget(self) - self.setCentralWidget(self._canvas_stack) - self._activate_canvas(self._canvas_backend) - self._build_tooling() + self._build_central_canvas() + self._tool_controller = ToolController(self._canvas, self._vm, self) + self._select_tool = SelectTool(self._canvas, self._vm, self) + self._draw_frame_tool: DrawFrameTool | None = None # lazy-created on activation + self._draw_node_tool: DrawNodeTool | None = None + self._draw_truss_tool: DrawTrussTool | None = None self._build_docks() self._build_actions() @@ -93,206 +74,13 @@ class MainWindow( self._build_status_bar() self._wire() self._refresh_action_enablement() - self.restore_layout() - # ── canvas backend ─────────────────────────────────────────────── - def _stored_canvas_backend(self) -> str: - """The backend chosen last session, falling back to PyVista.""" - stored = self._settings.value("canvas/backend", "pyvista") - name = str(stored) if stored is not None else "pyvista" - if name == "plotly" and plotly_available(): - return "plotly" - return "pyvista" - - def _ensure_canvas(self, backend: str) -> Any: - """Create a backend's canvas on first use and cache it. - - Both canvases are kept alive and swapped with ``setCentralWidget`` - rather than destroyed: tearing a VTK render window down mid-session - leaves dangling make-current callbacks, and the widgets are cheap to - retain once built. - """ - canvas = self._canvases.get(backend) - if canvas is not None: - return canvas - if backend == "plotly": - # Imported lazily so a plotly-less install still boots PyVista. - from otko.views.canvas_plotly import PlotlyCanvas - - canvas = PlotlyCanvas( - self, - style=self._style, - selection=self._selection, - log=self._log_canvas_message, - ) - else: - canvas = ModelCanvas(self, style=self._style, selection=self._selection) - self._canvases[backend] = canvas - return canvas - - def _activate_canvas(self, backend: str) -> None: - """Make the backend's canvas the central widget and rewire overlays.""" - if backend not in CANVAS_BACKENDS: - backend = "pyvista" - canvas = self._ensure_canvas(backend) - if self._canvas_stack.indexOf(canvas) < 0: - self._canvas_stack.addWidget(canvas) - self._canvas_stack.setCurrentWidget(canvas) - self._canvas = canvas - self._canvas_backend = backend - if backend == "pyvista": - renderer = self._diagram_renderers.get("pyvista") - if renderer is None: - # Diagram overlay paints onto the same plotter as the model. - renderer = DiagramRenderer(canvas) - self._diagram_renderers["pyvista"] = renderer - self._diagram_renderer = renderer - else: - self._diagram_renderer = None - - def canvas_backend(self) -> str: - """Name of the active canvas backend (``"pyvista"`` or ``"plotly"``).""" - return self._canvas_backend - - def _log_canvas_message(self, message: str, severity: str = "info") -> None: - """Route a JS console line from the Plotly page to the Console dock. - - The dock may not exist yet while canvases are being constructed, so - the lookup is guarded. - """ - dock = getattr(self, "_console_dock", None) - if dock is not None: - dock.log(message, severity=severity) - - # ── plot style ─────────────────────────────────────────────────── - def _load_plot_style(self) -> RenderStyle: - """Style from the last session, ignoring anything unrecognised.""" - raw = self._settings.value("plot/props") - if not raw: - return RenderStyle() - try: - data = json.loads(str(raw)) - except ValueError: - return RenderStyle() - if not isinstance(data, dict): - return RenderStyle() - allowed = {name: value for name, value in data.items() if name in RenderStyle.EDITABLE} - try: - return RenderStyle(**allowed) - except (TypeError, ValueError): - return RenderStyle() - - def _save_plot_style(self) -> None: - self._settings.setValue("plot/props", json.dumps(self._style.editable_values())) - - def set_plot_style(self, style: RenderStyle, *, persist: bool = True) -> None: - """Apply a style to every canvas (live preview when ``persist`` is off).""" - self._style = style - for canvas in self._canvases.values(): - canvas.set_style(style) - if persist: - self._save_plot_style() - - def _build_tooling(self) -> None: - """(Re)build the tool controller and tools against the current canvas.""" - self._tool_controller = ToolController(self._canvas, self._vm, self) - self._select_tool = SelectTool(self._canvas, self._vm, self) - self._draw_frame_tool: DrawFrameTool | None = None # lazy-created on activation - self._draw_node_tool: DrawNodeTool | None = None - self._draw_truss_tool: DrawTrussTool | None = None - self._tool_controller.toolChanged.connect(self._on_tool_changed) - self._tool_controller.toolChanged.connect(self._sync_tool_actions) - - def swap_canvas_backend(self, backend: str) -> None: - """Switch the live canvas backend, preserving model/selection state. - - The camera pose is intentionally re-framed rather than transferred: - the two engines have different camera models, and a wrong pose is - worse than a clean isometric reset. - """ - if backend not in CANVAS_BACKENDS or backend == self._canvas_backend: - return - if backend == "plotly" and not plotly_available(): - return - - project = self._vm.project - plane = self._canvas.working_plane_type() - offset = self._canvas.working_plane_offset() - active_name = getattr(self._tool_controller.active, "name", None) - - self._activate_canvas(backend) - self._build_tooling() - - # Project → re-frames the camera; working plane rebuilds the grid. - self._canvas.show_project(project) - if plane is not None and offset is not None: - self._canvas.set_working_plane(plane, offset) - # Re-apply the overlays whose state lives on the toolbar actions. - self._canvas.set_show_section_extrusions(self._act_show_extruded.isChecked()) - self._canvas.set_show_local_axes(self._act_show_local_axes.isChecked()) - self._canvas.set_display_options( - show_node_labels=self._show_node_labels, - show_element_labels=self._show_element_labels, - ) - # Keep the active tool (and its snap preview) alive across the swap. - # Re-entering through the action handlers recreates the tool lazily - # against the new canvas. - if active_name == "Draw Node": - self._on_draw_node_tool() - elif active_name == "Draw Frame": - self._on_draw_frame_tool() - elif active_name == "Draw Truss": - self._on_draw_truss_tool() - - self._settings.setValue("canvas/backend", self._canvas_backend) - self._sync_canvas_backend_actions() - self._refresh_action_enablement() - - # ── layout persistence ─────────────────────────────────────────── - def save_layout(self) -> None: - """Persist window geometry and dock/toolbar state for next launch.""" - self._settings.setValue("geometry", self.saveGeometry()) - self._settings.setValue("windowState", self.saveState()) - - def restore_layout(self) -> None: - """Restore a previously saved geometry/dock layout, if any. - - On first run no values are stored, so the defaults from ``__init__`` - (``resize`` + dock construction) stay in effect. - """ - geometry = self._settings.value("geometry") - if geometry is not None: - self.restoreGeometry(geometry) - state = self._settings.value("windowState") - if state is not None: - self.restoreState(state) - - def closeEvent(self, event: QCloseEvent) -> None: # (Qt override) - """Prompt to save unsaved work, then persist the window layout. - - The prompt is shown only for a visible window: Qt also closes hidden - widgets during teardown (for example under ``pytest-qt``), where a - modal dialog would block with no user to answer it. - """ - if self._vm.is_dirty and self.isVisible(): - buttons = QMessageBox.StandardButton - choice = QMessageBox.warning( - self, - "Unsaved changes", - "Save changes to the current project before closing?", - buttons.Save | buttons.Discard | buttons.Cancel, - buttons.Save, - ) - if choice == buttons.Cancel: - event.ignore() - return - if choice == buttons.Save: - self._on_save() - if self._vm.is_dirty: - event.ignore() - return - self.save_layout() - event.accept() + # ── construction ───────────────────────────────────────────────── + def _build_central_canvas(self) -> None: + self._canvas = ModelCanvas(self) + self.setCentralWidget(self._canvas) + # Diagram overlay paints onto the same plotter as the model. + self._diagram_renderer = DiagramRenderer(self._canvas) # ── wiring ──────────────────────────────────────────────────────── def _wire(self) -> None: @@ -309,7 +97,6 @@ class MainWindow( self._act_about.triggered.connect(self._on_about) self._act_quick_guide.triggered.connect(self._on_quick_guide) self._act_set_units.triggered.connect(self._on_set_units) - self._act_plot_properties.triggered.connect(self._on_plot_properties) # Edit self._act_delete.triggered.connect(self._on_delete) @@ -324,12 +111,9 @@ class MainWindow( self._act_tool_draw_node.triggered.connect(self._on_draw_node_tool) self._act_tool_draw_frame.triggered.connect(self._on_draw_frame_tool) self._act_tool_draw_truss.triggered.connect(self._on_draw_truss_tool) - # toolChanged → toolbar/menu sync is wired in _build_tooling(), which - # also runs on a canvas-backend swap (then a fresh controller exists). - - # Canvas backend - self._act_canvas_pyvista.triggered.connect(lambda: self.swap_canvas_backend("pyvista")) - self._act_canvas_plotly.triggered.connect(lambda: self.swap_canvas_backend("plotly")) + self._tool_controller.toolChanged.connect(self._on_tool_changed) + # Keep the Tools toolbar checked state in sync with the controller. + self._tool_controller.toolChanged.connect(self._sync_tool_actions) # Define self._act_grid.triggered.connect(self._on_grid_system) @@ -357,7 +141,6 @@ class MainWindow( # Analyze self._act_case_manager.triggered.connect(self._on_case_manager) - self._act_combo_manager.triggered.connect(self._on_combo_manager) self._act_run.triggered.connect(self._on_run_analysis) # Display @@ -373,7 +156,7 @@ class MainWindow( self._act_back_to_model.triggered.connect(self._on_back_to_model) # AnalysisRunner: stream log to console + show results in panel - self._runner.log.connect(self._console_dock.log) + self._runner.log.connect(self._console.appendPlainText) self._runner.finished.connect(self._on_analysis_finished) self._runner.failed.connect(self._on_analysis_failed) diff --git a/src/otko/views/menu_builder.py b/src/otko/views/menu_builder.py index 5e7a271..6d5bb94 100644 --- a/src/otko/views/menu_builder.py +++ b/src/otko/views/menu_builder.py @@ -155,7 +155,6 @@ class MenuBuilder: # Analyze self._act_case_manager = QAction("&Cases…", self, shortcut="Ctrl+Shift+A") - self._act_combo_manager = QAction("&Load Combinations…", self) self._act_run = QAction("&Run…", self, shortcut="F5") # View (Iso/Top/Front/Right are an exclusive checkable group so the @@ -192,46 +191,9 @@ class MenuBuilder: # No shortcut: F1/F2/F3/F5 are taken by the draw tools and Run. self._act_quick_guide = QAction("&Quick Guide", self) self._act_set_units = QAction("Set Display &Units…", self) - self._act_plot_properties = QAction("&Plot Properties…", self) - self._act_plot_properties.setToolTip( - "Colours and sizes used by both canvas backends (persisted)." - ) - - # Canvas backend — exclusive radio pair live-switching the 3D view. - from otko.views.canvas_base import plotly_available - - self._canvas_backend_group = QActionGroup(self) - self._canvas_backend_group.setExclusive(True) - self._act_canvas_pyvista = QAction("&PyVista (Native)", self, checkable=True) - self._act_canvas_pyvista.setToolTip( - "Native VTK/OpenGL viewport — fastest for large models." - ) - self._act_canvas_plotly = QAction("&Plotly (WebGL)", self, checkable=True) - self._act_canvas_plotly.setToolTip( - "plotly.js rendered in a WebEngine view — richer graphics, higher overhead." - ) - self._canvas_backend_group.addAction(self._act_canvas_pyvista) - self._canvas_backend_group.addAction(self._act_canvas_plotly) - if not plotly_available(): - self._act_canvas_plotly.setEnabled(False) - self._act_canvas_plotly.setToolTip( - 'Install the optional GUI extra (pip install -e ".[gui]") to enable plotly.' - ) self._apply_toolbar_icons() - def _sync_canvas_backend_actions(self) -> None: - """Mirror the active canvas backend onto the Options radio pair.""" - active = getattr( - self, - "_act_canvas_plotly" if self._canvas_backend == "plotly" else "_act_canvas_pyvista", - ) - self._canvas_backend_group.blockSignals(True) - try: - active.setChecked(True) - finally: - self._canvas_backend_group.blockSignals(False) - def _apply_toolbar_icons(self) -> None: """Assign icons + self-documenting tips to every toolbar action. @@ -323,11 +285,6 @@ class MenuBuilder: "Analysis Cases (Ctrl+Shift+A)", "Manage analysis cases.", ), - "_act_combo_manager": ( - "cases", - "Load Combinations", - "Manage named case-result load combinations.", - ), "_act_show_deformed": ( "show-deformed", "Show Deformed Shape", @@ -358,6 +315,11 @@ class MenuBuilder: "Assign Point Loads (Ctrl+L)", "Assign point loads to the selected nodes.", ), + "_act_assign_distributed_load": ( + "assign-distributed", + "Assign Distributed Loads…", + "Assign distributed loads to the selected frame elements.", + ), "_act_quick_guide": ( "quick-guide", "Quick Guide", @@ -463,7 +425,6 @@ class MenuBuilder: m_analyze = mb.addMenu("&Analyze") m_analyze.addAction(self._act_case_manager) - m_analyze.addAction(self._act_combo_manager) m_analyze.addSeparator() m_analyze.addAction(self._act_run) @@ -516,11 +477,6 @@ class MenuBuilder: m_options = mb.addMenu("&Options") m_options.addAction(self._act_set_units) - m_options.addAction(self._act_plot_properties) - m_backend = m_options.addMenu("Canvas &Backend") - m_backend.addAction(self._act_canvas_pyvista) - m_backend.addAction(self._act_canvas_plotly) - self._sync_canvas_backend_actions() m_help = mb.addMenu("&Help") m_help.addAction(self._act_quick_guide) @@ -531,7 +487,7 @@ class MenuBuilder: tb.setMovable(True) tb.setIconSize(QSize(24, 24)) tb.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) - # Top edge; Tools shares the top area (built next). + # Top edge; Tools stays in the left area independently (built next). self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb) self._view_toolbar = tb tb.addAction(self._act_zoom_extents) @@ -577,8 +533,9 @@ class MenuBuilder: tb.setMovable(True) tb.setIconSize(QSize(24, 24)) tb.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) - # Top edge alongside the View toolbar (built just before). - self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb) + # View lives on the top edge, so Tools stays left independently + # in its own left-area column. + self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, tb) self._tools_toolbar = tb tb.addAction(self._act_tool_select) tb.addAction(self._act_tool_draw_node) @@ -589,6 +546,7 @@ class MenuBuilder: tb.addSeparator() tb.addAction(self._act_assign_support) tb.addAction(self._act_assign_load) + tb.addAction(self._act_assign_distributed_load) def _build_status_bar(self) -> None: self._status_label = QLabel("No project") @@ -617,12 +575,14 @@ class MenuBuilder: def _on_units_combo_changed(self, _idx: int) -> None: """Status-bar unit picker → project.meta.units. - Qt stores the userData as a bare string (UnitSystem inherits - from str), so we re-cast to the enum before writing through - and logging. + Routed through ``SetUnitsCommand`` (same as Options → Set Display + Units) so the change is undoable and the whole UI refreshes via + ``modelMutated``. Qt stores the userData as a bare string + (UnitSystem inherits from str), so we re-cast to the enum first. """ if self._vm.project is None: return + from otko.commands import SetUnitsCommand from otko.core import UnitSystem raw = self._units_combo.currentData() @@ -631,8 +591,9 @@ class MenuBuilder: chosen = raw if isinstance(raw, UnitSystem) else UnitSystem(str(raw)) if chosen == self._vm.project.meta.units: return - self._vm.project.meta.units = chosen - self._vm.mark_dirty() + self._vm.apply_command(SetUnitsCommand(self._vm, chosen)) + self._sync_units_combo() + self._refresh_dock_units() self._log(f"Display units set to {chosen.value}.") def _sync_view_display_state(self, _project: object = None) -> None: diff --git a/src/otko/views/render_controls.py b/src/otko/views/render_controls.py index 06db479..221f934 100644 --- a/src/otko/views/render_controls.py +++ b/src/otko/views/render_controls.py @@ -50,23 +50,6 @@ class RenderControls: ) # ── SAP2000-style view switching + working level ─────────────── - def _on_plot_properties(self) -> None: - """Edit the shared visual style. - - Edits preview live on both canvases; Cancel restores the style the - dialog opened with, and only OK persists to ``QSettings``. - """ - from otko.views.dialogs import PlotPropertiesDialog - - original = self._style - dialog = PlotPropertiesDialog(original, self) - dialog.changed.connect(lambda style: self.set_plot_style(style, persist=False)) - if dialog.exec() == QDialog.DialogCode.Accepted: - self.set_plot_style(dialog.plot_style()) - self._log("Plot properties updated.") - else: - self.set_plot_style(original, persist=False) - def _on_toggle_parallel(self, on: bool) -> None: cam = self._canvas.camera cam.parallel_projection = on @@ -276,8 +259,47 @@ class RenderControls: self._props.update_for_selection( self._canvas.selection.nodes, self._canvas.selection.elements ) + self._sync_units_everywhere() self._refresh_action_enablement() + def _sync_units_everywhere(self) -> None: + """Refresh every units-aware label after a units change. + + Runs on every ``modelMutated`` but short-circuits (combo check) + when the units did not actually change, so ordinary edits skip + the work. Covers the status combo, the Results panel, and any + open post dock — whether the change came from the combo, the + Options menu, or undo/redo. Table headers and the property + editor read live project units on their own refresh, which this + same ``modelMutated`` already triggers. Never raises. + """ + try: + project = self._vm.project + if project is None: + return + try: + combo_data = self._units_combo.currentData() + except Exception: + combo_data = None + if combo_data == project.meta.units: + return + self._sync_units_combo() + try: + self._results_panel.set_units( # type: ignore[attr-defined] + project.meta.units, + project.ndf, + ) + except Exception: + pass + try: + fn = getattr(self, "refresh_open_dock_units", None) + if callable(fn): + fn() + except Exception: + pass + except Exception: + pass + def _on_dirty_changed(self, _dirty: bool) -> None: self._refresh_status() @@ -476,7 +498,6 @@ class RenderControls: has_pushover = isinstance(self._latest_results, PushoverResults) has_rs = isinstance(self._latest_results, ResponseSpectrumResults) n_sel = len(self._canvas.selection.nodes) - caps = self._canvas.capabilities self._act_save.setEnabled(has_project) self._act_save_as.setEnabled(has_project) self._act_export_opspy.setEnabled(has_project) @@ -500,11 +521,9 @@ class RenderControls: self._act_tool_draw_truss.setEnabled(has_project) self._act_show_deformed.setEnabled(has_static) self._act_show_mode_shape.setEnabled(has_modal) - # Force diagrams + video export are backend capabilities, not just - # data availability (Plotly cannot do either yet). - self._act_show_force_diagram.setEnabled(has_static and caps.diagrams) + self._act_show_force_diagram.setEnabled(has_static) self._act_show_time_history.setEnabled(has_transient) - self._act_export_th_animation.setEnabled(has_transient and caps.animation_export) + self._act_export_th_animation.setEnabled(has_transient) self._act_show_hysteresis.setEnabled(has_transient) self._act_show_pushover.setEnabled(has_pushover) self._act_show_response_spectrum.setEnabled(has_rs) @@ -516,5 +535,5 @@ class RenderControls: self._act_clear_display.setEnabled(has_project) def _log(self, message: str) -> None: - self._console_dock.log(message) + self._console.appendPlainText(message) self.statusBar().showMessage(message, 5000) diff --git a/src/otko/views/tools/__init__.py b/src/otko/views/tools/__init__.py index 8c593d9..992e462 100644 --- a/src/otko/views/tools/__init__.py +++ b/src/otko/views/tools/__init__.py @@ -5,11 +5,5 @@ from otko.views.tools.draw_frame import DrawFrameTool from otko.views.tools.draw_node import DrawNodeTool from otko.views.tools.draw_truss import DrawTrussTool -__all__ = [ - "CanvasTool", - "SelectTool", - "ToolController", - "DrawFrameTool", - "DrawNodeTool", - "DrawTrussTool", -] +__all__ = ["CanvasTool", "SelectTool", "ToolController", + "DrawFrameTool", "DrawNodeTool", "DrawTrussTool"] diff --git a/src/otko/views/tools/base.py b/src/otko/views/tools/base.py index bfb6072..2ffe593 100644 --- a/src/otko/views/tools/base.py +++ b/src/otko/views/tools/base.py @@ -35,8 +35,8 @@ class CanvasTool(QObject): def __init__( self, - canvas: ModelCanvas, - vm: ProjectViewModel, + canvas: "ModelCanvas", + vm: "ProjectViewModel", parent: QObject | None = None, ) -> None: super().__init__(parent) @@ -84,11 +84,10 @@ class SelectTool(CanvasTool): class ToolController(QObject): """Owns the active tool and routes canvas pick signals to it.""" - toolChanged = Signal(object) # emits the new CanvasTool (or None for default) + toolChanged = Signal(object) # emits the new CanvasTool (or None for default) - def __init__( - self, canvas: ModelCanvas, vm: ProjectViewModel, parent: QObject | None = None - ) -> None: + def __init__(self, canvas: "ModelCanvas", vm: "ProjectViewModel", + parent: QObject | None = None) -> None: super().__init__(parent) self._canvas = canvas self._vm = vm diff --git a/src/otko/views/tools/draw_frame.py b/src/otko/views/tools/draw_frame.py index 980716a..d0bd47a 100644 --- a/src/otko/views/tools/draw_frame.py +++ b/src/otko/views/tools/draw_frame.py @@ -41,9 +41,9 @@ class DrawFrameTool(LinearElementDrawTool): def __init__( self, - canvas: ModelCanvas, - vm: ProjectViewModel, - parent: QObject | None = None, + canvas: "ModelCanvas", + vm: "ProjectViewModel", + parent: "QObject | None" = None, ) -> None: super().__init__(canvas, vm, parent) @@ -67,9 +67,7 @@ class DrawFrameTool(LinearElementDrawTool): section_id = self._ensure_default_section() element_id = project.next_element_id() elem = ElasticBeamColumn( - id=element_id, - nodes=(n1, n2), - section_id=section_id, + id=element_id, nodes=(n1, n2), section_id=section_id, ) self._vm.apply_command(AddElementsCommand(self._vm, [elem])) finally: diff --git a/src/otko/views/tools/draw_node.py b/src/otko/views/tools/draw_node.py index 0f286be..b82660a 100644 --- a/src/otko/views/tools/draw_node.py +++ b/src/otko/views/tools/draw_node.py @@ -27,20 +27,14 @@ if TYPE_CHECKING: def _snap_to_grid( - x: float, - y: float, - z: float, - x_lines: list[float], - y_lines: list[float], - z_lines: list[float], + x: float, y: float, z: float, + x_lines: list[float], y_lines: list[float], z_lines: list[float], ) -> tuple[float, float, float]: """Snap each coordinate to its closest grid line (identity on empty axes).""" - def nearest(v: float, lines: list[float]) -> float: if not lines: return v return min(lines, key=lambda c: abs(c - v)) - return nearest(x, x_lines), nearest(y, y_lines), nearest(z, z_lines) @@ -76,17 +70,16 @@ def _snap_with_distance( if not (grid.x_lines or grid.y_lines or grid.z_lines): continue lx, ly, lz = cs.coord.world_to_local(world) - sl = _snap_to_grid(lx, ly, lz, grid.x_lines, grid.y_lines, grid.z_lines) + sl = _snap_to_grid(lx, ly, lz, + grid.x_lines, grid.y_lines, grid.z_lines) candidate = cs.coord.local_to_world(sl) - d2 = ( - (candidate[0] - world[0]) ** 2 - + (candidate[1] - world[1]) ** 2 - + (candidate[2] - world[2]) ** 2 - ) + d2 = ((candidate[0] - world[0]) ** 2 + + (candidate[1] - world[1]) ** 2 + + (candidate[2] - world[2]) ** 2) if d2 < best_d2: best = candidate best_d2 = d2 - return best, (best_d2**0.5) if best is not None else float("inf") + return best, (best_d2 ** 0.5) if best is not None else float("inf") def _min_grid_spacing(systems: list[CoordinateGridSystem]) -> float | None: @@ -115,9 +108,9 @@ class DrawNodeTool(CanvasTool): def __init__( self, - canvas: ModelCanvas, - vm: ProjectViewModel, - parent: QObject | None = None, + canvas: "ModelCanvas", + vm: "ProjectViewModel", + parent: "QObject | None" = None, ) -> None: super().__init__(canvas, vm, parent) @@ -131,13 +124,13 @@ class DrawNodeTool(CanvasTool): super().deactivate() def prompt(self) -> str: - return ( - "Draw Node: click a grid intersection " - "to place a node. Switch to Select tool to finish." - ) + return ("Draw Node: click a grid intersection " + "to place a node. Switch to Select tool to finish.") def on_node_picked(self, node_id: int) -> None: - self.statusChanged.emit(f"Draw Node: node {node_id} already exists at that spot.") + self.statusChanged.emit( + f"Draw Node: node {node_id} already exists at that spot." + ) def on_empty_clicked(self, x: float, y: float, z: float) -> None: """Called only when the canvas has already confirmed a grid snap. @@ -160,5 +153,9 @@ class DrawNodeTool(CanvasTool): nid = project.next_node_id() node = Node(id=nid, coords=(x, y, z)) - self._vm.apply_command(AddNodesCommand(self._vm, [node], text=f"Add node {nid}")) - self.statusChanged.emit(f"Draw Node: added node {nid} at ({x:g}, {y:g}, {z:g}).") + self._vm.apply_command( + AddNodesCommand(self._vm, [node], text=f"Add node {nid}") + ) + self.statusChanged.emit( + f"Draw Node: added node {nid} at ({x:g}, {y:g}, {z:g})." + ) diff --git a/src/otko/views/tools/draw_truss.py b/src/otko/views/tools/draw_truss.py index 4ee8254..1eeb69d 100644 --- a/src/otko/views/tools/draw_truss.py +++ b/src/otko/views/tools/draw_truss.py @@ -37,9 +37,9 @@ class DrawTrussTool(LinearElementDrawTool): def __init__( self, - canvas: ModelCanvas, - vm: ProjectViewModel, - parent: QObject | None = None, + canvas: "ModelCanvas", + vm: "ProjectViewModel", + parent: "QObject | None" = None, ) -> None: super().__init__(canvas, vm, parent) diff --git a/tests/conftest.py b/tests/conftest.py index c91c064..930c400 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,52 +1,22 @@ -"""Shared pytest configuration. +"""Shared pytest fixtures. -The GUI suite is headless by default: unless the caller already chose a -platform, ``QT_QPA_PLATFORM=offscreen`` is set here, before pytest-qt -builds its session ``QApplication``. An explicit caller value is respected. - -OpenseesPy is a process-global singleton, so a lazy autouse guard wipes the -domain after every test. The import is inside the teardown so that -``tests/unit`` never imports the solver. +`pytest-qt` automatically provides a `qtbot` fixture and a QApplication +instance. We add convenience fixtures here as the suite grows. """ from __future__ import annotations -import importlib.util -import os -from collections.abc import Iterator - import pytest -os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") - - -@pytest.fixture(autouse=True, scope="session") -def _isolate_qsettings(tmp_path_factory: pytest.TempPathFactory) -> Iterator[None]: - """Redirect ``QSettings`` to a temp dir so tests never touch user state. - - ``MainWindow`` persists preferences (window layout, canvas backend) under - ``QSettings("OTKO", "OTKO")``. Without redirection a developer who, say, - switched to the Plotly backend would see PyVista-specific GUI tests fail - against their real settings. Skipped entirely when Qt is not installed, so - the headless job stays free of Qt. - """ - if importlib.util.find_spec("PySide6") is None: - yield - return - from PySide6.QtCore import QSettings - - settings_dir = tmp_path_factory.mktemp("qsettings") - for fmt in (QSettings.Format.NativeFormat, QSettings.Format.IniFormat): - QSettings.setPath(fmt, QSettings.Scope.UserScope, str(settings_dir)) - yield - @pytest.fixture(autouse=True) -def _wipe_opensees_domain() -> Iterator[None]: - """Wipe the OpenseesPy domain after a test if the solver was imported.""" - yield +def _isolate_opensees() -> None: + """Reset OpenSees domain between tests if openseespy is importable. + + Imported lazily so that pure-core tests don't pull in the C++ runtime. + """ try: - from openseespy.opensees import wipe - except Exception: # pragma: no cover - solver not installed / import side effects + import openseespy.opensees as ops + except ImportError: return - wipe() + ops.wipe() diff --git a/tests/gui/test_analysis_commands.py b/tests/gui/test_analysis_commands.py index fed67e3..ca5ecf8 100644 --- a/tests/gui/test_analysis_commands.py +++ b/tests/gui/test_analysis_commands.py @@ -6,12 +6,12 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( +from otko.commands import ( # noqa: E402 AddAnalysisCasesCommand, DeleteAnalysisCasesCommand, UpdateAnalysisCaseCommand, ) -from otko.core import ( +from otko.core import ( # noqa: E402 LinearTimeSeries, ModalCase, NodalLoad, @@ -20,7 +20,7 @@ from otko.core import ( StaticCase, TransientCase, ) -from otko.viewmodels import ProjectViewModel +from otko.viewmodels import ProjectViewModel # noqa: E402 def _vm_with_pattern() -> ProjectViewModel: @@ -29,9 +29,8 @@ def _vm_with_pattern() -> ProjectViewModel: vm.project.nodes.append(Node(id=1, coords=(0, 0, 0))) vm.project.time_series.append(LinearTimeSeries(id=1)) vm.project.load_patterns.append( - PlainLoadPattern( - id=1, time_series_id=1, nodal_loads=[NodalLoad(node_id=1, forces=(100, 0, 0, 0, 0, 0))] - ) + PlainLoadPattern(id=1, time_series_id=1, + nodal_loads=[NodalLoad(node_id=1, forces=(100, 0, 0, 0, 0, 0))]) ) return vm @@ -58,14 +57,9 @@ def test_add_modal_case_no_pattern_needed(qtbot) -> None: # type: ignore[no-unt def test_add_emits_modelMutated(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_with_pattern() with qtbot.waitSignal(vm.modelMutated, timeout=500): - vm.apply_command( - AddAnalysisCasesCommand( - vm, - [ - StaticCase(id=1, pattern_ids=[1]), - ], - ) - ) + vm.apply_command(AddAnalysisCasesCommand(vm, [ + StaticCase(id=1, pattern_ids=[1]), + ])) @pytest.mark.gui @@ -80,14 +74,9 @@ def test_add_duplicate_id_raises(qtbot) -> None: # type: ignore[no-untyped-def] @pytest.mark.gui def test_update_static_changes_n_steps(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_with_pattern() - vm.apply_command( - AddAnalysisCasesCommand( - vm, - [ - StaticCase(id=1, name="A", pattern_ids=[1], n_steps=1), - ], - ) - ) + vm.apply_command(AddAnalysisCasesCommand(vm, [ + StaticCase(id=1, name="A", pattern_ids=[1], n_steps=1), + ])) new = StaticCase(id=1, name="A", pattern_ids=[1], n_steps=10) vm.apply_command(UpdateAnalysisCaseCommand(vm, new)) assert vm.project.analyses[0].n_steps == 10 @@ -98,14 +87,9 @@ def test_update_static_changes_n_steps(qtbot) -> None: # type: ignore[no-untype @pytest.mark.gui def test_update_changes_case_type(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_with_pattern() - vm.apply_command( - AddAnalysisCasesCommand( - vm, - [ - StaticCase(id=1, pattern_ids=[1]), - ], - ) - ) + vm.apply_command(AddAnalysisCasesCommand(vm, [ + StaticCase(id=1, pattern_ids=[1]), + ])) swapped = ModalCase(id=1, name="Mode swap", n_modes=5) vm.apply_command(UpdateAnalysisCaseCommand(vm, swapped)) assert isinstance(vm.project.analyses[0], ModalCase) @@ -125,15 +109,10 @@ def test_update_unknown_id_raises(qtbot) -> None: # type: ignore[no-untyped-def @pytest.mark.gui def test_delete_round_trip(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_with_pattern() - vm.apply_command( - AddAnalysisCasesCommand( - vm, - [ - StaticCase(id=1, pattern_ids=[1]), - ModalCase(id=2), - ], - ) - ) + vm.apply_command(AddAnalysisCasesCommand(vm, [ + StaticCase(id=1, pattern_ids=[1]), + ModalCase(id=2), + ])) vm.apply_command(DeleteAnalysisCasesCommand(vm, {1})) assert {c.id for c in vm.project.analyses} == {2} vm.undo_stack.undo() diff --git a/tests/gui/test_analysis_runner.py b/tests/gui/test_analysis_runner.py index 14c100a..b2f982c 100644 --- a/tests/gui/test_analysis_runner.py +++ b/tests/gui/test_analysis_runner.py @@ -12,21 +12,21 @@ import pytest pytest.importorskip("PySide6") pytest.importorskip("openseespy.opensees") -from otko.commands import ( +from otko.commands import ( # noqa: E402 AddAnalysisCasesCommand, AddElementsCommand, AddNodalLoadsCommand, AddNodesCommand, AddSectionsCommand, ) -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticBeamColumn, ElasticSection, Node, StaticCase, ) -from otko.services.results import StaticResults -from otko.viewmodels import AnalysisRunner, ProjectViewModel +from otko.services.results import StaticResults # noqa: E402 +from otko.viewmodels import AnalysisRunner, ProjectViewModel # noqa: E402 @pytest.fixture @@ -35,42 +35,21 @@ def cantilever_vm() -> ProjectViewModel: L = 5.0 vm = ProjectViewModel() vm.new_project(ndm=2, ndf=3) - vm.apply_command( - AddNodesCommand( - vm, - [ - Node( - id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, False, False, False, True) - ), - Node(id=2, coords=(L, 0.0, 0.0)), - ], - ) - ) - vm.apply_command( - AddSectionsCommand( - vm, - [ - ElasticSection(id=1, E=200e9, A=0.01, Iz=8.333e-6), - ], - ) - ) - vm.apply_command( - AddElementsCommand( - vm, - [ - ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), - ], - ) - ) + vm.apply_command(AddNodesCommand(vm, [ + Node(id=1, coords=(0.0, 0.0, 0.0), + restraint=(True, True, False, False, False, True)), + Node(id=2, coords=(L, 0.0, 0.0)), + ])) + vm.apply_command(AddSectionsCommand(vm, [ + ElasticSection(id=1, E=200e9, A=0.01, Iz=8.333e-6), + ])) + vm.apply_command(AddElementsCommand(vm, [ + ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), + ])) vm.apply_command(AddNodalLoadsCommand(vm, {2}, (0.0, -1000.0, 0.0, 0.0, 0.0, 0.0))) - vm.apply_command( - AddAnalysisCasesCommand( - vm, - [ - StaticCase(id=1, name="Cantilever", pattern_ids=[1]), - ], - ) - ) + vm.apply_command(AddAnalysisCasesCommand(vm, [ + StaticCase(id=1, name="Cantilever", pattern_ids=[1]), + ])) return vm diff --git a/tests/gui/test_assign_load_pattern_name.py b/tests/gui/test_assign_load_pattern_name.py index b0b4fba..151b1d2 100644 --- a/tests/gui/test_assign_load_pattern_name.py +++ b/tests/gui/test_assign_load_pattern_name.py @@ -6,14 +6,15 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import AddNodalLoadsCommand -from otko.core import ( +from otko.commands import AddNodalLoadsCommand # noqa: E402 +from otko.core import ( # noqa: E402 LinearTimeSeries, Node, PlainLoadPattern, + Project, ) -from otko.viewmodels import ProjectViewModel -from otko.views.dialogs.assign_load import AssignLoadDialog +from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.views.dialogs.assign_load import AssignLoadDialog # noqa: E402 @pytest.mark.gui @@ -47,11 +48,10 @@ def test_dialog_lists_existing_patterns(qtbot) -> None: # type: ignore[no-untyp @pytest.mark.gui def test_dialog_enables_name_field_for_new_pattern(qtbot) -> None: # type: ignore[no-untyped-def] dlg = AssignLoadDialog( - n_selected=1, - existing_patterns=[(1, "Gravity")], + n_selected=1, existing_patterns=[(1, "Gravity")], ) qtbot.addWidget(dlg) - dlg._pattern_cb.setCurrentIndex(1) # "" + dlg._pattern_cb.setCurrentIndex(1) # "" assert dlg.selected_pattern_id() is None assert dlg._new_name_edit.isEnabled() dlg._new_name_edit.setText("RefMoment") @@ -68,23 +68,14 @@ def test_command_creates_named_pattern(qtbot) -> None: # type: ignore[no-untype # Pre-existing 'Gravity' pattern — must NOT be reused when the user # asks for a new one named 'RefMoment'. vm.project.time_series.append(LinearTimeSeries(id=1, name="Gravity")) - vm.project.load_patterns.append( - PlainLoadPattern( - id=1, - name="Gravity", - time_series_id=1, - ) - ) + vm.project.load_patterns.append(PlainLoadPattern( + id=1, name="Gravity", time_series_id=1, + )) - vm.apply_command( - AddNodalLoadsCommand( - vm, - {1}, - (0, 0, 0, 0, 0, 1.0), - pattern_id=None, - new_pattern_name="RefMoment", - ) - ) + vm.apply_command(AddNodalLoadsCommand( + vm, {1}, (0, 0, 0, 0, 0, 1.0), + pattern_id=None, new_pattern_name="RefMoment", + )) # Two patterns now: Gravity (id=1) and RefMoment (id=2). names = [p.name for p in vm.project.load_patterns] assert "Gravity" in names and "RefMoment" in names @@ -99,20 +90,13 @@ def test_command_creates_constant_timeseries_when_requested(qtbot) -> None: # t the new pattern — required for axial preloads in moment-curvature runs where pseudoTime must NOT scale the axial force.""" from otko.core import ConstantTimeSeries - vm = ProjectViewModel() vm.new_project(ndm=2, ndf=3) vm.project.nodes.append(Node(id=1, coords=(0, 0, 0))) - vm.apply_command( - AddNodalLoadsCommand( - vm, - {1}, - (-180, 0, 0, 0, 0, 0), - pattern_id=None, - new_pattern_name="AxialP", - new_ts_type="Constant", - ) - ) + vm.apply_command(AddNodalLoadsCommand( + vm, {1}, (-180, 0, 0, 0, 0, 0), + pattern_id=None, new_pattern_name="AxialP", new_ts_type="Constant", + )) # Pattern + TimeSeries both created with the "AxialP" name. assert vm.project.load_patterns[0].name == "AxialP" ts = vm.project.time_series[0] @@ -127,20 +111,11 @@ def test_command_reuses_existing_pattern_by_id(qtbot) -> None: # type: ignore[n vm.new_project(ndm=2, ndf=3) vm.project.nodes.append(Node(id=1, coords=(0, 0, 0))) vm.project.time_series.append(LinearTimeSeries(id=1, name="Existing")) - vm.project.load_patterns.append( - PlainLoadPattern( - id=1, - name="Existing", - time_series_id=1, - ) - ) - vm.apply_command( - AddNodalLoadsCommand( - vm, - {1}, - (0, 0, 0, 0, 0, 5.0), - pattern_id=1, - ) - ) + vm.project.load_patterns.append(PlainLoadPattern( + id=1, name="Existing", time_series_id=1, + )) + vm.apply_command(AddNodalLoadsCommand( + vm, {1}, (0, 0, 0, 0, 0, 5.0), pattern_id=1, + )) assert len(vm.project.load_patterns) == 1 assert len(vm.project.load_patterns[0].nodal_loads) == 1 diff --git a/tests/gui/test_assign_masses.py b/tests/gui/test_assign_masses.py index 3ba0dda..9501620 100644 --- a/tests/gui/test_assign_masses.py +++ b/tests/gui/test_assign_masses.py @@ -6,7 +6,7 @@ import pytest pytest.importorskip("PySide6") -from otko.views.dialogs.assign_masses import AssignMassesDialog +from otko.views.dialogs.assign_masses import AssignMassesDialog # noqa: E402 @pytest.mark.gui @@ -23,9 +23,7 @@ def test_mass_vector_reads_spinboxes(qtbot) -> None: # type: ignore[no-untyped- dlg._mx.setValue(5000.0) dlg._my.setValue(5000.0) dlg._mz.setValue(100.0) - dlg._mxx.setValue(0.0) - dlg._myy.setValue(0.0) - dlg._mzz.setValue(0.0) + dlg._mxx.setValue(0.0); dlg._myy.setValue(0.0); dlg._mzz.setValue(0.0) assert dlg.mass_vector() == (5000.0, 5000.0, 100.0, 0.0, 0.0, 0.0) diff --git a/tests/gui/test_assign_zls.py b/tests/gui/test_assign_zls.py index 9a8cb66..183e173 100644 --- a/tests/gui/test_assign_zls.py +++ b/tests/gui/test_assign_zls.py @@ -6,36 +6,27 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticSection, Node, Project, ) -from otko.views.dialogs.assign_zls import ( +from otko.views.dialogs.assign_zls import ( # noqa: E402 AssignZeroLengthSectionDialog, ) def _project_with_section() -> Project: return Project( - ndm=2, - ndf=3, + ndm=2, ndf=3, nodes=[ Node(id=1, coords=(0, 0, 0)), Node(id=2, coords=(0, 0, 0)), ], - sections=[ - ElasticSection( - id=3, - name="MK", - E=30000, - A=200, - Iz=6667, - Iy=6667, - G=12000, - J=100, - ) - ], + sections=[ElasticSection( + id=3, name="MK", E=30000, A=200, Iz=6667, Iy=6667, + G=12000, J=100, + )], ) @@ -52,8 +43,7 @@ def test_dialog_lists_project_sections(qtbot) -> None: # type: ignore[no-untype @pytest.mark.gui def test_dialog_disabled_when_no_sections(qtbot) -> None: # type: ignore[no-untyped-def] p = Project( - ndm=2, - ndf=3, + ndm=2, ndf=3, nodes=[Node(id=1, coords=(0, 0, 0)), Node(id=2, coords=(0, 0, 0))], ) dlg = AssignZeroLengthSectionDialog(p, (1, 2)) diff --git a/tests/gui/test_canvas_backend_switch.py b/tests/gui/test_canvas_backend_switch.py deleted file mode 100644 index ed77be1..0000000 --- a/tests/gui/test_canvas_backend_switch.py +++ /dev/null @@ -1,122 +0,0 @@ -"""GUI tests for the live canvas-backend switch (PyVista ⇄ Plotly). - -QSettings are injected per-test so a swap never leaks into the real user -configuration or the next test. -""" - -from __future__ import annotations - -import pytest - -pytest.importorskip("PySide6") -pytest.importorskip("pyvistaqt") -pytest.importorskip("plotly") - -from PySide6.QtCore import QSettings - - -def _make_window(tmp_path, qtbot): # type: ignore[no-untyped-def] - from otko.views.main_window import MainWindow - - settings = QSettings(str(tmp_path / "otko.ini"), QSettings.Format.IniFormat) - window = MainWindow(settings=settings) - qtbot.addWidget(window) - return window - - -@pytest.mark.gui -def test_default_backend_is_pyvista(tmp_path, qtbot) -> None: # type: ignore[no-untyped-def] - from otko.views.canvas3d import ModelCanvas - - window = _make_window(tmp_path, qtbot) - assert window.canvas_backend() == "pyvista" - assert isinstance(window._canvas, ModelCanvas) - assert window._diagram_renderer is not None - - -@pytest.mark.gui -def test_switch_to_plotly_preserves_selection(tmp_path, qtbot) -> None: # type: ignore[no-untyped-def] - from otko.views.canvas_plotly import PlotlyCanvas - - window = _make_window(tmp_path, qtbot) - window._vm.new_project() - window._canvas.selection.select_node(2) - - window.swap_canvas_backend("plotly") - - assert window.canvas_backend() == "plotly" - assert isinstance(window._canvas, PlotlyCanvas) - # One shared SelectionState: the tree/table/properties stay bound. - assert window._canvas.selection is window._selection - assert window._canvas.selection.nodes == frozenset({2}) - # Both canvases live in the stack; the PyVista one was not destroyed. - assert window._canvas_stack.count() == 2 - - -@pytest.mark.gui -def test_plotly_gates_pyvista_only_features(tmp_path, qtbot) -> None: # type: ignore[no-untyped-def] - window = _make_window(tmp_path, qtbot) - assert window._canvas.capabilities.diagrams is True - - window.swap_canvas_backend("plotly") - caps = window._canvas.capabilities - assert caps.diagrams is False - assert caps.animation_export is False - assert caps.extrusions is True and caps.labels is True - - window._refresh_action_enablement() - assert window._act_show_force_diagram.isEnabled() is False - assert window._act_export_th_animation.isEnabled() is False - - -@pytest.mark.gui -def test_swapping_back_reuses_the_same_canvases(tmp_path, qtbot) -> None: # type: ignore[no-untyped-def] - window = _make_window(tmp_path, qtbot) - pyvista_canvas = window._canvas - - window.swap_canvas_backend("plotly") - plotly_canvas = window._canvas - window.swap_canvas_backend("pyvista") - - assert window._canvas is pyvista_canvas - assert window._diagram_renderer is not None - # No duplicate widgets accumulate across repeated swaps. - window.swap_canvas_backend("plotly") - assert window._canvas is plotly_canvas - assert window._diagram_renderer is None # diagrams are PyVista-only - assert window._canvas_stack.count() == 2 - - -@pytest.mark.gui -def test_backend_choice_is_persisted(tmp_path, qtbot) -> None: # type: ignore[no-untyped-def] - window = _make_window(tmp_path, qtbot) - window.swap_canvas_backend("plotly") - assert window._settings.value("canvas/backend") == "plotly" - - # A fresh window with the same settings boots straight into Plotly. - from otko.views.canvas_plotly import PlotlyCanvas - from otko.views.main_window import MainWindow - - settings = QSettings(str(tmp_path / "otko.ini"), QSettings.Format.IniFormat) - second = MainWindow(settings=settings) - qtbot.addWidget(second) - assert second.canvas_backend() == "plotly" - assert isinstance(second._canvas, PlotlyCanvas) - - -@pytest.mark.gui -def test_working_plane_survives_the_swap(tmp_path, qtbot) -> None: # type: ignore[no-untyped-def] - window = _make_window(tmp_path, qtbot) - window._vm.open(_example("basic_truss")) - window._canvas.set_working_plane("XY", 0.0) - - window.swap_canvas_backend("plotly") - - assert window._canvas.working_plane_type() == "XY" - assert window._canvas.working_plane_offset() == 0.0 - - -def _example(name: str) -> str: - from pathlib import Path - - return str(Path(__file__).resolve().parents[2] / "examples" / f"{name}.osmodel") diff --git a/tests/gui/test_canvas_grid_snap.py b/tests/gui/test_canvas_grid_snap.py index 02ddb4b..81e8bd3 100644 --- a/tests/gui/test_canvas_grid_snap.py +++ b/tests/gui/test_canvas_grid_snap.py @@ -12,7 +12,7 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( +from otko.core import ( # noqa: E402 CoordinateGridSystem, CoordinateSystem, GridSystem, @@ -23,8 +23,7 @@ from otko.core import ( # ────────────────────── logic helpers (no VTK) ────────────────────── def _nearest_snap( - cx: float, - cy: float, + cx: float, cy: float, world_pts: np.ndarray, screen_pts: np.ndarray, tol_px: float, @@ -34,7 +33,7 @@ def _nearest_snap( return None d2 = (screen_pts[:, 0] - cx) ** 2 + (screen_pts[:, 1] - cy) ** 2 idx = int(np.argmin(d2)) - if d2[idx] <= tol_px**2: + if d2[idx] <= tol_px ** 2: return tuple(float(v) for v in world_pts[idx]) # type: ignore[return-value] return None @@ -66,7 +65,6 @@ def test_grid_intersections_world_includes_all_visible_systems(qtbot) -> None: """ModelCanvas._grid_intersections_world combines every visible system's intersections (transformed by that system's origin/rotation).""" from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) @@ -103,17 +101,15 @@ def test_grid_intersections_world_includes_all_visible_systems(qtbot) -> None: def test_grid_intersections_world_returns_none_without_grid(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) - canvas.show_project(Project()) # default: Global system with no grid lines + canvas.show_project(Project()) # default: Global system with no grid lines assert canvas._grid_intersections_world() is None def test_hide_all_suppresses_intersections(qtbot) -> None: # type: ignore[no-untyped-def] """A system with ``hide_all=True`` must not contribute snap targets.""" from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) @@ -138,23 +134,20 @@ def test_hide_all_suppresses_intersections(qtbot) -> None: # type: ignore[no-un def test_hover_snap_marker_round_trips(qtbot) -> None: # type: ignore[no-untyped-def] """set_hover_snap(pt) creates an actor; passing None removes it.""" from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) - canvas.show_project( - Project( - coord_systems=[ - CoordinateGridSystem( - name="Global", - grid=GridSystem( - x_grid_lines=make_grid_lines("X", [0.0, 3.0]), - y_grid_lines=make_grid_lines("Y", [0.0, 4.0]), - z_grid_lines=make_grid_lines("Z", [0.0]), - ), + canvas.show_project(Project( + coord_systems=[ + CoordinateGridSystem( + name="Global", + grid=GridSystem( + x_grid_lines=make_grid_lines("X", [0.0, 3.0]), + y_grid_lines=make_grid_lines("Y", [0.0, 4.0]), + z_grid_lines=make_grid_lines("Z", [0.0]), ), - ], - ) - ) + ), + ], + )) r = canvas._renderer assert r._hover_actor is None r.set_hover_snap((3.0, 0.0, 0.0)) @@ -165,23 +158,20 @@ def test_hover_snap_marker_round_trips(qtbot) -> None: # type: ignore[no-untype def test_snap_preview_flag_clears_marker(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) - canvas.show_project( - Project( - coord_systems=[ - CoordinateGridSystem( - name="Global", - grid=GridSystem( - x_grid_lines=make_grid_lines("X", [0.0]), - y_grid_lines=make_grid_lines("Y", [0.0]), - z_grid_lines=make_grid_lines("Z", [0.0]), - ), + canvas.show_project(Project( + coord_systems=[ + CoordinateGridSystem( + name="Global", + grid=GridSystem( + x_grid_lines=make_grid_lines("X", [0.0]), + y_grid_lines=make_grid_lines("Y", [0.0]), + z_grid_lines=make_grid_lines("Z", [0.0]), ), - ], - ) - ) + ), + ], + )) canvas.set_snap_preview_enabled(True) canvas._renderer.set_hover_snap((0.0, 0.0, 0.0)) assert canvas._renderer._hover_actor is not None @@ -193,7 +183,7 @@ def test_snap_preview_flag_clears_marker(qtbot) -> None: # type: ignore[no-unty def _pt_to_segment_d2(p: np.ndarray, a: np.ndarray, b: np.ndarray) -> np.ndarray: """Vectorised point-to-segment squared distance, used to verify logic.""" ab = b - a - ab_sq = (ab**2).sum(axis=1) + ab_sq = (ab ** 2).sum(axis=1) ab_sq = np.where(ab_sq == 0, 1.0, ab_sq) pa = p - a t = (pa * ab).sum(axis=1) / ab_sq @@ -214,21 +204,21 @@ def test_point_to_segment_midpoint_hit() -> None: def test_point_to_segment_endpoint_hit() -> None: """A click near an endpoint must also hit — the bug fix this test guards.""" - p = np.array([1.0, 51.0]) # 1 px off node-a on a horizontal frame + p = np.array([1.0, 51.0]) # 1 px off node-a on a horizontal frame a = np.array([[0.0, 50.0]]) b = np.array([[100.0, 50.0]]) d2 = _pt_to_segment_d2(p, a, b) - assert d2[0] <= 2.0**2 # well inside a 5-pixel test tolerance + assert d2[0] <= 2.0 ** 2 # well inside a 5-pixel test tolerance def test_point_to_segment_orthogonal_miss() -> None: """Clicking far from a short frame must produce a large distance.""" p = np.array([50.0, 200.0]) a = np.array([[0.0, 50.0]]) - b = np.array([[10.0, 50.0]]) # short frame at (0..10, 50) + b = np.array([[10.0, 50.0]]) # short frame at (0..10, 50) d2 = _pt_to_segment_d2(p, a, b) # Closest point is (10, 50), distance ≈ sqrt(40² + 150²) ≈ 155 - assert d2[0] > 150.0**2 + assert d2[0] > 150.0 ** 2 def test_single_node_radius_uses_grid_extent(qtbot) -> None: # type: ignore[no-untyped-def] @@ -236,7 +226,6 @@ def test_single_node_radius_uses_grid_extent(qtbot) -> None: # type: ignore[no- grid bounds so the node remains visible (regression guard).""" from otko.core import Node from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) p = Project( diff --git a/tests/gui/test_case_forms.py b/tests/gui/test_case_forms.py index 6672806..c911141 100644 --- a/tests/gui/test_case_forms.py +++ b/tests/gui/test_case_forms.py @@ -6,13 +6,13 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( +from otko.core import ( # noqa: E402 NodalLoad, PlainLoadPattern, StaticCase, TransientCase, ) -from otko.views.dialogs.case_forms import TransientCaseForm +from otko.views.dialogs.case_forms import TransientCaseForm # noqa: E402 def _patterns(): # type: ignore[no-untyped-def] diff --git a/tests/gui/test_commands.py b/tests/gui/test_commands.py index 0457390..a02bdc0 100644 --- a/tests/gui/test_commands.py +++ b/tests/gui/test_commands.py @@ -11,14 +11,15 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( +from otko.commands import ( # noqa: E402 AddElementsCommand, AddNodalLoadsCommand, AddNodesCommand, + DeleteElementsCommand, DeleteNodesCommand, SetRestraintCommand, ) -from otko.core import ( +from otko.core import ( # noqa: E402 LinearTimeSeries, NodalLoad, Node, @@ -26,7 +27,7 @@ from otko.core import ( Steel01, TrussElement, ) -from otko.viewmodels import ProjectViewModel +from otko.viewmodels import ProjectViewModel # noqa: E402 # ─────────────────────────── helpers ──────────────────────────────── @@ -76,27 +77,17 @@ def test_add_nodes_duplicate_id_raises(qtbot) -> None: # type: ignore[no-untype @pytest.mark.gui def test_delete_node_cascades_to_elements(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm() - vm.apply_command( - AddNodesCommand( - vm, - [ - Node(id=1, coords=(0, 0, 0)), - Node(id=2, coords=(1, 0, 0)), - Node(id=3, coords=(2, 0, 0)), - ], - ) - ) + vm.apply_command(AddNodesCommand(vm, [ + Node(id=1, coords=(0, 0, 0)), + Node(id=2, coords=(1, 0, 0)), + Node(id=3, coords=(2, 0, 0)), + ])) # Add a material so the element is valid. vm.project.materials.append(Steel01(id=1, Fy=420e6, E0=200e9, b=0.01)) - vm.apply_command( - AddElementsCommand( - vm, - [ - TrussElement(id=1, nodes=(1, 2), area=1e-3, material_id=1), - TrussElement(id=2, nodes=(2, 3), area=1e-3, material_id=1), - ], - ) - ) + vm.apply_command(AddElementsCommand(vm, [ + TrussElement(id=1, nodes=(1, 2), area=1e-3, material_id=1), + TrussElement(id=2, nodes=(2, 3), area=1e-3, material_id=1), + ])) # Delete node 2 → both elements should disappear. vm.apply_command(DeleteNodesCommand(vm, {2})) assert {n.id for n in vm.project.nodes} == {1, 3} @@ -111,15 +102,10 @@ def test_delete_node_cascades_to_elements(qtbot) -> None: # type: ignore[no-unt @pytest.mark.gui def test_set_restraint_round_trip(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm() - vm.apply_command( - AddNodesCommand( - vm, - [ - Node(id=1, coords=(0, 0, 0)), - Node(id=2, coords=(1, 0, 0)), - ], - ) - ) + vm.apply_command(AddNodesCommand(vm, [ + Node(id=1, coords=(0, 0, 0)), + Node(id=2, coords=(1, 0, 0)), + ])) fix = (True, True, True, True, True, True) vm.apply_command(SetRestraintCommand(vm, {1, 2}, fix)) assert vm.project.node(1).restraint == fix diff --git a/tests/gui/test_convert_element_type.py b/tests/gui/test_convert_element_type.py index 426c23b..fa14511 100644 --- a/tests/gui/test_convert_element_type.py +++ b/tests/gui/test_convert_element_type.py @@ -6,82 +6,52 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( +from otko.commands import ( # noqa: E402 AddElementsCommand, AddMaterialsCommand, AddNodesCommand, AddSectionsCommand, ConvertElementTypeCommand, ) -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticBeamColumn, ElasticSection, ElasticUniaxial, Node, TrussElement, ) -from otko.viewmodels import ProjectViewModel +from otko.viewmodels import ProjectViewModel # noqa: E402 def _vm_setup() -> ProjectViewModel: vm = ProjectViewModel() vm.new_project() - vm.apply_command( - AddNodesCommand( - vm, - [ - Node(id=1, coords=(0, 0, 0)), - Node(id=2, coords=(3, 0, 0)), - ], - ) - ) - vm.apply_command( - AddMaterialsCommand( - vm, - [ - ElasticUniaxial(id=1, name="Steel", E=200e9), - ], - ) - ) - vm.apply_command( - AddSectionsCommand( - vm, - [ - ElasticSection( - id=1, - name="Default", - E=200e9, - A=0.01, - Iz=8.33e-6, - Iy=8.33e-6, - G=80e9, - J=1e-6, - ), - ], - ) - ) + vm.apply_command(AddNodesCommand(vm, [ + Node(id=1, coords=(0, 0, 0)), + Node(id=2, coords=(3, 0, 0)), + ])) + vm.apply_command(AddMaterialsCommand(vm, [ + ElasticUniaxial(id=1, name="Steel", E=200e9), + ])) + vm.apply_command(AddSectionsCommand(vm, [ + ElasticSection( + id=1, name="Default", E=200e9, A=0.01, + Iz=8.33e-6, Iy=8.33e-6, G=80e9, J=1e-6, + ), + ])) return vm @pytest.mark.gui def test_frame_to_truss_conversion(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_setup() - vm.apply_command( - AddElementsCommand( - vm, - [ - ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), - ], - ) - ) - vm.apply_command( - ConvertElementTypeCommand( - vm, - {1}, - "Truss", - defaults={"material_id": 1, "area": 0.001}, - ) - ) + vm.apply_command(AddElementsCommand(vm, [ + ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), + ])) + vm.apply_command(ConvertElementTypeCommand( + vm, {1}, "Truss", + defaults={"material_id": 1, "area": 0.001}, + )) el = vm.project.element(1) assert isinstance(el, TrussElement) assert el.nodes == (1, 2) @@ -92,22 +62,13 @@ def test_frame_to_truss_conversion(qtbot) -> None: # type: ignore[no-untyped-de @pytest.mark.gui def test_truss_to_frame_conversion(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_setup() - vm.apply_command( - AddElementsCommand( - vm, - [ - TrussElement(id=1, nodes=(1, 2), area=0.001, material_id=1), - ], - ) - ) - vm.apply_command( - ConvertElementTypeCommand( - vm, - {1}, - "ElasticBeamColumn", - defaults={"section_id": 1}, - ) - ) + vm.apply_command(AddElementsCommand(vm, [ + TrussElement(id=1, nodes=(1, 2), area=0.001, material_id=1), + ])) + vm.apply_command(ConvertElementTypeCommand( + vm, {1}, "ElasticBeamColumn", + defaults={"section_id": 1}, + )) el = vm.project.element(1) assert isinstance(el, ElasticBeamColumn) assert el.nodes == (1, 2) @@ -117,22 +78,12 @@ def test_truss_to_frame_conversion(qtbot) -> None: # type: ignore[no-untyped-de @pytest.mark.gui def test_convert_preserves_id_and_is_undoable(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_setup() - vm.apply_command( - AddElementsCommand( - vm, - [ - ElasticBeamColumn(id=42, name="A", nodes=(1, 2), section_id=1), - ], - ) - ) - vm.apply_command( - ConvertElementTypeCommand( - vm, - {42}, - "Truss", - defaults={"material_id": 1, "area": 0.002}, - ) - ) + vm.apply_command(AddElementsCommand(vm, [ + ElasticBeamColumn(id=42, name="A", nodes=(1, 2), section_id=1), + ])) + vm.apply_command(ConvertElementTypeCommand( + vm, {42}, "Truss", defaults={"material_id": 1, "area": 0.002}, + )) assert isinstance(vm.project.element(42), TrussElement) assert vm.project.element(42).name == "A" vm.undo_stack.undo() @@ -142,23 +93,13 @@ def test_convert_preserves_id_and_is_undoable(qtbot) -> None: # type: ignore[no @pytest.mark.gui def test_convert_same_type_is_noop(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_setup() - vm.apply_command( - AddElementsCommand( - vm, - [ - TrussElement(id=1, nodes=(1, 2), area=0.001, material_id=1), - ], - ) - ) + vm.apply_command(AddElementsCommand(vm, [ + TrussElement(id=1, nodes=(1, 2), area=0.001, material_id=1), + ])) before = vm.project.element(1) - vm.apply_command( - ConvertElementTypeCommand( - vm, - {1}, - "Truss", - defaults={"material_id": 1, "area": 0.002}, - ) - ) + vm.apply_command(ConvertElementTypeCommand( + vm, {1}, "Truss", defaults={"material_id": 1, "area": 0.002}, + )) # Class is still Truss and the element object is unchanged. assert vm.project.element(1) is before @@ -166,20 +107,10 @@ def test_convert_same_type_is_noop(qtbot) -> None: # type: ignore[no-untyped-de @pytest.mark.gui def test_unknown_target_type_raises(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_setup() - vm.apply_command( - AddElementsCommand( - vm, - [ - TrussElement(id=1, nodes=(1, 2), area=0.001, material_id=1), - ], - ) - ) + vm.apply_command(AddElementsCommand(vm, [ + TrussElement(id=1, nodes=(1, 2), area=0.001, material_id=1), + ])) with pytest.raises(ValueError): - vm.apply_command( - ConvertElementTypeCommand( - vm, - {1}, - "NotAType", - defaults={}, - ) - ) + vm.apply_command(ConvertElementTypeCommand( + vm, {1}, "NotAType", defaults={}, + )) diff --git a/tests/gui/test_define_dialogs.py b/tests/gui/test_define_dialogs.py index 4b6f9b3..c671a1e 100644 --- a/tests/gui/test_define_dialogs.py +++ b/tests/gui/test_define_dialogs.py @@ -6,9 +6,9 @@ import pytest pytest.importorskip("PySide6") -from otko.core import LinearTimeSeries, Project -from otko.views.dialogs.linear_time_series import LinearTimeSeriesDialog -from otko.views.dialogs.plain_pattern import PlainPatternDialog +from otko.core import LinearTimeSeries, Project # noqa: E402 +from otko.views.dialogs.linear_time_series import LinearTimeSeriesDialog # noqa: E402 +from otko.views.dialogs.plain_pattern import PlainPatternDialog # noqa: E402 @pytest.mark.gui @@ -27,12 +27,10 @@ def test_linear_time_series_dialog_builds_entity(qtbot) -> None: # type: ignore @pytest.mark.gui def test_plain_pattern_dialog_uses_selected_time_series(qtbot) -> None: # type: ignore[no-untyped-def] - proj = Project( - time_series=[ - LinearTimeSeries(id=1, name="GravityTS"), - LinearTimeSeries(id=2, name="RampTS"), - ] - ) + proj = Project(time_series=[ + LinearTimeSeries(id=1, name="GravityTS"), + LinearTimeSeries(id=2, name="RampTS"), + ]) dlg = PlainPatternDialog(project=proj, next_pattern_id=4) qtbot.addWidget(dlg) diff --git a/tests/gui/test_draw_frame_tool.py b/tests/gui/test_draw_frame_tool.py index ff6370c..b98bb40 100644 --- a/tests/gui/test_draw_frame_tool.py +++ b/tests/gui/test_draw_frame_tool.py @@ -11,11 +11,11 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import AddNodesCommand -from otko.core import ElasticBeamColumn, ElasticSection, Node -from otko.viewmodels import ProjectViewModel -from otko.views.canvas3d import SelectionState -from otko.views.tools.draw_frame import DrawFrameTool +from otko.commands import AddNodesCommand # noqa: E402 +from otko.core import ElasticBeamColumn, ElasticSection, Node # noqa: E402 +from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.views.canvas3d import SelectionState # noqa: E402 +from otko.views.tools.draw_frame import DrawFrameTool # noqa: E402 class _CanvasStub: @@ -35,15 +35,10 @@ class _CanvasStub: def _vm_with_two_nodes() -> ProjectViewModel: vm = ProjectViewModel() vm.new_project() - vm.apply_command( - AddNodesCommand( - vm, - [ - Node(id=1, coords=(0, 0, 0)), - Node(id=2, coords=(3, 0, 0)), - ], - ) - ) + vm.apply_command(AddNodesCommand(vm, [ + Node(id=1, coords=(0, 0, 0)), + Node(id=2, coords=(3, 0, 0)), + ])) return vm @@ -91,7 +86,7 @@ def test_self_pick_is_ignored(qtbot) -> None: # type: ignore[no-untyped-def] tool.activate() tool.on_node_picked(1) - tool.on_node_picked(1) # same node — should NOT create an element + tool.on_node_picked(1) # same node — should NOT create an element assert vm.project.elements == [] # First-node state preserved so user can finish the gesture. assert tool._first_node_id == 1 @@ -155,7 +150,6 @@ def test_reset_clears_first_pick(qtbot) -> None: # type: ignore[no-untyped-def] def test_empty_clicks_snap_and_create_frame(qtbot) -> None: # type: ignore[no-untyped-def] """Two clicks on empty grid intersections → 2 nodes + 1 frame.""" from otko.core import GridSystem - vm = ProjectViewModel() vm.new_project() vm.project.grid_system = GridSystem( # type: ignore[union-attr] @@ -171,9 +165,9 @@ def test_empty_clicks_snap_and_create_frame(qtbot) -> None: # type: ignore[no-u tool.on_empty_clicked(0.0, 0.0, 0.0) tool.on_empty_clicked(3.0, 4.0, 0.0) - assert len(vm.project.nodes) == 2 # type: ignore[union-attr] - assert len(vm.project.elements) == 1 # type: ignore[union-attr] - elem = vm.project.elements[0] # type: ignore[union-attr] + assert len(vm.project.nodes) == 2 # type: ignore[union-attr] + assert len(vm.project.elements) == 1 # type: ignore[union-attr] + elem = vm.project.elements[0] # type: ignore[union-attr] assert isinstance(elem, ElasticBeamColumn) n1 = next(n for n in vm.project.nodes if n.id == elem.nodes[0]) # type: ignore[union-attr] n2 = next(n for n in vm.project.nodes if n.id == elem.nodes[1]) # type: ignore[union-attr] @@ -185,21 +179,18 @@ def test_empty_clicks_snap_and_create_frame(qtbot) -> None: # type: ignore[no-u def test_empty_click_reuses_coincident_node(qtbot) -> None: # type: ignore[no-untyped-def] """An empty click at an existing node's location must not duplicate it.""" from otko.core import GridSystem - - vm = _vm_with_two_nodes() # nodes 1, 2 at (0,0,0) and (3,0,0) + vm = _vm_with_two_nodes() # nodes 1, 2 at (0,0,0) and (3,0,0) vm.project.grid_system = GridSystem( # type: ignore[union-attr] - x_lines=[0.0, 3.0], - y_lines=[0.0], - z_lines=[0.0], + x_lines=[0.0, 3.0], y_lines=[0.0], z_lines=[0.0], ) tool = DrawFrameTool(_CanvasStub(), vm) # type: ignore[arg-type] tool.activate() - tool.on_empty_clicked(0.0, 0.0, 0.0) # existing node 1 at (0,0,0) - tool.on_empty_clicked(3.0, 0.0, 0.0) # existing node 2 at (3,0,0) + tool.on_empty_clicked(0.0, 0.0, 0.0) # existing node 1 at (0,0,0) + tool.on_empty_clicked(3.0, 0.0, 0.0) # existing node 2 at (3,0,0) - assert len(vm.project.nodes) == 2 # type: ignore[union-attr] (no new nodes) - elem = vm.project.elements[0] # type: ignore[union-attr] + assert len(vm.project.nodes) == 2 # type: ignore[union-attr] (no new nodes) + elem = vm.project.elements[0] # type: ignore[union-attr] assert set(elem.nodes) == {1, 2} @@ -207,22 +198,19 @@ def test_empty_click_reuses_coincident_node(qtbot) -> None: # type: ignore[no-u def test_mixed_node_pick_then_empty_click(qtbot) -> None: # type: ignore[no-untyped-def] """First click picks existing node; second click creates new node + frame.""" from otko.core import GridSystem - vm = _vm_with_two_nodes() vm.project.grid_system = GridSystem( # type: ignore[union-attr] - x_lines=[0.0, 3.0, 6.0], - y_lines=[0.0], - z_lines=[0.0], + x_lines=[0.0, 3.0, 6.0], y_lines=[0.0], z_lines=[0.0], ) tool = DrawFrameTool(_CanvasStub(), vm) # type: ignore[arg-type] tool.activate() - tool.on_node_picked(1) # start at node 1 = (0,0,0) + tool.on_node_picked(1) # start at node 1 = (0,0,0) tool.on_empty_clicked(6.0, 0.0, 0.0) # canvas emits exact snap - assert len(vm.project.nodes) == 3 # type: ignore[union-attr] (new node added) + assert len(vm.project.nodes) == 3 # type: ignore[union-attr] (new node added) assert len(vm.project.elements) == 1 - new_node = vm.project.nodes[-1] # type: ignore[union-attr] + new_node = vm.project.nodes[-1] # type: ignore[union-attr] assert new_node.coords == (6.0, 0.0, 0.0) elem = vm.project.elements[0] assert set(elem.nodes) == {1, new_node.id} diff --git a/tests/gui/test_draw_node_tool.py b/tests/gui/test_draw_node_tool.py index aab389d..26a9edb 100644 --- a/tests/gui/test_draw_node_tool.py +++ b/tests/gui/test_draw_node_tool.py @@ -10,10 +10,10 @@ import pytest pytest.importorskip("PySide6") -from otko.core import GridSystem, Node -from otko.viewmodels import ProjectViewModel -from otko.views.canvas3d.selection import SelectionState -from otko.views.tools.draw_node import ( +from otko.core import GridSystem, Node, Project # noqa: E402 +from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.views.canvas3d.selection import SelectionState # noqa: E402 +from otko.views.tools.draw_node import ( # noqa: E402 DrawNodeTool, _snap_to_grid, ) @@ -46,12 +46,8 @@ def _vm_with_grid() -> ProjectViewModel: # ────────────────────────── snap helper ───────────────────────────── def test_snap_to_grid_picks_nearest_lines() -> None: x, y, z = _snap_to_grid( - 2.2, - 3.7, - 0.6, - x_lines=[0.0, 3.0], - y_lines=[0.0, 4.0], - z_lines=[0.0, 3.0], + 2.2, 3.7, 0.6, + x_lines=[0.0, 3.0], y_lines=[0.0, 4.0], z_lines=[0.0, 3.0], ) assert (x, y, z) == (3.0, 4.0, 0.0) @@ -103,11 +99,10 @@ def test_grid_less_canvas_never_emits_to_tool(qtbot) -> None: # type: ignore[no target. For a grid-less project, the canvas never emits, so the tool is never invoked. We therefore don't test rejection here. """ - vm = ProjectViewModel() - vm.new_project() + vm = ProjectViewModel(); vm.new_project() tool = DrawNodeTool(_CanvasStub(), vm) # type: ignore[arg-type] tool.activate() - assert vm.project.nodes == [] # type: ignore[union-attr] + assert vm.project.nodes == [] # type: ignore[union-attr] @pytest.mark.gui diff --git a/tests/gui/test_draw_truss_tool.py b/tests/gui/test_draw_truss_tool.py index bcc2879..3cc5d52 100644 --- a/tests/gui/test_draw_truss_tool.py +++ b/tests/gui/test_draw_truss_tool.py @@ -6,17 +6,18 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( +from otko.core import ( # noqa: E402 CoordinateGridSystem, + CoordinateSystem, ElasticUniaxial, GridSystem, Node, TrussElement, make_grid_lines, ) -from otko.viewmodels import ProjectViewModel -from otko.views.canvas3d.selection import SelectionState -from otko.views.tools.draw_truss import DrawTrussTool +from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.views.canvas3d.selection import SelectionState # noqa: E402 +from otko.views.tools.draw_truss import DrawTrussTool # noqa: E402 class _CanvasStub: @@ -33,12 +34,10 @@ class _CanvasStub: def _vm_with_grid_and_nodes() -> ProjectViewModel: vm = ProjectViewModel() vm.new_project() - vm.project.nodes.extend( - [ - Node(id=1, coords=(0, 0, 0)), - Node(id=2, coords=(3, 0, 0)), - ] - ) + vm.project.nodes.extend([ + Node(id=1, coords=(0, 0, 0)), + Node(id=2, coords=(3, 0, 0)), + ]) vm.project.coord_systems = [ CoordinateGridSystem( name="Global", @@ -61,13 +60,13 @@ def test_two_clicks_create_truss(qtbot) -> None: # type: ignore[no-untyped-def] tool.on_node_picked(1) tool.on_node_picked(2) - assert len(vm.project.elements) == 1 # type: ignore[union-attr] - el = vm.project.elements[0] # type: ignore[union-attr] + assert len(vm.project.elements) == 1 # type: ignore[union-attr] + el = vm.project.elements[0] # type: ignore[union-attr] assert isinstance(el, TrussElement) assert el.nodes == (1, 2) assert el.area > 0 # A default ElasticUniaxial must have been created for the material. - assert len(vm.project.materials) == 1 # type: ignore[union-attr] + assert len(vm.project.materials) == 1 # type: ignore[union-attr] assert isinstance(vm.project.materials[0], ElasticUniaxial) @@ -79,9 +78,9 @@ def test_draw_truss_reuses_existing_material(qtbot) -> None: # type: ignore[no- tool.activate() tool.on_node_picked(1) tool.on_node_picked(2) - el = vm.project.elements[0] # type: ignore[union-attr] + el = vm.project.elements[0] # type: ignore[union-attr] assert el.material_id == 1 - assert len(vm.project.materials) == 1 # type: ignore[union-attr] (no new material) + assert len(vm.project.materials) == 1 # type: ignore[union-attr] (no new material) @pytest.mark.gui @@ -105,8 +104,8 @@ def test_empty_click_creates_node_then_truss(qtbot) -> None: # type: ignore[no- tool.on_empty_clicked(3.0, 0.0, 0.0) # Two nodes created + one truss element. - assert len(vm.project.nodes) == 2 # type: ignore[union-attr] - assert len(vm.project.elements) == 1 # type: ignore[union-attr] + assert len(vm.project.nodes) == 2 # type: ignore[union-attr] + assert len(vm.project.elements) == 1 # type: ignore[union-attr] assert isinstance(vm.project.elements[0], TrussElement) @@ -116,5 +115,5 @@ def test_self_pick_is_ignored(qtbot) -> None: # type: ignore[no-untyped-def] tool = DrawTrussTool(_CanvasStub(), vm) # type: ignore[arg-type] tool.activate() tool.on_node_picked(1) - tool.on_node_picked(1) # same node — must not create a zero-length truss - assert vm.project.elements == [] # type: ignore[union-attr] + tool.on_node_picked(1) # same node — must not create a zero-length truss + assert vm.project.elements == [] # type: ignore[union-attr] diff --git a/tests/gui/test_grid_dialog.py b/tests/gui/test_grid_dialog.py index 0013ad4..489d64b 100644 --- a/tests/gui/test_grid_dialog.py +++ b/tests/gui/test_grid_dialog.py @@ -6,7 +6,7 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( +from otko.core import ( # noqa: E402 CoordinateGridSystem, CoordinateSystem, GridLine, @@ -19,18 +19,11 @@ from otko.core import ( @pytest.mark.gui def test_quick_grid_lines_produces_ordinates(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.dialogs.quick_grid_lines import QuickGridLinesDialog - dlg = QuickGridLinesDialog() qtbot.addWidget(dlg) - dlg._x_n.setValue(4) - dlg._x_s.setValue(3.0) - dlg._x_f.setValue(0.0) - dlg._y_n.setValue(2) - dlg._y_s.setValue(4.0) - dlg._y_f.setValue(0.0) - dlg._z_n.setValue(3) - dlg._z_s.setValue(3.0) - dlg._z_f.setValue(-3.0) + dlg._x_n.setValue(4); dlg._x_s.setValue(3.0); dlg._x_f.setValue(0.0) + dlg._y_n.setValue(2); dlg._y_s.setValue(4.0); dlg._y_f.setValue(0.0) + dlg._z_n.setValue(3); dlg._z_s.setValue(3.0); dlg._z_f.setValue(-3.0) xs, ys, zs = dlg.ordinates() assert xs == [0.0, 3.0, 6.0, 9.0] assert ys == [0.0, 4.0] @@ -40,7 +33,6 @@ def test_quick_grid_lines_produces_ordinates(qtbot) -> None: # type: ignore[no- @pytest.mark.gui def test_quick_grid_zero_lines_produces_empty(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.dialogs.quick_grid_lines import QuickGridLinesDialog - dlg = QuickGridLinesDialog() qtbot.addWidget(dlg) dlg._x_n.setValue(0) @@ -56,7 +48,6 @@ def test_locate_origin_round_trip(qtbot) -> None: # type: ignore[no-untyped-def from otko.views.dialogs.locate_origin import ( CoordSystemLocationOrientationDialog, ) - dlg = CoordSystemLocationOrientationDialog( origin=(1.5, -2.0, 3.0), rotation_deg=(0.0, 0.0, 45.0), @@ -74,7 +65,6 @@ def test_define_grid_data_round_trip(qtbot) -> None: # type: ignore[no-untyped- from otko.views.dialogs.define_grid_data import ( DefineGridSystemDataDialog, ) - cs = CoordinateGridSystem( name="Floor2", coord=CoordinateSystem(origin=(0, 0, 3.5)), @@ -106,7 +96,6 @@ def test_define_grid_data_spacing_mode_conversion(qtbot) -> None: # type: ignor from otko.views.dialogs.define_grid_data import ( DefineGridSystemDataDialog, ) - cs = CoordinateGridSystem( name="Test", grid=GridSystem(x_grid_lines=make_grid_lines("X", [0.0, 3.0, 6.0, 10.0])), @@ -128,7 +117,6 @@ def test_define_grid_data_global_name_locked(qtbot) -> None: # type: ignore[no- from otko.views.dialogs.define_grid_data import ( DefineGridSystemDataDialog, ) - dlg = DefineGridSystemDataDialog(is_global=True) qtbot.addWidget(dlg) assert dlg._name_edit.text() == "Global" @@ -142,7 +130,6 @@ def test_define_grid_data_add_and_delete_row(qtbot) -> None: # type: ignore[no- from otko.views.dialogs.define_grid_data import ( DefineGridSystemDataDialog, ) - dlg = DefineGridSystemDataDialog() qtbot.addWidget(dlg) # Start with empty X table. diff --git a/tests/gui/test_main_window.py b/tests/gui/test_main_window.py index a57a00f..8ce4d0b 100644 --- a/tests/gui/test_main_window.py +++ b/tests/gui/test_main_window.py @@ -20,22 +20,3 @@ def test_main_window_opens(qtbot) -> None: # type: ignore[no-untyped-def] window.show() assert window.isVisible() assert window.windowTitle() == "OTKO" - - -@pytest.mark.gui -def test_close_dirty_hidden_window_does_not_block(qtbot) -> None: # type: ignore[no-untyped-def] - """Closing a dirty but hidden window must not raise a modal prompt. - - Qt closes hidden widgets during teardown (as ``pytest-qt`` does); a - modal "unsaved changes" dialog there would block forever with no user - to answer it. The prompt is therefore gated on visibility. - """ - from otko.views.main_window import MainWindow - - window = MainWindow() - qtbot.addWidget(window) - window._vm.new_project() - window._vm.mark_dirty() - assert not window.isVisible() - assert window.close() is True - assert window._vm.is_dirty # unchanged: the prompt was skipped diff --git a/tests/gui/test_material_dialog_concrete04.py b/tests/gui/test_material_dialog_concrete04.py index 23523bc..4671343 100644 --- a/tests/gui/test_material_dialog_concrete04.py +++ b/tests/gui/test_material_dialog_concrete04.py @@ -6,10 +6,10 @@ import pytest pytest.importorskip("PySide6") -from otko.core import Concrete04 -from otko.views.dialogs.material_forms import ( - FORM_REGISTRY, +from otko.core import Concrete04 # noqa: E402 +from otko.views.dialogs.material_forms import ( # noqa: E402 Concrete04Form, + FORM_REGISTRY, ) @@ -37,7 +37,7 @@ def test_concrete04_read_no_tension(qtbot) -> None: # type: ignore[no-untyped-d form._epsc0.setValue(-0.002) form._epscu.setValue(-0.005) form._Ec.setValue(30e9) - form._fct.setValue(0.0) # 0 → no tension + form._fct.setValue(0.0) # 0 → no tension form._et.setValue(0.0) form._material_id = 1 @@ -77,14 +77,9 @@ def test_concrete04_read_with_tension(qtbot) -> None: # type: ignore[no-untyped def test_concrete04_populate_round_trip(qtbot) -> None: # type: ignore[no-untyped-def] """populate(mat) + read() reproduces the original model object.""" original = Concrete04( - id=3, - name="C40-Tension", - fpc=-40e6, - epsc0=-0.0022, - epscu=-0.006, - Ec=32e9, - fct=2.5e6, - et=8e-5, + id=3, name="C40-Tension", + fpc=-40e6, epsc0=-0.0022, epscu=-0.006, Ec=32e9, + fct=2.5e6, et=8e-5, ) form = Concrete04Form() @@ -107,8 +102,8 @@ def test_concrete04_accepts_ksi_values(qtbot) -> None: # type: ignore[no-untype """Form must not clamp US-customary (ksi) values for fpc.""" form = Concrete04Form() qtbot.addWidget(form) - form._fpc.setValue(-6.0) # ksi - form._Ec.setValue(3600.0) # ksi + form._fpc.setValue(-6.0) # ksi + form._Ec.setValue(3600.0) # ksi assert form._fpc.value() == pytest.approx(-6.0) assert form._Ec.value() == pytest.approx(3600.0) diff --git a/tests/gui/test_material_form_ranges.py b/tests/gui/test_material_form_ranges.py index 54c1899..13a6eb3 100644 --- a/tests/gui/test_material_form_ranges.py +++ b/tests/gui/test_material_form_ranges.py @@ -10,7 +10,7 @@ import pytest pytest.importorskip("PySide6") -from otko.views.dialogs.material_forms import ( +from otko.views.dialogs.material_forms import ( # noqa: E402 Concrete01Form, Concrete02Form, Steel01Form, @@ -50,7 +50,7 @@ def test_concrete02_accepts_ksi_values(qtbot) -> None: # type: ignore[no-untype qtbot.addWidget(form) form._fpc.setValue(-6.0) form._fpcu.setValue(-5.0) - form._ft.setValue(0.6) # ksi tensile strength + form._ft.setValue(0.6) # ksi tensile strength assert form._fpc.value() == pytest.approx(-6.0) assert form._fpcu.value() == pytest.approx(-5.0) assert form._ft.value() == pytest.approx(0.6) @@ -60,8 +60,8 @@ def test_concrete02_accepts_ksi_values(qtbot) -> None: # type: ignore[no-untype def test_steel01_accepts_ksi_values(qtbot) -> None: # type: ignore[no-untyped-def] form = Steel01Form() qtbot.addWidget(form) - form._fy.setValue(60.0) # ksi - form._e0.setValue(30000.0) # ksi + form._fy.setValue(60.0) # ksi + form._e0.setValue(30000.0) # ksi form._b.setValue(0.01) assert form._fy.value() == pytest.approx(60.0) assert form._e0.value() == pytest.approx(30000.0) @@ -83,7 +83,6 @@ def test_spinbox_uses_c_locale_for_decimal_separator(qtbot) -> None: # type: ig the OS locale. On tr_TR / de_DE Windows the default QLocale expects ',' and silently rejects Tcl-style '-0.004' inputs.""" from PySide6.QtCore import QLocale - form = Concrete01Form() qtbot.addWidget(form) assert form._epsc0.locale().decimalPoint() == "." diff --git a/tests/gui/test_path_ts_dialogs.py b/tests/gui/test_path_ts_dialogs.py index 5f1e132..1571fc3 100644 --- a/tests/gui/test_path_ts_dialogs.py +++ b/tests/gui/test_path_ts_dialogs.py @@ -8,16 +8,17 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( +from otko.commands import ( # noqa: E402 AddLoadPatternCommand, AddTimeSeriesCommand, ) -from otko.core import ( +from otko.core import ( # noqa: E402 + Node, PathTimeSeries, Project, UniformExcitationPattern, ) -from otko.viewmodels import ProjectViewModel +from otko.viewmodels import ProjectViewModel # noqa: E402 # ─────────────── AddTimeSeriesCommand / AddLoadPatternCommand ────────────── @@ -27,9 +28,9 @@ def test_add_time_series_command_is_undoable(qtbot) -> None: # type: ignore[no- vm.new_project() ts = PathTimeSeries(id=1, name="GM", dt=0.01, factor=386.4, values=[0.1, 0.2]) vm.apply_command(AddTimeSeriesCommand(vm, ts)) - assert len(vm.project.time_series) == 1 # type: ignore[union-attr] + assert len(vm.project.time_series) == 1 # type: ignore[union-attr] vm.undo_stack.undo() - assert vm.project.time_series == [] # type: ignore[union-attr] + assert vm.project.time_series == [] # type: ignore[union-attr] vm.undo_stack.redo() assert vm.project.time_series[0].name == "GM" # type: ignore[union-attr] @@ -38,14 +39,9 @@ def test_add_time_series_command_is_undoable(qtbot) -> None: # type: ignore[no- def test_add_time_series_rejects_duplicate_id(qtbot) -> None: # type: ignore[no-untyped-def] vm = ProjectViewModel() vm.new_project() - vm.project.time_series.append( - PathTimeSeries( # type: ignore[union-attr] - id=1, - name="Existing", - dt=0.01, - values=[0.0], - ) - ) + vm.project.time_series.append(PathTimeSeries( # type: ignore[union-attr] + id=1, name="Existing", dt=0.01, values=[0.0], + )) ts2 = PathTimeSeries(id=1, name="Duplicate", dt=0.01, values=[0.0]) with pytest.raises(ValueError): vm.apply_command(AddTimeSeriesCommand(vm, ts2)) @@ -55,31 +51,22 @@ def test_add_time_series_rejects_duplicate_id(qtbot) -> None: # type: ignore[no def test_add_load_pattern_command_is_undoable(qtbot) -> None: # type: ignore[no-untyped-def] vm = ProjectViewModel() vm.new_project() - vm.project.time_series.append( - PathTimeSeries( # type: ignore[union-attr] - id=1, - name="GM", - dt=0.01, - values=[0.1, 0.2], - ) - ) + vm.project.time_series.append(PathTimeSeries( # type: ignore[union-attr] + id=1, name="GM", dt=0.01, values=[0.1, 0.2], + )) pat = UniformExcitationPattern( - id=1, - name="EQ", - direction=1, - accel_series_id=1, + id=1, name="EQ", direction=1, accel_series_id=1, ) vm.apply_command(AddLoadPatternCommand(vm, pat)) - assert len(vm.project.load_patterns) == 1 # type: ignore[union-attr] + assert len(vm.project.load_patterns) == 1 # type: ignore[union-attr] vm.undo_stack.undo() - assert vm.project.load_patterns == [] # type: ignore[union-attr] + assert vm.project.load_patterns == [] # type: ignore[union-attr] # ─────────────── PathTimeSeriesDialog ────────────── @pytest.mark.gui def test_path_ts_dialog_manual_entry(qtbot, tmp_path) -> None: # type: ignore[no-untyped-def] from otko.views.dialogs.path_time_series import PathTimeSeriesDialog - dlg = PathTimeSeriesDialog(next_ts_id=1) qtbot.addWidget(dlg) # Simulate a plain-values import by directly seeding the values @@ -100,24 +87,20 @@ def test_path_ts_dialog_manual_entry(qtbot, tmp_path) -> None: # type: ignore[n def test_path_ts_dialog_imports_peer(qtbot, tmp_path) -> None: # type: ignore[no-untyped-def] """_on_import_peer populates dt / npts from the header + value list.""" from otko.views.dialogs.path_time_series import PathTimeSeriesDialog - rec = tmp_path / "test.at2" - rec.write_text( - textwrap.dedent("""\ + rec.write_text(textwrap.dedent("""\ PEER PACIFIC EL CENTRO 1940 ACCELERATION IN G 3 0.025 NPTS, DT 0.01 -0.02 0.03 - """) - ) + """)) dlg = PathTimeSeriesDialog(next_ts_id=1) qtbot.addWidget(dlg) # Feed the file path directly through the parser + state-setter # logic. Simulating QFileDialog in a headless test is fragile; # calling _on_import_peer's internals is the stable path. from otko.services.peer_record import parse_peer_record - dt, npts, vals = parse_peer_record(rec) dlg._values = vals dlg._dt_spin.setValue(dt) @@ -131,7 +114,6 @@ def test_path_ts_dialog_imports_peer(qtbot, tmp_path) -> None: # type: ignore[n def test_uniform_excitation_dialog_requires_time_series(qtbot) -> None: # type: ignore[no-untyped-def] """With no TimeSeries defined, the dialog's picker is disabled.""" from otko.views.dialogs.uniform_excitation import UniformExcitationDialog - proj = Project() dlg = UniformExcitationDialog(project=proj, next_pattern_id=1) qtbot.addWidget(dlg) @@ -141,12 +123,9 @@ def test_uniform_excitation_dialog_requires_time_series(qtbot) -> None: # type: @pytest.mark.gui def test_uniform_excitation_dialog_builds_pattern(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.dialogs.uniform_excitation import UniformExcitationDialog - - proj = Project( - time_series=[ - PathTimeSeries(id=7, name="GM", dt=0.01, values=[0.0, 0.1]), - ] - ) + proj = Project(time_series=[ + PathTimeSeries(id=7, name="GM", dt=0.01, values=[0.0, 0.1]), + ]) dlg = UniformExcitationDialog(project=proj, next_pattern_id=2) qtbot.addWidget(dlg) # Pick direction = 1 (X) — already default. diff --git a/tests/gui/test_plot_properties.py b/tests/gui/test_plot_properties.py deleted file mode 100644 index 3ef3601..0000000 --- a/tests/gui/test_plot_properties.py +++ /dev/null @@ -1,131 +0,0 @@ -"""GUI tests for Options → Plot Properties. - -Covers the dialog itself plus MainWindow's live-apply / persist / revert -wiring. QSettings are injected so nothing touches real user state. -""" - -from __future__ import annotations - -import pytest - -pytest.importorskip("PySide6") -pytest.importorskip("pyvistaqt") - -from PySide6.QtCore import QObject, QSettings, Signal -from PySide6.QtWidgets import QDialog - - -def _settings(tmp_path): # type: ignore[no-untyped-def] - return QSettings(str(tmp_path / "otko.ini"), QSettings.Format.IniFormat) - - -def _window(tmp_path, qtbot): # type: ignore[no-untyped-def] - from otko.views.main_window import MainWindow - - window = MainWindow(settings=_settings(tmp_path)) - qtbot.addWidget(window) - return window - - -def test_default_style_is_the_documented_palette(tmp_path, qtbot) -> None: # type: ignore[no-untyped-def] - from otko.views.canvas3d.style import RenderStyle - - window = _window(tmp_path, qtbot) - assert window._style == RenderStyle() - - -def test_set_plot_style_reaches_both_canvases_and_persists(tmp_path, qtbot) -> None: # type: ignore[no-untyped-def] - window = _window(tmp_path, qtbot) - window.swap_canvas_backend("plotly") # build the second canvas too - - window.set_plot_style( - window._style.with_updates(element_beam_color="#ff0000", label_font_size=20) - ) - - assert set(window._canvases) == {"pyvista", "plotly"} - for canvas in window._canvases.values(): - assert canvas._style.element_beam_color == "#ff0000" - assert canvas._style.label_font_size == 20 - - # A fresh window sharing the settings file reloads the customisation. - from otko.views.main_window import MainWindow - - fresh = MainWindow(settings=_settings(tmp_path)) - qtbot.addWidget(fresh) - assert fresh._style.element_beam_color == "#ff0000" - assert fresh._style.label_font_size == 20 - - -def test_preview_does_not_persist(tmp_path, qtbot) -> None: # type: ignore[no-untyped-def] - window = _window(tmp_path, qtbot) - window.set_plot_style(window._style.with_updates(node_color="#010203"), persist=False) - - assert window._canvas._style.node_color == "#010203" - assert window._settings.value("plot/props") is None - - -def test_cancel_reverts_the_style(tmp_path, qtbot, monkeypatch) -> None: # type: ignore[no-untyped-def] - window = _window(tmp_path, qtbot) - original = window._style - preview = original.with_updates(node_color="#010203") - - class _RejectingDialog(QObject): - changed = Signal(object) - - def __init__(self, style: object, parent: QObject | None = None) -> None: - super().__init__(parent) - self._style = preview - - def exec(self) -> QDialog.DialogCode: - self.changed.emit(preview) # live preview happens... - return QDialog.DialogCode.Rejected # ...but the user cancels - - def plot_style(self): # type: ignore[no-untyped-def] - return self._style - - monkeypatch.setattr("otko.views.dialogs.PlotPropertiesDialog", _RejectingDialog) - window._on_plot_properties() - - assert window._style == original - assert window._canvas._style.node_color == original.node_color - assert window._settings.value("plot/props") is None - - -def test_accept_applies_and_persists(tmp_path, qtbot, monkeypatch) -> None: # type: ignore[no-untyped-def] - window = _window(tmp_path, qtbot) - preview = window._style.with_updates(node_color="#010203") - - class _AcceptingDialog(QObject): - changed = Signal(object) - - def __init__(self, style: object, parent: QObject | None = None) -> None: - super().__init__(parent) - self._style = preview - - def exec(self) -> QDialog.DialogCode: - return QDialog.DialogCode.Accepted - - def plot_style(self): # type: ignore[no-untyped-def] - return self._style - - monkeypatch.setattr("otko.views.dialogs.PlotPropertiesDialog", _AcceptingDialog) - window._on_plot_properties() - - assert window._style.node_color == "#010203" - assert window._canvas._style.node_color == "#010203" - assert "#010203" in str(window._settings.value("plot/props")) - - -def test_dialog_edits_and_reset(tmp_path, qtbot) -> None: # type: ignore[no-untyped-def] - from otko.views.canvas3d.style import RenderStyle - from otko.views.dialogs import PlotPropertiesDialog - - dialog = PlotPropertiesDialog(RenderStyle()) - qtbot.addWidget(dialog) - assert dialog.plot_style() == RenderStyle() - - dialog._colors["node_color"].set_color("#abcdef") - assert dialog.plot_style().node_color == "#abcdef" - - dialog._reset() - assert dialog.plot_style() == RenderStyle() diff --git a/tests/gui/test_plotly_hover.py b/tests/gui/test_plotly_hover.py deleted file mode 100644 index 78d5160..0000000 --- a/tests/gui/test_plotly_hover.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Regression tests for the Plotly hover/snap-marker contract. - -``plotly_hover``/``plotly_unhover`` fire continuously while the mouse moves. -Touching the plot on each one made it redraw per mouse move, re-firing hover -until the stack blew (``RangeError: Maximum call stack size exceeded``) and the -canvas froze. The marker is therefore coalesced and only touched when its -shown/hidden state actually changes. -""" - -from __future__ import annotations - -from pathlib import Path - -import pytest - -pytest.importorskip("PySide6") -pytest.importorskip("plotly") - -from otko.services import load_project - -EXAMPLES = Path(__file__).resolve().parents[2] / "examples" - -#: 200 mouse-move events over a *non-snap* point with snapping armed. -_CHURN_PROBE = """(function(){ - var gd = document.getElementById('plot'); - var n = 0; var original = Plotly.restyle; - Plotly.restyle = function () { n++; return original.apply(Plotly, arguments); }; - var point = { data: gd.data[0], customdata: [0, 0, 0] }; - try { - for (var i = 0; i < 200; i++) { - gd.emit('plotly_hover', { points: [point] }); - gd.emit('plotly_unhover', {}); - } - return n; - } finally { Plotly.restyle = original; } -})()""" - -_HOVER_MARKER_X = ( - "JSON.stringify((document.getElementById('plot').data" - ".find(function(d){return d.meta && d.meta.kind === 'hover';}) || {}).x)" -) - -_EMIT_SNAP_HOVER = """(function(){ - var gd = document.getElementById('plot'); - var snap = gd.data.filter(function(d){return d.meta && d.meta.kind === 'snap';})[0]; - var point = { data: snap, customdata: [snap.x[0], snap.y[0], snap.z[0]] }; - gd.emit('plotly_hover', { points: [point] }); - return true; -})()""" - - -def _open_canvas(qtbot, name: str): # type: ignore[no-untyped-def] - from otko.views.canvas_plotly import PlotlyCanvas - - canvas = PlotlyCanvas() - qtbot.addWidget(canvas) - canvas.show_project(load_project(EXAMPLES / name)) - qtbot.waitUntil(lambda: canvas._ready, timeout=30000) - qtbot.wait(1500) # let the first Plotly.react settle - return canvas - - -def _run_js(canvas, qtbot, script: str, timeout: int = 15000): # type: ignore[no-untyped-def] - box: dict[str, object] = {} - canvas._web.page().runJavaScript(script, lambda value: box.update(value=value)) - qtbot.waitUntil(lambda: "value" in box, timeout=timeout) - return box["value"] - - -@pytest.mark.gui -def test_ordinary_mouse_movement_does_not_redraw(qtbot) -> None: # type: ignore[no-untyped-def] - """The unfixed version restyled once per hover/unhover event.""" - canvas = _open_canvas(qtbot, "space_frame_3d.osmodel") - - restyles = _run_js(canvas, qtbot, _CHURN_PROBE) - - assert restyles == 0, f"mouse movement caused {restyles} plot restyles" - - -@pytest.mark.gui -def test_snap_marker_shows_and_clears(qtbot) -> None: # type: ignore[no-untyped-def] - canvas = _open_canvas(qtbot, "basic_truss.osmodel") - assert _run_js(canvas, qtbot, _HOVER_MARKER_X) == "[]" - - canvas.set_snap_preview_enabled(True) - qtbot.wait(100) - _run_js(canvas, qtbot, _EMIT_SNAP_HOVER) - qtbot.wait(250) - assert _run_js(canvas, qtbot, _HOVER_MARKER_X) != "[]", "snap target not shown" - - _run_js(canvas, qtbot, "document.getElementById('plot').emit('plotly_unhover',{})") - qtbot.wait(250) - assert _run_js(canvas, qtbot, _HOVER_MARKER_X) == "[]", "snap target not cleared" diff --git a/tests/gui/test_plotly_view_preservation.py b/tests/gui/test_plotly_view_preservation.py deleted file mode 100644 index 97ef3ee..0000000 --- a/tests/gui/test_plotly_view_preservation.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Regression tests for the Plotly canvas push contract. - -``Plotly.react`` resets any scene attribute the incoming layout omits, so a -data-only push must be flagged ``preserveView`` (the JS side then carries the -live camera and axis ranges forward). Only explicit re-frames may send the -computed framing. These tests capture the JavaScript the canvas emits instead -of driving the browser, so they stay fast and deterministic. -""" - -from __future__ import annotations - -import json -from pathlib import Path - -import pytest - -pytest.importorskip("PySide6") -pytest.importorskip("plotly") - -from otko.services import load_project - -EXAMPLES = Path(__file__).resolve().parents[2] / "examples" - - -def _canvas_with_captured_js(qtbot): # type: ignore[no-untyped-def] - from otko.views.canvas_plotly import PlotlyCanvas - - canvas = PlotlyCanvas() - qtbot.addWidget(canvas) - # Pretend the page finished loading, then capture instead of running JS. - canvas._ready = True - calls: list[str] = [] - canvas._eval = lambda js: calls.append(js) # type: ignore[method-assign] - return canvas, calls - - -def _parse_call(call: str) -> tuple[dict, bool]: - """Split a captured ``window.otkoUpdate(, )`` call.""" - body = call[len("window.otkoUpdate(") : -1] - literal, _sep, flag = body.rpartition(", ") - return json.loads(json.loads(literal)), flag == "true" - - -@pytest.mark.gui -def test_framing_push_is_not_marked_preserve_view(qtbot) -> None: # type: ignore[no-untyped-def] - canvas, calls = _canvas_with_captured_js(qtbot) - - canvas.show_project(load_project(EXAMPLES / "cantilever.osmodel")) - - assert calls, "show_project must push a figure" - payload, preserve = _parse_call(calls[-1]) - assert preserve is False, "initial push must be a re-frame" - # The framing push carries the camera and the padded ranges. - scene = payload["layout"]["scene"] - assert "camera" in scene - assert "range" in scene["xaxis"] - assert scene["xaxis"]["autorange"] is False - - -@pytest.mark.gui -def test_data_update_preserves_the_view(qtbot) -> None: # type: ignore[no-untyped-def] - canvas, calls = _canvas_with_captured_js(qtbot) - - canvas.show_project(load_project(EXAMPLES / "cantilever.osmodel")) - calls.clear() - - canvas.selection.select_node(1) # data-only update - - assert calls, "selection change must push" - payload, preserve = _parse_call(calls[-1]) - assert preserve is True, "data update must preserve the view" - assert "camera" not in payload["layout"]["scene"], "must not re-send the camera" - - -@pytest.mark.gui -def test_view_presets_and_reset_re_frame(qtbot) -> None: # type: ignore[no-untyped-def] - canvas, calls = _canvas_with_captured_js(qtbot) - - canvas.show_project(load_project(EXAMPLES / "cantilever.osmodel")) - - for action in (canvas.view_xy, canvas.view_xz, canvas.view_yz, canvas.reset_camera): - calls.clear() - action() - assert calls[-1].endswith(", false)"), f"{action.__name__} must re-frame" - - -@pytest.mark.gui -def test_style_change_keeps_the_view_but_updates_the_layout(qtbot) -> None: # type: ignore[no-untyped-def] - """Re-colouring must not snap the camera back to the preset.""" - from otko.views.canvas3d.style import RenderStyle - - canvas, calls = _canvas_with_captured_js(qtbot) - canvas.show_project(load_project(EXAMPLES / "cantilever.osmodel")) - calls.clear() - - canvas.set_style(RenderStyle(show_axis_outline=True)) - - assert calls, "a style change must push" - payload, preserve = _parse_call(calls[-1]) - assert preserve is True, "colour changes must not re-frame the view" - assert payload["layout"]["scene"]["xaxis"]["showgrid"] is True # layout applied - - -@pytest.mark.gui -def test_js_console_messages_reach_the_log(qtbot) -> None: # type: ignore[no-untyped-def] - """A plotly failure inside the page must not be invisible from Python.""" - from PySide6.QtWebEngineCore import QWebEnginePage - - from otko.views.canvas_plotly.plotly_canvas import _LoggingPage - - messages: list[tuple[str, str]] = [] - page = _LoggingPage(lambda msg, severity="info": messages.append((severity, msg))) - - levels = QWebEnginePage.JavaScriptConsoleMessageLevel - page.javaScriptConsoleMessage(levels.ErrorMessageLevel, "boom", 7, "file:///tmp/plot.html") - page.javaScriptConsoleMessage(levels.WarningMessageLevel, "careful", 8, "file:///tmp/plot.html") - page.javaScriptConsoleMessage(levels.InfoMessageLevel, "hello", 9, "") - - assert messages == [ - ("error", "[web] plot.html:7 boom"), - ("warning", "[web] plot.html:8 careful"), - ("info", "[web] plotly:9 hello"), - ] diff --git a/tests/gui/test_property_commands.py b/tests/gui/test_property_commands.py index 8a49e26..117c445 100644 --- a/tests/gui/test_property_commands.py +++ b/tests/gui/test_property_commands.py @@ -6,7 +6,7 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( +from otko.commands import ( # noqa: E402 AddElementsCommand, AddMaterialsCommand, AddNodesCommand, @@ -16,7 +16,7 @@ from otko.commands import ( UpdateMaterialCommand, UpdateSectionCommand, ) -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticBeamColumn, ElasticSection, Node, @@ -24,40 +24,24 @@ from otko.core import ( Steel02, TrussElement, ) -from otko.viewmodels import ProjectViewModel +from otko.viewmodels import ProjectViewModel # noqa: E402 def _vm_with_steel() -> ProjectViewModel: vm = ProjectViewModel() vm.new_project() - vm.apply_command( - AddNodesCommand( - vm, - [ - Node(id=1, coords=(0, 0, 0)), - Node(id=2, coords=(1, 0, 0)), - Node(id=3, coords=(2, 0, 0)), - ], - ) - ) - vm.apply_command( - AddMaterialsCommand( - vm, - [ - Steel01(id=1, name="S420", Fy=420e6, E0=200e9, b=0.01), - ], - ) - ) - vm.apply_command( - AddSectionsCommand( - vm, - [ - ElasticSection( - id=1, name="Default", E=200e9, A=0.01, Iz=8.33e-6, Iy=8.33e-6, G=80e9, J=1e-6 - ), - ], - ) - ) + vm.apply_command(AddNodesCommand(vm, [ + Node(id=1, coords=(0, 0, 0)), + Node(id=2, coords=(1, 0, 0)), + Node(id=3, coords=(2, 0, 0)), + ])) + vm.apply_command(AddMaterialsCommand(vm, [ + Steel01(id=1, name="S420", Fy=420e6, E0=200e9, b=0.01), + ])) + vm.apply_command(AddSectionsCommand(vm, [ + ElasticSection(id=1, name="Default", E=200e9, A=0.01, + Iz=8.33e-6, Iy=8.33e-6, G=80e9, J=1e-6), + ])) return vm @@ -107,7 +91,8 @@ def test_update_unknown_id_raises(qtbot) -> None: # type: ignore[no-untyped-def @pytest.mark.gui def test_update_section_changes_inertia(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_with_steel() - new = ElasticSection(id=1, name="Updated", E=200e9, A=0.02, Iz=2e-5, Iy=2e-5, G=80e9, J=2e-6) + new = ElasticSection(id=1, name="Updated", E=200e9, A=0.02, + Iz=2e-5, Iy=2e-5, G=80e9, J=2e-6) vm.apply_command(UpdateSectionCommand(vm, new)) s = vm.project.section(1) assert s.A == 0.02 @@ -121,23 +106,14 @@ def test_update_section_changes_inertia(qtbot) -> None: # type: ignore[no-untyp def test_assign_section_to_frames(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_with_steel() # Add a second section and two frames. - vm.apply_command( - AddSectionsCommand( - vm, - [ - ElasticSection(id=2, name="Big", E=200e9, A=0.05, Iz=4e-5, Iy=4e-5, G=80e9, J=2e-6), - ], - ) - ) - vm.apply_command( - AddElementsCommand( - vm, - [ - ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), - ElasticBeamColumn(id=2, nodes=(2, 3), section_id=1), - ], - ) - ) + vm.apply_command(AddSectionsCommand(vm, [ + ElasticSection(id=2, name="Big", E=200e9, A=0.05, + Iz=4e-5, Iy=4e-5, G=80e9, J=2e-6), + ])) + vm.apply_command(AddElementsCommand(vm, [ + ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), + ElasticBeamColumn(id=2, nodes=(2, 3), section_id=1), + ])) vm.apply_command(AssignSectionCommand(vm, {1, 2}, section_id=2)) assert vm.project.element(1).section_id == 2 assert vm.project.element(2).section_id == 2 @@ -150,15 +126,10 @@ def test_assign_section_to_frames(qtbot) -> None: # type: ignore[no-untyped-def def test_assign_section_skips_truss_elements(qtbot) -> None: # type: ignore[no-untyped-def] """Truss has no section_id — command should silently skip it.""" vm = _vm_with_steel() - vm.apply_command( - AddElementsCommand( - vm, - [ - TrussElement(id=1, nodes=(1, 2), area=1e-3, material_id=1), - ElasticBeamColumn(id=2, nodes=(2, 3), section_id=1), - ], - ) - ) + vm.apply_command(AddElementsCommand(vm, [ + TrussElement(id=1, nodes=(1, 2), area=1e-3, material_id=1), + ElasticBeamColumn(id=2, nodes=(2, 3), section_id=1), + ])) vm.apply_command(AssignSectionCommand(vm, {1, 2}, section_id=1)) # Frame ok, truss unchanged (still material-based). assert vm.project.element(2).section_id == 1 @@ -169,23 +140,13 @@ def test_assign_section_skips_truss_elements(qtbot) -> None: # type: ignore[no- @pytest.mark.gui def test_assign_material_to_truss(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_with_steel() - vm.apply_command( - AddMaterialsCommand( - vm, - [ - Steel02(id=2, name="S355", Fy=355e6, E0=210e9, b=0.005), - ], - ) - ) - vm.apply_command( - AddElementsCommand( - vm, - [ - TrussElement(id=1, nodes=(1, 2), area=1e-3, material_id=1), - TrussElement(id=2, nodes=(2, 3), area=1e-3, material_id=1), - ], - ) - ) + vm.apply_command(AddMaterialsCommand(vm, [ + Steel02(id=2, name="S355", Fy=355e6, E0=210e9, b=0.005), + ])) + vm.apply_command(AddElementsCommand(vm, [ + TrussElement(id=1, nodes=(1, 2), area=1e-3, material_id=1), + TrussElement(id=2, nodes=(2, 3), area=1e-3, material_id=1), + ])) vm.apply_command(AssignMaterialCommand(vm, {1, 2}, material_id=2)) assert vm.project.element(1).material_id == 2 assert vm.project.element(2).material_id == 2 @@ -196,15 +157,10 @@ def test_assign_material_to_truss(qtbot) -> None: # type: ignore[no-untyped-def @pytest.mark.gui def test_assign_material_skips_frame_elements(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_with_steel() - vm.apply_command( - AddElementsCommand( - vm, - [ - ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), - TrussElement(id=2, nodes=(2, 3), area=1e-3, material_id=1), - ], - ) - ) + vm.apply_command(AddElementsCommand(vm, [ + ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), + TrussElement(id=2, nodes=(2, 3), area=1e-3, material_id=1), + ])) vm.apply_command(AssignMaterialCommand(vm, {1, 2}, material_id=1)) # Frame doesn't have material_id; only the truss is affected. assert vm.project.element(2).material_id == 1 diff --git a/tests/gui/test_property_forms.py b/tests/gui/test_property_forms.py index aed5ece..7a5d74c 100644 --- a/tests/gui/test_property_forms.py +++ b/tests/gui/test_property_forms.py @@ -10,10 +10,13 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( +from otko.core import ( # noqa: E402 + Concrete01, Concrete02, ElasticIsotropic, + ElasticPP, ElasticSection, + ElasticUniaxial, Steel01, Steel02, ) @@ -34,7 +37,8 @@ def test_steel01_form_round_trip(qtbot) -> None: # type: ignore[no-untyped-def] def test_steel02_form_round_trip(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.dialogs.material_forms import Steel02Form - original = Steel02(id=3, name="S355", Fy=355e6, E0=210e9, b=0.005, R0=18.0, cR1=0.925, cR2=0.15) + original = Steel02(id=3, name="S355", Fy=355e6, E0=210e9, b=0.005, + R0=18.0, cR1=0.925, cR2=0.15) form = Steel02Form() form.populate(original) restored = form.read() @@ -46,15 +50,8 @@ def test_concrete02_form_round_trip(qtbot) -> None: # type: ignore[no-untyped-d from otko.views.dialogs.material_forms import Concrete02Form original = Concrete02( - id=2, - name="C30", - fpc=-30e6, - epsc0=-0.002, - fpcu=-15e6, - epsU=-0.005, - ft=3e6, - Ets=2e9, - **{"lambda": 0.1}, + id=2, name="C30", fpc=-30e6, epsc0=-0.002, fpcu=-15e6, epsU=-0.005, + ft=3e6, Ets=2e9, **{"lambda": 0.1}, ) form = Concrete02Form() form.populate(original) @@ -78,14 +75,8 @@ def test_elastic_section_form_round_trip(qtbot) -> None: # type: ignore[no-unty from otko.views.dialogs.section_forms import ElasticSectionForm original = ElasticSection( - id=11, - name="W14x90", - E=200e9, - A=0.017, - Iz=4.16e-4, - Iy=1.29e-4, - G=80e9, - J=2.04e-6, + id=11, name="W14x90", + E=200e9, A=0.017, Iz=4.16e-4, Iy=1.29e-4, G=80e9, J=2.04e-6, ) form = ElasticSectionForm() form.populate(original) @@ -97,14 +88,10 @@ def test_elastic_section_form_round_trip(qtbot) -> None: # type: ignore[no-unty def test_form_for_dispatches_by_type(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.dialogs.material_forms import ( Steel01Form, - ) - from otko.views.dialogs.material_forms import ( form_for as material_form_for, ) from otko.views.dialogs.section_forms import ( ElasticSectionForm, - ) - from otko.views.dialogs.section_forms import ( form_for as section_form_for, ) diff --git a/tests/gui/test_pushover_curve_units.py b/tests/gui/test_pushover_curve_units.py index 9aee331..3c6515b 100644 --- a/tests/gui/test_pushover_curve_units.py +++ b/tests/gui/test_pushover_curve_units.py @@ -7,9 +7,9 @@ import pytest pytest.importorskip("PySide6") -from otko.core import UnitSystem -from otko.services.results import PushoverResults -from otko.views.docks.pushover_curve import ( +from otko.core import UnitSystem # noqa: E402 +from otko.services.results import PushoverResults # noqa: E402 +from otko.views.docks.pushover_curve import ( # noqa: E402 PushoverCurveView, _is_rotation_dof, ) @@ -17,11 +17,8 @@ from otko.views.docks.pushover_curve import ( def _pushover(control_dof: int = 1) -> PushoverResults: return PushoverResults( - case_id=1, - case_name="tst", - n_steps=5, - control_node=2, - control_dof=control_dof, + case_id=1, case_name="tst", n_steps=5, + control_node=2, control_dof=control_dof, control_disp=np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]), base_shear=np.array([0.0, 100.0, 200.0, 300.0, 400.0, 500.0]), ) @@ -37,15 +34,15 @@ def test_is_rotation_dof_ndf3() -> None: def test_is_rotation_dof_ndf6() -> None: """In a 3D/ndf=6 model, DOFs 4, 5, 6 are rotational.""" assert _is_rotation_dof(1, ndf=6) is False - assert _is_rotation_dof(3, ndf=6) is False # Uz in 3D - assert _is_rotation_dof(4, ndf=6) is True # Rx - assert _is_rotation_dof(5, ndf=6) is True # Ry - assert _is_rotation_dof(6, ndf=6) is True # Rz + assert _is_rotation_dof(3, ndf=6) is False # Uz in 3D + assert _is_rotation_dof(4, ndf=6) is True # Rx + assert _is_rotation_dof(5, ndf=6) is True # Ry + assert _is_rotation_dof(6, ndf=6) is True # Rz @pytest.mark.gui def test_si_translation_labels(qtbot) -> None: # type: ignore[no-untyped-def] - v = PushoverCurveView(units=UnitSystem.SI_M_N, ndf=6) + v = PushoverCurveView(units=UnitSystem.METRIC, ndf=6) qtbot.addWidget(v) v.set_results(_pushover(control_dof=1)) assert "m" in v._plot.getAxis("bottom").labelText @@ -56,7 +53,7 @@ def test_si_translation_labels(qtbot) -> None: # type: ignore[no-untyped-def] @pytest.mark.gui def test_si_rotation_labels_show_curvature_and_moment(qtbot) -> None: # type: ignore[no-untyped-def] - v = PushoverCurveView(units=UnitSystem.SI_M_N, ndf=3) + v = PushoverCurveView(units=UnitSystem.METRIC, ndf=3) qtbot.addWidget(v) v.set_results(_pushover(control_dof=3)) assert "1/m" in v._plot.getAxis("bottom").labelText @@ -68,7 +65,7 @@ def test_si_rotation_labels_show_curvature_and_moment(qtbot) -> None: # type: i @pytest.mark.gui def test_us_in_kip_rotation_labels(qtbot) -> None: # type: ignore[no-untyped-def] """kip-in Moment-Curvature project must NOT show cm / kN anywhere.""" - v = PushoverCurveView(units=UnitSystem.US_IN_KIP, ndf=3) + v = PushoverCurveView(units=UnitSystem.IMPERIAL, ndf=3) qtbot.addWidget(v) v.set_results(_pushover(control_dof=3)) x_label = v._plot.getAxis("bottom").labelText @@ -81,7 +78,7 @@ def test_us_in_kip_rotation_labels(qtbot) -> None: # type: ignore[no-untyped-de @pytest.mark.gui def test_us_in_kip_translation_labels(qtbot) -> None: # type: ignore[no-untyped-def] - v = PushoverCurveView(units=UnitSystem.US_IN_KIP, ndf=3) + v = PushoverCurveView(units=UnitSystem.IMPERIAL, ndf=3) qtbot.addWidget(v) v.set_results(_pushover(control_dof=1)) assert "in" in v._plot.getAxis("bottom").labelText @@ -93,13 +90,14 @@ def test_no_auto_scaling_applied_to_values(qtbot) -> None: # type: ignore[no-un """Regression: earlier versions divided base_shear by 1000 to display kN. The values must now be drawn exactly as stored so kip-in users don't see nonsense scaling.""" - v = PushoverCurveView(units=UnitSystem.US_IN_KIP, ndf=3) + v = PushoverCurveView(units=UnitSystem.IMPERIAL, ndf=3) qtbot.addWidget(v) r = _pushover(control_dof=3) v.set_results(r) # Take the single line item that was added; the item's data # should match the input arrays point-for-point. - items = [it for it in v._plot.listDataItems() if hasattr(it, "getData")] + items = [it for it in v._plot.listDataItems() + if hasattr(it, "getData")] assert items, "Pushover curve has no plot items" xs, ys = items[0].getData() # The first line item is the actual data (reference line is second). diff --git a/tests/gui/test_run_analysis.py b/tests/gui/test_run_analysis.py index 5281fd8..0e40752 100644 --- a/tests/gui/test_run_analysis.py +++ b/tests/gui/test_run_analysis.py @@ -8,11 +8,11 @@ import pytest pytest.importorskip("PySide6") -from PySide6.QtCore import QObject, Signal +from PySide6.QtCore import QObject, Signal # noqa: E402 -from otko.core import LinearTimeSeries, PlainLoadPattern, TransientCase -from otko.viewmodels import ProjectViewModel -from otko.views.dialogs.run_analysis import RunAnalysisDialog +from otko.core import LinearTimeSeries, PlainLoadPattern, Project, TransientCase # noqa: E402 +from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.views.dialogs.run_analysis import RunAnalysisDialog # noqa: E402 class _FakeRunner(QObject): @@ -41,18 +41,16 @@ def test_run_dialog_applies_transient_damping_overrides(qtbot, tmp_path) -> None vm.new_project() vm.project.time_series.append(LinearTimeSeries(id=1, name="Ramp")) # type: ignore[union-attr] vm.project.load_patterns.append(PlainLoadPattern(id=1, name="P1", time_series_id=1)) # type: ignore[union-attr] - vm.project.analyses.append( - TransientCase( # type: ignore[union-attr] - id=1, - name="EQ", - pattern_ids=[1], - dt=0.01, - n_steps=10, - rayleigh_alpha_m=0.1, - rayleigh_beta_k=0.002, - rayleigh_mode1_damping=0.02, - ) - ) + vm.project.analyses.append(TransientCase( # type: ignore[union-attr] + id=1, + name="EQ", + pattern_ids=[1], + dt=0.01, + n_steps=10, + rayleigh_alpha_m=0.1, + rayleigh_beta_k=0.002, + rayleigh_mode1_damping=0.02, + )) vm._path = Path(tmp_path) / "demo.osmodel" # type: ignore[attr-defined] runner = _FakeRunner() diff --git a/tests/gui/test_section_extrusion.py b/tests/gui/test_section_extrusion.py index 3f66eb4..449b24e 100644 --- a/tests/gui/test_section_extrusion.py +++ b/tests/gui/test_section_extrusion.py @@ -6,7 +6,7 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticBeamColumn, ElasticSection, Node, @@ -18,25 +18,18 @@ def _frame_project() -> Project: # Rectangular section 0.3×0.5 m. b, h = 0.30, 0.50 A = b * h - Iz = b * h**3 / 12.0 - Iy = h * b**3 / 12.0 + Iz = b * h ** 3 / 12.0 + Iy = h * b ** 3 / 12.0 return Project( nodes=[ - Node(id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, True, True, True, True)), + Node(id=1, coords=(0.0, 0.0, 0.0), + restraint=(True, True, True, True, True, True)), Node(id=2, coords=(6.0, 0.0, 0.0)), ], - sections=[ - ElasticSection( - id=1, - name="Rect", - E=200e9, - A=A, - Iz=Iz, - Iy=Iy, - G=80e9, - J=1e-6, - ) - ], + sections=[ElasticSection( + id=1, name="Rect", E=200e9, A=A, + Iz=Iz, Iy=Iy, G=80e9, J=1e-6, + )], elements=[ ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), ], @@ -46,7 +39,6 @@ def _frame_project() -> Project: @pytest.mark.gui def test_toggle_creates_and_removes_extrusion_actor(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) canvas.show_project(_frame_project()) @@ -66,7 +58,6 @@ def test_extrusion_skips_elements_without_section(qtbot) -> None: # type: ignor """A truss element has no section — it must be skipped silently.""" from otko.core import ElasticUniaxial, TrussElement from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) p = Project( diff --git a/tests/gui/test_section_forms.py b/tests/gui/test_section_forms.py index 4747cec..9e68a75 100644 --- a/tests/gui/test_section_forms.py +++ b/tests/gui/test_section_forms.py @@ -7,14 +7,14 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( +from otko.core import ( # noqa: E402 AggregatorDOF, ElasticSection, FiberSection, RectangularPatch, SectionAggregator, ) -from otko.views.dialogs.section_forms import ( +from otko.views.dialogs.section_forms import ( # noqa: E402 FORM_REGISTRY, form_for, ) @@ -31,14 +31,7 @@ def test_section_aggregator_has_registered_form() -> None: @pytest.mark.gui def test_form_for_elastic_section_returns_editable_form(qtbot) -> None: # type: ignore[no-untyped-def] s = ElasticSection( - id=1, - name="Rect", - E=200e9, - A=0.01, - Iz=1e-5, - Iy=1e-5, - G=80e9, - J=1e-6, + id=1, name="Rect", E=200e9, A=0.01, Iz=1e-5, Iy=1e-5, G=80e9, J=1e-6, ) f = form_for(s) qtbot.addWidget(f) @@ -48,19 +41,11 @@ def test_form_for_elastic_section_returns_editable_form(qtbot) -> None: # type: @pytest.mark.gui def test_form_for_fiber_section_returns_summary(qtbot) -> None: # type: ignore[no-untyped-def] s = FiberSection( - id=1, - name="RC", - patches=[ - RectangularPatch( - material_id=1, - n_fib_y=4, - n_fib_z=4, - y_i=-0.1, - z_i=-0.15, - y_j=0.1, - z_j=0.15, - ) - ], + id=1, name="RC", + patches=[RectangularPatch( + material_id=1, n_fib_y=4, n_fib_z=4, + y_i=-0.1, z_i=-0.15, y_j=0.1, z_j=0.15, + )], ) f = form_for(s) qtbot.addWidget(f) @@ -74,9 +59,7 @@ def test_form_for_fiber_section_returns_summary(qtbot) -> None: # type: ignore[ @pytest.mark.gui def test_form_for_section_aggregator_returns_summary(qtbot) -> None: # type: ignore[no-untyped-def] s = SectionAggregator( - id=2, - name="Agg", - section_id=1, + id=2, name="Agg", section_id=1, pairings=[AggregatorDOF(material_id=3, dof="T")], ) f = form_for(s) @@ -88,12 +71,10 @@ def test_form_for_section_aggregator_returns_summary(qtbot) -> None: # type: ig def test_form_for_unknown_type_falls_back(qtbot) -> None: # type: ignore[no-untyped-def] """An unknown section type must NOT raise — the dialog gets a placeholder form so the whole UI doesn't go down.""" - class _Mystery: type = "NotRegistered" id = 99 name = "???" - f = form_for(_Mystery()) qtbot.addWidget(f) # Placeholder form exists; no exception raised. diff --git a/tests/gui/test_selection.py b/tests/gui/test_selection.py index 365c03c..c795493 100644 --- a/tests/gui/test_selection.py +++ b/tests/gui/test_selection.py @@ -6,7 +6,7 @@ import pytest pytest.importorskip("PySide6") -from otko.views.canvas3d import SelectionState +from otko.views.canvas3d import SelectionState # noqa: E402 @pytest.mark.gui diff --git a/tests/gui/test_transforms.py b/tests/gui/test_transforms.py index fe13b70..120cc64 100644 --- a/tests/gui/test_transforms.py +++ b/tests/gui/test_transforms.py @@ -6,44 +6,34 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( +from otko.commands import ( # noqa: E402 AddElementsCommand, AddNodesCommand, MirrorCommand, MoveNodesCommand, ReplicateCommand, ) -from otko.core import Node, Steel01, TrussElement -from otko.viewmodels import ProjectViewModel +from otko.core import Node, Steel01, TrussElement # noqa: E402 +from otko.viewmodels import ProjectViewModel # noqa: E402 def _populated_vm() -> ProjectViewModel: """A small VM with 4 corner nodes + 4 truss elements forming a square.""" vm = ProjectViewModel() vm.new_project() - vm.apply_command( - AddNodesCommand( - vm, - [ - Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), - Node(id=2, coords=(1, 0, 0)), - Node(id=3, coords=(1, 1, 0)), - Node(id=4, coords=(0, 1, 0)), - ], - ) - ) + vm.apply_command(AddNodesCommand(vm, [ + Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), + Node(id=2, coords=(1, 0, 0)), + Node(id=3, coords=(1, 1, 0)), + Node(id=4, coords=(0, 1, 0)), + ])) vm.project.materials.append(Steel01(id=1, Fy=420e6, E0=200e9, b=0.01)) - vm.apply_command( - AddElementsCommand( - vm, - [ - TrussElement(id=1, nodes=(1, 2), area=1e-3, material_id=1), - TrussElement(id=2, nodes=(2, 3), area=1e-3, material_id=1), - TrussElement(id=3, nodes=(3, 4), area=1e-3, material_id=1), - TrussElement(id=4, nodes=(4, 1), area=1e-3, material_id=1), - ], - ) - ) + vm.apply_command(AddElementsCommand(vm, [ + TrussElement(id=1, nodes=(1, 2), area=1e-3, material_id=1), + TrussElement(id=2, nodes=(2, 3), area=1e-3, material_id=1), + TrussElement(id=3, nodes=(3, 4), area=1e-3, material_id=1), + TrussElement(id=4, nodes=(4, 1), area=1e-3, material_id=1), + ])) return vm @@ -54,7 +44,7 @@ def test_move_translates_in_place(qtbot) -> None: # type: ignore[no-untyped-def vm.apply_command(MoveNodesCommand(vm, {1, 2, 3, 4}, (10.0, 0.0, 0.0))) assert vm.project.node(1).coords == (10.0, 0.0, 0.0) assert vm.project.node(2).coords == (11.0, 0.0, 0.0) - assert len(vm.project.nodes) == 4 # no copies created + assert len(vm.project.nodes) == 4 # no copies created @pytest.mark.gui @@ -78,9 +68,8 @@ def test_move_preserves_restraint(qtbot) -> None: # type: ignore[no-untyped-def @pytest.mark.gui def test_replicate_creates_n_copies(qtbot) -> None: # type: ignore[no-untyped-def] vm = _populated_vm() - vm.apply_command( - ReplicateCommand(vm, {1, 2, 3, 4}, {1, 2, 3, 4}, offset=(0, 0, 3.0), n_copies=2) - ) + vm.apply_command(ReplicateCommand(vm, {1, 2, 3, 4}, {1, 2, 3, 4}, + offset=(0, 0, 3.0), n_copies=2)) # Original 4 + 2*4 copies = 12 nodes; original 4 + 2*4 elements = 12 elements assert len(vm.project.nodes) == 12 assert len(vm.project.elements) == 12 @@ -92,9 +81,8 @@ def test_replicate_creates_n_copies(qtbot) -> None: # type: ignore[no-untyped-d @pytest.mark.gui def test_replicate_undo_removes_only_copies(qtbot) -> None: # type: ignore[no-untyped-def] vm = _populated_vm() - vm.apply_command( - ReplicateCommand(vm, {1, 2, 3, 4}, {1, 2, 3, 4}, offset=(0, 0, 3.0), n_copies=2) - ) + vm.apply_command(ReplicateCommand(vm, {1, 2, 3, 4}, {1, 2, 3, 4}, + offset=(0, 0, 3.0), n_copies=2)) vm.undo_stack.undo() assert len(vm.project.nodes) == 4 assert len(vm.project.elements) == 4 @@ -106,7 +94,8 @@ def test_replicate_undo_removes_only_copies(qtbot) -> None: # type: ignore[no-u def test_replicate_skips_elements_with_unselected_endpoints(qtbot) -> None: # type: ignore[no-untyped-def] vm = _populated_vm() # Select only node 1 and 2; elements 1 (1↔2) is fully covered, others aren't. - vm.apply_command(ReplicateCommand(vm, {1, 2}, {1, 2, 3, 4}, offset=(0, 0, 3.0), n_copies=1)) + vm.apply_command(ReplicateCommand(vm, {1, 2}, {1, 2, 3, 4}, + offset=(0, 0, 3.0), n_copies=1)) # 2 new nodes + 1 new element (only element 1 was fully bracketed). assert len(vm.project.nodes) == 6 assert len(vm.project.elements) == 5 diff --git a/tests/gui/test_tree_multi_select.py b/tests/gui/test_tree_multi_select.py index 235ee7e..9b5e288 100644 --- a/tests/gui/test_tree_multi_select.py +++ b/tests/gui/test_tree_multi_select.py @@ -6,16 +6,16 @@ import pytest pytest.importorskip("PySide6") -from PySide6.QtWidgets import QTreeWidget +from PySide6.QtCore import Qt # noqa: E402 +from PySide6.QtWidgets import QTreeWidget # noqa: E402 -from otko.core import Node +from otko.core import Node # noqa: E402 @pytest.mark.gui def test_tree_is_in_extended_selection_mode(qtbot) -> None: # type: ignore[no-untyped-def] """Default QTreeWidget is single-select; we need ExtendedSelection.""" from otko.views.main_window import MainWindow - mw = MainWindow() qtbot.addWidget(mw) assert mw._tree.selectionMode() == QTreeWidget.SelectionMode.ExtendedSelection @@ -27,17 +27,14 @@ def test_tree_multi_select_syncs_canvas(qtbot) -> None: # type: ignore[no-untyp selection with both ids — the precondition for the Zero-Length Section dialog (exactly 2 joints).""" from otko.views.main_window import MainWindow - mw = MainWindow() qtbot.addWidget(mw) mw._vm.new_project(ndm=2, ndf=3) - mw._vm.project.nodes.extend( - [ - Node(id=1, coords=(0, 0, 0)), - Node(id=2, coords=(0, 0, 0)), - Node(id=3, coords=(1, 0, 0)), - ] - ) + mw._vm.project.nodes.extend([ + Node(id=1, coords=(0, 0, 0)), + Node(id=2, coords=(0, 0, 0)), + Node(id=3, coords=(1, 0, 0)), + ]) mw._refresh_tree(mw._vm.project) nodes_cat = mw._tree_categories["Nodes"] diff --git a/tests/gui/test_units_status_bar.py b/tests/gui/test_units_status_bar.py index 4eaaae1..6b58a39 100644 --- a/tests/gui/test_units_status_bar.py +++ b/tests/gui/test_units_status_bar.py @@ -6,7 +6,7 @@ import pytest pytest.importorskip("PySide6") -from otko.core import UnitSystem +from otko.core import UnitSystem # noqa: E402 @pytest.mark.gui @@ -25,9 +25,9 @@ def test_status_bar_combo_reflects_project_units(qtbot) -> None: # type: ignore mw = MainWindow() qtbot.addWidget(mw) mw._vm.new_project() - mw._vm.project.meta.units = UnitSystem.US_IN_KIP + mw._vm.project.meta.units = UnitSystem.IMPERIAL mw._sync_units_combo() - assert mw._units_combo.currentData() == UnitSystem.US_IN_KIP + assert mw._units_combo.currentData() == UnitSystem.IMPERIAL @pytest.mark.gui @@ -38,10 +38,10 @@ def test_status_bar_combo_write_updates_project(qtbot) -> None: # type: ignore[ mw = MainWindow() qtbot.addWidget(mw) mw._vm.new_project() - # Default is SI_M_N — switch to US_IN_KIP via the combo. - target_idx = mw._units_combo.findData(UnitSystem.US_IN_KIP) + # Default is Metric — switch to Imperial via the combo. + target_idx = mw._units_combo.findData(UnitSystem.IMPERIAL) mw._units_combo.setCurrentIndex(target_idx) - assert mw._vm.project.meta.units == UnitSystem.US_IN_KIP + assert mw._vm.project.meta.units == UnitSystem.IMPERIAL assert mw._vm.is_dirty diff --git a/tests/gui/test_update_element_fields.py b/tests/gui/test_update_element_fields.py index f83cd67..e684e5a 100644 --- a/tests/gui/test_update_element_fields.py +++ b/tests/gui/test_update_element_fields.py @@ -6,48 +6,33 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( +from otko.commands import ( # noqa: E402 AddElementsCommand, AddMaterialsCommand, AddNodesCommand, UpdateElementFieldsCommand, ) -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticUniaxial, Node, TrussElement, ) -from otko.viewmodels import ProjectViewModel +from otko.viewmodels import ProjectViewModel # noqa: E402 def _vm_with_truss() -> ProjectViewModel: vm = ProjectViewModel() vm.new_project() - vm.apply_command( - AddNodesCommand( - vm, - [ - Node(id=1, coords=(0, 0, 0)), - Node(id=2, coords=(3, 0, 0)), - ], - ) - ) - vm.apply_command( - AddMaterialsCommand( - vm, - [ - ElasticUniaxial(id=1, name="Steel", E=200e9), - ], - ) - ) - vm.apply_command( - AddElementsCommand( - vm, - [ - TrussElement(id=1, nodes=(1, 2), area=0.001, material_id=1), - ], - ) - ) + vm.apply_command(AddNodesCommand(vm, [ + Node(id=1, coords=(0, 0, 0)), + Node(id=2, coords=(3, 0, 0)), + ])) + vm.apply_command(AddMaterialsCommand(vm, [ + ElasticUniaxial(id=1, name="Steel", E=200e9), + ])) + vm.apply_command(AddElementsCommand(vm, [ + TrussElement(id=1, nodes=(1, 2), area=0.001, material_id=1), + ])) return vm @@ -72,13 +57,9 @@ def test_update_area_is_undoable(qtbot) -> None: # type: ignore[no-untyped-def] def test_unknown_field_is_silently_ignored(qtbot) -> None: # type: ignore[no-untyped-def] """Fields not in the element's model are filtered out — no exception.""" vm = _vm_with_truss() - vm.apply_command( - UpdateElementFieldsCommand( - vm, - 1, - {"section_id": 42}, # TrussElement has no section_id - ) - ) + vm.apply_command(UpdateElementFieldsCommand( + vm, 1, {"section_id": 42}, # TrussElement has no section_id + )) # The element is unchanged. assert vm.project.element(1).area == pytest.approx(0.001) assert vm.project.element(1).material_id == 1 @@ -87,13 +68,9 @@ def test_unknown_field_is_silently_ignored(qtbot) -> None: # type: ignore[no-un @pytest.mark.gui def test_update_multiple_fields_at_once(qtbot) -> None: # type: ignore[no-untyped-def] vm = _vm_with_truss() - vm.apply_command( - UpdateElementFieldsCommand( - vm, - 1, - {"area": 0.003, "rho": 7850.0}, - ) - ) + vm.apply_command(UpdateElementFieldsCommand( + vm, 1, {"area": 0.003, "rho": 7850.0}, + )) el = vm.project.element(1) assert el.area == pytest.approx(0.003) assert el.rho == pytest.approx(7850.0) diff --git a/tests/gui/test_working_plane.py b/tests/gui/test_working_plane.py index 012c1c0..b0fc2c8 100644 --- a/tests/gui/test_working_plane.py +++ b/tests/gui/test_working_plane.py @@ -7,7 +7,7 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( +from otko.core import ( # noqa: E402 CoordinateGridSystem, GridSystem, Project, @@ -33,7 +33,6 @@ def _multi_z_project() -> Project: @pytest.mark.gui def test_no_working_plane_returns_all_intersections(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) canvas.show_project(_multi_z_project()) @@ -46,7 +45,6 @@ def test_no_working_plane_returns_all_intersections(qtbot) -> None: # type: ign @pytest.mark.gui def test_xy_plane_filters_to_single_z_level(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) canvas.show_project(_multi_z_project()) @@ -63,7 +61,6 @@ def test_xy_plane_filters_to_single_z_level(qtbot) -> None: # type: ignore[no-u @pytest.mark.gui def test_xz_plane_filters_to_single_y_level(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) canvas.show_project(_multi_z_project()) @@ -77,7 +74,6 @@ def test_xz_plane_filters_to_single_y_level(qtbot) -> None: # type: ignore[no-u @pytest.mark.gui def test_yz_plane_filters_to_single_x_level(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) canvas.show_project(_multi_z_project()) @@ -91,7 +87,6 @@ def test_yz_plane_filters_to_single_x_level(qtbot) -> None: # type: ignore[no-u @pytest.mark.gui def test_clear_working_plane_restores_all(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) canvas.show_project(_multi_z_project()) @@ -105,7 +100,6 @@ def test_clear_working_plane_restores_all(qtbot) -> None: # type: ignore[no-unt @pytest.mark.gui def test_invalid_plane_raises(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) with pytest.raises(ValueError): @@ -118,11 +112,10 @@ def test_offset_with_no_matching_grid_returns_none(qtbot) -> None: # type: igno (e.g. typed a custom value off the grid), the snap list is empty so draw-click does nothing.""" from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) canvas.show_project(_multi_z_project()) - canvas.set_working_plane("XY", 1.234) # not in [0, 3, 6] + canvas.set_working_plane("XY", 1.234) # not in [0, 3, 6] assert canvas._grid_intersections_world() is None @@ -133,7 +126,6 @@ def test_switching_levels_rebuilds_grid_actors(qtbot) -> None: # type: ignore[n renders — ensuring the screen actually changes when the user switches Z=0 → Z=3.""" from otko.views.canvas3d.model_canvas import ModelCanvas - canvas = ModelCanvas() qtbot.addWidget(canvas) canvas.show_project(_multi_z_project()) @@ -141,7 +133,7 @@ def test_switching_levels_rebuilds_grid_actors(qtbot) -> None: # type: ignore[n # Without a working plane the renderer builds grid + vertical # connectors across all 3 Z-levels. Count aux actors as a proxy. base_count = len(canvas._renderer._aux_actors) - assert base_count >= 2 # at least one grid-lines + one dots actor + assert base_count >= 2 # at least one grid-lines + one dots actor # Pick an XY plane at Z=3 → render rebuilt. canvas.set_working_plane("XY", 3.0) @@ -170,7 +162,6 @@ def test_switching_levels_rebuilds_grid_actors(qtbot) -> None: # type: ignore[n @pytest.mark.gui def test_top_button_populates_level_combo_with_z_ordinates(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.main_window import MainWindow - mw = MainWindow() qtbot.addWidget(mw) mw._vm.new_project() diff --git a/tests/integration/test_basic_truss.py b/tests/integration/test_basic_truss.py index 20be03c..db4b471 100644 --- a/tests/integration/test_basic_truss.py +++ b/tests/integration/test_basic_truss.py @@ -11,9 +11,7 @@ import pytest pytest.importorskip("openseespy") -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def test_basic_truss_matches_opensees_tcl_reference() -> None: @@ -25,7 +23,7 @@ def test_basic_truss_matches_opensees_tcl_reference() -> None: a linear rescale of the Tcl model — any deviation would flag a real solver/translation issue. """ - from examples.basic_truss import IN_TO_M, build_basic_truss + from examples.basic_truss import build_basic_truss, IN_TO_M proj = build_basic_truss() runner = OpenSeesRunner(proj) @@ -45,7 +43,6 @@ def test_basic_truss_matches_opensees_tcl_reference() -> None: def test_basic_truss_round_trips(tmp_path) -> None: # type: ignore[no-untyped-def] """The example project must survive save/load without any information loss.""" from examples.basic_truss import build_basic_truss - from otko.services import load_project, save_project p = build_basic_truss() diff --git a/tests/integration/test_beam_quad_2d.py b/tests/integration/test_beam_quad_2d.py index 4dc4a60..6ca31a5 100644 --- a/tests/integration/test_beam_quad_2d.py +++ b/tests/integration/test_beam_quad_2d.py @@ -9,10 +9,8 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def test_beam_quad_2d_midspan_deflection(tmp_path) -> None: # type: ignore[no-untyped-def] @@ -25,7 +23,6 @@ def test_beam_quad_2d_midspan_deflection(tmp_path) -> None: # type: ignore[no-u _mid_top_node_id, build_beam_quad_2d, ) - proj = build_beam_quad_2d() proj.validate_references() @@ -52,10 +49,10 @@ def test_beam_quad_2d_midspan_deflection(tmp_path) -> None: # type: ignore[no-u # By symmetry, left and right support reactions must balance the two # 10-kip midspan loads (total -20 kip vertical). - assert reloaded.nodes[0].id == 1 # left pin + assert reloaded.nodes[0].id == 1 # left pin reaction_sum_fy = sum( result.node_reaction[nid][-1, 1] - for nid in (1, 17) # node 17 = (L, 0) = roller + for nid in (1, 17) # node 17 = (L, 0) = roller ) assert reaction_sum_fy == pytest.approx(20.0, abs=1e-6) @@ -72,7 +69,6 @@ def test_beam_quad_2d_free_vibration_chain(tmp_path) -> None: # type: ignore[no _mid_bottom_node_id, build_beam_quad_2d, ) - proj = build_beam_quad_2d() proj.validate_references() @@ -85,7 +81,6 @@ def test_beam_quad_2d_free_vibration_chain(tmp_path) -> None: # type: ignore[no r = OpenSeesRunner(reloaded).run(reloaded.analyses[1], results_dir=results_dir) import h5py - with h5py.File(r.h5_path) as f: t = f["time"][:] u = f[f"nodes/{_mid_bottom_node_id()}/disp"][:, 1] diff --git a/tests/integration/test_combinations.py b/tests/integration/test_combinations.py deleted file mode 100644 index 8ba5ca7..0000000 --- a/tests/integration/test_combinations.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Load-combination integration: 1.2×D + 1.6×L by superposition == one combined run.""" - -from __future__ import annotations - -import math - -import pytest - -ops = pytest.importorskip("openseespy.opensees") # skip if OpenSeesPy not installed - -from otko.core import ( - ElasticBeamColumn, - ElasticSection, - LinearTimeSeries, - LoadCombination, - LoadCombinationItem, - NodalLoad, - Node, - PlainLoadPattern, - Project, - StaticCase, -) -from otko.services import OpenSeesRunner, evaluate_combination -from otko.services.results import StaticResults - -pytestmark = pytest.mark.slow - - -def _two_pattern_cantilever() -> Project: - elastic_mod = 200e9 - area = 0.01 - inertia = 8.333e-6 - return Project( - ndm=2, - ndf=3, - nodes=[ - Node( - id=1, - coords=(0.0, 0.0, 0.0), - restraint=(True, True, False, False, False, True), - ), - Node(id=2, coords=(5.0, 0.0, 0.0)), - ], - sections=[ElasticSection(id=1, E=elastic_mod, A=area, Iz=inertia)], - elements=[ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1)], - time_series=[LinearTimeSeries(id=1), LinearTimeSeries(id=2)], - load_patterns=[ - PlainLoadPattern( - id=1, - name="Dead", - time_series_id=1, - nodal_loads=[NodalLoad(node_id=2, forces=(0.0, -1000.0, 0.0, 0.0, 0.0, 0.0))], - ), - PlainLoadPattern( - id=2, - name="Live", - time_series_id=2, - nodal_loads=[NodalLoad(node_id=2, forces=(0.0, -500.0, 0.0, 0.0, 0.0, 0.0))], - ), - ], - analyses=[ - StaticCase(id=1, name="Dead", pattern_ids=[1]), - StaticCase(id=2, name="Live", pattern_ids=[2]), - ], - ) - - -def test_superposition_matches_combined_run() -> None: - """1.2*Dead + 1.6*Live via evaluate_combination == single StaticCase run.""" - proj = _two_pattern_cantilever() - dead = OpenSeesRunner(proj).run(proj.analyses[0]) - live = OpenSeesRunner(proj).run(proj.analyses[1]) - combo = LoadCombination( - id=1, - name="1.2D+1.6L", - kind="Linear", - items=[ - LoadCombinationItem(case_id=1, factor=1.2), - LoadCombinationItem(case_id=2, factor=1.6), - ], - ) - combined = evaluate_combination({1: dead, 2: live}, combo) - assert isinstance(combined, StaticResults) - # Single run with both patterns factored identically. - direct = OpenSeesRunner(proj).run( - StaticCase(id=3, name="direct", pattern_ids=[1, 2], pattern_factors={1: 1.2, 2: 1.6}) - ) - assert math.isclose( - combined.disp(node_id=2, dof=2), direct.disp(node_id=2, dof=2), rel_tol=1e-9 - ) diff --git a/tests/integration/test_concrete04_runner.py b/tests/integration/test_concrete04_runner.py index 4293dea..f8c1c36 100644 --- a/tests/integration/test_concrete04_runner.py +++ b/tests/integration/test_concrete04_runner.py @@ -18,7 +18,7 @@ import pytest pytest.importorskip("openseespy") -from otko.core import ( +from otko.core import ( # noqa: E402 Concrete04, FiberSection, ForceBeamColumn, @@ -32,77 +32,61 @@ from otko.core import ( StaticCase, UnitSystem, ) -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow - +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 # ── Model constants ──────────────────────────────────────────────────────────── -L = 1.0 # column height [m] -B = H = 0.3 # cross-section dimensions [m] -A = B * H # section area [m²] +L = 1.0 # column height [m] +B = H = 0.3 # cross-section dimensions [m] +A = B * H # section area [m²] -FC = -30e6 # peak compressive strength [Pa] (negative) -EPSC0 = -0.002 # strain at peak strength (negative) -EPSCU = -0.005 # ultimate compressive strain (negative) -EC = 30e9 # initial tangent modulus [Pa] +FC = -30e6 # peak compressive strength [Pa] (negative) +EPSC0 = -0.002 # strain at peak strength (negative) +EPSCU = -0.005 # ultimate compressive strain (negative) +EC = 30e9 # initial tangent modulus [Pa] # Applied axial load: small enough (< 1 % of capacity) that the Popovics # curve is indistinguishable from its linear tangent at origin. -P_AXIAL = -1200.0 # N (downward → compressive) +P_AXIAL = -1200.0 # N (downward → compressive) # Analytical axial shortening: P * L / (Ec * A) -EXPECTED_UY = P_AXIAL * L / (EC * A) # ≈ -1.333e-7 m +EXPECTED_UY = P_AXIAL * L / (EC * A) # ≈ -1.333e-7 m def _build_project() -> Project: return Project( meta=ProjectMeta( name="Concrete04 fiber-section cantilever", - units=UnitSystem.SI_M_N, + units=UnitSystem.METRIC, ), ndm=2, ndf=3, nodes=[ - Node( - id=1, - name="Base", - coords=(0.0, 0.0, 0.0), - restraint=(True, True, True, False, False, False), - ), - Node(id=2, name="Top", coords=(0.0, L, 0.0)), + Node(id=1, name="Base", coords=(0.0, 0.0, 0.0), + restraint=(True, True, True, False, False, False)), + Node(id=2, name="Top", coords=(0.0, L, 0.0)), ], materials=[ Concrete04( - id=1, - name="C30-Popovics", - fpc=FC, - epsc0=EPSC0, - epscu=EPSCU, - Ec=EC, + id=1, name="C30-Popovics", + fpc=FC, epsc0=EPSC0, epscu=EPSCU, Ec=EC, ), ], sections=[ FiberSection( - id=1, - name="RC-Fiber", + id=1, name="RC-Fiber", patches=[ RectangularPatch( material_id=1, - n_fib_y=4, - n_fib_z=4, - y_i=-H / 2, - z_i=-B / 2, - y_j=H / 2, - z_j=B / 2, + n_fib_y=4, n_fib_z=4, + y_i=-H / 2, z_i=-B / 2, + y_j= H / 2, z_j= B / 2, ), ], ), ], elements=[ ForceBeamColumn( - id=1, - name="Column", + id=1, name="Column", nodes=(1, 2), section_id=1, integration_points=3, @@ -112,9 +96,7 @@ def _build_project() -> Project: time_series=[LinearTimeSeries(id=1, name="Ramp")], load_patterns=[ PlainLoadPattern( - id=1, - name="Gravity", - time_series_id=1, + id=1, name="Gravity", time_series_id=1, nodal_loads=[ NodalLoad(node_id=2, forces=(0.0, P_AXIAL, 0.0, 0.0, 0.0, 0.0)), ], @@ -122,8 +104,7 @@ def _build_project() -> Project: ], analyses=[ StaticCase( - id=1, - name="Gravity", + id=1, name="Gravity", pattern_ids=[1], n_steps=1, load_factor_increment=1.0, @@ -158,14 +139,9 @@ def test_concrete04_with_tension_does_not_raise() -> None: proj = _build_project() # Replace material with tensile-branch variant. proj.materials[0] = Concrete04( - id=1, - name="C30-WithTension", - fpc=FC, - epsc0=EPSC0, - epscu=EPSCU, - Ec=EC, - fct=3.0e6, - et=1e-4, + id=1, name="C30-WithTension", + fpc=FC, epsc0=EPSC0, epscu=EPSCU, Ec=EC, + fct=3.0e6, et=1e-4, ) case = proj.analyses[0] result = OpenSeesRunner(proj).run(case) diff --git a/tests/integration/test_dof_coverage.py b/tests/integration/test_dof_coverage.py index 7e9e37e..087b461 100644 --- a/tests/integration/test_dof_coverage.py +++ b/tests/integration/test_dof_coverage.py @@ -11,7 +11,7 @@ import pytest pytest.importorskip("openseespy") -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticUniaxial, LinearTimeSeries, NodalLoad, @@ -21,18 +21,17 @@ from otko.core import ( StaticCase, TrussElement, ) -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def _make_truss_project(ndf: int) -> Project: return Project( - ndm=2, - ndf=ndf, + ndm=2, ndf=ndf, nodes=[ - Node(id=1, coords=(0, 0, 0), restraint=(True, True, False, False, False, False)), - Node(id=2, coords=(3, 0, 0), restraint=(True, True, False, False, False, False)), + Node(id=1, coords=(0, 0, 0), + restraint=(True, True, False, False, False, False)), + Node(id=2, coords=(3, 0, 0), + restraint=(True, True, False, False, False, False)), Node(id=3, coords=(1.5, 2, 0)), ], materials=[ElasticUniaxial(id=1, E=200e9)], @@ -41,13 +40,10 @@ def _make_truss_project(ndf: int) -> Project: TrussElement(id=2, nodes=(2, 3), area=1e-3, material_id=1), ], time_series=[LinearTimeSeries(id=1, name="R")], - load_patterns=[ - PlainLoadPattern( - id=1, - time_series_id=1, - nodal_loads=[NodalLoad(node_id=3, forces=(1e3, -5e3, 0, 0, 0, 0))], - ) - ], + load_patterns=[PlainLoadPattern( + id=1, time_series_id=1, + nodal_loads=[NodalLoad(node_id=3, forces=(1e3, -5e3, 0, 0, 0, 0))], + )], analyses=[StaticCase(id=1, name="Static", pattern_ids=[1], n_steps=1)], ) diff --git a/tests/integration/test_eigen_two_storey_one_bay_frame.py b/tests/integration/test_eigen_two_storey_one_bay_frame.py index 83cd67e..5f76282 100644 --- a/tests/integration/test_eigen_two_storey_one_bay_frame.py +++ b/tests/integration/test_eigen_two_storey_one_bay_frame.py @@ -8,11 +8,9 @@ import pytest pytest.importorskip("openseespy") -from otko.core import ModalCase -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.core import ModalCase # noqa: E402 +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def _reload(proj, tmp_path): # type: ignore[no-untyped-def] diff --git a/tests/integration/test_eigen_two_storey_shear_frame.py b/tests/integration/test_eigen_two_storey_shear_frame.py index 793f98d..c0cc4db 100644 --- a/tests/integration/test_eigen_two_storey_shear_frame.py +++ b/tests/integration/test_eigen_two_storey_shear_frame.py @@ -8,11 +8,9 @@ import pytest pytest.importorskip("openseespy") -from otko.core import ModalCase -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.core import ModalCase # noqa: E402 +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def _reload(proj, tmp_path): # type: ignore[no-untyped-def] diff --git a/tests/integration/test_elastic_frame.py b/tests/integration/test_elastic_frame.py index 6f04163..54edec4 100644 --- a/tests/integration/test_elastic_frame.py +++ b/tests/integration/test_elastic_frame.py @@ -8,11 +8,9 @@ import pytest pytest.importorskip("openseespy") -from otko.core import ModalCase, StaticCase -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.core import ModalCase, StaticCase # noqa: E402 +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 BASE_NODES = (1, 2, 3, 4) @@ -29,17 +27,13 @@ def _reload(proj, tmp_path): # type: ignore[no-untyped-def] def test_elastic_frame_gravity_reactions(tmp_path) -> None: # type: ignore[no-untyped-def] """ΣFy at base = total applied gravity (distributed w × beam × floors).""" from examples.elastic_frame import ( - LOAD_F1, - LOAD_F2, - LOAD_F3, - N_BAYS, - build_elastic_frame, + BAY, LOAD_F1, LOAD_F2, LOAD_F3, N_BAYS, build_elastic_frame, ) - proj = _reload(build_elastic_frame(), tmp_path) gravity_case = next( - c for c in proj.analyses if isinstance(c, StaticCase) and c.name == "Gravity" + c for c in proj.analyses + if isinstance(c, StaticCase) and c.name == "Gravity" ) r = OpenSeesRunner(proj).run(gravity_case) @@ -57,32 +51,24 @@ def test_elastic_frame_gravity_reactions(tmp_path) -> None: # type: ignore[no-u # By symmetry exterior-column reactions pair up, as do interior. assert r.node_reaction[1][-1, 1] == pytest.approx( - r.node_reaction[4][-1, 1], - abs=1e-6, + r.node_reaction[4][-1, 1], abs=1e-6, ) assert r.node_reaction[2][-1, 1] == pytest.approx( - r.node_reaction[3][-1, 1], - abs=1e-6, + r.node_reaction[3][-1, 1], abs=1e-6, ) def test_elastic_frame_gravity_plus_lateral_reactions(tmp_path) -> None: # type: ignore[no-untyped-def] """ΣFx at base must equal -(lateral applied) within PDelta tolerance.""" from examples.elastic_frame import ( - LOAD_F1, - LOAD_F2, - LOAD_F3, - N_BAYS, - P_F1, - P_F2, - P_F3, - build_elastic_frame, + BAY, LOAD_F1, LOAD_F2, LOAD_F3, + N_BAYS, P_F1, P_F2, P_F3, build_elastic_frame, ) - proj = _reload(build_elastic_frame(), tmp_path) combined = next( - c for c in proj.analyses if isinstance(c, StaticCase) and c.name == "Gravity+Lateral" + c for c in proj.analyses + if isinstance(c, StaticCase) and c.name == "Gravity+Lateral" ) r = OpenSeesRunner(proj).run(combined) @@ -101,7 +87,6 @@ def test_elastic_frame_gravity_plus_lateral_reactions(tmp_path) -> None: # type def test_elastic_frame_modal_periods(tmp_path) -> None: # type: ignore[no-untyped-def] """5-mode eigen analysis: first periods match the Tcl reference values.""" from examples.elastic_frame import build_elastic_frame - proj = _reload(build_elastic_frame(), tmp_path) modal = next(c for c in proj.analyses if isinstance(c, ModalCase)) @@ -118,5 +103,7 @@ def test_elastic_frame_modal_periods(tmp_path) -> None: # type: ignore[no-untyp # 0.1930, 0.1562, 0.130 s. Our solve nails these within 1.5%. expected = [1.040, 0.3526, 0.1930, 0.1562, 0.130] periods = [2.0 * math.pi / math.sqrt(v) for v in r.eigenvalues] - for i, (T, T_ref) in enumerate(zip(periods, expected, strict=False), start=1): - assert pytest.approx(T_ref, rel=0.02) == T, f"T{i} = {T:.4f} s, reference {T_ref:.4f} s" + for i, (T, T_ref) in enumerate(zip(periods, expected), start=1): + assert T == pytest.approx(T_ref, rel=0.02), ( + f"T{i} = {T:.4f} s, reference {T_ref:.4f} s" + ) diff --git a/tests/integration/test_ex1a_canti2d.py b/tests/integration/test_ex1a_canti2d.py index a20d589..660810a 100644 --- a/tests/integration/test_ex1a_canti2d.py +++ b/tests/integration/test_ex1a_canti2d.py @@ -9,11 +9,9 @@ import pytest pytest.importorskip("openseespy") -from otko.core import PushoverCase, TransientCase -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.core import PushoverCase, TransientCase # noqa: E402 +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def _reload(proj, tmp_path): # type: ignore[no-untyped-def] diff --git a/tests/integration/test_ex1a_canti2d_eq.py b/tests/integration/test_ex1a_canti2d_eq.py index 264d30c..246af57 100644 --- a/tests/integration/test_ex1a_canti2d_eq.py +++ b/tests/integration/test_ex1a_canti2d_eq.py @@ -9,10 +9,8 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def test_ex1a_canti2d_eq_runs_and_oscillates(tmp_path) -> None: # type: ignore[no-untyped-def] diff --git a/tests/integration/test_ex1b_portal2d.py b/tests/integration/test_ex1b_portal2d.py index f47bdd2..8e3f2a4 100644 --- a/tests/integration/test_ex1b_portal2d.py +++ b/tests/integration/test_ex1b_portal2d.py @@ -9,11 +9,9 @@ import pytest pytest.importorskip("openseespy") -from otko.core import PushoverCase, TransientCase -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.core import PushoverCase, TransientCase # noqa: E402 +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def _reload(proj, tmp_path): # type: ignore[no-untyped-def] diff --git a/tests/integration/test_ex2a_canti2d_elastic_element.py b/tests/integration/test_ex2a_canti2d_elastic_element.py index 71bfb6d..5a79a65 100644 --- a/tests/integration/test_ex2a_canti2d_elastic_element.py +++ b/tests/integration/test_ex2a_canti2d_elastic_element.py @@ -9,11 +9,9 @@ import pytest pytest.importorskip("openseespy") -from otko.core import PushoverCase, TransientCase -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.core import PushoverCase, TransientCase # noqa: E402 +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def _reload(proj, tmp_path): # type: ignore[no-untyped-def] diff --git a/tests/integration/test_ex2b_canti2d_inelastic_section.py b/tests/integration/test_ex2b_canti2d_inelastic_section.py index 3766581..4afae36 100644 --- a/tests/integration/test_ex2b_canti2d_inelastic_section.py +++ b/tests/integration/test_ex2b_canti2d_inelastic_section.py @@ -9,11 +9,9 @@ import pytest pytest.importorskip("openseespy") -from otko.core import PushoverCase, TransientCase -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.core import PushoverCase, TransientCase # noqa: E402 +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def _reload(proj, tmp_path): # type: ignore[no-untyped-def] diff --git a/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py b/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py index 1cae060..a9caf9c 100644 --- a/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py +++ b/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py @@ -9,11 +9,9 @@ import pytest pytest.importorskip("openseespy") -from otko.core import PushoverCase, TransientCase -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.core import PushoverCase, TransientCase # noqa: E402 +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def _reload(proj, tmp_path): # type: ignore[no-untyped-def] diff --git a/tests/integration/test_ex3_canti2d_variants.py b/tests/integration/test_ex3_canti2d_variants.py index 2486519..273610d 100644 --- a/tests/integration/test_ex3_canti2d_variants.py +++ b/tests/integration/test_ex3_canti2d_variants.py @@ -9,11 +9,9 @@ import pytest pytest.importorskip("openseespy") -from otko.core import PushoverCase, TransientCase -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.core import PushoverCase, TransientCase # noqa: E402 +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def _reload(proj, tmp_path, stem: str): # type: ignore[no-untyped-def] @@ -29,16 +27,10 @@ def _reload(proj, tmp_path, stem: str): # type: ignore[no-untyped-def] [ ("build_ex3_canti2d_elastic_element", "examples.ex3_canti2d_elastic_element", False), ("build_ex3_canti2d_inelastic_section", "examples.ex3_canti2d_inelastic_section", True), - ( - "build_ex3_canti2d_inelastic_fiber_section", - "examples.ex3_canti2d_inelastic_fiber_section", - True, - ), + ("build_ex3_canti2d_inelastic_fiber_section", "examples.ex3_canti2d_inelastic_fiber_section", True), ], ) -def test_ex3_variant_pushover_runs( - tmp_path, builder_name: str, module_name: str, nonlinear: bool -) -> None: # type: ignore[no-untyped-def] +def test_ex3_variant_pushover_runs(tmp_path, builder_name: str, module_name: str, nonlinear: bool) -> None: # type: ignore[no-untyped-def] mod = __import__(module_name, fromlist=[builder_name, "PUSH_STEP", "PUSH_TARGET"]) proj = _reload(getattr(mod, builder_name)(), tmp_path, builder_name) push_case = next(c for c in proj.analyses if isinstance(c, PushoverCase)) @@ -49,12 +41,8 @@ def test_ex3_variant_pushover_runs( assert result.control_disp[-1] == pytest.approx(mod.PUSH_TARGET, rel=1e-6) assert max(result.base_shear) > 0.0 - early = (result.base_shear[5] - result.base_shear[0]) / ( - result.control_disp[5] - result.control_disp[0] - ) - late = (result.base_shear[-1] - result.base_shear[-6]) / ( - result.control_disp[-1] - result.control_disp[-6] - ) + early = (result.base_shear[5] - result.base_shear[0]) / (result.control_disp[5] - result.control_disp[0]) + late = (result.base_shear[-1] - result.base_shear[-6]) / (result.control_disp[-1] - result.control_disp[-6]) if nonlinear: assert early > 2.0 * late else: @@ -66,10 +54,7 @@ def test_ex3_variant_pushover_runs( [ ("build_ex3_canti2d_elastic_element", "examples.ex3_canti2d_elastic_element"), ("build_ex3_canti2d_inelastic_section", "examples.ex3_canti2d_inelastic_section"), - ( - "build_ex3_canti2d_inelastic_fiber_section", - "examples.ex3_canti2d_inelastic_fiber_section", - ), + ("build_ex3_canti2d_inelastic_fiber_section", "examples.ex3_canti2d_inelastic_fiber_section"), ], ) def test_ex3_variant_earthquake_runs(tmp_path, builder_name: str, module_name: str) -> None: # type: ignore[no-untyped-def] diff --git a/tests/integration/test_ex4_portal2d_variants.py b/tests/integration/test_ex4_portal2d_variants.py index d4943cb..875942f 100644 --- a/tests/integration/test_ex4_portal2d_variants.py +++ b/tests/integration/test_ex4_portal2d_variants.py @@ -9,11 +9,9 @@ import pytest pytest.importorskip("openseespy") -from otko.core import PushoverCase, TransientCase -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.core import PushoverCase, TransientCase # noqa: E402 +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def _reload(proj, tmp_path, stem: str): # type: ignore[no-untyped-def] @@ -29,16 +27,10 @@ def _reload(proj, tmp_path, stem: str): # type: ignore[no-untyped-def] [ ("build_ex4_portal2d_elastic_element", "examples.ex4_portal2d_elastic_element", False), ("build_ex4_portal2d_inelastic_section", "examples.ex4_portal2d_inelastic_section", True), - ( - "build_ex4_portal2d_inelastic_fiber_section", - "examples.ex4_portal2d_inelastic_fiber_section", - True, - ), + ("build_ex4_portal2d_inelastic_fiber_section", "examples.ex4_portal2d_inelastic_fiber_section", True), ], ) -def test_ex4_variant_pushover_runs( - tmp_path, builder_name: str, module_name: str, nonlinear: bool -) -> None: # type: ignore[no-untyped-def] +def test_ex4_variant_pushover_runs(tmp_path, builder_name: str, module_name: str, nonlinear: bool) -> None: # type: ignore[no-untyped-def] mod = __import__(module_name, fromlist=[builder_name, "PUSH_STEP", "PUSH_TARGET"]) proj = _reload(getattr(mod, builder_name)(), tmp_path, builder_name) push_case = next(c for c in proj.analyses if isinstance(c, PushoverCase)) @@ -53,12 +45,8 @@ def test_ex4_variant_pushover_runs( assert result.control_disp[-1] == pytest.approx(mod.PUSH_TARGET, abs=5e-3) assert max(result.base_shear) > 0.0 - early = (result.base_shear[5] - result.base_shear[0]) / ( - result.control_disp[5] - result.control_disp[0] - ) - late = (result.base_shear[-1] - result.base_shear[-6]) / ( - result.control_disp[-1] - result.control_disp[-6] - ) + early = (result.base_shear[5] - result.base_shear[0]) / (result.control_disp[5] - result.control_disp[0]) + late = (result.base_shear[-1] - result.base_shear[-6]) / (result.control_disp[-1] - result.control_disp[-6]) if nonlinear: assert early > 1.25 * late else: @@ -70,10 +58,7 @@ def test_ex4_variant_pushover_runs( [ ("build_ex4_portal2d_elastic_element", "examples.ex4_portal2d_elastic_element"), ("build_ex4_portal2d_inelastic_section", "examples.ex4_portal2d_inelastic_section"), - ( - "build_ex4_portal2d_inelastic_fiber_section", - "examples.ex4_portal2d_inelastic_fiber_section", - ), + ("build_ex4_portal2d_inelastic_fiber_section", "examples.ex4_portal2d_inelastic_fiber_section"), ], ) def test_ex4_variant_sine_runs(tmp_path, builder_name: str, module_name: str) -> None: # type: ignore[no-untyped-def] diff --git a/tests/integration/test_material_tester.py b/tests/integration/test_material_tester.py index 6463b21..8978be6 100644 --- a/tests/integration/test_material_tester.py +++ b/tests/integration/test_material_tester.py @@ -34,22 +34,18 @@ from otko.services.material_tester import ( test_uniaxial_material, ) -pytestmark = pytest.mark.slow - - # ---- helpers --------------------------------------------------------------- def _simple_cantilever() -> Project: """Minimal 2-node elastic cantilever for the interleave test.""" return Project( - meta=ProjectMeta(name="interleave-ref", units=UnitSystem.SI_M_N), + meta=ProjectMeta(name="interleave-ref", units=UnitSystem.METRIC), ndm=2, ndf=3, nodes=[ Node( - id=1, - name="Base", + id=1, name="Base", coords=(0.0, 0.0, 0.0), # 2D-frame DOF mapping: (Ux, Uy, Uz, Rx, Ry, Rz) -> runner uses (0,1,5). # Fixed base: Ux=True, Uy=True, Rz=True (index 5). @@ -65,25 +61,17 @@ def _simple_cantilever() -> Project: time_series=[LinearTimeSeries(id=1)], load_patterns=[ PlainLoadPattern( - id=1, - time_series_id=1, + id=1, time_series_id=1, # Downward tip load (Uy direction). nodal_loads=[NodalLoad(node_id=2, forces=(0.0, -1.0e4, 0.0, 0.0, 0.0, 0.0))], ), ], analyses=[ StaticCase( - id=1, - pattern_ids=[1], - n_steps=1, - load_factor_increment=1.0, - system="BandGeneral", - constraints="Plain", - integrator="LoadControl", - algorithm="Newton", - test="NormDispIncr", - tolerance=1e-8, - max_iter=10, + id=1, pattern_ids=[1], n_steps=1, load_factor_increment=1.0, + system="BandGeneral", constraints="Plain", + integrator="LoadControl", algorithm="Newton", + test="NormDispIncr", tolerance=1e-8, max_iter=10, ), ], ) @@ -122,8 +110,8 @@ def test_elastic_uniaxial_monotonic_stress_strain() -> None: def test_elastic_pp_compressive_plateau() -> None: """ElasticPP: stress is exactly -Fy for all strains past compressive yield.""" e_mod = 200e9 - epsy = 1.25e-3 # yield strain in tension - fy = e_mod * epsy # implied yield stress = 250 MPa + epsy = 1.25e-3 # yield strain in tension + fy = e_mod * epsy # implied yield stress = 250 MPa mat = ElasticPP(id=1, E=e_mod, epsy_pos=epsy) protocol = LoadProtocol( @@ -141,9 +129,9 @@ def test_elastic_pp_compressive_plateau() -> None: assert len(past_yield) > 0, "no post-yield data points found" for s, sig in past_yield: - assert sig == pytest.approx( - -fy, rel=1e-6 - ), f"plateau broken at strain={s:.4g}: got {sig:.4g}, expected {-fy:.4g}" + assert sig == pytest.approx(-fy, rel=1e-6), ( + f"plateau broken at strain={s:.4g}: got {sig:.4g}, expected {-fy:.4g}" + ) # ---- Steel01 cyclic energy ------------------------------------------------- @@ -159,9 +147,9 @@ def test_steel01_cyclic_hysteresis_energy() -> None: fy = 250e6 e0 = 200e9 b = 0.0 - ey = fy / e0 # = 1.25e-3 - ea = 5.0 * ey # = 6.25e-3 - n = 100 # steps per branch + ey = fy / e0 # = 1.25e-3 + ea = 5.0 * ey # = 6.25e-3 + n = 100 # steps per branch mat = Steel01(id=1, Fy=fy, E0=e0, b=b) protocol = LoadProtocol( @@ -174,28 +162,30 @@ def test_steel01_cyclic_hysteresis_energy() -> None: result = test_uniaxial_material(mat, protocol) # Theoretical energy per stable cycle (EPP closed-form) - e_ref = 4.0 * fy * (ea - ey) # = 5 000 000 J/m^3 + e_ref = 4.0 * fy * (ea - ey) # = 5 000 000 J/m^3 - pts_per_cycle = 3 * n # = 300 (three branches per cycle) + pts_per_cycle = 3 * n # = 300 (three branches per cycle) total_pts = len(result.strain) assert total_pts == 3 * pts_per_cycle, f"expected 900 points, got {total_pts}" - for i_cycle in [1, 2]: # stable cycles 1 and 2 (0-indexed); closed loops + for i_cycle in [1, 2]: # stable cycles 1 and 2 (0-indexed); closed loops # Include the last point of the preceding cycle as the opening vertex # so the integration path is a closed loop. lo = i_cycle * pts_per_cycle - 1 - hi = (i_cycle + 1) * pts_per_cycle # Python slice: exclusive upper bound + hi = (i_cycle + 1) * pts_per_cycle # Python slice: exclusive upper bound strain_loop = result.strain[lo:hi] stress_loop = result.stress[lo:hi] assert len(strain_loop) == pts_per_cycle + 1 # 301 points # Trapezoidal area of closed stress-strain loop = dissipated energy. e_num = sum( - 0.5 * (stress_loop[j] + stress_loop[j + 1]) * (strain_loop[j + 1] - strain_loop[j]) + 0.5 * (stress_loop[j] + stress_loop[j + 1]) + * (strain_loop[j + 1] - strain_loop[j]) for j in range(len(strain_loop) - 1) ) assert abs(e_num) == pytest.approx(e_ref, rel=0.01), ( - f"cycle {i_cycle + 1}: numerical energy {abs(e_num):.4g} " f"vs reference {e_ref:.4g}" + f"cycle {i_cycle + 1}: numerical energy {abs(e_num):.4g} " + f"vs reference {e_ref:.4g}" ) @@ -217,7 +207,7 @@ def test_concrete04_monotonic_popovics_envelope() -> None: epsc0 = -0.002 epscu = -0.005 ec = 30e9 - n_steps = 200 # enough resolution to detect a kink clearly + n_steps = 200 # enough resolution to detect a kink clearly mat = Concrete04(id=1, fpc=fpc, epsc0=epsc0, epscu=epscu, Ec=ec) protocol = LoadProtocol( @@ -253,21 +243,23 @@ def test_concrete04_monotonic_popovics_envelope() -> None: ) # C1 continuity at peak: tangent slope ~ 0 from both sides. - d_eps = strain[peak_idx] - strain[peak_idx - 1] # negative step size + d_eps = strain[peak_idx] - strain[peak_idx - 1] # negative step size slope_before = (stress[peak_idx] - stress[peak_idx - 1]) / d_eps slope_after = (stress[peak_idx + 1] - stress[peak_idx]) / ( strain[peak_idx + 1] - strain[peak_idx] ) # Both slopes must be near zero (Popovics curve is C1 at the peak). - assert ( - abs(slope_before) / ec < 0.05 - ), f"slope before peak too large: {slope_before / ec:.4f} x Ec" - assert abs(slope_after) / ec < 0.05, f"slope after peak too large: {slope_after / ec:.4f} x Ec" + assert abs(slope_before) / ec < 0.05, ( + f"slope before peak too large: {slope_before / ec:.4f} x Ec" + ) + assert abs(slope_after) / ec < 0.05, ( + f"slope after peak too large: {slope_after / ec:.4f} x Ec" + ) # No kink: slope change at the peak must be smooth (< 5% of Ec). - assert ( - abs(slope_before - slope_after) / ec < 0.05 - ), f"kink detected at peak: delta_slope = {abs(slope_before - slope_after) / ec:.4f} x Ec" + assert abs(slope_before - slope_after) / ec < 0.05, ( + f"kink detected at peak: delta_slope = {abs(slope_before - slope_after) / ec:.4f} x Ec" + ) # ---- state-cleanup proof --------------------------------------------------- @@ -289,8 +281,12 @@ def test_state_cleanup_ten_consecutive_calls() -> None: ref_strain = results[0].strain ref_stress = results[0].stress for i, r in enumerate(results[1:], start=1): - assert r.strain == pytest.approx(ref_strain, rel=1e-9), f"strain diverged on call {i + 1}" - assert r.stress == pytest.approx(ref_stress, rel=1e-9), f"stress diverged on call {i + 1}" + assert r.strain == pytest.approx(ref_strain, rel=1e-9), ( + f"strain diverged on call {i + 1}" + ) + assert r.stress == pytest.approx(ref_stress, rel=1e-9), ( + f"stress diverged on call {i + 1}" + ) # ---- interleave test ------------------------------------------------------- @@ -327,6 +323,6 @@ def test_interleave_with_runner_analysis() -> None: # Uy at node 2 (DOF 1 in 0-indexed = DOF 2 in 1-indexed) must be identical. uy1 = float(result1.node_disp[2][0, 1]) uy2 = float(result2.node_disp[2][0, 1]) - assert uy1 == pytest.approx( - uy2, rel=1e-9 - ), f"runner Uy changed after interleaved material test: {uy1} vs {uy2}" + assert uy1 == pytest.approx(uy2, rel=1e-9), ( + f"runner Uy changed after interleaved material test: {uy1} vs {uy2}" + ) diff --git a/tests/integration/test_moment_curvature.py b/tests/integration/test_moment_curvature.py index 12439d4..8d17046 100644 --- a/tests/integration/test_moment_curvature.py +++ b/tests/integration/test_moment_curvature.py @@ -8,11 +8,13 @@ behaviour is exercised by the Phase 9 pushover tests. from __future__ import annotations +import math + import pytest pytest.importorskip("openseespy") -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticUniaxial, FiberSection, LinearTimeSeries, @@ -24,9 +26,7 @@ from otko.core import ( StaticCase, ZeroLengthSectionElement, ) -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def _moment_curvature_project(moment: float) -> Project: @@ -36,44 +36,32 @@ def _moment_curvature_project(moment: float) -> Project: at node 2's DOF 3 (Rz). With a linear-elastic fibre material the curvature should be ``moment / (E·I)``. """ - E = 30000.0 # Elastic modulus - b, h = 10.0, 20.0 # width × depth (in) + E = 30000.0 # Elastic modulus + b, h = 10.0, 20.0 # width × depth (in) return Project( - ndm=2, - ndf=3, + ndm=2, ndf=3, nodes=[ - Node(id=1, coords=(0, 0, 0), restraint=(True, True, False, False, False, True)), - Node(id=2, coords=(0, 0, 0), restraint=(False, True, False, False, False, False)), + Node(id=1, coords=(0, 0, 0), + restraint=(True, True, False, False, False, True)), + Node(id=2, coords=(0, 0, 0), + restraint=(False, True, False, False, False, False)), ], materials=[ElasticUniaxial(id=1, name="Elastic", E=E)], - sections=[ - FiberSection( - id=1, - name="Rect", - patches=[ - RectangularPatch( - material_id=1, - n_fib_y=20, - n_fib_z=1, - y_i=-h / 2, - z_i=-b / 2, - y_j=h / 2, - z_j=b / 2, - ) - ], - ) - ], + sections=[FiberSection( + id=1, name="Rect", patches=[RectangularPatch( + material_id=1, n_fib_y=20, n_fib_z=1, + y_i=-h / 2, z_i=-b / 2, y_j=h / 2, z_j=b / 2, + )], + )], elements=[ZeroLengthSectionElement(id=1, nodes=(1, 2), section_id=1)], time_series=[LinearTimeSeries(id=1, name="R")], - load_patterns=[ - PlainLoadPattern( - id=1, - time_series_id=1, - # NodalLoad.forces = (Fx, Fy, Fz, Mx, My, Mz). Moment around - # z (= curvature driver in 2D) goes into index 5, not 2. - nodal_loads=[NodalLoad(node_id=2, forces=(0, 0, 0, 0, 0, moment))], - ) - ], + load_patterns=[PlainLoadPattern( + id=1, time_series_id=1, + # NodalLoad.forces = (Fx, Fy, Fz, Mx, My, Mz). Moment around + # z (= curvature driver in 2D) goes into index 5, not 2. + nodal_loads=[NodalLoad(node_id=2, + forces=(0, 0, 0, 0, 0, moment))], + )], analyses=[StaticCase(id=1, name="MK", pattern_ids=[1], n_steps=1)], ) @@ -84,16 +72,16 @@ def test_zero_length_section_elastic_curvature_matches_closed_form() -> None: M = 500.0 E = 30000.0 b, h = 10.0, 20.0 - I = b * h**3 / 12.0 + I = b * h ** 3 / 12.0 expected_kappa = M / (E * I) proj = _moment_curvature_project(moment=M) result = OpenSeesRunner(proj).run(proj.analyses[0]) # Rz at node 2 IS the curvature for a zero-length section. ux, uy, rz = result.node_disp[2][-1] - assert rz == pytest.approx( - expected_kappa, rel=5e-3 - ), f"κ = {rz:.6e}, expected {expected_kappa:.6e}" + assert rz == pytest.approx(expected_kappa, rel=5e-3), ( + f"κ = {rz:.6e}, expected {expected_kappa:.6e}" + ) def test_pushover_drives_rotation_for_moment_curvature() -> None: @@ -106,39 +94,34 @@ def test_pushover_drives_rotation_for_moment_curvature() -> None: straight line through the origin with slope E·I. """ from otko.core import PushoverCase - E = 30000.0 b, h = 10.0, 20.0 - I = b * h**3 / 12.0 + I = b * h ** 3 / 12.0 target_kappa = 1e-5 steps = 20 # PushoverCase with DisplacementControl scales the load pattern — # needs a *non-zero* reference moment at the control DOF. proj = _moment_curvature_project(moment=1.0) - proj.analyses = [ - PushoverCase( - id=1, - name="MK-push", - pattern_ids=[1], - control_node=2, - control_dof=3, # DOF 3 = Rz - target_disp=target_kappa, # "displacement" == curvature here - step_size=target_kappa / steps, - base_nodes=[1], - ) - ] + proj.analyses = [PushoverCase( + id=1, name="MK-push", + pattern_ids=[1], + control_node=2, control_dof=3, # DOF 3 = Rz + target_disp=target_kappa, # "displacement" == curvature here + step_size=target_kappa / steps, + base_nodes=[1], + )] result = OpenSeesRunner(proj).run(proj.analyses[0]) # Every (κ, M) point must satisfy M = E·I·κ (1 % tolerance allows # for the ~20-fibre discretisation of the rectangular section). - for kappa, moment in zip(result.control_disp, result.base_shear, strict=False): + for kappa, moment in zip(result.control_disp, result.base_shear): if abs(kappa) < 1e-12: continue expected_M = E * I * kappa - assert moment == pytest.approx( - expected_M, rel=1e-2 - ), f"at κ={kappa:.3e}: M={moment:.3e}, expected {expected_M:.3e}" + assert moment == pytest.approx(expected_M, rel=1e-2), ( + f"at κ={kappa:.3e}: M={moment:.3e}, expected {expected_M:.3e}" + ) # Terminal curvature must reach the target. assert result.control_disp[-1] == pytest.approx(target_kappa, rel=1e-3) @@ -162,7 +145,6 @@ def test_moment_curvature_with_constant_axial_preload() -> None: Steel01, StraightLayer, ) - colWidth = 15.0 colDepth = 24.0 cover = 1.5 @@ -171,104 +153,58 @@ def test_moment_curvature_with_constant_axial_preload() -> None: z1 = colWidth / 2 proj = Project( - ndm=2, - ndf=3, + ndm=2, ndf=3, nodes=[ - Node(id=1, coords=(0, 0, 0), restraint=(True, True, False, False, False, True)), - Node(id=2, coords=(0, 0, 0), restraint=(False, True, False, False, False, False)), + Node(id=1, coords=(0, 0, 0), + restraint=(True, True, False, False, False, True)), + Node(id=2, coords=(0, 0, 0), + restraint=(False, True, False, False, False, False)), ], materials=[ - Concrete01(id=1, name="Core", fpc=-6.0, epsc0=-0.004, fpcu=-5.0, epsU=-0.014), - Concrete01(id=2, name="Cover", fpc=-5.0, epsc0=-0.002, fpcu=0.0, epsU=-0.006), + Concrete01(id=1, name="Core", + fpc=-6.0, epsc0=-0.004, + fpcu=-5.0, epsU=-0.014), + Concrete01(id=2, name="Cover", + fpc=-5.0, epsc0=-0.002, + fpcu=0.0, epsU=-0.006), Steel01(id=3, name="Steel", Fy=60.0, E0=30000.0, b=0.01), ], - sections=[ - FiberSection( - id=1, - name="RC", - patches=[ - # Core (confined) - RectangularPatch( - material_id=1, - n_fib_y=10, - n_fib_z=1, - y_i=cover - y1, - z_i=cover - z1, - y_j=y1 - cover, - z_j=z1 - cover, - ), - # Top cover - RectangularPatch( - material_id=2, - n_fib_y=10, - n_fib_z=1, - y_i=-y1, - z_i=z1 - cover, - y_j=y1, - z_j=z1, - ), - # Bottom cover - RectangularPatch( - material_id=2, - n_fib_y=10, - n_fib_z=1, - y_i=-y1, - z_i=-z1, - y_j=y1, - z_j=cover - z1, - ), - # Left cover - RectangularPatch( - material_id=2, - n_fib_y=2, - n_fib_z=1, - y_i=-y1, - z_i=cover - z1, - y_j=cover - y1, - z_j=z1 - cover, - ), - # Right cover - RectangularPatch( - material_id=2, - n_fib_y=2, - n_fib_z=1, - y_i=y1 - cover, - z_i=cover - z1, - y_j=y1, - z_j=z1 - cover, - ), - ], - layers=[ - StraightLayer( - material_id=3, - n_bars=3, - bar_area=As, - y_start=y1 - cover, - z_start=z1 - cover, - y_end=y1 - cover, - z_end=cover - z1, - ), - StraightLayer( - material_id=3, - n_bars=2, - bar_area=As, - y_start=0.0, - z_start=z1 - cover, - y_end=0.0, - z_end=cover - z1, - ), - StraightLayer( - material_id=3, - n_bars=3, - bar_area=As, - y_start=cover - y1, - z_start=z1 - cover, - y_end=cover - y1, - z_end=cover - z1, - ), - ], - ) - ], + sections=[FiberSection( + id=1, name="RC", + patches=[ + # Core (confined) + RectangularPatch(material_id=1, n_fib_y=10, n_fib_z=1, + y_i=cover - y1, z_i=cover - z1, + y_j=y1 - cover, z_j=z1 - cover), + # Top cover + RectangularPatch(material_id=2, n_fib_y=10, n_fib_z=1, + y_i=-y1, z_i=z1 - cover, + y_j=y1, z_j=z1), + # Bottom cover + RectangularPatch(material_id=2, n_fib_y=10, n_fib_z=1, + y_i=-y1, z_i=-z1, + y_j=y1, z_j=cover - z1), + # Left cover + RectangularPatch(material_id=2, n_fib_y=2, n_fib_z=1, + y_i=-y1, z_i=cover - z1, + y_j=cover - y1, z_j=z1 - cover), + # Right cover + RectangularPatch(material_id=2, n_fib_y=2, n_fib_z=1, + y_i=y1 - cover, z_i=cover - z1, + y_j=y1, z_j=z1 - cover), + ], + layers=[ + StraightLayer(material_id=3, n_bars=3, bar_area=As, + y_start=y1 - cover, z_start=z1 - cover, + y_end=y1 - cover, z_end=cover - z1), + StraightLayer(material_id=3, n_bars=2, bar_area=As, + y_start=0.0, z_start=z1 - cover, + y_end=0.0, z_end=cover - z1), + StraightLayer(material_id=3, n_bars=3, bar_area=As, + y_start=cover - y1, z_start=z1 - cover, + y_end=cover - y1, z_end=cover - z1), + ], + )], elements=[ZeroLengthSectionElement(id=1, nodes=(1, 2), section_id=1)], time_series=[ ConstantTimeSeries(id=1, name="AxialP"), @@ -276,16 +212,14 @@ def test_moment_curvature_with_constant_axial_preload() -> None: ], load_patterns=[ PlainLoadPattern( - id=1, - name="AxialP", - time_series_id=1, - nodal_loads=[NodalLoad(node_id=2, forces=(-180.0, 0, 0, 0, 0, 0))], + id=1, name="AxialP", time_series_id=1, + nodal_loads=[NodalLoad(node_id=2, + forces=(-180.0, 0, 0, 0, 0, 0))], ), PlainLoadPattern( - id=2, - name="RefMoment", - time_series_id=2, - nodal_loads=[NodalLoad(node_id=2, forces=(0, 0, 0, 0, 0, 1.0))], + id=2, name="RefMoment", time_series_id=2, + nodal_loads=[NodalLoad(node_id=2, + forces=(0, 0, 0, 0, 0, 1.0))], ), ], analyses=[], @@ -293,29 +227,25 @@ def test_moment_curvature_with_constant_axial_preload() -> None: # Yield curvature estimate from the Tcl example. d = colDepth - cover Ky = 60.0 / 30000.0 / (0.7 * d) - target = Ky * 15 # μ = 15 - proj.analyses = [ - PushoverCase( - id=1, - name="MK", - pattern_ids=[1, 2], - control_node=2, - control_dof=3, - target_disp=target, - step_size=target / 100, - base_nodes=[1], - test="NormUnbalance", - tolerance=1e-9, - max_iter=25, - ) - ] + target = Ky * 15 # μ = 15 + proj.analyses = [PushoverCase( + id=1, name="MK", + pattern_ids=[1, 2], + control_node=2, control_dof=3, + target_disp=target, + step_size=target / 100, + base_nodes=[1], + test="NormUnbalance", + tolerance=1e-9, max_iter=25, + )] result = OpenSeesRunner(proj).run(proj.analyses[0]) # Analysis must actually converge past yield (not collapse at # step 1 like it did before the two-stage preload fix). assert len(result.control_disp) > 50, ( - f"Converged for only {len(result.control_disp)} of 100 steps — " "preload stage broken?" + f"Converged for only {len(result.control_disp)} of 100 steps — " + "preload stage broken?" ) # Curvature reached or passed yield. kappa_max = float(max(abs(k) for k in result.control_disp)) diff --git a/tests/integration/test_moment_curvature_example.py b/tests/integration/test_moment_curvature_example.py index 7648e51..1c27358 100644 --- a/tests/integration/test_moment_curvature_example.py +++ b/tests/integration/test_moment_curvature_example.py @@ -6,30 +6,26 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services import load_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def test_moment_curvature_example_round_trips_and_converges(tmp_path) -> None: # type: ignore[no-untyped-def] """build_moment_curvature() → save → load → run → expected shape.""" from examples.moment_curvature import ( + build_moment_curvature, COL_DEPTH, COVER, E_STEEL, FY, MU, NUM_INCR, - build_moment_curvature, ) - proj = build_moment_curvature() proj.validate_references() # Save + reload — catches schema drift. from otko.services import save_project - path = tmp_path / "mk.osmodel" save_project(proj, path) reloaded = load_project(path) @@ -43,14 +39,15 @@ def test_moment_curvature_example_round_trips_and_converges(tmp_path) -> None: ky = (FY / E_STEEL) / (0.7 * d) # At least half of the NUM_INCR pushover steps converged. - assert ( - len(result.control_disp) > NUM_INCR * 0.5 - ), "Pushover bailed out prematurely — check Concrete01 softening" + assert len(result.control_disp) > NUM_INCR * 0.5, ( + "Pushover bailed out prematurely — check Concrete01 softening" + ) # Reached the mu * Ky target. assert result.control_disp[-1] == pytest.approx(MU * ky, rel=1e-2) # Moment at yield curvature is plausible: > 3 kip·in and < 10 kip·in per rebar # → for 8 bars total, the section moment capacity is roughly O(3000-6000) kip·in. peak_moment = max(abs(m) for m in result.base_shear) assert 2000 < peak_moment < 10000, ( - f"Peak moment {peak_moment:.1f} kip·in is outside the " "expected RC section range" + f"Peak moment {peak_moment:.1f} kip·in is outside the " + "expected RC section range" ) diff --git a/tests/integration/test_pattern_factors.py b/tests/integration/test_pattern_factors.py index 3c05c3e..f67c645 100644 --- a/tests/integration/test_pattern_factors.py +++ b/tests/integration/test_pattern_factors.py @@ -8,7 +8,7 @@ import pytest ops = pytest.importorskip("openseespy.opensees") # skip if OpenSeesPy not installed -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticBeamColumn, ElasticSection, LinearTimeSeries, @@ -18,9 +18,7 @@ from otko.core import ( Project, StaticCase, ) -from otko.services import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services import OpenSeesRunner # noqa: E402 def _cantilever() -> Project: diff --git a/tests/integration/test_rc_frame_earthquake.py b/tests/integration/test_rc_frame_earthquake.py index 132535f..8f2a7a8 100644 --- a/tests/integration/test_rc_frame_earthquake.py +++ b/tests/integration/test_rc_frame_earthquake.py @@ -9,19 +9,17 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def test_rc_frame_earthquake_runs_and_has_oscillatory_response(tmp_path) -> None: # type: ignore[no-untyped-def] """Synthetic ground motion produces bounded, oscillatory response.""" from examples.rc_frame_earthquake import ( - N_PTS, build_rc_frame_earthquake, + DT, + N_PTS, ) - proj = build_rc_frame_earthquake() proj.validate_references() @@ -35,9 +33,9 @@ def test_rc_frame_earthquake_runs_and_has_oscillatory_response(tmp_path) -> None # Simulation covers most of the 4-second record (ModifiedNewton # fallback may trim a few steps at stiffness jumps; we allow that). - assert result.n_steps >= int( - 0.9 * N_PTS - ), f"Only {result.n_steps}/{N_PTS} steps — fallback didn't recover" + assert result.n_steps >= int(0.9 * N_PTS), ( + f"Only {result.n_steps}/{N_PTS} steps — fallback didn't recover" + ) # Node 3 Ux history: bounded, non-trivial, some positive AND some # negative (oscillation confirms the base excitation actually @@ -52,4 +50,4 @@ def test_rc_frame_earthquake_runs_and_has_oscillatory_response(tmp_path) -> None # Uy on the top nodes — small compared to Ux (gravity holds, base # excitation is horizontal). uy = h3[:, 1] - assert max(abs(uy)) < 1.0, f"max |Uy| = {max(abs(uy)):.4f} in too large" + assert max(abs(uy)) < 1.0, f"max |Uy| = {max(abs(uy)):.4f} in too large" \ No newline at end of file diff --git a/tests/integration/test_rc_frame_gravity.py b/tests/integration/test_rc_frame_gravity.py index 0c91190..a213681 100644 --- a/tests/integration/test_rc_frame_gravity.py +++ b/tests/integration/test_rc_frame_gravity.py @@ -6,10 +6,8 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def test_rc_frame_gravity_matches_opensees_reference(tmp_path) -> None: # type: ignore[no-untyped-def] @@ -22,8 +20,7 @@ def test_rc_frame_gravity_matches_opensees_reference(tmp_path) -> None: # type: The column axial force is 180 kip compression (from the 180 kip load stepped onto each top node). """ - from examples.rc_frame_gravity import P_LOAD, build_rc_frame_gravity - + from examples.rc_frame_gravity import build_rc_frame_gravity, P_LOAD proj = build_rc_frame_gravity() proj.validate_references() @@ -57,7 +54,7 @@ def test_rc_frame_gravity_matches_opensees_reference(tmp_path) -> None: # type: # OpenSees equilibrium sign, i.e. the first component equals the # applied vertical load on end i. col1 = result.element_forces[1][-1] - assert abs(col1[0]) == pytest.approx( - P_LOAD, abs=1.0 - ), f"Column 1 axial {col1[0]:.2f} ≠ ±{P_LOAD} kip" + assert abs(col1[0]) == pytest.approx(P_LOAD, abs=1.0), ( + f"Column 1 axial {col1[0]:.2f} ≠ ±{P_LOAD} kip" + ) assert abs(col1[3]) == pytest.approx(P_LOAD, abs=1.0) diff --git a/tests/integration/test_rc_frame_pushover.py b/tests/integration/test_rc_frame_pushover.py index 5bd9ed0..d7d6df5 100644 --- a/tests/integration/test_rc_frame_pushover.py +++ b/tests/integration/test_rc_frame_pushover.py @@ -6,10 +6,8 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project, save_project -from otko.services.opensees_runner import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services import load_project, save_project # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 def test_rc_frame_pushover_reaches_target_with_fallback(tmp_path) -> None: # type: ignore[no-untyped-def] @@ -19,13 +17,11 @@ def test_rc_frame_pushover_reaches_target_with_fallback(tmp_path) -> None: # ty AND shows expected nonlinear shape. """ from examples.rc_frame_pushover import ( + build_rc_frame_pushover, D_STEP, D_TARGET, - build_rc_frame_pushover, ) - from otko.core import PushoverCase - proj = build_rc_frame_pushover() proj.validate_references() @@ -39,7 +35,7 @@ def test_rc_frame_pushover_reaches_target_with_fallback(tmp_path) -> None: # ty push_case = next(c for c in reloaded.analyses if isinstance(c, PushoverCase)) result = OpenSeesRunner(reloaded).run(push_case) - expected_pts = int(D_TARGET / D_STEP) + 1 # 151 including step 0 + expected_pts = int(D_TARGET / D_STEP) + 1 # 151 including step 0 assert len(result.control_disp) == expected_pts, ( f"Got {len(result.control_disp)} points, expected {expected_pts} — " "ModifiedNewton fallback probably didn't kick in." @@ -68,6 +64,6 @@ def test_rc_frame_pushover_reaches_target_with_fallback(tmp_path) -> None: # ty # of the pushover (step 1) should be close to P = 180 kip. step1_col1 = result.element_forces[1][1] axial_step1 = abs(step1_col1[0]) - assert ( - 100.0 < axial_step1 < 260.0 - ), f"Col 1 axial at step 1 = {axial_step1:.1f} kip — gravity preload lost?" + assert 100.0 < axial_step1 < 260.0, ( + f"Col 1 axial at step 1 = {axial_step1:.1f} kip — gravity preload lost?" + ) diff --git a/tests/integration/test_runner_imposed_motion.py b/tests/integration/test_runner_imposed_motion.py index 33c352b..3565fb7 100644 --- a/tests/integration/test_runner_imposed_motion.py +++ b/tests/integration/test_runner_imposed_motion.py @@ -23,7 +23,7 @@ import pytest ops = pytest.importorskip("openseespy.opensees") h5py = pytest.importorskip("h5py") -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticUniaxial, ImposedSupportMotionPattern, Node, @@ -33,10 +33,7 @@ from otko.core import ( UniformExcitationPattern, ZeroLengthElement, ) -from otko.services import OpenSeesRunner - -pytestmark = pytest.mark.slow - +from otko.services import OpenSeesRunner # noqa: E402 W = 1.57 # drive (rad/s) DT_SERIES = 0.01 diff --git a/tests/integration/test_runner_modal.py b/tests/integration/test_runner_modal.py index bf2a843..753210b 100644 --- a/tests/integration/test_runner_modal.py +++ b/tests/integration/test_runner_modal.py @@ -15,18 +15,16 @@ import math import pytest -ops = pytest.importorskip("openseespy.opensees") +ops = pytest.importorskip("openseespy.opensees") # noqa: F401 -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticBeamColumn, ElasticSection, ModalCase, Node, Project, ) -from otko.services import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services import OpenSeesRunner # noqa: E402 def test_sdof_pole_first_frequency_matches_kspring_over_m() -> None: @@ -38,11 +36,11 @@ def test_sdof_pole_first_frequency_matches_kspring_over_m() -> None: m_tip = 1000.0 project = Project( - ndm=2, - ndf=3, + ndm=2, ndf=3, nodes=[ Node(id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, False, False, False, True)), - Node(id=2, coords=(0.0, L, 0.0), mass=(m_tip, m_tip, 0.0, 0.0, 0.0, 0.0)), + Node(id=2, coords=(0.0, L, 0.0), + mass=(m_tip, m_tip, 0.0, 0.0, 0.0, 0.0)), ], sections=[ElasticSection(id=1, E=E, A=A, Iz=I)], elements=[ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1)], @@ -56,9 +54,9 @@ def test_sdof_pole_first_frequency_matches_kspring_over_m() -> None: omega_expected = math.sqrt(k / m_tip) omega_actual = float(results.angular_frequencies[0]) - assert math.isclose( - omega_actual, omega_expected, rel_tol=5e-3 - ), f"ω₁ mismatch: expected {omega_expected:.4f} rad/s, got {omega_actual:.4f} rad/s" + assert math.isclose(omega_actual, omega_expected, rel_tol=5e-3), ( + f"ω₁ mismatch: expected {omega_expected:.4f} rad/s, got {omega_actual:.4f} rad/s" + ) def test_runner_falls_back_to_lapack_for_small_models() -> None: @@ -66,11 +64,11 @@ def test_runner_falls_back_to_lapack_for_small_models() -> None: from unittest.mock import MagicMock project = Project( - ndm=2, - ndf=3, + ndm=2, ndf=3, nodes=[ Node(id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, False, False, False, True)), - Node(id=2, coords=(0.0, 3.0, 0.0), mass=(1000.0, 1000.0, 0.0, 0.0, 0.0, 0.0)), + Node(id=2, coords=(0.0, 3.0, 0.0), + mass=(1000.0, 1000.0, 0.0, 0.0, 0.0, 0.0)), ], sections=[ElasticSection(id=1, E=200e9, A=0.01, Iz=8.333e-6)], elements=[ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1)], diff --git a/tests/integration/test_runner_static.py b/tests/integration/test_runner_static.py index bd9cac1..da5aa9d 100644 --- a/tests/integration/test_runner_static.py +++ b/tests/integration/test_runner_static.py @@ -12,7 +12,7 @@ import pytest ops = pytest.importorskip("openseespy.opensees") # skip if OpenSeesPy not installed -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticBeamColumn, ElasticSection, LinearTimeSeries, @@ -22,9 +22,7 @@ from otko.core import ( Project, StaticCase, ) -from otko.services import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services import OpenSeesRunner # noqa: E402 def test_cantilever_tip_deflection_matches_closed_form() -> None: @@ -36,8 +34,7 @@ def test_cantilever_tip_deflection_matches_closed_form() -> None: I = 8.333e-6 project = Project( - ndm=2, - ndf=3, + ndm=2, ndf=3, nodes=[ Node(id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, False, False, False, True)), Node(id=2, coords=(L, 0.0, 0.0)), @@ -47,8 +44,7 @@ def test_cantilever_tip_deflection_matches_closed_form() -> None: time_series=[LinearTimeSeries(id=1)], load_patterns=[ PlainLoadPattern( - id=1, - time_series_id=1, + id=1, time_series_id=1, # Fy at the tip (downward); Rz of node 1 is restrained, others free. nodal_loads=[NodalLoad(node_id=2, forces=(0.0, -P, 0.0, 0.0, 0.0, 0.0))], ) @@ -58,12 +54,12 @@ def test_cantilever_tip_deflection_matches_closed_form() -> None: case = StaticCase(id=1, name="Cantilever", pattern_ids=[1]) results = OpenSeesRunner(project).run(case) - delta_expected = -P * L**3 / (3.0 * E * I) # negative (downward) - delta_actual = results.disp(node_id=2, dof=2) # Uy at node 2 + delta_expected = -P * L**3 / (3.0 * E * I) # negative (downward) + delta_actual = results.disp(node_id=2, dof=2) # Uy at node 2 - assert math.isclose( - delta_actual, delta_expected, rel_tol=1e-3 - ), f"Tip deflection mismatch: expected {delta_expected:.6e}, got {delta_actual:.6e}" + assert math.isclose(delta_actual, delta_expected, rel_tol=1e-3), ( + f"Tip deflection mismatch: expected {delta_expected:.6e}, got {delta_actual:.6e}" + ) # Reaction at the support equals the applied load. Fy_reaction = results.node_reaction[1][0, 1] diff --git a/tests/integration/test_runner_transient.py b/tests/integration/test_runner_transient.py index a2be365..69c5f3b 100644 --- a/tests/integration/test_runner_transient.py +++ b/tests/integration/test_runner_transient.py @@ -12,10 +12,10 @@ import math import numpy as np import pytest -ops = pytest.importorskip("openseespy.opensees") -h5py = pytest.importorskip("h5py") +ops = pytest.importorskip("openseespy.opensees") # noqa: F401 +h5py = pytest.importorskip("h5py") # noqa: F401 -from otko.core import ( +from otko.core import ( # noqa: E402 ConstantTimeSeries, ElasticBeamColumn, ElasticSection, @@ -27,9 +27,7 @@ from otko.core import ( StaticCase, TransientCase, ) -from otko.services import OpenSeesRunner - -pytestmark = pytest.mark.slow +from otko.services import OpenSeesRunner # noqa: E402 def test_sdof_free_vibration_matches_cosine(tmp_path) -> None: # type: ignore[no-untyped-def] @@ -51,29 +49,28 @@ def test_sdof_free_vibration_matches_cosine(tmp_path) -> None: # type: ignore[n # (already done), then verify dt-step Newmark integration of free # vibration starting from a static IC. F0 = 100.0 + u0 = F0 / k project = Project( - ndm=2, - ndf=3, + ndm=2, ndf=3, nodes=[ Node(id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, False, False, False, True)), - Node(id=2, coords=(0.0, L, 0.0), mass=(m_tip, m_tip, 0.0, 0.0, 0.0, 0.0)), + Node(id=2, coords=(0.0, L, 0.0), + mass=(m_tip, m_tip, 0.0, 0.0, 0.0, 0.0)), ], sections=[ElasticSection(id=1, E=E, A=A, Iz=I)], elements=[ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1)], time_series=[ ConstantTimeSeries(id=1, factor=1.0), # static initial - LinearTimeSeries(id=2), # transient (zero load) + LinearTimeSeries(id=2), # transient (zero load) ], load_patterns=[ PlainLoadPattern( - id=1, - time_series_id=1, + id=1, time_series_id=1, nodal_loads=[NodalLoad(node_id=2, forces=(F0, 0.0, 0.0, 0.0, 0.0, 0.0))], ), PlainLoadPattern( - id=2, - time_series_id=2, + id=2, time_series_id=2, nodal_loads=[NodalLoad(node_id=2, forces=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0))], ), ], @@ -88,11 +85,8 @@ def test_sdof_free_vibration_matches_cosine(tmp_path) -> None: # type: ignore[n n_steps = 200 dt = T / 50.0 case = TransientCase( - id=2, - name="FreeVib", - pattern_ids=[2], - dt=dt, - n_steps=n_steps, + id=2, name="FreeVib", pattern_ids=[2], + dt=dt, n_steps=n_steps, # Average-acceleration Newmark is unconditionally stable. integrator_params=(0.5, 0.25), ) @@ -117,8 +111,7 @@ def test_sdof_free_vibration_matches_cosine(tmp_path) -> None: # type: ignore[n def test_transient_writes_hdf5_with_time_dataset(tmp_path) -> None: # type: ignore[no-untyped-def] """Transient run must produce an HDF5 with a /time dataset of length n_steps.""" project = Project( - ndm=2, - ndf=3, + ndm=2, ndf=3, nodes=[ Node(id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, False, False, False, True)), Node(id=2, coords=(0.0, 3.0, 0.0), mass=(1000.0,) * 3 + (0.0,) * 3), @@ -128,8 +121,7 @@ def test_transient_writes_hdf5_with_time_dataset(tmp_path) -> None: # type: ign time_series=[LinearTimeSeries(id=1)], load_patterns=[ PlainLoadPattern( - id=1, - time_series_id=1, + id=1, time_series_id=1, nodal_loads=[NodalLoad(node_id=2, forces=(10.0, 0, 0, 0, 0, 0))], ) ], diff --git a/tests/services/__init__.py b/tests/services/__init__.py deleted file mode 100644 index c495081..0000000 --- a/tests/services/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Service-layer tests: runner, persistence, export, results I/O.""" diff --git a/tests/tools/test_parse_schemas.py b/tests/tools/test_parse_schemas.py index 506496c..cf9dc38 100644 --- a/tests/tools/test_parse_schemas.py +++ b/tests/tools/test_parse_schemas.py @@ -13,7 +13,6 @@ import json from pathlib import Path import pytest - from tools.gidopensees_import.parse_schemas import ( ParseError, _parse_dependencies, @@ -298,9 +297,9 @@ class TestRealFiles: steel2 = _find_entry(catalog, "Steel02") assert steel2 is not None form = next(f for f in steel2.fields if f.name == "Formulation") - assert ( - len(form.dependencies) == 3 - ), f"Expected 3 dependency rules on Formulation, got {len(form.dependencies)}" + assert len(form.dependencies) == 3, ( + f"Expected 3 dependency rules on Formulation, got {len(form.dependencies)}" + ) def test_steel02_formulation_dep_triggers(self, catalog: CatalogSpec) -> None: steel2 = _find_entry(catalog, "Steel02") @@ -317,18 +316,18 @@ class TestRealFiles: def test_all_mat_entries_have_book(self, catalog: CatalogSpec) -> None: for book in catalog.mat_books: for entry in book.entries: - assert ( - entry.book == book.name - ), f"{entry.name!r} has book={entry.book!r}, expected {book.name!r}" + assert entry.book == book.name, ( + f"{entry.name!r} has book={entry.book!r}, expected {book.name!r}" + ) def test_all_fields_have_valid_widget_type(self, catalog: CatalogSpec) -> None: valid = {"CB", "UNITS", "MAT", "SCALAR", "TUPLE"} for book in catalog.mat_books: for entry in book.entries: for f in entry.fields: - assert ( - f.widget_type in valid - ), f"{entry.name}.{f.name}: unknown widget_type={f.widget_type!r}" + assert f.widget_type in valid, ( + f"{entry.name}.{f.name}: unknown widget_type={f.widget_type!r}" + ) def test_point_restraints_condtype(self, catalog: CatalogSpec) -> None: entry = _find_cnd_entry(catalog, "Point_Restraints") @@ -355,9 +354,9 @@ class TestRealFiles: for fspec in entry.fields: for rule in fspec.dependencies: for act in rule.actions: - assert ( - act.action in valid_actions - ), f"{entry.name}.{fspec.name}: invalid action {act.action!r}" + assert act.action in valid_actions, ( + f"{entry.name}.{fspec.name}: invalid action {act.action!r}" + ) # --------------------------------------------------------------------------- diff --git a/tests/unit/core/materials/test_concrete04.py b/tests/unit/core/materials/test_concrete04.py index 5ea8311..ccec7eb 100644 --- a/tests/unit/core/materials/test_concrete04.py +++ b/tests/unit/core/materials/test_concrete04.py @@ -24,14 +24,9 @@ class TestConcrete04Instantiation: def test_with_tension_construct(self) -> None: m = Concrete04( - id=2, - name="C30-Tension", - fpc=-30e6, - epsc0=-0.002, - epscu=-0.005, - Ec=30e9, - fct=3.0e6, - et=1e-4, + id=2, name="C30-Tension", + fpc=-30e6, epsc0=-0.002, epscu=-0.005, Ec=30e9, + fct=3.0e6, et=1e-4, ) assert m.fct == 3.0e6 assert m.et == 1e-4 @@ -39,15 +34,9 @@ class TestConcrete04Instantiation: def test_with_beta_construct(self) -> None: m = Concrete04( - id=3, - name="C30-Cyclic", - fpc=-30e6, - epsc0=-0.002, - epscu=-0.005, - Ec=30e9, - fct=3.0e6, - et=1e-4, - beta=0.1, + id=3, name="C30-Cyclic", + fpc=-30e6, epsc0=-0.002, epscu=-0.005, Ec=30e9, + fct=3.0e6, et=1e-4, beta=0.1, ) assert m.beta == pytest.approx(0.1) @@ -94,14 +83,8 @@ class TestConcrete04TensileParamConsistency: def test_beta_bounds(self) -> None: with pytest.raises(ValidationError): Concrete04( - id=1, - fpc=-30e6, - epsc0=-0.002, - epscu=-0.005, - Ec=30e9, - fct=3e6, - et=1e-4, - beta=1.5, + id=1, fpc=-30e6, epsc0=-0.002, epscu=-0.005, Ec=30e9, + fct=3e6, et=1e-4, beta=1.5, ) @@ -118,14 +101,9 @@ class TestConcrete04JsonRoundTrip: def test_with_tension_round_trip(self) -> None: original = Concrete04( - id=2, - name="C30-T", - fpc=-30e6, - epsc0=-0.002, - epscu=-0.005, - Ec=30e9, - fct=3.0e6, - et=1e-4, + id=2, name="C30-T", + fpc=-30e6, epsc0=-0.002, epscu=-0.005, Ec=30e9, + fct=3.0e6, et=1e-4, ) payload = material_adapter.dump_python(original, mode="json", by_alias=True) restored = material_adapter.validate_python(payload) @@ -136,7 +114,6 @@ class TestConcrete04JsonRoundTrip: def test_old_osmodel_without_concrete04_loads_cleanly(self) -> None: """An osmodel payload that doesn't mention Concrete04 is unaffected.""" from pathlib import Path - from otko.services import load_project osmodel = Path(__file__).parents[4] / "examples" / "cantilever.osmodel" diff --git a/tests/unit/test_architecture.py b/tests/unit/test_architecture.py deleted file mode 100644 index 7b3f95c..0000000 --- a/tests/unit/test_architecture.py +++ /dev/null @@ -1,201 +0,0 @@ -"""Architecture and layering rules - see ``docs/architecture.md``. - -The documented dependency stack is ``views -> viewmodels -> services -> core``; -each layer may only import from itself or a lower layer, ``core`` stays free of -Qt/solver/plotting libraries, and the rendering backends are confined to the -canvas packages (``views/canvas3d``, ``views/canvas_plotly``). These tests never -import the application modules; they read the source tree with ``ast`` so the -import discipline can be checked without executing Qt or the solver. -Directories that do not exist yet simply have nothing to walk, which trivially -satisfies a "forbidden import" rule. -""" - -from __future__ import annotations - -import ast -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -SRC = ROOT / "src" / "otko" -CANVAS_DIRS = ( - SRC / "views" / "canvas3d", - SRC / "views" / "canvas_plotly", -) - -# Qt binding roots, in every spelling the project might use. -QT_ROOTS = {"PySide6", "PyQt5", "PyQt6", "PySide"} - -# core/ stays pure - no Qt and no solver/mesh/render libraries. -CORE_FORBIDDEN = QT_ROOTS | { - "openseespy", - "opstool", - "shapely", - "vtk", - "pyvista", - "plotly", - "matplotlib", -} - -# Only the canvas packages may touch a 3D or plotting backend. -VTK_ROOTS = {"pyvista", "pyvistaqt", "vtk", "plotly"} - - -def _iter_python_files(root: Path) -> list[Path]: - """Return every ``*.py`` file below ``root``, or ``[]`` if it is absent.""" - if not root.exists(): - return [] - return sorted(root.rglob("*.py")) - - -def _root_imports(path: Path) -> set[str]: - """Return the top-level module names imported by a Python source file. - - Handles ``import X``, ``import X.Y``, ``from X import ...`` and - ``from X.Y import ...``. Relative imports are ignored: they cannot reach - a third-party package. - """ - tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) - roots: set[str] = set() - for node in ast.walk(tree): - if isinstance(node, ast.Import): - for alias in node.names: - roots.add(alias.name.split(".", 1)[0]) - elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: - roots.add(node.module.split(".", 1)[0]) - return roots - - -def _is_within(path: Path, directory: Path) -> bool: - """Return True when ``path`` lies inside ``directory`` (lexical check).""" - try: - path.relative_to(directory) - except ValueError: - return False - return True - - -def _violations( - root: Path, - forbidden: set[str], - *, - skip: tuple[Path, ...] = (), - exclude_dirs: tuple[Path, ...] = (), -) -> list[str]: - """Return ``": "`` entries for every forbidden import found.""" - found: list[str] = [] - for path in _iter_python_files(root): - if path in skip: - continue - if any(_is_within(path, directory) for directory in exclude_dirs): - continue - for module in sorted(_root_imports(path) & forbidden): - found.append(f"{path.relative_to(ROOT)}: {module}") - return found - - -def test_architecture_core_has_no_qt_or_solver() -> None: - """core/ imports no Qt, solver, mesh, or plotting library.""" - violations = _violations(SRC / "core", CORE_FORBIDDEN) - assert violations == [], f"forbidden imports in core/: {violations}" - - -def test_architecture_services_no_qt_except_worker() -> None: - """No Qt in services/ except services/qt_workers.py.""" - violations = _violations( - SRC / "services", - QT_ROOTS, - skip=(SRC / "services" / "qt_workers.py",), - ) - assert violations == [], f"forbidden Qt imports in services/: {violations}" - - -def test_architecture_views_no_solver() -> None: - """views/ imports neither openseespy nor opstool.""" - violations = _violations(SRC / "views", {"openseespy", "opstool"}) - assert violations == [], f"solver imports in views/: {violations}" - - -def test_no_vtk_import_outside_canvas() -> None: - """3D/plot backends stay under the canvas packages.""" - violations = _violations(SRC, VTK_ROOTS, exclude_dirs=CANVAS_DIRS) - assert violations == [], f"backend imports outside the canvas packages: {violations}" - - -# The documented dependency stack; ``commands`` sits beside viewmodels (a VM -# creates commands) and ``views`` is the top, so it is excluded from this map. -_LAYER_ORDER = {"core": 0, "services": 1, "viewmodels": 2, "views": 3} - - -def _internal_imports(path: Path) -> set[str]: - """Return the fully-qualified ``otko.*`` modules imported by a source file.""" - tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) - found: set[str] = set() - for node in ast.walk(tree): - if isinstance(node, ast.Import): - found.update(alias.name for alias in node.names) - elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: - found.add(node.module) - return {module for module in found if module.startswith("otko.")} - - -def test_architecture_lower_layers_do_not_import_higher() -> None: - """core/services/viewmodels never import a higher layer.""" - violations: list[str] = [] - for path in _iter_python_files(SRC): - layer = path.relative_to(SRC).parts[0] - if layer not in _LAYER_ORDER: - continue - for module in _internal_imports(path): - target = module.split(".")[1] if "." in module else "" - if target in _LAYER_ORDER and _LAYER_ORDER[target] > _LAYER_ORDER[layer]: - violations.append(f"{path.relative_to(ROOT)}: {layer} -> {module}") - assert violations == [], f"lower layer imports a higher layer: {violations}" - - -def test_architecture_viewmodels_do_not_construct_widgets() -> None: - """viewmodels emit signals and import no Qt widgets.""" - violations = _violations(SRC / "viewmodels", {"QtWidgets"}) - assert violations == [], f"viewmodels import widgets: {violations}" - for path in _iter_python_files(SRC / "viewmodels"): - assert "PySide6.QtWidgets" not in path.read_text(encoding="utf-8"), path - - -def test_architecture_report_is_qt_and_solver_free() -> None: - """report generation imports no Qt or solver (vacuous if absent).""" - report = SRC / "services" / "report" - violations = _violations(report, QT_ROOTS | {"openseespy", "opstool"}) - assert violations == [], f"report imports Qt/solver: {violations}" - - -def test_architecture_project_vm_owns_the_undo_stack() -> None: - """Only project_vm.py holds a QUndoStack.""" - holders = [ - path.name - for path in _iter_python_files(SRC / "viewmodels") - if "QUndoStack" in path.read_text(encoding="utf-8") - ] - assert holders == ["project_vm.py"], f"QUndoStack holders: {holders}" - - -def test_architecture_no_environment_configuration() -> None: - """No env-var configuration anywhere in src/otko.""" - offenders = [ - str(path.relative_to(ROOT)) - for path in _iter_python_files(SRC) - if "os.environ" in path.read_text(encoding="utf-8") - or "os.getenv" in path.read_text(encoding="utf-8") - ] - assert offenders == [], f"environment configuration in: {offenders}" - - -def test_architecture_runner_splits_emit_and_run() -> None: - """Model emission and analysis live in separate modules.""" - emit_path = SRC / "services" / "_emit.py" - run_path = SRC / "services" / "_run.py" - assert emit_path.exists() - assert run_path.exists() - emit_source = emit_path.read_text(encoding="utf-8") - run_source = run_path.read_text(encoding="utf-8") - assert "class OpenSeesEmitter" in emit_source - assert "class OpenSeesAnalysisRunner" in run_source - assert "OpenSeesEmitter" in run_source diff --git a/tests/unit/test_ci_config.py b/tests/unit/test_ci_config.py deleted file mode 100644 index 063f9b3..0000000 --- a/tests/unit/test_ci_config.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Meta-gate checks for the CI workflow and pre-commit hook configuration.""" - -from __future__ import annotations - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -CI = ROOT / ".github" / "workflows" / "ci.yml" -PRECOMMIT = ROOT / ".pre-commit-config.yaml" - -HEADLESS_COMMAND = 'pytest tests/unit tests/services -m "not slow"' - - -def test_ci_declares_all_jobs() -> None: - """The workflow exposes the lint/type/test split as separate jobs.""" - text = CI.read_text(encoding="utf-8") - for job in ("lint:", "type:", "test-headless:", "test-gui:", "test-integration:"): - assert f"\n {job}\n" in text, f"missing CI job: {job}" - - -def test_ci_runs_the_headless_command() -> None: - """The headless job runs the unit + services, non-slow command verbatim.""" - assert HEADLESS_COMMAND in CI.read_text(encoding="utf-8") - - -def test_precommit_declares_expected_hooks() -> None: - """The pre-commit config wires up the lint/format/type/hygiene hooks.""" - text = PRECOMMIT.read_text(encoding="utf-8") - for hook in ( - "id: ruff", - "id: ruff-format", - "id: mypy", - "id: trailing-whitespace", - "id: end-of-file-fixer", - "id: check-yaml", - "id: check-added-large-files", - "id: check-merge-conflict", - ): - assert hook in text, f"missing pre-commit hook: {hook}" diff --git a/tests/unit/test_combinations.py b/tests/unit/test_combinations.py deleted file mode 100644 index 6a5133a..0000000 --- a/tests/unit/test_combinations.py +++ /dev/null @@ -1,180 +0,0 @@ -"""Case-result load combinations — core validation, project wiring, combine math.""" - -from __future__ import annotations - -import numpy as np -import pytest - -from otko.core import ( - LoadCombination, - LoadCombinationItem, - Project, - StaticCase, -) -from otko.services.combinations import ( - combine_static_results, - envelope_static_results, - evaluate_combination, -) -from otko.services.results import ModalResults, StaticResults - - -def _static( - case_id: int, disp_val: float, reac_val: float, elem_val: float, n_steps: int = 1 -) -> StaticResults: - return StaticResults( - case_id=case_id, - case_name=f"case-{case_id}", - n_steps=n_steps, - node_disp={1: np.full((n_steps, 2), disp_val)}, - node_reaction={1: np.full((n_steps, 2), reac_val)}, - element_forces={1: np.full((n_steps, 3), elem_val)}, - ) - - -class TestEntity: - def test_duplicate_case_rejected(self) -> None: - with pytest.raises(ValueError, match="duplicate"): - LoadCombination( - id=1, - name="bad", - items=[ - LoadCombinationItem(case_id=1, factor=1.0), - LoadCombinationItem(case_id=1, factor=2.0), - ], - ) - - def test_non_finite_factor_rejected(self) -> None: - with pytest.raises(ValueError, match="finite"): - LoadCombination(id=1, items=[LoadCombinationItem(case_id=1, factor=float("inf"))]) - - def test_empty_items_rejected(self) -> None: - with pytest.raises(ValueError): - LoadCombination(id=1, items=[]) - - def test_round_trip(self) -> None: - combo = LoadCombination( - id=3, - name="1.2D+1.6L", - kind="Linear", - items=[ - LoadCombinationItem(case_id=1, factor=1.2), - LoadCombinationItem(case_id=2, factor=1.6), - ], - ) - assert LoadCombination.model_validate(combo.model_dump()) == combo - assert LoadCombination.model_validate_json(combo.model_dump_json()) == combo - - -class TestProject: - def test_next_id_and_lookup(self) -> None: - proj = Project() - assert proj.next_combination_id() == 1 - proj.combinations.append(LoadCombination(id=1, items=[LoadCombinationItem(case_id=1)])) - assert proj.next_combination_id() == 2 - assert proj.combination(1).id == 1 - with pytest.raises(KeyError): - proj.combination(99) - - def test_duplicate_ids_rejected(self) -> None: - with pytest.raises(ValueError, match="Duplicate combination"): - Project( - combinations=[ - LoadCombination(id=1, items=[LoadCombinationItem(case_id=1)]), - LoadCombination(id=1, items=[LoadCombinationItem(case_id=2)]), - ] - ) - - def test_validate_references_missing_case(self) -> None: - proj = Project( - analyses=[StaticCase(id=1, pattern_ids=[1])], - combinations=[LoadCombination(id=1, items=[LoadCombinationItem(case_id=9)])], - ) - with pytest.raises(ValueError, match="missing analysis case 9"): - proj.validate_references() - - def test_persistence_round_trip(self, tmp_path) -> None: # type: ignore[no-untyped-def] - from otko.services import load_project, save_project - - proj = Project( - analyses=[StaticCase(id=1, pattern_ids=[1])], - combinations=[ - LoadCombination( - id=1, - name="combo", - items=[LoadCombinationItem(case_id=1, factor=1.2)], - ) - ], - ) - path = save_project(proj, tmp_path / "c.osmodel") - assert load_project(path).combinations[0].name == "combo" - - -class TestCombine: - def test_linear_weighted_sum(self) -> None: - out = combine_static_results( - [_static(1, 1.0, 10.0, 100.0), _static(2, 2.0, 20.0, 200.0)], - [1.2, 1.6], - case_id=7, - case_name="combo", - ) - assert out.case_id == 7 - assert out.node_disp[1][0, 0] == pytest.approx(1.2 * 1.0 + 1.6 * 2.0) - assert out.node_reaction[1][0, 1] == pytest.approx(1.2 * 10.0 + 1.6 * 20.0) - assert out.element_forces[1][0, 2] == pytest.approx(1.2 * 100.0 + 1.6 * 200.0) - - def test_step_mismatch_rejected(self) -> None: - with pytest.raises(ValueError, match="Step-count"): - combine_static_results( - [_static(1, 1.0, 1.0, 1.0, n_steps=1), _static(2, 1.0, 1.0, 1.0, n_steps=2)], - [1.0, 1.0], - case_id=1, - case_name="x", - ) - - def test_length_mismatch_rejected(self) -> None: - with pytest.raises(ValueError, match="length mismatch"): - combine_static_results([_static(1, 1, 1, 1)], [1.0, 2.0], case_id=1, case_name="x") - - -class TestEnvelope: - def test_max_min(self) -> None: - out = envelope_static_results( - [_static(1, 1.0, 10.0, 5.0), _static(2, -4.0, -20.0, 7.0)], - [1.0, 1.0], - case_id=3, - case_name="env", - ) - assert out.node_disp_max[1][0, 0] == pytest.approx(1.0) - assert out.node_disp_min[1][0, 0] == pytest.approx(-4.0) - assert out.node_reaction_max[1][0, 0] == pytest.approx(10.0) - assert out.node_reaction_min[1][0, 0] == pytest.approx(-20.0) - assert out.element_forces_max[1][0, 0] == pytest.approx(7.0) - assert out.element_forces_min[1][0, 0] == pytest.approx(5.0) - - -class TestEvaluate: - def test_linear_dispatch(self) -> None: - combo = LoadCombination( - id=5, - name="c", - kind="Linear", - items=[ - LoadCombinationItem(case_id=1, factor=2.0), - LoadCombinationItem(case_id=2, factor=3.0), - ], - ) - out = evaluate_combination({1: _static(1, 1, 1, 1), 2: _static(2, 1, 1, 1)}, combo) - assert isinstance(out, StaticResults) - assert out.node_disp[1][0, 0] == pytest.approx(5.0) - - def test_missing_results_keyerror(self) -> None: - combo = LoadCombination(id=1, items=[LoadCombinationItem(case_id=2)]) - with pytest.raises(KeyError, match="no results for case 2"): - evaluate_combination({1: _static(1, 1, 1, 1)}, combo) - - def test_non_static_typeerror(self) -> None: - combo = LoadCombination(id=1, items=[LoadCombinationItem(case_id=1)]) - modal = ModalResults(case_id=1, case_name="m", eigenvalues=np.array([1.0]), mode_shapes={}) - with pytest.raises(TypeError, match="not StaticResults"): - evaluate_combination({1: modal}, combo) diff --git a/tests/unit/test_diagram_renderer.py b/tests/unit/test_diagram_renderer.py index 425606b..a1cd1d1 100644 --- a/tests/unit/test_diagram_renderer.py +++ b/tests/unit/test_diagram_renderer.py @@ -15,21 +15,21 @@ import pytest # Force pyvista off-screen before any pyvista import in this module's chain. os.environ.setdefault("PYVISTA_OFF_SCREEN", "true") -import pyvista as pv +import pyvista as pv # noqa: E402 -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticBeamColumn, ElasticSection, Node, Project, ) -from otko.services.element_forces import ( +from otko.services.element_forces import ( # noqa: E402 DiagramData, ForceComponent, extract_diagram_data, ) -from otko.services.results import StaticResults -from otko.views.canvas3d.diagram_renderer import DiagramRenderer +from otko.services.results import StaticResults # noqa: E402 +from otko.views.canvas3d.diagram_renderer import DiagramRenderer # noqa: E402 pv.OFF_SCREEN = True @@ -59,9 +59,12 @@ def project_3d() -> Project: @pytest.fixture def static_results() -> StaticResults: - f10 = np.array([[100.0, 5.0, 0.0, 0.0, 0.0, 9.0, -100.0, -5.0, 0.0, 0.0, 0.0, -9.0]]) - f20 = np.array([[-50.0, 2.0, 0.0, 0.0, 0.0, 3.0, 50.0, -2.0, 0.0, 0.0, 0.0, -3.0]]) - return StaticResults(case_id=1, case_name="t", n_steps=1, element_forces={10: f10, 20: f20}) + f10 = np.array([[100.0, 5.0, 0.0, 0.0, 0.0, 9.0, + -100.0, -5.0, 0.0, 0.0, 0.0, -9.0]]) + f20 = np.array([[-50.0, 2.0, 0.0, 0.0, 0.0, 3.0, + 50.0, -2.0, 0.0, 0.0, 0.0, -3.0]]) + return StaticResults(case_id=1, case_name="t", n_steps=1, + element_forces={10: f10, 20: f20}) def test_render_axial_creates_actor(offscreen_plotter, project_3d, static_results) -> None: # type: ignore[no-untyped-def] @@ -90,17 +93,14 @@ def test_render_empty_data_does_not_create_actor(offscreen_plotter, project_3d) empty = DiagramData( component=ForceComponent.N, element_ids=np.empty(0, dtype=int), - values_i=np.empty(0), - values_j=np.empty(0), - abs_max=0.0, + values_i=np.empty(0), values_j=np.empty(0), abs_max=0.0, ) r.render(project_3d, empty, scale=1.0) assert r._actor is None def test_render_zero_magnitude_data_does_not_create_actor( - offscreen_plotter, - project_3d, + offscreen_plotter, project_3d, ) -> None: # type: ignore[no-untyped-def] """A diagram for a component that's identically zero shouldn't render an empty mesh + scalar bar — that's misleading visual noise.""" @@ -108,9 +108,7 @@ def test_render_zero_magnitude_data_does_not_create_actor( zero_for_two_elems = DiagramData( component=ForceComponent.T, element_ids=np.array([10, 20], dtype=int), - values_i=np.zeros(2), - values_j=np.zeros(2), - abs_max=0.0, + values_i=np.zeros(2), values_j=np.zeros(2), abs_max=0.0, ) r.render(project_3d, zero_for_two_elems, scale=1.0) assert r._actor is None diff --git a/tests/unit/test_display_name.py b/tests/unit/test_display_name.py new file mode 100644 index 0000000..bb10d09 --- /dev/null +++ b/tests/unit/test_display_name.py @@ -0,0 +1,93 @@ +"""Unit tests for the display-only ``display_name`` fallback on entities.""" + +from __future__ import annotations + +from otko.core import ( + BeamWithHingesElement, + CorotTrussElement, + DispBeamColumn, + ElasticBeamColumn, + ElementBase, + ForceBeamColumn, + Node, + QuadElement, + TrussElement, + ZeroLengthElement, + ZeroLengthSectionElement, +) +from otko.core._base import Entity + +_ALL_ELEMENT_TYPES = ( + TrussElement, + CorotTrussElement, + ElasticBeamColumn, + ForceBeamColumn, + DispBeamColumn, + ZeroLengthElement, + ZeroLengthSectionElement, + BeamWithHingesElement, + QuadElement, +) + + +class TestEntityBase: + def test_base_fallback_is_empty(self) -> None: + e = Entity(id=1) + assert e.default_name == "" + assert e.display_name == "" + + def test_display_name_is_not_persisted(self) -> None: + assert "display_name" not in Entity(id=1).model_dump() + assert "default_name" not in Entity(id=1).model_dump() + assert "display_name" not in Node(id=1, coords=(0, 0, 0)).model_dump() + + +class TestNodeDisplayName: + def test_blank_name_falls_back(self) -> None: + n = Node(id=1, coords=(0, 0, 0)) + assert n.display_name == "N1" + + def test_explicit_name_is_returned(self) -> None: + n = Node(id=1, name="Support A", coords=(0, 0, 0)) + assert n.display_name == "Support A" + + def test_whitespace_only_name_falls_back(self) -> None: + n = Node(id=2, name=" ", coords=(0, 0, 0)) + assert n.display_name == "N2" + + def test_explicit_name_is_stripped(self) -> None: + n = Node(id=1, name=" A1 ", coords=(0, 0, 0)) + assert n.display_name == "A1" + + def test_display_only_never_writes_back(self) -> None: + n = Node(id=1, coords=(0, 0, 0)) + assert n.display_name == "N1" + assert n.name == "" + + +class TestElementDisplayName: + def test_all_element_types_share_element_base(self) -> None: + for cls in _ALL_ELEMENT_TYPES: + assert issubclass(cls, ElementBase) + + def test_blank_truss_name_falls_back(self) -> None: + t = TrussElement(id=1, nodes=(1, 2), area=0.01, material_id=1) + assert t.display_name == "E1" + + def test_explicit_truss_name_is_returned(self) -> None: + t = TrussElement(id=1, name="Chord", nodes=(1, 2), area=0.01, material_id=1) + assert t.display_name == "Chord" + + def test_whitespace_only_truss_name_falls_back(self) -> None: + t = TrussElement(id=3, name=" ", nodes=(1, 2), area=0.01, material_id=1) + assert t.display_name == "E3" + + def test_blank_quad_name_falls_back(self) -> None: + q = QuadElement(id=1, nodes=(1, 2, 3, 4), thickness=0.1, material_id=1) + assert q.display_name == "E1" + + def test_display_only_never_writes_back(self) -> None: + t = TrussElement(id=1, nodes=(1, 2), area=0.01, material_id=1) + assert t.display_name == "E1" + assert t.name == "" + assert "display_name" not in t.model_dump() diff --git a/tests/unit/test_docs.py b/tests/unit/test_docs.py deleted file mode 100644 index f8c78e0..0000000 --- a/tests/unit/test_docs.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Documentation gates — assert the user-facing docs keep their key content. - -Pure pathlib; no Qt or solver imports so this stays part of the fast unit -suite. -""" - -from __future__ import annotations - -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[2] -DOCS_DIR = REPO_ROOT / "docs" - - -def _text(path: Path) -> str: - """Return a file's UTF-8 text with normalized line endings.""" - return path.read_text(encoding="utf-8").replace("\r\n", "\n") - - -def test_quick_guide_exists_and_covers_core_topics() -> None: - quick_guide = DOCS_DIR / "QUICK_GUIDE.md" - assert quick_guide.is_file(), "docs/QUICK_GUIDE.md is missing" - - lowered = _text(quick_guide).lower() - for topic in ("cantilever", "report", "modal", "units", "undo"): - assert topic in lowered, f"QUICK_GUIDE.md is missing {topic!r}" - - -def test_contributing_describes_this_repo() -> None: - text = _text(REPO_ROOT / "CONTRIBUTING.md") - - for required in ( - "views", - "viewmodels", - "services", - "core", - "commands", - "Conventional Commits", - "feat:", - "main", - "pre-commit install", - 'pip install -e ".[gui,dev]"', - "python -m otko", - 'pytest -m "not slow"', - ): - assert required in text, f"CONTRIBUTING.md is missing {required!r}" - - -def test_readme_links_quick_guide() -> None: - readme = _text(REPO_ROOT / "README.md") - assert "docs/QUICK_GUIDE.md" in readme diff --git a/tests/services/test_element_forces.py b/tests/unit/test_element_forces.py similarity index 90% rename from tests/services/test_element_forces.py rename to tests/unit/test_element_forces.py index 2f57387..3fc8dd8 100644 --- a/tests/services/test_element_forces.py +++ b/tests/unit/test_element_forces.py @@ -19,11 +19,11 @@ from otko.services.element_forces import ( ) from otko.services.results import StaticResults + # ────────────────────────────────────────────────────────────────────── # Fixtures: a tiny 3D project with 2 elements + canned force results. # ────────────────────────────────────────────────────────────────────── - @pytest.fixture def project_3d() -> Project: return Project( @@ -44,12 +44,12 @@ def project_3d() -> Project: def static_3d_results() -> StaticResults: """One step of analysis. Element 10 carries [N=100, …]; element 20 carries [N=-50, …] (compression). 12 components, single time step.""" - forces_10 = np.array([[100.0, 5.0, 7.0, 1.0, 8.0, 9.0, -100.0, -5.0, -7.0, -1.0, -8.0, -9.0]]) - forces_20 = np.array([[-50.0, 0.0, 0.0, 0.0, 0.0, 0.0, 50.0, 0.0, 0.0, 0.0, 0.0, 0.0]]) + forces_10 = np.array([[100.0, 5.0, 7.0, 1.0, 8.0, 9.0, + -100.0, -5.0, -7.0, -1.0, -8.0, -9.0]]) + forces_20 = np.array([[-50.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 50.0, 0.0, 0.0, 0.0, 0.0, 0.0]]) return StaticResults( - case_id=1, - case_name="tst", - n_steps=1, + case_id=1, case_name="tst", n_steps=1, element_forces={10: forces_10, 20: forces_20}, ) @@ -58,10 +58,8 @@ def static_3d_results() -> StaticResults: # extract_diagram_data # ────────────────────────────────────────────────────────────────────── - def test_extract_axial_yields_correct_per_end_values( - project_3d: Project, - static_3d_results: StaticResults, + project_3d: Project, static_3d_results: StaticResults, ) -> None: data = extract_diagram_data(project_3d, static_3d_results, ForceComponent.N) assert data.component is ForceComponent.N @@ -88,20 +86,22 @@ def test_extract_handles_2d_force_vectors(project_3d: Project) -> None: """2D OpenSees beams return 6 components: [N, Vy, Mz] × 2 ends. Asking for V3 / M2 in this case should yield no rows for that element.""" forces_10 = np.array([[100.0, 5.0, 9.0, -100.0, -5.0, -9.0]]) - results = StaticResults(case_id=1, case_name="2d", n_steps=1, element_forces={10: forces_10}) + results = StaticResults(case_id=1, case_name="2d", n_steps=1, + element_forces={10: forces_10}) n_data = extract_diagram_data(project_3d, results, ForceComponent.N) np.testing.assert_array_equal(n_data.values_i, [100.0]) v3_data = extract_diagram_data(project_3d, results, ForceComponent.V3) - assert v3_data.element_ids.size == 0 # V3 not available in 2D output + assert v3_data.element_ids.size == 0 # V3 not available in 2D output def test_extract_uses_specified_step(project_3d: Project) -> None: """Multi-step pushover: pick the first step explicitly.""" f = np.zeros((3, 12)) - f[0, 0] = 10.0 # step 0 axial @ node i + f[0, 0] = 10.0 # step 0 axial @ node i f[1, 0] = 20.0 f[2, 0] = 30.0 - results = StaticResults(case_id=1, case_name="push", n_steps=3, element_forces={10: f}) + results = StaticResults(case_id=1, case_name="push", n_steps=3, + element_forces={10: f}) data = extract_diagram_data(project_3d, results, ForceComponent.N, step=0) np.testing.assert_array_equal(data.values_i, [10.0]) data = extract_diagram_data(project_3d, results, ForceComponent.N, step=2) @@ -112,10 +112,8 @@ def test_extract_uses_specified_step(project_3d: Project) -> None: # auto_scale # ────────────────────────────────────────────────────────────────────── - def test_auto_scale_scales_to_target_fraction( - project_3d: Project, - static_3d_results: StaticResults, + project_3d: Project, static_3d_results: StaticResults, ) -> None: data = extract_diagram_data(project_3d, static_3d_results, ForceComponent.N) # Bounding-box diagonal of the 3-node line is 6.0 along X. @@ -129,9 +127,7 @@ def test_auto_scale_zero_force_returns_unity(project_3d: Project) -> None: empty = DiagramData( component=ForceComponent.N, element_ids=np.empty(0, dtype=int), - values_i=np.empty(0), - values_j=np.empty(0), - abs_max=0.0, + values_i=np.empty(0), values_j=np.empty(0), abs_max=0.0, ) assert auto_scale(project_3d, empty) == 1.0 @@ -142,10 +138,8 @@ def test_auto_scale_zero_force_returns_unity(project_3d: Project) -> None: @pytest.fixture def truss_project_2d() -> Project: from otko.core import ElasticUniaxial, TrussElement - return Project( - ndm=2, - ndf=2, + ndm=2, ndf=2, nodes=[ Node(id=1, coords=(0.0, 0.0, 0.0)), Node(id=2, coords=(3.0, 0.0, 0.0)), @@ -169,9 +163,7 @@ def test_truss_axial_uses_correct_index_map(truss_project_2d: Project) -> None: # (equilibrium signs: end-i points out, end-j points in). forces = np.array([[-1500.0, 0.0, 1500.0, 0.0]]) results = StaticResults( - case_id=1, - case_name="tst", - n_steps=1, + case_id=1, case_name="tst", n_steps=1, element_forces={1: forces}, ) data = extract_diagram_data(truss_project_2d, results, ForceComponent.N) @@ -186,9 +178,7 @@ def test_truss_has_no_shear_or_moment_components(truss_project_2d: Project) -> N """Requesting V2 / V3 / M3 on a truss returns an empty diagram.""" forces = np.array([[100.0, 0.0, -100.0, 0.0]]) results = StaticResults( - case_id=1, - case_name="tst", - n_steps=1, + case_id=1, case_name="tst", n_steps=1, element_forces={1: forces}, ) for comp in (ForceComponent.V2, ForceComponent.V3, ForceComponent.M3): diff --git a/tests/unit/test_elements.py b/tests/unit/test_elements.py index e3c971a..11987c6 100644 --- a/tests/unit/test_elements.py +++ b/tests/unit/test_elements.py @@ -6,8 +6,8 @@ import pytest from pydantic import TypeAdapter, ValidationError from otko.core import ( - ElasticBeamColumn, Element, + ElasticBeamColumn, ForceBeamColumn, TrussElement, ZeroLengthElement, diff --git a/tests/services/test_export.py b/tests/unit/test_export.py similarity index 98% rename from tests/services/test_export.py rename to tests/unit/test_export.py index 98c23ec..ae64063 100644 --- a/tests/services/test_export.py +++ b/tests/unit/test_export.py @@ -109,14 +109,14 @@ def _path_series_project() -> Project: class TestHeader: def test_opspy_header_pins_solver_version_and_units(self) -> None: script = export_opspy(_truss_2d()) - assert PINNED_OPENSEESPY == "openseespy==3.8.0.0" - assert "openseespy==3.8.0.0" in script + assert PINNED_OPENSEESPY == "openseespy==3.5.1.12" + assert "openseespy==3.5.1.12" in script assert __version__ in script assert _truss_2d().meta.units.value in script def test_tcl_header_pins_solver_version_and_units(self) -> None: script = export_tcl(_truss_2d()) - assert "openseespy==3.8.0.0" in script + assert "openseespy==3.5.1.12" in script assert __version__ in script diff --git a/tests/unit/test_formatting.py b/tests/unit/test_formatting.py deleted file mode 100644 index bc1c6db..0000000 --- a/tests/unit/test_formatting.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Unit tests for :mod:`otko.views.formatting` (no Qt required).""" - -from __future__ import annotations - -import pytest - -from otko.core import UnitSystem -from otko.views.formatting import ( - DOF_LABELS, - MASS_LABELS, - RESTRAINT_LABELS, - format_number, - parse_float, - parse_int, - unit_label, -) - - -def test_label_tuples() -> None: - assert DOF_LABELS == ("Ux", "Uy", "Uz", "Rx", "Ry", "Rz") - assert MASS_LABELS == ("Mx", "My", "Mz", "Ixx", "Iyy", "Izz") - assert RESTRAINT_LABELS == ("Ux", "Uy", "Uz", "Rx", "Ry", "Rz") - - -def test_format_number() -> None: - assert format_number(50.0) == "50" - assert format_number(1.5) == "1.5" - - -def test_parse_float_required() -> None: - with pytest.raises(ValueError): - parse_float("", "x") - - -def test_parse_float_allow_empty() -> None: - assert parse_float("", "x", allow_empty=True) == 0.0 - - -def test_parse_int_rejects_fractional() -> None: - with pytest.raises(ValueError): - parse_int("1.5", "x") - - -def test_unit_label() -> None: - assert unit_label("force", UnitSystem.SI_M_N) == "N" - assert unit_label("moment", UnitSystem.US_FT_KIP) == "kip·ft" - - -def test_unit_label_unknown_quantity() -> None: - with pytest.raises(ValueError): - unit_label("temperature", UnitSystem.SI_M_N) diff --git a/tests/unit/test_grid_system.py b/tests/unit/test_grid_system.py index 6688dce..5461d9b 100644 --- a/tests/unit/test_grid_system.py +++ b/tests/unit/test_grid_system.py @@ -2,6 +2,7 @@ from __future__ import annotations +import math from pathlib import Path import pytest @@ -85,7 +86,6 @@ def test_dialog_parse_spacings_formats() -> None: _coords_from_spacings, _parse_spacings, ) - # Blank → no lines. assert _parse_spacings("") == [] # Single integer → N-1 unit spacings. @@ -102,13 +102,12 @@ def test_dialog_parse_spacings_formats() -> None: def test_add_node_dialog_snaps(qtbot) -> None: # type: ignore[no-untyped-def] """AddNodeDialog snaps to nearest grid line when the flag is set.""" from otko.views.dialogs.add_node import AddNodeDialog - grid = GridSystem(x_lines=[0.0, 3.0, 6.0], y_lines=[0.0, 4.0], z_lines=[0.0]) dlg = AddNodeDialog(next_node_id=1, grid=grid, ndm=3) qtbot.addWidget(dlg) - dlg._x.setValue(3.4) # → should snap to 3.0 - dlg._y.setValue(3.9) # → should snap to 4.0 - dlg._z.setValue(-0.3) # → should snap to 0.0 + dlg._x.setValue(3.4) # → should snap to 3.0 + dlg._y.setValue(3.9) # → should snap to 4.0 + dlg._z.setValue(-0.3) # → should snap to 0.0 dlg._snap_cb.setChecked(True) node = dlg.node() assert node.coords == (3.0, 4.0, 0.0) @@ -117,13 +116,10 @@ def test_add_node_dialog_snaps(qtbot) -> None: # type: ignore[no-untyped-def] def test_add_node_dialog_no_snap(qtbot) -> None: # type: ignore[no-untyped-def] """Without the snap flag, AddNodeDialog preserves entered coordinates.""" from otko.views.dialogs.add_node import AddNodeDialog - grid = GridSystem(x_lines=[0.0, 3.0], y_lines=[0.0]) dlg = AddNodeDialog(next_node_id=1, grid=grid, ndm=3) qtbot.addWidget(dlg) - dlg._x.setValue(1.7) - dlg._y.setValue(0.2) - dlg._z.setValue(5.5) + dlg._x.setValue(1.7); dlg._y.setValue(0.2); dlg._z.setValue(5.5) dlg._snap_cb.setChecked(False) node = dlg.node() assert node.coords == pytest.approx((1.7, 0.2, 5.5)) @@ -173,7 +169,7 @@ def test_coord_system_round_trip_world_local() -> None: p_local = (1.5, -0.5, 2.0) p_world = cs.local_to_world(p_local) p_back = cs.world_to_local(p_world) - for a, b in zip(p_local, p_back, strict=False): + for a, b in zip(p_local, p_back): assert a == pytest.approx(b, abs=1e-9) @@ -209,8 +205,7 @@ def test_multiple_coord_systems_round_trip(tmp_path: Path) -> None: CoordinateGridSystem( name="Floor2", coord=CoordinateSystem( - origin=(0, 0, 3.5), - rotation_deg=(0, 0, 30), + origin=(0, 0, 3.5), rotation_deg=(0, 0, 30), ), grid=GridSystem(x_lines=[0.0, 6.0], y_lines=[0.0, 4.0]), ), diff --git a/tests/services/test_imposed_support_motion.py b/tests/unit/test_imposed_support_motion.py similarity index 100% rename from tests/services/test_imposed_support_motion.py rename to tests/unit/test_imposed_support_motion.py diff --git a/tests/unit/test_materials.py b/tests/unit/test_materials.py index 43bde52..36086a5 100644 --- a/tests/unit/test_materials.py +++ b/tests/unit/test_materials.py @@ -37,14 +37,8 @@ class TestSteel01: class TestConcrete02: def test_lambda_alias(self) -> None: c = Concrete02( - id=1, - fpc=-30e6, - epsc0=-0.002, - fpcu=-15e6, - epsU=-0.005, - ft=3e6, - Ets=2e9, - **{"lambda": 0.1}, + id=1, fpc=-30e6, epsc0=-0.002, fpcu=-15e6, epsU=-0.005, + ft=3e6, Ets=2e9, **{"lambda": 0.1}, ) assert c.lambda_ == 0.1 # Round-trip should preserve the alias. @@ -55,14 +49,8 @@ class TestConcrete02: def test_negative_signs_enforced(self) -> None: with pytest.raises(ValidationError): Concrete02( - id=1, - fpc=30e6, - epsc0=-0.002, - fpcu=-15e6, - epsU=-0.005, - ft=3e6, - Ets=2e9, - **{"lambda": 0.1}, + id=1, fpc=30e6, epsc0=-0.002, fpcu=-15e6, epsU=-0.005, + ft=3e6, Ets=2e9, **{"lambda": 0.1}, ) @@ -78,8 +66,7 @@ class TestHystereticSM: # The wire-rope axial backbone: 7 positive points (to ~69 kN), an # independent, softer negative (compression) envelope. m = HystereticSM( - id=1, - name="iso-axial", + id=1, name="iso-axial", pos_env=[(1.57, 0.00207), (3.0, 0.00436), (69.1, 0.0399)], neg_env=[(-1.4, -0.00057), (-15.31, -0.0483)], ) diff --git a/tests/unit/test_meta_gates.py b/tests/unit/test_meta_gates.py deleted file mode 100644 index 67ee070..0000000 --- a/tests/unit/test_meta_gates.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Meta-gates tying the test markers to the CI commands that consume them.""" - -from __future__ import annotations - -from pathlib import Path - -try: - import tomllib -except ModuleNotFoundError: # Python < 3.11 - import tomli as tomllib - -ROOT = Path(__file__).resolve().parents[2] -PYPROJECT = ROOT / "pyproject.toml" -CI = ROOT / ".github" / "workflows" / "ci.yml" - -HEADLESS_COMMAND = 'pytest tests/unit tests/services -m "not slow"' - - -def test_slow_marker_is_declared() -> None: - """``[tool.pytest.ini_options]`` registers the ``slow`` marker.""" - with PYPROJECT.open("rb") as handle: - config = tomllib.load(handle) - markers = config["tool"]["pytest"]["ini_options"]["markers"] - assert any(marker.startswith("slow:") for marker in markers), markers - - -def test_ci_runs_the_headless_command() -> None: - """The headless CI command matches the marker the suite expects.""" - assert HEADLESS_COMMAND in CI.read_text(encoding="utf-8") diff --git a/tests/unit/test_node.py b/tests/unit/test_node.py index a2e9c74..fbfbe22 100644 --- a/tests/unit/test_node.py +++ b/tests/unit/test_node.py @@ -19,9 +19,8 @@ class TestNodeConstruction: assert not n.is_restrained def test_with_name_and_mass(self) -> None: - n = Node( - id=42, name="A1", coords=(1.0, 2.0, 3.0), mass=(100.0, 100.0, 100.0, 0.0, 0.0, 0.0) - ) + n = Node(id=42, name="A1", coords=(1.0, 2.0, 3.0), + mass=(100.0, 100.0, 100.0, 0.0, 0.0, 0.0)) assert n.name == "A1" assert n.mass[0] == 100.0 diff --git a/tests/unit/test_packaging.py b/tests/unit/test_packaging.py deleted file mode 100644 index 7b16ea4..0000000 --- a/tests/unit/test_packaging.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Packaging invariants — version/solver single-sourcing and dependency hygiene. - -Pure static checks over the repository files. No imports of :mod:`otko`, -Qt or the solver stack, so this test runs anywhere (including Windows 3.12). -""" - -from __future__ import annotations - -import re -from pathlib import Path - -try: # Python 3.11+ - import tomllib -except ModuleNotFoundError: # pragma: no cover - Python 3.10 backport - import tomli as tomllib - -REPO_ROOT = Path(__file__).resolve().parents[2] -SRC_ROOT = REPO_ROOT / "src" / "otko" -PYPROJECT = REPO_ROOT / "pyproject.toml" - -_VERSION_RE = re.compile(r"^__version__\s*=", re.MULTILINE) - - -def _pyproject() -> dict: - with PYPROJECT.open("rb") as handle: - return tomllib.load(handle) - - -def _src_files() -> list[Path]: - return sorted(SRC_ROOT.rglob("*.py")) - - -def _dep_name(requirement: str) -> str: - """Return the lowercased distribution name from a PEP 508 requirement.""" - match = re.match(r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)", requirement) - return match.group(1).lower().replace("_", "-") if match else "" - - -def test_console_script_entrypoint() -> None: - assert _pyproject()["project"]["scripts"]["otko"] == "otko.__main__:main" - - -def test_project_version_is_dynamic() -> None: - project = _pyproject()["project"] - assert "version" not in project - assert project["dynamic"] == ["version"] - - -def test_single_version_assignment() -> None: - hits = [path for path in _src_files() if _VERSION_RE.search(path.read_text(encoding="utf-8"))] - assert len(hits) == 1, f"expected exactly one __version__ assignment, got {hits}" - assert hits[0] == SRC_ROOT / "_const.py" - - -def test_solver_version_literal_only_in_const() -> None: - hits = [path for path in _src_files() if "3.8.0.0" in path.read_text(encoding="utf-8")] - assert hits == [SRC_ROOT / "_const.py"], f"unexpected 3.8.0.0 occurrences: {hits}" - - -def test_ruff_line_length_and_ignore() -> None: - ruff = _pyproject()["tool"]["ruff"] - assert ruff["line-length"] == 100 - assert "E501" in ruff["lint"]["ignore"] - - -def test_no_phantom_scipy_pandas_dependencies() -> None: - project = _pyproject()["project"] - groups = [project.get("dependencies", [])] - groups.extend(project.get("optional-dependencies", {}).values()) - declared = {_dep_name(req) for group in groups for req in group} - assert "scipy" not in declared - assert "pandas" not in declared diff --git a/tests/services/test_peer_record.py b/tests/unit/test_peer_record.py similarity index 100% rename from tests/services/test_peer_record.py rename to tests/unit/test_peer_record.py diff --git a/tests/services/test_persistence.py b/tests/unit/test_persistence.py similarity index 90% rename from tests/services/test_persistence.py rename to tests/unit/test_persistence.py index 93319ff..07a4ca6 100644 --- a/tests/services/test_persistence.py +++ b/tests/unit/test_persistence.py @@ -26,9 +26,8 @@ from otko.services import PROJECT_FILE_SUFFIX, load_project, save_project def _sample_project() -> Project: return Project( - meta=ProjectMeta(name="Sample", author="Ozan", units=UnitSystem.SI_M_N), - ndm=3, - ndf=6, + meta=ProjectMeta(name="Sample", author="Ozan", units=UnitSystem.METRIC), + ndm=3, ndf=6, nodes=[ Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), Node(id=2, coords=(0, 0, 3.0)), @@ -36,14 +35,8 @@ def _sample_project() -> Project: materials=[ Steel01(id=1, Fy=420e6, E0=200e9, b=0.01), Concrete02( - id=2, - fpc=-30e6, - epsc0=-0.002, - fpcu=-15e6, - epsU=-0.005, - ft=3e6, - Ets=2e9, - **{"lambda": 0.1}, + id=2, fpc=-30e6, epsc0=-0.002, fpcu=-15e6, epsU=-0.005, + ft=3e6, Ets=2e9, **{"lambda": 0.1}, ), ], sections=[ElasticSection(id=1, E=200e9, A=0.01, Iz=8.33e-6, Iy=8.33e-6, G=80e9, J=1e-6)], @@ -54,8 +47,7 @@ def _sample_project() -> Project: time_series=[LinearTimeSeries(id=1, factor=1.0)], load_patterns=[ PlainLoadPattern( - id=1, - time_series_id=1, + id=1, time_series_id=1, nodal_loads=[NodalLoad(node_id=2, forces=(0, 0, -10e3, 0, 0, 0))], ) ], diff --git a/tests/unit/test_phase8a.py b/tests/unit/test_phase8a.py index c9a3842..033ef00 100644 --- a/tests/unit/test_phase8a.py +++ b/tests/unit/test_phase8a.py @@ -8,6 +8,7 @@ from __future__ import annotations +import numpy as np import pytest from otko.commands.nodes import SetMassCommand @@ -66,8 +67,7 @@ def test_uniform_element_load_schema() -> None: def test_plain_load_pattern_accepts_element_loads() -> None: pat = PlainLoadPattern( - id=1, - time_series_id=1, + id=1, time_series_id=1, nodal_loads=[NodalLoad(node_id=1, forces=(0, 0, 0, 0, 0, 0))], element_loads=[UniformElementLoad(element_id=10, wy=-500.0)], ) @@ -77,13 +77,10 @@ def test_plain_load_pattern_accepts_element_loads() -> None: def test_project_with_element_loads_round_trips(tiny_project: Project, tmp_path) -> None: # type: ignore[no-untyped-def] tiny_project.time_series.append(LinearTimeSeries(id=1, name="Ramp")) - tiny_project.load_patterns.append( - PlainLoadPattern( - id=1, - time_series_id=1, - element_loads=[UniformElementLoad(element_id=10, wy=-1500.0, wz=10.0)], - ) - ) + tiny_project.load_patterns.append(PlainLoadPattern( + id=1, time_series_id=1, + element_loads=[UniformElementLoad(element_id=10, wy=-1500.0, wz=10.0)], + )) path = tmp_path / "p.osmodel" save_project(tiny_project, path) @@ -124,7 +121,7 @@ def test_set_mass_command_leaves_unselected_alone(tiny_project: Project) -> None cmd.redo() assert tiny_project.nodes[0].mass[0] == 99.0 - assert tiny_project.nodes[1].mass[0] == 5.0 # unchanged + assert tiny_project.nodes[1].mass[0] == 5.0 # unchanged cmd.undo() assert tiny_project.nodes[0].mass[0] == 0.0 @@ -139,21 +136,16 @@ def test_transient_case_rayleigh_defaults_to_zero() -> None: def test_transient_case_rayleigh_round_trip(tmp_path) -> None: # type: ignore[no-untyped-def] p = Project( - nodes=[Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), Node(id=2, coords=(1, 0, 0))], + nodes=[Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), + Node(id=2, coords=(1, 0, 0))], sections=[ElasticSection(id=1, E=2e11, A=0.01, Iz=1e-5, Iy=1e-5)], elements=[ElasticBeamColumn(id=10, nodes=(1, 2), section_id=1)], time_series=[LinearTimeSeries(id=1, name="Ramp")], load_patterns=[PlainLoadPattern(id=1, time_series_id=1)], - analyses=[ - TransientCase( - id=1, - pattern_ids=[1], - dt=0.01, - n_steps=50, - rayleigh_alpha_m=0.5, - rayleigh_beta_k=1.5e-4, - ) - ], + analyses=[TransientCase( + id=1, pattern_ids=[1], dt=0.01, n_steps=50, + rayleigh_alpha_m=0.5, rayleigh_beta_k=1.5e-4, + )], ) path = tmp_path / "r.osmodel" save_project(p, path) @@ -173,20 +165,16 @@ def test_transient_case_rayleigh_init_comm_defaults() -> None: def test_transient_case_rayleigh_init_round_trip(tmp_path) -> None: # type: ignore[no-untyped-def] p = Project( - nodes=[Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), Node(id=2, coords=(1, 0, 0))], + nodes=[Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), + Node(id=2, coords=(1, 0, 0))], sections=[ElasticSection(id=1, E=2e11, A=0.01, Iz=1e-5, Iy=1e-5)], elements=[ElasticBeamColumn(id=10, nodes=(1, 2), section_id=1)], time_series=[LinearTimeSeries(id=1, name="Ramp")], load_patterns=[PlainLoadPattern(id=1, time_series_id=1)], - analyses=[ - TransientCase( - id=1, - pattern_ids=[1], - dt=0.01, - n_steps=50, - rayleigh_beta_k_init=0.01309796, - ) - ], + analyses=[TransientCase( + id=1, pattern_ids=[1], dt=0.01, n_steps=50, + rayleigh_beta_k_init=0.01309796, + )], ) path = tmp_path / "rk.osmodel" save_project(p, path) @@ -199,13 +187,14 @@ def test_zero_length_do_rayleigh_default_and_round_trip(tmp_path) -> None: # ty from otko.core import ElasticUniaxial, ZeroLengthElement p = Project( - ndm=3, - ndf=6, - nodes=[Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), Node(id=2, coords=(0, 0, 0))], + ndm=3, ndf=6, + nodes=[Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), + Node(id=2, coords=(0, 0, 0))], materials=[ElasticUniaxial(id=1, E=1000.0)], elements=[ ZeroLengthElement(id=1, nodes=(1, 2), material_ids=(1,), dofs=(1,)), - ZeroLengthElement(id=2, nodes=(1, 2), material_ids=(1,), dofs=(1,), do_rayleigh=True), + ZeroLengthElement(id=2, nodes=(1, 2), material_ids=(1,), dofs=(1,), + do_rayleigh=True), ], ) assert p.elements[0].do_rayleigh is False # default off (unchanged emission) @@ -228,7 +217,6 @@ class _RecordingOps: def rec(*args, **kwargs): # type: ignore[no-untyped-def] self.calls.append((name, args, kwargs)) return None - return rec @@ -236,10 +224,8 @@ def _transient_setup_rayleigh_calls(case: TransientCase) -> list[tuple]: from otko.services import OpenSeesRunner p = Project( - nodes=[ - Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), - Node(id=2, coords=(1, 0, 0), mass=(1.0,) * 3 + (0.0,) * 3), - ], + nodes=[Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), + Node(id=2, coords=(1, 0, 0), mass=(1.0,) * 3 + (0.0,) * 3)], sections=[ElasticSection(id=1, E=2e11, A=0.01, Iz=1e-5, Iy=1e-5)], elements=[ElasticBeamColumn(id=10, nodes=(1, 2), section_id=1)], time_series=[LinearTimeSeries(id=1, name="Ramp")], @@ -256,10 +242,7 @@ def test_kinit_rayleigh_issues_exactly_one_call_in_beta_kinit_slot() -> None: """A Kinit (initial-stiffness) βK is emitted ONCE as ``rayleigh 0 0 βKinit 0`` — slot 3 — the single-command requirement (a second call would replace it).""" case = TransientCase( - id=1, - pattern_ids=[1], - dt=0.01, - n_steps=10, + id=1, pattern_ids=[1], dt=0.01, n_steps=10, rayleigh_beta_k_init=0.01309796, ) rcalls = _transient_setup_rayleigh_calls(case) @@ -270,11 +253,7 @@ def test_kinit_rayleigh_issues_exactly_one_call_in_beta_kinit_slot() -> None: def test_current_k_rayleigh_unchanged_slot2() -> None: """The classical current-K βK still emits in slot 2 (backward compatible).""" case = TransientCase( - id=1, - pattern_ids=[1], - dt=0.01, - n_steps=10, - rayleigh_beta_k=0.002, + id=1, pattern_ids=[1], dt=0.01, n_steps=10, rayleigh_beta_k=0.002, ) rcalls = _transient_setup_rayleigh_calls(case) assert len(rcalls) == 1 @@ -294,13 +273,14 @@ def test_zero_length_do_rayleigh_emits_flag() -> None: from otko.services import OpenSeesRunner p = Project( - ndm=3, - ndf=6, - nodes=[Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), Node(id=2, coords=(0, 0, 0))], + ndm=3, ndf=6, + nodes=[Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), + Node(id=2, coords=(0, 0, 0))], materials=[ElasticUniaxial(id=1, E=1000.0)], elements=[ ZeroLengthElement(id=1, nodes=(1, 2), material_ids=(1,), dofs=(1,)), - ZeroLengthElement(id=2, nodes=(1, 2), material_ids=(1,), dofs=(1,), do_rayleigh=True), + ZeroLengthElement(id=2, nodes=(1, 2), material_ids=(1,), dofs=(1,), + do_rayleigh=True), ], ) rec = _RecordingOps() diff --git a/tests/unit/test_phase8b.py b/tests/unit/test_phase8b.py index 41d50fb..b6e4383 100644 --- a/tests/unit/test_phase8b.py +++ b/tests/unit/test_phase8b.py @@ -29,61 +29,33 @@ from otko.services import load_project, save_project # ── HystereticMaterial ─────────────────────────────────────────────── def test_hysteretic_material_schema() -> None: mat = HystereticMaterial( - id=1, - name="HingeMat", - s1p=100.0, - e1p=0.001, - s2p=200.0, - e2p=0.01, - s3p=210.0, - e3p=0.05, - s1n=-100.0, - e1n=-0.001, - s2n=-200.0, - e2n=-0.01, - s3n=-210.0, - e3n=-0.05, + id=1, name="HingeMat", + s1p=100.0, e1p=0.001, s2p=200.0, e2p=0.01, s3p=210.0, e3p=0.05, + s1n=-100.0, e1n=-0.001, s2n=-200.0, e2n=-0.01, s3n=-210.0, e3n=-0.05, ) assert mat.type == "Hysteretic" assert mat.s1p == 100.0 - assert mat.px == 1.0 # default pinching factor - assert mat.beta == 0.0 # no degradation + assert mat.px == 1.0 # default pinching factor + assert mat.beta == 0.0 # no degradation def test_hysteretic_positive_envelope_must_be_positive() -> None: with pytest.raises(ValueError): HystereticMaterial( - id=1, - s1p=-1.0, - e1p=0.001, - s2p=200.0, - e2p=0.01, - s3p=210.0, - e3p=0.05, - s1n=-100.0, - e1n=-0.001, - s2n=-200.0, - e2n=-0.01, - s3n=-210.0, - e3n=-0.05, + id=1, s1p=-1.0, e1p=0.001, s2p=200.0, e2p=0.01, + s3p=210.0, e3p=0.05, + s1n=-100.0, e1n=-0.001, s2n=-200.0, e2n=-0.01, + s3n=-210.0, e3n=-0.05, ) # ── BeamWithHingesElement ──────────────────────────────────────────── def test_beam_with_hinges_3d_schema() -> None: el = BeamWithHingesElement( - id=1, - nodes=(1, 2), - section_i_id=10, - section_j_id=10, - lp_i=0.1, - lp_j=0.1, - E=2e11, - A=0.01, - Iz=1e-5, - Iy=1e-5, - G=8e10, - J=1e-6, + id=1, nodes=(1, 2), + section_i_id=10, section_j_id=10, + lp_i=0.1, lp_j=0.1, + E=2e11, A=0.01, Iz=1e-5, Iy=1e-5, G=8e10, J=1e-6, ) assert el.type == "BeamWithHinges" assert el.lp_i == 0.1 @@ -92,15 +64,10 @@ def test_beam_with_hinges_3d_schema() -> None: def test_beam_with_hinges_2d_schema_has_optional_3d_fields() -> None: el = BeamWithHingesElement( - id=1, - nodes=(1, 2), - section_i_id=10, - section_j_id=10, - lp_i=0.1, - lp_j=0.1, - E=2e11, - A=0.01, - Iz=1e-5, + id=1, nodes=(1, 2), + section_i_id=10, section_j_id=10, + lp_i=0.1, lp_j=0.1, + E=2e11, A=0.01, Iz=1e-5, ) assert el.Iy is None assert el.G is None @@ -110,72 +77,55 @@ def test_beam_with_hinges_2d_schema_has_optional_3d_fields() -> None: # ── PushoverCase ───────────────────────────────────────────────────── def test_pushover_case_schema() -> None: c = PushoverCase( - id=1, - pattern_ids=[1], - control_node=2, - control_dof=1, - target_disp=0.1, - step_size=0.001, + id=1, pattern_ids=[1], + control_node=2, control_dof=1, + target_disp=0.1, step_size=0.001, ) assert c.type == "Pushover" assert c.control_node == 2 assert c.step_size == 0.001 - assert c.base_nodes == [] # defaults to empty → all supports + assert c.base_nodes == [] # defaults to empty → all supports def test_pushover_case_round_trip(tmp_path) -> None: # type: ignore[no-untyped-def] p = Project( - meta=ProjectMeta(name="PO test", units=UnitSystem.SI_M_N), - ndm=3, - ndf=6, + meta=ProjectMeta(name="PO test", units=UnitSystem.METRIC), + ndm=3, ndf=6, nodes=[ Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), - Node(id=2, coords=(0, 0, 3.0), mass=(1e3, 1e3, 1e3, 0, 0, 0)), + Node(id=2, coords=(0, 0, 3.0), + mass=(1e3, 1e3, 1e3, 0, 0, 0)), ], materials=[ HystereticMaterial( - id=1, - name="Hinge", - s1p=100e3, - e1p=0.001, - s2p=150e3, - e2p=0.01, - s3p=160e3, - e3p=0.05, - s1n=-100e3, - e1n=-0.001, - s2n=-150e3, - e2n=-0.01, - s3n=-160e3, - e3n=-0.05, + id=1, name="Hinge", + s1p=100e3, e1p=0.001, + s2p=150e3, e2p=0.01, + s3p=160e3, e3p=0.05, + s1n=-100e3, e1n=-0.001, + s2n=-150e3, e2n=-0.01, + s3n=-160e3, e3n=-0.05, ), ], sections=[ - ElasticSection(id=1, E=2e11, A=0.01, Iz=1e-5, Iy=1e-5, G=8e10, J=1e-6), + ElasticSection(id=1, E=2e11, A=0.01, Iz=1e-5, Iy=1e-5, + G=8e10, J=1e-6), ], elements=[ ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), ], time_series=[LinearTimeSeries(id=1, name="Ramp")], - load_patterns=[ - PlainLoadPattern( - id=1, - time_series_id=1, - nodal_loads=[NodalLoad(node_id=2, forces=(1.0, 0, 0, 0, 0, 0))], - ) - ], - analyses=[ - PushoverCase( - id=1, - name="Push X", - pattern_ids=[1], - control_node=2, - control_dof=1, - target_disp=0.05, - step_size=0.001, - base_nodes=[1], - ) - ], + load_patterns=[PlainLoadPattern( + id=1, time_series_id=1, + nodal_loads=[NodalLoad(node_id=2, forces=(1.0, 0, 0, 0, 0, 0))], + )], + analyses=[PushoverCase( + id=1, name="Push X", + pattern_ids=[1], + control_node=2, control_dof=1, + target_disp=0.05, step_size=0.001, + base_nodes=[1], + )], ) path = tmp_path / "po.osmodel" save_project(p, path) @@ -194,30 +144,22 @@ def test_pushover_case_round_trip(tmp_path) -> None: # type: ignore[no-untyped- def test_beam_with_hinges_round_trip(tmp_path) -> None: # type: ignore[no-untyped-def] p = Project( - meta=ProjectMeta(name="BWH", units=UnitSystem.SI_M_N), - ndm=3, - ndf=6, + meta=ProjectMeta(name="BWH", units=UnitSystem.METRIC), + ndm=3, ndf=6, nodes=[ Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), Node(id=2, coords=(0, 0, 3.0)), ], sections=[ - ElasticSection(id=10, E=2e11, A=0.01, Iz=1e-5, Iy=1e-5, G=8e10, J=1e-6), + ElasticSection(id=10, E=2e11, A=0.01, Iz=1e-5, Iy=1e-5, + G=8e10, J=1e-6), ], elements=[ BeamWithHingesElement( - id=1, - nodes=(1, 2), - section_i_id=10, - section_j_id=10, - lp_i=0.3, - lp_j=0.3, - E=2e11, - A=0.01, - Iz=1e-5, - Iy=1e-5, - G=8e10, - J=1e-6, + id=1, nodes=(1, 2), + section_i_id=10, section_j_id=10, + lp_i=0.3, lp_j=0.3, + E=2e11, A=0.01, Iz=1e-5, Iy=1e-5, G=8e10, J=1e-6, ), ], ) diff --git a/tests/unit/test_phase8c.py b/tests/unit/test_phase8c.py index 557ed34..38c8e50 100644 --- a/tests/unit/test_phase8c.py +++ b/tests/unit/test_phase8c.py @@ -27,8 +27,7 @@ from otko.services.spectrum import ( # ── ResponseSpectrum schema ────────────────────────────────────────── def test_response_spectrum_basic() -> None: s = ResponseSpectrum( - id=1, - name="EC8 type 1", + id=1, name="EC8 type 1", periods=[0.0001, 0.1, 0.4, 1.0, 4.0], accelerations=[0.5, 1.0, 2.5, 1.0, 0.25], damping_ratio=0.05, @@ -40,8 +39,7 @@ def test_response_spectrum_basic() -> None: def test_response_spectrum_rejects_unsorted_periods() -> None: with pytest.raises(ValueError, match="strictly increasing"): ResponseSpectrum( - id=1, - periods=[0.1, 0.5, 0.4], # 0.5 then 0.4 = not increasing + id=1, periods=[0.1, 0.5, 0.4], # 0.5 then 0.4 = not increasing accelerations=[1.0, 2.0, 1.5], ) @@ -49,8 +47,7 @@ def test_response_spectrum_rejects_unsorted_periods() -> None: def test_response_spectrum_rejects_length_mismatch() -> None: with pytest.raises(ValueError, match="length mismatch"): ResponseSpectrum( - id=1, - periods=[0.1, 0.5, 1.0], + id=1, periods=[0.1, 0.5, 1.0], accelerations=[1.0, 2.0], ) @@ -58,8 +55,7 @@ def test_response_spectrum_rejects_length_mismatch() -> None: def test_response_spectrum_rejects_zero_period() -> None: with pytest.raises(ValueError): ResponseSpectrum( - id=1, - periods=[0.0, 0.5], + id=1, periods=[0.0, 0.5], accelerations=[1.0, 2.0], ) @@ -67,18 +63,16 @@ def test_response_spectrum_rejects_zero_period() -> None: # ── interp_sa ──────────────────────────────────────────────────────── def test_interp_sa_clamps_outside_table() -> None: s = ResponseSpectrum( - id=1, - periods=[0.1, 1.0], + id=1, periods=[0.1, 1.0], accelerations=[0.5, 0.2], ) - assert interp_sa(s, 0.05) == 0.5 # below: clamp to first - assert interp_sa(s, 5.0) == 0.2 # above: clamp to last + assert interp_sa(s, 0.05) == 0.5 # below: clamp to first + assert interp_sa(s, 5.0) == 0.2 # above: clamp to last def test_interp_sa_linear_in_table() -> None: s = ResponseSpectrum( - id=1, - periods=[0.1, 1.1], + id=1, periods=[0.1, 1.1], accelerations=[1.0, 0.0], ) # midpoint linearly = 0.5 @@ -93,27 +87,30 @@ def _two_dof_modal(masses: list[float]) -> tuple[Project, ModalResults]: φ = (1, -1). Eigenvalues set to ω² = 100 and 400 (T = 0.628 s, 0.314 s). """ p = Project( - meta=ProjectMeta(name="2dof", units=UnitSystem.SI_M_N), - ndm=3, - ndf=6, + meta=ProjectMeta(name="2dof", units=UnitSystem.METRIC), + ndm=3, ndf=6, nodes=[ Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), - Node(id=2, coords=(0, 0, 1.0), mass=(masses[0], 0, 0, 0, 0, 0)), - Node(id=3, coords=(0, 0, 2.0), mass=(masses[1], 0, 0, 0, 0, 0)), + Node(id=2, coords=(0, 0, 1.0), + mass=(masses[0], 0, 0, 0, 0, 0)), + Node(id=3, coords=(0, 0, 2.0), + mass=(masses[1], 0, 0, 0, 0, 0)), ], - sections=[ElasticSection(id=1, E=2e11, A=0.01, Iz=1e-5, Iy=1e-5, G=8e10, J=1e-6)], + sections=[ElasticSection(id=1, E=2e11, A=0.01, + Iz=1e-5, Iy=1e-5, G=8e10, J=1e-6)], elements=[ ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), ElasticBeamColumn(id=2, nodes=(2, 3), section_id=1), ], ) modal = ModalResults( - case_id=1, - case_name="Modal", + case_id=1, case_name="Modal", eigenvalues=np.array([100.0, 400.0]), mode_shapes={ - 1: {2: np.array([1.0, 0, 0, 0, 0, 0]), 3: np.array([1.0, 0, 0, 0, 0, 0])}, - 2: {2: np.array([1.0, 0, 0, 0, 0, 0]), 3: np.array([-1.0, 0, 0, 0, 0, 0])}, + 1: {2: np.array([1.0, 0, 0, 0, 0, 0]), + 3: np.array([1.0, 0, 0, 0, 0, 0])}, + 2: {2: np.array([1.0, 0, 0, 0, 0, 0]), + 3: np.array([-1.0, 0, 0, 0, 0, 0])}, }, ) return p, modal @@ -127,7 +124,7 @@ def test_mass_participation_two_dof_equal_mass() -> None: # Mode 1 (φ = (1, 1)): Γ = (1·1 + 1·1) / (1·1 + 1·1) = 1; M_eff = 1²·2 = 2 assert modes[0].participation_factor == pytest.approx(1.0) assert modes[0].effective_mass == pytest.approx(2.0) - assert modes[0].mass_ratio == pytest.approx(1.0) # 100% of total mass + assert modes[0].mass_ratio == pytest.approx(1.0) # 100% of total mass # Mode 2 (φ = (1, -1)): Γ = (1·1 + 1·-1) / (1 + 1) = 0; M_eff = 0 assert modes[1].participation_factor == pytest.approx(0.0) @@ -147,8 +144,10 @@ def test_srss_recovers_single_mode_when_only_one_active() -> None: p, modal = _two_dof_modal([1.0, 1.0]) modes = mass_participation(p, modal, direction=1) # Spectrum: constant 1.0 m/s² so Sa(T) = 1 for any T. - s = ResponseSpectrum(id=1, periods=[0.01, 100.0], accelerations=[1.0, 1.0]) - combined, _ = combine_modal_response(modes, s, modal, direction=1, method="SRSS") + s = ResponseSpectrum(id=1, periods=[0.01, 100.0], + accelerations=[1.0, 1.0]) + combined, _ = combine_modal_response(modes, s, modal, + direction=1, method="SRSS") # Mode 1 only contributes (Γ_2 = 0). u_2 = Γ_1·φ_1·Sa/ω_1² = 1·1·1/100 = 0.01 assert combined[2][0] == pytest.approx(0.01) assert combined[3][0] == pytest.approx(0.01) @@ -158,9 +157,12 @@ def test_cqc_equals_srss_for_well_separated_modes() -> None: """When modes are well-separated (ω₂/ω₁ = 2), CQC ≈ SRSS.""" p, modal = _two_dof_modal([1.0, 1.0]) modes = mass_participation(p, modal, direction=1) - s = ResponseSpectrum(id=1, periods=[0.01, 100.0], accelerations=[1.0, 1.0]) - srss, _ = combine_modal_response(modes, s, modal, direction=1, method="SRSS") - cqc, _ = combine_modal_response(modes, s, modal, direction=1, method="CQC") + s = ResponseSpectrum(id=1, periods=[0.01, 100.0], + accelerations=[1.0, 1.0]) + srss, _ = combine_modal_response(modes, s, modal, direction=1, + method="SRSS") + cqc, _ = combine_modal_response(modes, s, modal, direction=1, + method="CQC") # Mode 2 is silent (Γ=0) so both should give identical answers. np.testing.assert_array_almost_equal(srss[2], cqc[2], decimal=10) @@ -168,30 +170,22 @@ def test_cqc_equals_srss_for_well_separated_modes() -> None: # ── ResponseSpectrumCase persistence ───────────────────────────────── def test_response_spectrum_case_round_trip(tmp_path) -> None: # type: ignore[no-untyped-def] p = Project( - meta=ProjectMeta(name="rs", units=UnitSystem.SI_M_N), - ndm=3, - ndf=6, - nodes=[Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), Node(id=2, coords=(0, 0, 3))], - sections=[ElasticSection(id=1, E=2e11, A=0.01, Iz=1e-5, Iy=1e-5, G=8e10, J=1e-6)], + meta=ProjectMeta(name="rs", units=UnitSystem.METRIC), + ndm=3, ndf=6, + nodes=[Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), + Node(id=2, coords=(0, 0, 3))], + sections=[ElasticSection(id=1, E=2e11, A=0.01, Iz=1e-5, + Iy=1e-5, G=8e10, J=1e-6)], elements=[ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1)], - spectra=[ - ResponseSpectrum( - id=1, - name="Demo", - periods=[0.1, 0.5, 2.0], - accelerations=[2.5, 2.5, 0.5], - ) - ], - analyses=[ - ResponseSpectrumCase( - id=1, - modal_case_id=2, - spectrum_id=1, - direction=1, - combination="CQC", - damping_ratio=0.03, - ) - ], + spectra=[ResponseSpectrum( + id=1, name="Demo", + periods=[0.1, 0.5, 2.0], + accelerations=[2.5, 2.5, 0.5], + )], + analyses=[ResponseSpectrumCase( + id=1, modal_case_id=2, spectrum_id=1, + direction=1, combination="CQC", damping_ratio=0.03, + )], ) path = tmp_path / "rs.osmodel" save_project(p, path) diff --git a/tests/unit/test_phase9.py b/tests/unit/test_phase9.py index 24b38b9..1b6413e 100644 --- a/tests/unit/test_phase9.py +++ b/tests/unit/test_phase9.py @@ -10,7 +10,9 @@ import pytest from otko.core import ( AggregatorDOF, CircularPatch, + ElasticSection, FiberSection, + Fibre, Project, RectangularPatch, SectionAggregator, @@ -27,17 +29,10 @@ from otko.services.section_properties import ( def test_rectangular_patch_expands_to_correct_count() -> None: sec = FiberSection( id=1, - patches=[ - RectangularPatch( - material_id=1, - n_fib_y=5, - n_fib_z=4, - y_i=-0.1, - z_i=-0.1, - y_j=0.1, - z_j=0.1, - ) - ], + patches=[RectangularPatch( + material_id=1, n_fib_y=5, n_fib_z=4, + y_i=-0.1, z_i=-0.1, y_j=0.1, z_j=0.1, + )], ) fibres = expand_fibres(sec) assert fibres.shape == (20, 3) @@ -48,17 +43,10 @@ def test_rectangular_patch_expands_to_correct_count() -> None: def test_rectangular_patch_centroid_at_origin() -> None: sec = FiberSection( id=1, - patches=[ - RectangularPatch( - material_id=1, - n_fib_y=8, - n_fib_z=8, - y_i=-0.15, - z_i=-0.15, - y_j=0.15, - z_j=0.15, - ) - ], + patches=[RectangularPatch( + material_id=1, n_fib_y=8, n_fib_z=8, + y_i=-0.15, z_i=-0.15, y_j=0.15, z_j=0.15, + )], ) props = compute_section_props(sec) assert props.centroid_y == pytest.approx(0.0, abs=1e-12) @@ -70,18 +58,13 @@ def test_circular_patch_area_approaches_pi_r_squared() -> None: r = 0.15 sec = FiberSection( id=1, - patches=[ - CircularPatch( - material_id=1, - n_fib_circ=32, - n_fib_rad=8, - r_inner=0.0, - r_outer=r, - ) - ], + patches=[CircularPatch( + material_id=1, n_fib_circ=32, n_fib_rad=8, + r_inner=0.0, r_outer=r, + )], ) props = compute_section_props(sec) - expected = math.pi * r**2 + expected = math.pi * r ** 2 # With 32×8 = 256 fibres, area should be close (within ~1%). assert props.area == pytest.approx(expected, rel=0.01) assert props.centroid_y == pytest.approx(0.0, abs=1e-6) @@ -92,17 +75,11 @@ def test_circular_patch_area_approaches_pi_r_squared() -> None: def test_straight_layer_expands_correctly() -> None: sec = FiberSection( id=1, - layers=[ - StraightLayer( - material_id=2, - n_bars=4, - bar_area=0.0005, - y_start=-0.1, - z_start=-0.12, - y_end=0.1, - z_end=-0.12, - ) - ], + layers=[StraightLayer( + material_id=2, n_bars=4, bar_area=0.0005, + y_start=-0.1, z_start=-0.12, + y_end=0.1, z_end=-0.12, + )], ) fibres = expand_fibres(sec) assert fibres.shape == (4, 3) @@ -114,8 +91,7 @@ def test_straight_layer_expands_correctly() -> None: # ── SectionAggregator ─────────────────────────────────────────────── def test_section_aggregator_schema() -> None: agg = SectionAggregator( - id=5, - name="Fiber+Torsion", + id=5, name="Fiber+Torsion", section_id=1, pairings=[AggregatorDOF(material_id=3, dof="T")], ) @@ -126,78 +102,50 @@ def test_section_aggregator_schema() -> None: # ── Mixed section → Iy / Iz ───────────────────────────────────────── def test_Iz_of_rect_matches_analytical() -> None: """Iz = b·h³/12 for a rectangle centred at origin.""" - b, h = 0.3, 0.5 # y-range: -0.15..0.15, z-range: -0.25..0.25 + b, h = 0.3, 0.5 # y-range: -0.15..0.15, z-range: -0.25..0.25 sec = FiberSection( id=1, - patches=[ - RectangularPatch( - material_id=1, - n_fib_y=20, - n_fib_z=20, - y_i=-b / 2, - z_i=-h / 2, - y_j=b / 2, - z_j=h / 2, - ) - ], + patches=[RectangularPatch( + material_id=1, n_fib_y=20, n_fib_z=20, + y_i=-b / 2, z_i=-h / 2, y_j=b / 2, z_j=h / 2, + )], ) props = compute_section_props(sec) # Iz = about z-axis = b·h³/12? No — our convention: # Iz = Σ A_i · (y_i - ȳ)² (second moment about the z-axis). # For rect: Iz = h·b³/12. - Iz_exact = h * b**3 / 12.0 - Iy_exact = b * h**3 / 12.0 + Iz_exact = h * b ** 3 / 12.0 + Iy_exact = b * h ** 3 / 12.0 assert props.Iz == pytest.approx(Iz_exact, rel=0.01) assert props.Iy == pytest.approx(Iy_exact, rel=0.01) # ── Round-trip persistence ─────────────────────────────────────────── def test_fiber_section_with_patches_round_trips(tmp_path) -> None: # type: ignore[no-untyped-def] - from otko.core import ElasticBeamColumn, Node - + from otko.core import Node, ElasticBeamColumn p = Project( nodes=[Node(id=1, coords=(0, 0, 0)), Node(id=2, coords=(1, 0, 0))], sections=[ FiberSection( - id=1, - name="RC-Beam", - patches=[ - RectangularPatch( - material_id=1, - n_fib_y=4, - n_fib_z=4, - y_i=-0.15, - z_i=-0.25, - y_j=0.15, - z_j=0.25, - ) - ], - layers=[ - StraightLayer( - material_id=2, - n_bars=3, - bar_area=0.0005, - y_start=-0.12, - z_start=-0.22, - y_end=0.12, - z_end=-0.22, - ) - ], + id=1, name="RC-Beam", + patches=[RectangularPatch( + material_id=1, n_fib_y=4, n_fib_z=4, + y_i=-0.15, z_i=-0.25, y_j=0.15, z_j=0.25, + )], + layers=[StraightLayer( + material_id=2, n_bars=3, bar_area=0.0005, + y_start=-0.12, z_start=-0.22, + y_end=0.12, z_end=-0.22, + )], ), SectionAggregator( - id=2, - name="Agg", - section_id=1, + id=2, name="Agg", section_id=1, pairings=[AggregatorDOF(material_id=3, dof="T")], ), ], - elements=[ - ElasticBeamColumn( - id=1, - nodes=(1, 2), - section_id=1, - ) - ], + elements=[ElasticBeamColumn( + id=1, nodes=(1, 2), section_id=1, + )], ) path = tmp_path / "fib.osmodel" save_project(p, path) diff --git a/tests/unit/test_plotly_trace_builder.py b/tests/unit/test_plotly_trace_builder.py deleted file mode 100644 index 45b2b21..0000000 --- a/tests/unit/test_plotly_trace_builder.py +++ /dev/null @@ -1,262 +0,0 @@ -"""Unit tests for the pure Plotly trace builder (no Qt, no plotly import).""" - -from __future__ import annotations - -import json -from pathlib import Path - -import numpy as np -import pytest - -from otko.services import load_project -from otko.views.canvas3d.style import RenderStyle -from otko.views.canvas_plotly.trace_builder import ( - PlotlyTraceBuilder, - SceneOptions, -) - -EXAMPLES = Path(__file__).resolve().parents[2] / "examples" - - -def _load(name: str): # type: ignore[no-untyped-def] - return load_project(EXAMPLES / f"{name}.osmodel") - - -def _traces(scene, name: str) -> list[dict]: # type: ignore[no-untyped-def] - return [trace for trace in scene.data if trace.get("name") == name] - - -def _kinds(scene) -> list[str]: # type: ignore[no-untyped-def] - return [trace.get("meta", {}).get("kind", trace["type"]) for trace in scene.data] - - -def test_builds_grid_nodes_and_frames() -> None: - scene = PlotlyTraceBuilder().build(_load("basic_truss"), SceneOptions()) - names = {trace.get("name") for trace in scene.data} - assert "grid-active" in names - assert "nodes" in names - assert "elements" in names - assert scene.diagonal > 0 - # The hover-snap marker is always present so JS can restyle it. - assert scene.data[scene.hover_trace]["meta"]["kind"] == "hover" - - -def test_nodes_carry_ids_as_customdata() -> None: - project = _load("basic_truss") - scene = PlotlyTraceBuilder().build(project, SceneOptions()) - (nodes,) = _traces(scene, "nodes") - assert nodes["meta"]["kind"] == "node" - assert nodes["customdata"] == [node.id for node in project.nodes] - assert len(nodes["marker"]["color"]) == len(project.nodes) - - -def _color_for_index(trace: dict, index: float) -> str: - """Map a ``line.color`` scalar index to its colour via the colorscale.""" - scale = sorted(trace["line"]["colorscale"], key=lambda item: item[0]) - return scale[int(index)][1] - - -def test_frames_are_one_trace_with_per_element_colours() -> None: - """A single line trace carries the palette through its ``line.color`` array.""" - project = _load("cantilever") - style = RenderStyle() - scene = PlotlyTraceBuilder(style).build( - project, SceneOptions(selection_elements=frozenset({2})) - ) - frames = _traces(scene, "elements") - assert len(frames) == 1, "selection must not split the frame trace" - line = frames[0]["line"] - assert isinstance(line["color"], list) - assert len(line["color"]) == len(frames[0]["x"]) - assert line["colorscale"] - - colors = line["color"] - customdata = frames[0]["customdata"] - - def index_of(element_id: int) -> set[float]: - return {colors[k] for k, value in enumerate(customdata) if value == element_id} - - selected = index_of(2) - unselected = index_of(1) - assert len(selected) == 1 and len(unselected) == 1 - assert selected != unselected - assert _color_for_index(frames[0], next(iter(selected))) == style.selected_color - assert _color_for_index(frames[0], next(iter(unselected))) == style.element_beam_color - - -def test_element_palette_distinguishes_beam_from_truss() -> None: - style = RenderStyle() - builder = PlotlyTraceBuilder(style) - beam = _traces(builder.build(_load("cantilever"), SceneOptions()), "elements")[0] - truss = _traces(builder.build(_load("basic_truss"), SceneOptions()), "elements")[0] - assert _color_for_index(beam, beam["line"]["color"][0]) == style.element_beam_color - assert _color_for_index(truss, truss["line"]["color"][0]) == style.element_truss_color - - -def test_selected_node_is_highlighted_by_colour() -> None: - project = _load("basic_truss") - target = project.nodes[1].id - scene = PlotlyTraceBuilder().build(project, SceneOptions(selection_nodes=frozenset({target}))) - (nodes,) = _traces(scene, "nodes") - ids = nodes["customdata"] - assert nodes["marker"]["color"][ids.index(target)] != nodes["marker"]["color"][0] - - -def test_working_plane_filters_grid_segments() -> None: - project = _load("basic_truss") - builder = PlotlyTraceBuilder() - iso = builder.build(project, SceneOptions()) - plane = builder.build(project, SceneOptions(working_plane=("XY", 0.0))) - (iso_grid,) = _traces(iso, "grid-active") - (plane_grid,) = _traces(plane, "grid-active") - assert len(plane_grid["x"]) <= len(iso_grid["x"]) - - -def test_snap_targets_carry_world_coordinates() -> None: - scene = PlotlyTraceBuilder().build(_load("basic_truss"), SceneOptions()) - (snap,) = _traces(scene, "snap") - assert snap["meta"]["kind"] == "snap" - assert snap["customdata"], "expected at least one snappable intersection" - assert all(len(point) == 3 for point in snap["customdata"]) - assert all(isinstance(coord, float) for point in snap["customdata"] for coord in point) - - -def test_extrusions_add_a_mesh3d_trace() -> None: - project = _load("cantilever") - scene = PlotlyTraceBuilder().build(project, SceneOptions(show_extrusions=True)) - (mesh,) = _traces(scene, "extrusions") - assert mesh["type"] == "mesh3d" - assert mesh["i"] and mesh["j"] and mesh["k"] - assert max(mesh["i"]) < len(mesh["x"]) - - -def test_local_axes_add_coloured_cones() -> None: - project = _load("cantilever") - scene = PlotlyTraceBuilder().build(project, SceneOptions(show_local_axes=True)) - names = {trace.get("name") for trace in scene.data} - assert {"local-x", "local-y", "local-z"} <= names - for trace in scene.data: - if trace.get("name", "").startswith("local-"): - assert trace["type"] == "cone" - assert len(trace["u"]) == len(trace["x"]) > 0 - - -def test_labels_traces_are_text_only() -> None: - project = _load("cantilever") - scene = PlotlyTraceBuilder().build( - project, SceneOptions(show_node_labels=True, show_element_labels=True) - ) - (node_labels,) = _traces(scene, "node-labels") - (element_labels,) = _traces(scene, "element-labels") - assert node_labels["mode"] == "text" - assert len(node_labels["text"]) == len(project.nodes) - assert element_labels["mode"] == "text" - - -def test_deformation_shifts_node_coordinates() -> None: - project = _load("cantilever") - - class _Shift: - def shifted(self, points: np.ndarray, node_ids: list[int]) -> np.ndarray: - out = points.copy() - out[:, 2] += 1.0 - return out - - base = PlotlyTraceBuilder().build(project, SceneOptions()) - moved = PlotlyTraceBuilder().build(project, SceneOptions(deformation=_Shift())) - (base_nodes,) = _traces(base, "nodes") - (moved_nodes,) = _traces(moved, "nodes") - assert moved_nodes["z"] == pytest.approx([z + 1.0 for z in base_nodes["z"]]) - - -def test_payload_is_json_serialisable() -> None: - scene = PlotlyTraceBuilder().build( - _load("cantilever"), - SceneOptions( - selection_nodes=frozenset({1}), - selection_elements=frozenset({1}), - working_plane=("XY", 0.0), - show_extrusions=True, - show_local_axes=True, - show_node_labels=True, - show_element_labels=True, - ), - ) - payload = json.dumps(scene.to_payload()) - assert '"data"' in payload and '"layout"' in payload - assert scene.layout["scene"]["aspectmode"] == "data" - - -def test_empty_project_yields_only_the_hover_marker() -> None: - scene = PlotlyTraceBuilder().build(None, SceneOptions()) - assert scene.data == [] - assert scene.hover_trace == -1 - - from otko.core import Project - - empty = PlotlyTraceBuilder().build(Project(ndm=3, ndf=6), SceneOptions()) - assert _kinds(empty) == ["hover"] - - -def test_frame_trace_meta_marks_elements_pickable() -> None: - project = _load("cantilever") - scene = PlotlyTraceBuilder().build(project, SceneOptions()) - (frames,) = _traces(scene, "elements") - assert frames["meta"]["kind"] == "element" - # None separators break the line into per-element segments. - assert None in frames["x"] - assert len(frames["customdata"]) == len(frames["x"]) - - -# ── scene recipe (opstool-derived framing) ─────────────────────────────── -def test_axis_bounds_are_padded_around_the_model() -> None: - project = _load("cantilever") - scene = PlotlyTraceBuilder().build(project, SceneOptions()) - points = np.array([node.coords for node in project.nodes], dtype=float) - for index, axis in enumerate(("x", "y", "z")): - low, high = scene.axis_bounds[axis] - assert low < points[:, index].min() or low <= points[:, index].min() - assert high > points[:, index].max() or high >= points[:, index].max() - assert scene.diagonal > 0 - - -def test_planar_model_gets_unit_slack_on_the_flat_axis() -> None: - project = _load("basic_truss") - scene = PlotlyTraceBuilder().build(project, SceneOptions()) - low, high = scene.axis_bounds["z"] - assert high - low > 0 # a degenerate axis must not collapse the view - - -def test_axis_overrides_pin_ranges_with_autorange_off() -> None: - scene = PlotlyTraceBuilder().build(_load("cantilever"), SceneOptions()) - overrides = scene.axis_overrides() - assert set(overrides) == {"xaxis", "yaxis", "zaxis"} - for axis, override in overrides.items(): - assert override["autorange"] is False - assert len(override["range"]) == 2 - assert override["range"] == list(scene.axis_bounds[axis[0]]) - - -def test_hover_templates_identify_entities() -> None: - scene = PlotlyTraceBuilder().build(_load("basic_truss"), SceneOptions()) - (nodes,) = _traces(scene, "nodes") - (frames,) = _traces(scene, "elements") - assert nodes["hovertemplate"] == "Node #%{customdata}" - assert frames["hovertemplate"] == "Element #%{customdata}" - # Hover must not fall back to the raw-id "skip" mode. - assert "hoverinfo" not in nodes - - -def test_axis_outline_flag_toggles_grid_and_ticks() -> None: - plain = RenderStyle() - outlined = RenderStyle(show_axis_outline=True) - project = _load("cantilever") - scene_plain = PlotlyTraceBuilder(plain).build(project, SceneOptions()) - scene_outlined = PlotlyTraceBuilder(outlined).build(project, SceneOptions()) - - off_axis = scene_plain.layout["scene"]["xaxis"] - on_axis = scene_outlined.layout["scene"]["xaxis"] - assert off_axis["showgrid"] is False and off_axis["showticklabels"] is False - assert on_axis["showgrid"] is True and on_axis["showticklabels"] is True - # The coloured axis lines stay visible either way (orientation cue). - assert off_axis["visible"] is True and on_axis["visible"] is True diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index fe36fd5..aa9d200 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -31,7 +31,7 @@ def test_empty_project_is_valid() -> None: assert p.schema_version == 1 assert p.ndm == 3 and p.ndf == 6 assert p.nodes == [] - assert p.meta.units == UnitSystem.SI_M_N + assert p.meta.units == UnitSystem.METRIC def test_invalid_ndm_ndf_pair_rejected() -> None: @@ -42,9 +42,9 @@ def test_invalid_ndm_ndf_pair_rejected() -> None: def test_meta_round_trip() -> None: - p = Project(meta=ProjectMeta(name="Bridge", author="Ozan", units=UnitSystem.SI_MM_N)) + p = Project(meta=ProjectMeta(name="Bridge", author="Ozan", units=UnitSystem.IMPERIAL)) assert p.meta.name == "Bridge" - assert p.meta.units == UnitSystem.SI_MM_N + assert p.meta.units == UnitSystem.IMPERIAL # ────────────────────────── id allocation ────────────────────────── diff --git a/tests/unit/test_render_style.py b/tests/unit/test_render_style.py deleted file mode 100644 index c179eb7..0000000 --- a/tests/unit/test_render_style.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Unit tests for the shared render style and its editable subset.""" - -from __future__ import annotations - -import dataclasses - -import pytest - -from otko.core import ElasticBeamColumn, Node, Project, TrussElement, ZeroLengthElement -from otko.views.canvas3d.style import ( - EDITABLE_FIELDS, - SELECTED_STATE, - STYLE_FIELDS, - RenderStyle, - element_family_index, - family_palette, -) - - -def test_defaults_are_the_documented_palette() -> None: - style = RenderStyle() - assert style.node_color == "#d9d9d9" - assert style.selected_color == "#00ffff" - # opstool-derived element palette. - assert style.element_beam_color == "#0652ff" - assert style.element_truss_color == "#FF8C00" - assert style.element_link_color == "#39FF14" - assert len(style.response_scale_colors) == 10 - assert style.response_scale_colors[0] == "#313695" # cold end - assert style.response_scale_colors[-1] == "#a50026" # warm end - - -def test_with_updates_is_immutable() -> None: - original = RenderStyle() - updated = original.with_updates(node_color="#123456", label_font_size=20) - assert updated is not original - assert updated.node_color == "#123456" - assert updated.label_font_size == 20 - # The source instance is untouched (frozen dataclass contract). - assert original.node_color == "#d9d9d9" - assert original.label_font_size == 12 - - -def test_editable_subset_matches_the_field_table() -> None: - style = RenderStyle() - assert RenderStyle.EDITABLE == EDITABLE_FIELDS - assert set(style.editable_values()) == set(EDITABLE_FIELDS) - # Every advertised field must exist on the dataclass with the right kind. - kinds = {name: kind for name, _label, kind in STYLE_FIELDS} - for name in EDITABLE_FIELDS: - assert hasattr(style, name), name - assert kinds[name] in {"color", "float", "int", "bool"} - - -def test_response_colorscale_is_an_evenly_spaced_mapping() -> None: - style = RenderStyle() - scale = style.response_colorscale() - assert len(scale) == len(style.response_scale_colors) - assert scale[0] == (0.0, style.response_scale_colors[0]) - assert scale[-1] == (1.0, style.response_scale_colors[-1]) - positions = [position for position, _color in scale] - assert positions == sorted(positions) - - -@pytest.mark.parametrize( - ("element", "expected"), - [ - (ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), 0), - (TrussElement(id=2, nodes=(1, 2), area=0.01, material_id=1), 1), - (ZeroLengthElement(id=3, nodes=(1, 1), material_ids=(1,), dofs=(1,)), 2), - ], -) -def test_element_family_index(element: object, expected: int) -> None: - assert element_family_index(element) == expected - - -def test_family_palette_order_and_selected_slot() -> None: - style = RenderStyle(node_color="#010101") - palette = family_palette(style) - assert palette == [ - style.element_beam_color, - style.element_truss_color, - style.element_link_color, - style.selected_color, - ] - assert len(palette) - 1 == SELECTED_STATE - - -def test_editable_values_round_trip_through_replace() -> None: - style = RenderStyle() - values = style.editable_values() - assert RenderStyle(**values) == style - # And the persisted shape is JSON-friendly (str/int/float only). - import json - - assert json.loads(json.dumps(values)) == values - - -def test_style_is_a_frozen_slots_dataclass() -> None: - assert dataclasses.is_dataclass(RenderStyle) - with pytest.raises(dataclasses.FrozenInstanceError): - RenderStyle().node_color = "#000000" # type: ignore[misc] - - -def test_project_builder_helper_is_unused_but_valid() -> None: - """A style constructed from a project's elements classifies every family.""" - project = Project( - ndm=2, - ndf=3, - nodes=[Node(id=1, coords=(0, 0, 0)), Node(id=2, coords=(1, 0, 0))], - elements=[ - ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1), - TrussElement(id=2, nodes=(1, 2), area=0.01, material_id=1), - ], - ) - indices = [element_family_index(el) for el in project.elements] - assert indices == [0, 1] diff --git a/tests/unit/test_renderer.py b/tests/unit/test_renderer.py index 04529d6..e1c8fb0 100644 --- a/tests/unit/test_renderer.py +++ b/tests/unit/test_renderer.py @@ -5,9 +5,9 @@ from __future__ import annotations import pytest pv = pytest.importorskip("pyvista") -import numpy as np +import numpy as np # noqa: E402 -from otko.core import ( +from otko.core import ( # noqa: E402 ElasticBeamColumn, ElasticSection, LinearTimeSeries, @@ -18,13 +18,12 @@ from otko.core import ( Steel01, TrussElement, ) -from otko.services.deformation import DeformationSource -from otko.views.canvas3d.model_renderer import ( +from otko.services.deformation import DeformationSource # noqa: E402 +from otko.views.canvas3d.model_renderer import ( # noqa: E402 ModelRenderer, RendererMode, _classify_support, ) -from otko.views.canvas3d.style import SELECTED_STATE # ──────────────────────────── support classification ──────────────────────────── @@ -126,13 +125,9 @@ def test_update_selection_writes_state_array(offscreen_plotter, small_3d_project frame_states = np.asarray(r._frame_pd.cell_data["_oss_state"]).tolist() # Nodes 1 and 3 selected → row 0 and row 2 assert node_states == [1, 0, 1] - # Frame cells carry the palette slot: the element family normally, and - # SELECTED_STATE when selected. + # Element 2 selected → it's the second frame (index 1 in frame_ids_ordered) selected_frame_idx = r._frame_id_to_row[2] - assert frame_states[selected_frame_idx] == SELECTED_STATE - for index, family in enumerate(r._frame_family): - expected = SELECTED_STATE if index == selected_frame_idx else family - assert frame_states[index] == expected + assert frame_states[selected_frame_idx] == 1 def test_clear_selection(offscreen_plotter, small_3d_project) -> None: # type: ignore[no-untyped-def] diff --git a/tests/services/test_runner_translation.py b/tests/unit/test_runner_translation.py similarity index 89% rename from tests/services/test_runner_translation.py rename to tests/unit/test_runner_translation.py index 1903183..c4d4e15 100644 --- a/tests/services/test_runner_translation.py +++ b/tests/unit/test_runner_translation.py @@ -23,6 +23,7 @@ from otko.core import ( Node, PlainLoadPattern, Project, + StaticCase, Steel01, TrussElement, ZeroLengthElement, @@ -33,8 +34,7 @@ from otko.services.opensees_runner import OpenSeesRunner, _dof_indices # ───────────────────────── helpers ───────────────────────── def _truss_2d() -> Project: return Project( - ndm=2, - ndf=2, + ndm=2, ndf=2, nodes=[ Node(id=1, coords=(0, 0, 0), restraint=(True, True, False, False, False, False)), Node(id=2, coords=(4, 0, 0), restraint=(False, True, False, False, False, False)), @@ -48,8 +48,7 @@ def _truss_2d() -> Project: time_series=[LinearTimeSeries(id=1)], load_patterns=[ PlainLoadPattern( - id=1, - time_series_id=1, + id=1, time_series_id=1, nodal_loads=[NodalLoad(node_id=3, forces=(0, -1000, 0, 0, 0, 0))], ) ], @@ -58,8 +57,7 @@ def _truss_2d() -> Project: def _portal_3d() -> Project: return Project( - ndm=3, - ndf=6, + ndm=3, ndf=6, nodes=[ Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), Node(id=2, coords=(0, 0, 3)), @@ -74,35 +72,27 @@ def _isolator_3d() -> Project: """A grounded HystereticSM isolator (zeroLength) under a -cMass frame — the wire-rope benchmark's building block in miniature.""" return Project( - ndm=3, - ndf=6, + ndm=3, ndf=6, nodes=[ Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), # grounded - Node(id=2, coords=(0, 0, 0)), # coincident + Node(id=2, coords=(0, 0, 0)), # coincident Node(id=3, coords=(0, 0, 3)), ], materials=[ HystereticSM( - id=1, - name="axial", + id=1, name="axial", pos_env=[(1.57, 0.00207), (69.1, 0.0399)], neg_env=[(-1.4, -0.00057), (-15.31, -0.0483)], ), HystereticSM(id=2, name="shear", pos_env=[(0.12, 0.00067), (9.21, 0.0804)]), ], sections=[ - ElasticSection( - id=1, E=2.1e8, A=9.13e-4, Iz=7.373e-7, Iy=7.373e-7, G=8.08e7, J=2.494e-8 - ), + ElasticSection(id=1, E=2.1e8, A=9.13e-4, Iz=7.373e-7, Iy=7.373e-7, G=8.08e7, J=2.494e-8), ], elements=[ ZeroLengthElement(id=1, nodes=(1, 2), material_ids=(1, 2), dofs=(3, 1)), ElasticBeamColumn( - id=2, - nodes=(2, 3), - section_id=1, - rho=0.00717, - consistent_mass=True, + id=2, nodes=(2, 3), section_id=1, rho=0.00717, consistent_mass=True, ), ], ) @@ -110,8 +100,7 @@ def _isolator_3d() -> Project: def _shear_frame_2d() -> Project: return Project( - ndm=2, - ndf=3, + ndm=2, ndf=3, nodes=[ Node(id=1, coords=(0, 0, 0), restraint=(True, True, False, False, False, True)), Node(id=2, coords=(4, 0, 0), restraint=(True, True, False, False, False, True)), @@ -221,18 +210,9 @@ class TestMaterialEmission: OpenSeesRunner(_isolator_3d(), ops_module=ops).build() # Pairs are flattened in (force, deformation) command order, both envelopes. ops.uniaxialMaterial.assert_any_call( - "HystereticSM", - 1, - "-posEnv", - 1.57, - 0.00207, - 69.1, - 0.0399, - "-negEnv", - -1.4, - -0.00057, - -15.31, - -0.0483, + "HystereticSM", 1, + "-posEnv", 1.57, 0.00207, 69.1, 0.0399, + "-negEnv", -1.4, -0.00057, -15.31, -0.0483, ) def test_hystereticsm_symmetric_omits_neg_envelope(self) -> None: @@ -240,13 +220,7 @@ class TestMaterialEmission: OpenSeesRunner(_isolator_3d(), ops_module=ops).build() # No neg_env ⇒ only -posEnv is emitted (OpenSees mirrors it). ops.uniaxialMaterial.assert_any_call( - "HystereticSM", - 2, - "-posEnv", - 0.12, - 0.00067, - 9.21, - 0.0804, + "HystereticSM", 2, "-posEnv", 0.12, 0.00067, 9.21, 0.0804, ) @@ -260,18 +234,24 @@ class TestElementEmission: ops = MagicMock() OpenSeesRunner(_portal_3d(), ops_module=ops).build() # geomTransf tag 1 was allocated for "Linear"; element should use it. - ops.element.assert_any_call("elasticBeamColumn", 1, 1, 2, 1, 1, "-mass", 0.0) + ops.element.assert_any_call( + "elasticBeamColumn", 1, 1, 2, 1, 1, "-mass", 0.0 + ) def test_elastic_beam_column_cmass_appends_flag(self) -> None: ops = MagicMock() OpenSeesRunner(_isolator_3d(), ops_module=ops).build() # cMass=True ⇒ the consistent-mass flag trails the -mass density. - ops.element.assert_any_call("elasticBeamColumn", 2, 2, 3, 1, 1, "-mass", 0.00717, "-cMass") + ops.element.assert_any_call( + "elasticBeamColumn", 2, 2, 3, 1, 1, "-mass", 0.00717, "-cMass" + ) def test_zero_length_emits_mat_and_dir_lists(self) -> None: ops = MagicMock() OpenSeesRunner(_isolator_3d(), ops_module=ops).build() - ops.element.assert_any_call("zeroLength", 1, 1, 2, "-mat", 1, 2, "-dir", 3, 1) + ops.element.assert_any_call( + "zeroLength", 1, 1, 2, "-mat", 1, 2, "-dir", 3, 1 + ) class TestPatternEmission: diff --git a/tests/services/test_section_bbox.py b/tests/unit/test_section_bbox.py similarity index 65% rename from tests/services/test_section_bbox.py rename to tests/unit/test_section_bbox.py index 25f6b24..68ee8cd 100644 --- a/tests/services/test_section_bbox.py +++ b/tests/unit/test_section_bbox.py @@ -10,6 +10,7 @@ from otko.core import ( CircularPatch, ElasticSection, FiberSection, + Project, RectangularPatch, StraightLayer, ) @@ -23,17 +24,11 @@ def test_elastic_section_back_solves_rectangle() -> None: # Known rectangle: b = 0.3, h = 0.5 → A = 0.15, Iz = 3.125e-3 b, h = 0.30, 0.50 A = b * h - Iz = b * h**3 / 12.0 - Iy = h * b**3 / 12.0 + Iz = b * h ** 3 / 12.0 + Iy = h * b ** 3 / 12.0 sec = ElasticSection( - id=1, - name="R", - E=200e9, - A=A, - Iz=Iz, - Iy=Iy, - G=80e9, - J=1e-6, + id=1, name="R", + E=200e9, A=A, Iz=Iz, Iy=Iy, G=80e9, J=1e-6, ) dims = bbox_for_section(sec) assert dims is not None @@ -46,17 +41,10 @@ def test_elastic_section_back_solves_rectangle() -> None: def test_fiber_section_bbox_from_rect_patch() -> None: sec = FiberSection( id=1, - patches=[ - RectangularPatch( - material_id=1, - n_fib_y=4, - n_fib_z=4, - y_i=-0.15, - z_i=-0.25, - y_j=0.15, - z_j=0.25, - ) - ], + patches=[RectangularPatch( + material_id=1, n_fib_y=4, n_fib_z=4, + y_i=-0.15, z_i=-0.25, y_j=0.15, z_j=0.25, + )], ) dims = bbox_for_section(sec) assert dims is not None @@ -68,16 +56,11 @@ def test_fiber_section_bbox_from_rect_patch() -> None: def test_fiber_section_bbox_includes_circ_patches() -> None: sec = FiberSection( id=1, - patches=[ - CircularPatch( - material_id=1, - n_fib_circ=8, - n_fib_rad=2, - y_center=0.0, - z_center=0.0, - r_outer=0.2, - ) - ], + patches=[CircularPatch( + material_id=1, n_fib_circ=8, n_fib_rad=2, + y_center=0.0, z_center=0.0, + r_outer=0.2, + )], ) dims = bbox_for_section(sec) assert dims is not None @@ -89,28 +72,15 @@ def test_fiber_section_bbox_grows_for_layers() -> None: """A bar layer that extends past the patches must widen the bbox.""" sec = FiberSection( id=1, - patches=[ - RectangularPatch( - material_id=1, - n_fib_y=2, - n_fib_z=2, - y_i=-0.10, - z_i=-0.10, - y_j=0.10, - z_j=0.10, - ) - ], - layers=[ - StraightLayer( - material_id=2, - n_bars=3, - bar_area=1e-4, - y_start=-0.15, - z_start=0.12, - y_end=0.15, - z_end=0.12, - ) - ], + patches=[RectangularPatch( + material_id=1, n_fib_y=2, n_fib_z=2, + y_i=-0.10, z_i=-0.10, y_j=0.10, z_j=0.10, + )], + layers=[StraightLayer( + material_id=2, n_bars=3, bar_area=1e-4, + y_start=-0.15, z_start=0.12, + y_end=0.15, z_end=0.12, + )], ) dims = bbox_for_section(sec) assert dims is not None diff --git a/tests/services/test_transient_and_export.py b/tests/unit/test_transient_and_export.py similarity index 80% rename from tests/services/test_transient_and_export.py rename to tests/unit/test_transient_and_export.py index 31b9199..30985b1 100644 --- a/tests/services/test_transient_and_export.py +++ b/tests/unit/test_transient_and_export.py @@ -22,15 +22,20 @@ def fake_transient_h5(tmp_path: Path) -> Path: f.create_dataset("time", data=np.linspace(0.0, 0.04, n_steps)) for nid in (1, 2): base = nid * 10.0 - f.create_dataset(f"nodes/{nid}/disp", data=np.full((n_steps, ndf), base)) - f.create_dataset(f"nodes/{nid}/vel", data=np.full((n_steps, ndf), base + 0.1)) - f.create_dataset(f"nodes/{nid}/accel", data=np.full((n_steps, ndf), base + 0.2)) - f.create_dataset("elements/100/forces", data=np.full((n_steps, 12), 5.0)) + f.create_dataset(f"nodes/{nid}/disp", + data=np.full((n_steps, ndf), base)) + f.create_dataset(f"nodes/{nid}/vel", + data=np.full((n_steps, ndf), base + 0.1)) + f.create_dataset(f"nodes/{nid}/accel", + data=np.full((n_steps, ndf), base + 0.2)) + f.create_dataset("elements/100/forces", + data=np.full((n_steps, 12), 5.0)) return h5_path def test_node_disp_vel_accel_history_round_trip(fake_transient_h5: Path) -> None: - r = TransientResults(case_id=1, case_name="t", h5_path=fake_transient_h5, n_steps=5, dt=0.01) + r = TransientResults(case_id=1, case_name="t", + h5_path=fake_transient_h5, n_steps=5, dt=0.01) np.testing.assert_array_equal(r.node_disp_history(1), np.full((5, 6), 10.0)) np.testing.assert_array_equal(r.node_vel_history(1), np.full((5, 6), 10.1)) np.testing.assert_array_equal(r.node_accel_history(1), np.full((5, 6), 10.2)) @@ -47,7 +52,8 @@ def test_missing_history_raises_keyerror(tmp_path: Path) -> None: f.create_dataset("time", data=np.array([0.0, 0.01])) f.create_dataset("nodes/1/disp", data=np.zeros((2, 6))) - r = TransientResults(case_id=1, case_name="x", h5_path=h5_path, n_steps=2, dt=0.01) + r = TransientResults(case_id=1, case_name="x", + h5_path=h5_path, n_steps=2, dt=0.01) # disp works: assert r.node_disp_history(1).shape == (2, 6) # vel/accel raise: @@ -101,7 +107,8 @@ def test_export_time_history_video_decimates(tmp_path: Path) -> None: out = tmp_path / "th.gif" # 100 steps with every=10 → 10 frames captured. - export_time_history_video(plotter, set_step, out, n_steps=100, fps=4, every=10) + export_time_history_video(plotter, set_step, out, n_steps=100, + fps=4, every=10) plotter.close() assert out.exists() diff --git a/tests/unit/test_unit_labels.py b/tests/unit/test_unit_labels.py index 999f0e4..877f716 100644 --- a/tests/unit/test_unit_labels.py +++ b/tests/unit/test_unit_labels.py @@ -2,11 +2,24 @@ from __future__ import annotations +import pytest + from otko.core import UnitSystem, labels_for +from otko.core.units import ( + QUANTITIES, + display_factor, + display_for, + display_label, + display_with_alias, + format_display, + format_rotation, + native_label, + to_display, +) -def test_si_m_n_labels() -> None: - lab = labels_for(UnitSystem.SI_M_N) +def test_metric_labels() -> None: + lab = labels_for(UnitSystem.METRIC) assert lab.length == "m" assert lab.force == "N" assert lab.moment == "N·m" @@ -14,8 +27,8 @@ def test_si_m_n_labels() -> None: assert lab.curvature == "1/m" -def test_us_in_kip_labels() -> None: - lab = labels_for(UnitSystem.US_IN_KIP) +def test_imperial_labels() -> None: + lab = labels_for(UnitSystem.IMPERIAL) assert lab.length == "in" assert lab.force == "kip" assert lab.moment == "kip·in" @@ -23,20 +36,25 @@ def test_us_in_kip_labels() -> None: assert lab.curvature == "1/in" -def test_us_ft_kip_labels() -> None: - lab = labels_for(UnitSystem.US_FT_KIP) - assert lab.length == "ft" - assert lab.force == "kip" - assert lab.moment == "kip·ft" - assert lab.stress == "ksf" - assert lab.curvature == "1/ft" +def test_only_two_unit_systems_remain() -> None: + """The status-bar dropdown offers Metric and Imperial only.""" + assert {u.name for u in UnitSystem} == {"METRIC", "IMPERIAL"} -def test_si_mm_n_labels() -> None: - lab = labels_for(UnitSystem.SI_MM_N) - assert lab.length == "mm" - assert lab.force == "N" - assert lab.stress == "MPa" +def test_legacy_unit_strings_migrate_on_load() -> None: + """Old .osmodel files (4-system era) load onto Metric/Imperial.""" + from otko.core import ProjectMeta + + assert ProjectMeta.model_validate({"units": "SI (m, N, kg, s, Pa)"}).units is UnitSystem.METRIC + assert ProjectMeta.model_validate({"units": "SI (mm, N, t, s, MPa)"}).units is UnitSystem.METRIC + assert ( + ProjectMeta.model_validate({"units": "US (ft, kip, slug, s, ksf)"}).units + is UnitSystem.IMPERIAL + ) + assert ( + ProjectMeta.model_validate({"units": "US (in, kip, kip·s²/in, s, ksi)"}).units + is UnitSystem.IMPERIAL + ) def test_labels_for_covers_every_unit_system() -> None: @@ -45,3 +63,80 @@ def test_labels_for_covers_every_unit_system() -> None: lab = labels_for(us) # sanity: at least length / force populated. assert lab.length and lab.force + + +def test_display_table_covers_taxonomy_per_system() -> None: + """Every system exposes every taxonomy quantity (§5 key set).""" + for us in UnitSystem: + for q in QUANTITIES: + label, factor = display_for(us, q) + assert label + assert factor > 0 + + +def test_display_identity_for_imperial() -> None: + """Imperial stores = displays (no factors).""" + for q in QUANTITIES: + assert display_factor(UnitSystem.IMPERIAL, q) == 1.0 + + +def test_display_spot_factors() -> None: + """Spot-check Metric factors: N→kN, Pa→MPa, m→mm.""" + assert display_for(UnitSystem.METRIC, "force") == ("kN", 1e-3) + assert display_for(UnitSystem.METRIC, "stress") == ("MPa", 1e-6) + assert display_for(UnitSystem.METRIC, "displacement") == ("mm", 1000.0) + assert display_for(UnitSystem.METRIC, "section_dim") == ("mm", 1000.0) + assert display_for(UnitSystem.METRIC, "area") == ("mm²", 1e6) + assert display_for(UnitSystem.METRIC, "inertia") == ("mm⁴", 1e12) + assert display_for(UnitSystem.IMPERIAL, "stress") == ("ksi", 1.0) + + +def test_rotation_always_factor_one() -> None: + """Rotation stays rad everywhere; ° is formatting only.""" + for us in UnitSystem: + assert display_for(us, "rotation") == ("rad", 1.0) + assert format_rotation(3.14159265).startswith("3.142") + assert "°" in format_rotation(3.14159265, use_deg=True) + + +def test_dimensionless_absent_from_table() -> None: + """Strain/drift/damping never take factors — KeyError by design.""" + for us in UnitSystem: + for q in ("strain", "drift", "damping_ratio", "mass_participation"): + with pytest.raises(KeyError): + display_for(us, q) + + +def test_to_display_applies_factor_on_read() -> None: + """1 m → 1000 mm; Imperial identity; values never written back.""" + assert to_display(1.0, UnitSystem.METRIC, "displacement") == pytest.approx(1000.0) + assert to_display(1.0, UnitSystem.IMPERIAL, "displacement") == pytest.approx(1.0) + assert to_display(2.0, UnitSystem.METRIC, "force") == pytest.approx(0.002) + assert format_display(1.0, UnitSystem.METRIC, "displacement") == "1000 mm" + assert display_label(UnitSystem.IMPERIAL, "moment") == "kip·in" + + +def test_native_labels_match_stored_units() -> None: + """Labels next to native values must be native units, not prefixes.""" + assert native_label(UnitSystem.METRIC, "force") == "N" + assert native_label(UnitSystem.METRIC, "stress") == "Pa" + assert native_label(UnitSystem.METRIC, "displacement") == "m" + assert native_label(UnitSystem.METRIC, "distributed_load") == "N/m" + assert native_label(UnitSystem.METRIC, "area") == "m²" + assert native_label(UnitSystem.METRIC, "inertia") == "m⁴" + assert native_label(UnitSystem.IMPERIAL, "force") == "kip" + assert native_label(UnitSystem.IMPERIAL, "stress") == "ksi" + assert native_label(UnitSystem.IMPERIAL, "length") == "in" + for us in UnitSystem: + for q in QUANTITIES: + assert native_label(us, q) + + +def test_small_value_lbf_alias() -> None: + """0.004 kip renders as 4.0 lbf; large values stay in kip.""" + val, label = display_with_alias(0.004, UnitSystem.IMPERIAL, "force") + assert label == "lbf" + assert val == pytest.approx(4.0) + val_big, label_big = display_with_alias(2.0, UnitSystem.IMPERIAL, "force") + assert label_big == "kip" + assert val_big == pytest.approx(2.0) diff --git a/tests/unit/test_zerolength_section.py b/tests/unit/test_zerolength_section.py index 69ef334..72d3409 100644 --- a/tests/unit/test_zerolength_section.py +++ b/tests/unit/test_zerolength_section.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest -from pydantic import ValidationError from otko.core import ( ElasticSection, @@ -22,11 +21,9 @@ def test_zero_length_section_schema_defaults() -> None: def test_zero_length_section_rejects_extra_fields() -> None: - with pytest.raises(ValidationError): + with pytest.raises(Exception): ZeroLengthSectionElement( - id=1, - nodes=(1, 2), - section_id=1, + id=1, nodes=(1, 2), section_id=1, bogus="not allowed", # type: ignore[call-arg] ) @@ -34,24 +31,17 @@ def test_zero_length_section_rejects_extra_fields() -> None: def test_zero_length_section_round_trips(tmp_path) -> None: # type: ignore[no-untyped-def] """A project containing a zeroLengthSection must save+load unchanged.""" p = Project( - ndm=2, - ndf=3, + ndm=2, ndf=3, nodes=[ - Node(id=1, coords=(0, 0, 0), restraint=(True, True, False, False, False, True)), - Node(id=2, coords=(0, 0, 0), restraint=(False, True, False, False, False, False)), - ], - sections=[ - ElasticSection( - id=1, - name="Box", - E=200e9, - A=0.01, - Iz=1e-5, - Iy=1e-5, - G=80e9, - J=1e-6, - ) + Node(id=1, coords=(0, 0, 0), + restraint=(True, True, False, False, False, True)), + Node(id=2, coords=(0, 0, 0), + restraint=(False, True, False, False, False, False)), ], + sections=[ElasticSection( + id=1, name="Box", E=200e9, A=0.01, Iz=1e-5, Iy=1e-5, + G=80e9, J=1e-6, + )], elements=[ ZeroLengthSectionElement(id=10, nodes=(1, 2), section_id=1), ],