Snapshots the current development tree, headlined by proper load combinations (user request): a reusable LoadCombination entity of weighted completed static-case results (e.g. 1.2xDead + 1.6xLive). - core: LoadCombination/LoadCombinationItem entities, Project integration (lookup, unique ids, reference validation) - services: combinations.py (linear superposition + envelope), exported via services __init__ - commands: undoable Add/Delete/Update for combinations - GUI: Load Combinations manager dialog, Run-dialog evaluation, envelope display in Results panel, Combinations tab in Table dock - tests: unit coverage (validation, math, error paths) + integration superposition check vs a single factored run
13 KiB
ADR-0001 — Import gidopensees Schemas into OTKO
| Field | Value |
|---|---|
| Status | Proposed |
| Date | 2026-05-22 |
| Author | ogunc |
| Deciders | Core maintainers |
| Source project | gidopensees — AUTh Lab of R/C and Masonry Structures |
1. Context
Why gidopensees?
gidopensees is the most complete published schema inventory for the OpenSees
material/element/condition set. It covers 60+ material types, 30+ element
types, and 31 boundary-condition and load types — all expressed as GiD
preprocessor .mat / .cnd BOOK definitions, with DEPENDENCIES (field
visibility rules), #UNITS# annotations, and TKWIDGET hooks for
auto-fill presets and Wiki links.
OTKO currently supports ~34 of these objects (see
docs/gap-analysis-gidopensees.md). Importing gidopensees schemas would
close 23 P1 gaps (Phase 8 targets) and 21 P2 gaps without requiring us to
reverse-engineer OpenSeesPy docs for each type.
What we are NOT doing
This ADR covers schema definitions only — Pydantic model fields, field metadata, and default values. It does not cover:
- The
bas/Python code-generation templates (separate ADR). - The
tcl/TKWIDGET Tcl implementations (separate ADR). - Any runtime OpenSeesPy command emission — that lives in
services/opensees_runner.pyand follows naturally once a schema is accepted.
2. Decision
2.1 New package: core/catalog/
A new package src/otko/core/catalog/ will hold
gidopensees-derived schema definitions alongside curated additions.
src/otko/core/catalog/
├── __init__.py # re-exports curated + generated public symbols
├── generated/ # output of the codegen tool — DO NOT edit by hand
│ ├── _header.py # shared attribution header (inserted by codegen)
│ ├── steel.py
│ ├── concrete.py
│ ├── other_uniaxial.py
│ ├── nd_materials.py
│ ├── sections.py
│ └── elements.py
└── curated/ # human-reviewed, hand-edited overrides and additions
├── README.md # explains the curated/ contract
└── …
Existing modules are untouched. The objects in core/materials/,
core/sections/, core/geometry/elements.py remain authoritative for
every type already supported. Catalog objects enter the UI only after
equivalence is established (see §2.7).
2.2 Codegen tool: tools/gidopensees_import/
A build-time parser + code-generator lives in
tools/gidopensees_import/, outside src/:
tools/gidopensees_import/
├── README.md
├── parse_mat.py # parses OpenSees.mat into an intermediate IR
├── parse_cnd.py # parses OpenSees.cnd into an intermediate IR
├── codegen.py # renders IR → Pydantic v2 model source files
├── ir.py # intermediate representation dataclasses
└── tests/ # unit tests for the parser and codegen
The tool runs once per gidopensees update. Its output (generated/) is
committed so CI never requires gidopensees to be present. The tool is
invoked manually by a maintainer:
python tools/gidopensees_import/codegen.py \
--mat path/to/OpenSees.mat \
--cnd path/to/OpenSees.cnd \
--out src/otko/core/catalog/generated/
Rationale: keeping the parser outside src/ prevents it from being
imported at runtime, avoids adding GiD file parsing as a dependency of
the installable package, and makes the "run once, commit output" contract
explicit to contributors.
2.3 Attribution
Every file under core/catalog/generated/ carries this header comment
(inserted by codegen.py):
# This file is derived from gidopensees.
# Source: https://github.com/rclab-auth/gidopensees
# Authors: AUTh Lab of R/C and Masonry Structures
# (https://rclab.civil.auth.gr/)
# Modifications: generated by tools/gidopensees_import/codegen.py
Files under curated/ carry a similar header when they derive from
gidopensees content.
2.4 .osmodel format backward-compatibility
The .osmodel JSON format uses Pydantic discriminated unions keyed on a
"type" field. New material types from core/catalog/ appear as new
discriminator values. The union in core/materials/__init__.py is
extended only when a catalog type is promoted to stable (see §2.7).
Old project files that do not contain the new discriminator values load
cleanly: Pydantic ignores unknown items in lists when
model_config = ConfigDict(extra="ignore"), and the Project validator
will log (not raise) on unknown type strings if we add a graceful
fallback.
Migration path:
- Catalog type is added with a temporary discriminator value
(e.g.
"catalog.Concrete04"). - After verification (§2.7) it is promoted to a stable value
(e.g.
"Concrete04") and the temporary value is kept as an alias for one minor version. - A
migrate_osmodel.pyscript intools/handles the rename if needed.
No existing .osmodel file ever breaks on open.
2.5 Field metadata for TKWIDGET hooks
gidopensees BOOK definitions contain TKWIDGET directives that fire
auto-fill presets (e.g. SteelUniaxMaterial::GenerateValues), Wiki links
(TK_MaterialWikiInfo), and the Material Tester dialog
(TK_MaterialTester). These are deferred from this ADR.
Each field or model where a TKWIDGET hook is relevant carries a
json_schema_extra annotation recording the hook name:
class Concrete04(CatalogEntity):
...
class model_config(ConfigDict):
json_schema_extra = {
"tkwidget_hooks": [
"ConcreteUniaxMaterial::GenerateValues",
"TK_MaterialWikiInfo",
"TK_MaterialTester",
]
}
This metadata is visible to future UI layers without coupling core/ to
Qt. Implementation of the actual preset dialogs and Wiki-link buttons
comes in a later UI phase.
2.6 DEPENDENCIES → dependent_schemas metadata (not validators)
gidopensees BOOK DEPENDENCIES express field-visibility rules:
(1, RESTORE, Gap_length, #CURRENT#), (0, HIDE, Gap_length, #CURRENT#)
These are viewmodel / UI concerns, not data-validity rules, and
therefore must not become Pydantic validators in core/.
Each Pydantic model that has visibility dependencies stores them as
json_schema_extra["dependencies"] — a list of dicts describing the
trigger field, trigger value, and affected fields. The viewmodel layer
reads these at dialog-construction time to wire up the show/hide logic.
Example:
class ViscousDamper(CatalogEntity):
activate_gap: Literal[0, 1] = 0
gap_length: float | None = None
model_config = ConfigDict(
json_schema_extra={
"dependencies": [
{"trigger": "activate_gap", "value": 1, "restore": ["gap_length"]},
{"trigger": "activate_gap", "value": 0, "hide": ["gap_length"]},
]
}
)
2.7 Unit-annotated fields (#UNITS#)
gidopensees marks numeric fields with #UNITS# where the value's
interpretation is unit-system–dependent (forces in kN, lengths in m, etc.).
OTKO already has core/units.py with UnitSystem and
UnitLabels. It does not currently attach unit metadata to individual
model fields — the unit system is a project-level property and all numeric
values are stored in the project's native unit system, with UnitLabels
used only for display.
Intended design: Generated catalog models will mark unit-annotated fields
using a Field metadata entry (Pydantic v2 Annotated style):
from typing import Annotated
from pydantic import Field
class UnitTag:
"""Marker for fields whose display label depends on UnitSystem."""
def __init__(self, quantity: str):
self.quantity = quantity # e.g. "force", "length", "stress"
ForceMagnitude = Annotated[float, UnitTag("force")]
LengthValue = Annotated[float, UnitTag("length")]
StressValue = Annotated[float, UnitTag("stress")]
These type aliases would live in core/catalog/_units.py. No conversion logic
is added to core/; the viewmodel layer reads UnitTag.quantity to
select the right UnitLabels field for axis labels and input hints.
Implementation status (2026-05-22) — DEFERRED: The codegen tool does not
yet emit UnitTag annotations. Fields corresponding to #UNITS# entries in
the gidopensees source are currently emitted as str with a
# TODO: unit-aware type comment preserving the gidopensees default string
(e.g. yield_stress_fy: str = '500 MPa' # TODO: unit-aware type). This is
a conscious deferral: the str placeholder keeps the field present and
round-trippable without binding the codebase to a unit-system convention that
is not yet finalised. A dedicated unit-system layer — covering UnitTag,
_units.py, and viewmodel wiring — is tracked as future work and will be
addressed in a follow-up ADR before any #UNITS# field is promoted to stable.
If this convention is inadequate (e.g. if we need per-field unit
conversion in the future), a follow-up ADR should address it before
the convention is applied beyond catalog/.
2.8 Verification plan
A catalog type is promoted from generated → stable only when all three of the following are satisfied:
-
Schema equivalence test (
tests/unit/catalog/test_<name>_schema.py): Constructs a model instance with the same arguments as the corresponding Tcl example from the OpenSees Wiki and asserts thatmodel.model_dump()produces the expected dict. No OpenSeesPy import. -
Round-trip test (
tests/unit/catalog/test_<name>_roundtrip.py): Serialises the model to JSON (.osmodelfragment), deserialises it back, and asserts equality. Confirms the discriminator and all field aliases survive the round-trip. -
Integration smoke test (
tests/integration/catalog/test_<name>.py): Builds a minimal project using the new type, runs it throughOpenSeesRunner, and checks that the runner does not raise and that at least one result quantity (reaction, displacement, or force) is finite. Tagged@pytest.mark.slowand skipped ifopenseespyis not installed.
Manual review checklist (for the PR that promotes a type):
- Attribution header present in the generated file.
json_schema_extra["tkwidget_hooks"]populated where applicable.json_schema_extra["dependencies"]populated for every DEPENDENCY in the source BOOK.#UNITS#fields use the correctUnitTagquantity string.- The type discriminator value does not collide with any existing type
in
core/materials/__init__.py,core/sections/__init__.py, orcore/geometry/elements.py. - The integration smoke test result has been spot-checked against the gidopensees wiki reference or an independent OpenSees Tcl run.
3. Alternatives considered
3A: Extend existing core/materials/__init__.py directly
Rejected. The existing module is small and well-tested; adding 60+
unverified types creates noise and makes equivalence tracking harder.
A separate catalog/ namespace keeps the boundary clear.
3B: Use gidopensees at runtime (import .mat on startup)
Rejected. The GiD BOOK format is proprietary and requires the GiD parser. Depending on gidopensees at runtime adds a third-party dependency to the installed package and makes offline / air-gapped installs harder. The codegen + committed-output approach keeps the package dependency-clean.
3C: Hand-write every new type without the codegen tool
Would work but loses the systematic relationship between gidopensees
DEPENDENCIES / TKWIDGET metadata and the Pydantic model. The codegen
pipeline preserves that metadata structurally so UI implementors can
reference it rather than re-reading .mat files.
4. Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
Namespace collision between catalog/ and existing core/ types |
Medium | Medium | Verify discriminator values before promotion; CI check added to the merge checklist |
gidopensees schema drift (upstream changes .mat) |
Low | Medium | generated/ is committed; only re-run codegen intentionally; diff the output and add a CHANGELOG entry |
| Testing surface explosion (60+ new types × 3 test tiers) | High | Low | Only promoted types get full test coverage; generated-but-not-yet-promoted types have schema + round-trip tests only |
| Attribution omission | Low | High | Codegen always inserts the header; a pre-commit hook (grep -r "rclab-auth/gidopensees" core/catalog/generated/ must pass) enforces it |
#UNITS# convention inadequacy |
Medium | Medium | Convention is isolated to core/catalog/_units.py; a follow-up ADR can replace it without touching existing core/ code |
5. Out of scope for this ADR
bas/Python template parsing and code generation (separate ADR).tcl/TKWIDGET Tcl implementations (separate ADR).- Seismic isolator element schemas (
elastomericBearing*,frictionPendulumBearing, etc.) — those are new elements, not directly in the gidopensees BOOK format; they get their own ADR. - IDA batch runner (analysis feature, not schema).
- Fiber-section visual editor UI polish (UI feature, not schema).
- The OpenSeesPy command emission side —
services/opensees_runner.pywill need updates for each promoted type, but those changes follow naturally from the schema and are reviewed in the same PR as the integration smoke test.