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

@ -34,6 +34,9 @@ from otko.services.material_tester import (
test_uniaxial_material,
)
pytestmark = pytest.mark.slow
# ---- helpers ---------------------------------------------------------------
@ -45,7 +48,8 @@ def _simple_cantilever() -> Project:
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).
@ -61,17 +65,25 @@ 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,
),
],
)
@ -110,8 +122,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(
@ -129,9 +141,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 -------------------------------------------------
@ -147,9 +159,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(
@ -162,30 +174,28 @@ 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}"
)
@ -207,7 +217,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(
@ -243,23 +253,21 @@ 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 ---------------------------------------------------
@ -281,12 +289,8 @@ 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 -------------------------------------------------------
@ -323,6 +327,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}"