chore: adopt remaining local development state

Catch-all for the intermixed residue of the unpushed otko-development
work ported into this tree: combinations/console-dock/quick-guide wiring
across commands, core, services, views and tests; repo-wide ruff-format
normalization; README/CONTRIBUTING updates; and the toolbar default
(both toolbars now open in the top area, quick guide text updated).

Splitting this further would require hunk-level surgery with low
confidence; the preceding commits in this branch isolate the
self-contained features.
This commit is contained in:
smillmorel 2026-09-16 12:03:22 -04:00
commit ba783718d4
152 changed files with 3394 additions and 1651 deletions

View file

@ -29,33 +29,61 @@ 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
@ -64,10 +92,15 @@ 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
@ -77,55 +110,72 @@ 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,
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)
@ -145,21 +195,29 @@ 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,
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,
),
],
)