From 12c78ea8cbb0ad95f12ef226767013f7cb54950c Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 12:02:47 -0400 Subject: [PATCH 01/28] build: read version from _const.py, drop phantom scipy/pandas deps Single source of truth for the package + pinned solver versions in src/otko/_const.py (hatch dynamic version). scipy/pandas were listed in [gui] but never imported. mypy strict mode rescoped to core/services/viewmodels; tomli added for tomllib-less Pythons. --- pyproject.toml | 24 +++++++++++++++--------- src/otko/__init__.py | 4 +++- src/otko/_const.py | 13 +++++++++++++ 3 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 src/otko/_const.py diff --git a/pyproject.toml b/pyproject.toml index 93bfc44..5e1e4bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "otko" -version = "0.1.0" +dynamic = ["version"] description = "OTKO — a modern, SAP2000-style desktop GUI for OpenSeesPy." readme = "README.md" requires-python = ">=3.10" @@ -42,10 +42,6 @@ gui = [ "vtk>=9.3", "pyqtgraph>=0.13", "imageio[ffmpeg]>=2.34", - # 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", @@ -55,6 +51,7 @@ dev = [ "mypy>=1.8", "pre-commit>=3.6", "hatch>=1.9", + "tomli>=2.0; python_version < '3.11'", ] docs = [ "sphinx>=7.2", @@ -72,6 +69,9 @@ 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 @@ -97,12 +97,13 @@ quote-style = "double" # ─────────── Mypy ─────────── [tool.mypy] python_version = "3.10" -strict = true -warn_unused_ignores = true -warn_return_any = true -disallow_untyped_defs = true +warn_unused_configs = true plugins = [] +[[tool.mypy.overrides]] +module = ["otko.core.*", "otko.services.*", "otko.viewmodels.*"] +strict = true + [[tool.mypy.overrides]] module = ["openseespy.*", "pyvista.*", "pyvistaqt.*", "vtk.*", "pyqtgraph.*"] ignore_missing_imports = true @@ -121,3 +122,8 @@ 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/src/otko/__init__.py b/src/otko/__init__.py index d9d3102..6291478 100644 --- a/src/otko/__init__.py +++ b/src/otko/__init__.py @@ -1,3 +1,5 @@ """OTKO — a desktop GUI for OpenSeesPy.""" -__version__ = "0.1.0" +from otko._const import __version__ + +__all__ = ["__version__"] diff --git a/src/otko/_const.py b/src/otko/_const.py new file mode 100644 index 0000000..51e97ce --- /dev/null +++ b/src/otko/_const.py @@ -0,0 +1,13 @@ +# 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" From dd9fa26b52dc7c661e458f8f74a9c8c923139327 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 12:02:55 -0400 Subject: [PATCH 02/28] style: apply ruff format to generated catalog Mechanical double-quote / line-wrap normalization over the schema-generated catalog classes; no semantic changes. --- src/otko/core/catalog/__init__.py | 202 +- src/otko/core/catalog/generated/__init__.py | 162 +- .../generated/auto_equal_constraint.py | 561 ++-- .../catalog/generated/auto_zero_length.py | 727 ++--- src/otko/core/catalog/generated/bondsp01.py | 113 +- .../concrete01_zero_tensile_strength.py | 217 +- .../concrete02_linear_tension_softening.py | 218 +- .../generated/concrete04_popovics_concrete.py | 216 +- src/otko/core/catalog/generated/concrete06.py | 214 +- src/otko/core/catalog/generated/concretecm.py | 240 +- .../catalog/generated/conditions/__init__.py | 189 +- .../line_beam_contact_master_end_nodes.py | 5 +- .../line_body_constraint_slave_nodes.py | 111 +- .../conditions/line_displacements.py | 17 +- .../line_equal_constraint_slave_nodes.py | 21 +- .../generated/conditions/line_forces.py | 17 +- .../catalog/generated/conditions/line_mass.py | 19 +- .../line_rayleigh_damping_elements.py | 5 +- .../conditions/line_rayleigh_damping_nodes.py | 5 +- .../generated/conditions/line_restraints.py | 351 +-- .../line_rigid_diaphragm_slave_nodes.py | 7 +- .../conditions/line_rigid_link_slave_nodes.py | 11 +- .../conditions/line_uniform_forces.py | 11 +- .../point_beam_contact_slave_nodes.py | 11 +- .../point_body_constraint_master_node.py | 9 +- .../point_body_constraint_slave_nodes.py | 111 +- .../conditions/point_displacements.py | 17 +- .../point_equal_constraint_master_node.py | 9 +- .../point_equal_constraint_slave_nodes.py | 21 +- .../generated/conditions/point_forces.py | 17 +- .../point_ground_motion_from_record.py | 9 +- .../generated/conditions/point_mass.py | 19 +- .../point_rayleigh_damping_nodes.py | 5 +- .../generated/conditions/point_restraints.py | 351 +-- .../point_rigid_diaphragm_master_node.py | 5 +- .../point_rigid_diaphragm_slave_nodes.py | 7 +- .../point_rigid_link_master_node.py | 5 +- .../point_rigid_link_slave_nodes.py | 11 +- .../conditions/point_sine_ground_motion.py | 57 +- .../generated/conditions/point_zerolength.py | 187 +- .../conditions/surface_displacements.py | 17 +- .../generated/conditions/surface_forces.py | 17 +- .../generated/conditions/surface_mass.py | 19 +- .../surface_rayleigh_damping_elements.py | 5 +- .../surface_rayleigh_damping_nodes.py | 5 +- .../conditions/surface_restraints.py | 351 +-- .../generated/conditions/volume_mass.py | 19 +- .../volume_rayleigh_damping_elements.py | 5 +- .../volume_rayleigh_damping_nodes.py | 5 +- .../generated/conditions/zerolength.py | 187 +- src/otko/core/catalog/generated/contact.py | 11 +- .../catalog/generated/corotational_truss.py | 257 +- src/otko/core/catalog/generated/damage2p.py | 23 +- .../displacement_based_beam_column.py | 25 +- src/otko/core/catalog/generated/elastic.py | 246 +- .../catalog/generated/elastic_beam_column.py | 365 ++- .../catalog/generated/elastic_isotropic.py | 68 +- .../generated/elastic_membrane_plate.py | 13 +- .../catalog/generated/elastic_orthotropic.py | 21 +- .../generated/elastic_perfectly_plastic.py | 446 +-- .../elastic_perfectly_plastic_with_gap.py | 408 +-- .../core/catalog/generated/elastic_section.py | 411 ++- .../elastic_timoshenko_beam_column.py | 400 +-- src/otko/core/catalog/generated/fiber.py | 2461 +++++++++-------- .../core/catalog/generated/fiber_custom.py | 61 +- src/otko/core/catalog/generated/fiberint.py | 43 +- ...eraction_displacement_based_beam_column.py | 25 +- .../generated/force_based_beam_column.py | 77 +- .../catalog/generated/ground_motion_record.py | 107 +- .../core/catalog/generated/hyperbolic_gap.py | 45 +- src/otko/core/catalog/generated/hysteretic.py | 765 +++-- .../core/catalog/generated/initial_strain.py | 9 +- .../core/catalog/generated/initial_stress.py | 11 +- .../core/catalog/generated/j2plasticity.py | 15 +- .../core/catalog/generated/layeredshell.py | 51 +- src/otko/core/catalog/generated/minmax.py | 205 +- src/otko/core/catalog/generated/parallel.py | 159 +- .../core/catalog/generated/plate_fiber.py | 15 +- .../generated/pressuredependmultiyield.py | 98 +- .../generated/pressuredependmultiyield02.py | 91 +- .../generated/pressureindependmultiyield.py | 81 +- src/otko/core/catalog/generated/pysimple1.py | 57 +- src/otko/core/catalog/generated/quad.py | 33 +- src/otko/core/catalog/generated/quadup.py | 70 +- src/otko/core/catalog/generated/qzsimple1.py | 58 +- .../catalog/generated/ramberg_osgood_steel.py | 47 +- .../catalog/generated/reinforcing_steel.py | 51 +- .../catalog/generated/section_aggregator.py | 223 +- src/otko/core/catalog/generated/series.py | 159 +- src/otko/core/catalog/generated/shell.py | 18 +- src/otko/core/catalog/generated/shelldkgq.py | 16 +- .../generated/standard_brick_element.py | 22 +- src/otko/core/catalog/generated/steel01.py | 210 +- src/otko/core/catalog/generated/steel02.py | 272 +- src/otko/core/catalog/generated/tri31.py | 27 +- src/otko/core/catalog/generated/truss.py | 257 +- src/otko/core/catalog/generated/tzsimple1.py | 52 +- .../core/catalog/generated/user_material.py | 11 +- src/otko/core/catalog/generated/viscous.py | 43 +- .../core/catalog/generated/viscous_damper.py | 75 +- 100 files changed, 7202 insertions(+), 6632 deletions(-) diff --git a/src/otko/core/catalog/__init__.py b/src/otko/core/catalog/__init__.py index 1581566..31978fc 100644 --- a/src/otko/core/catalog/__init__.py +++ b/src/otko/core/catalog/__init__.py @@ -8,7 +8,9 @@ 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 @@ -21,8 +23,12 @@ 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 @@ -50,10 +56,16 @@ 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 @@ -67,64 +79,124 @@ 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 d0aed63..170a391 100644 --- a/src/otko/core/catalog/generated/__init__.py +++ b/src/otko/core/catalog/generated/__init__.py @@ -7,7 +7,9 @@ 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 @@ -20,8 +22,12 @@ 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 @@ -49,10 +55,16 @@ 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 @@ -65,19 +77,141 @@ 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 63a05a4..52c13e1 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,29 +498,40 @@ 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 880646f..e25d1d4 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,34 +654,45 @@ 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 884e4f0..032d37f 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,16 +112,15 @@ 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 9e51c2b..7316257 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,15 +202,36 @@ 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 cf44214..13c9c60 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,18 +202,36 @@ 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 061e782..f4df18b 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,17 +202,35 @@ 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 4fce8b6..771a25b 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,20 +202,38 @@ 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 a8a3593..224cecf 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,19 +228,37 @@ 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 3f4328b..fd52e19 100644 --- a/src/otko/core/catalog/generated/conditions/__init__.py +++ b/src/otko/core/catalog/generated/conditions/__init__.py @@ -15,42 +15,173 @@ 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 e1c021d..3c063d7 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,10 +15,9 @@ 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 250dbec..16131a0 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,12 +100,13 @@ 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 d00e49a..140d8f2 100644 --- a/src/otko/core/catalog/generated/conditions/line_displacements.py +++ b/src/otko/core/catalog/generated/conditions/line_displacements.py @@ -15,15 +15,14 @@ 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 cdfa971..43d050a 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,16 +17,17 @@ 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 e03e48a..69a5422 100644 --- a/src/otko/core/catalog/generated/conditions/line_forces.py +++ b/src/otko/core/catalog/generated/conditions/line_forces.py @@ -15,15 +15,14 @@ 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 891c038..c4b57a2 100644 --- a/src/otko/core/catalog/generated/conditions/line_mass.py +++ b/src/otko/core/catalog/generated/conditions/line_mass.py @@ -17,16 +17,15 @@ 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 5f24808..dc8a0b1 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,4 +25,3 @@ 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 fbf062d..57679ed 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,4 +25,3 @@ 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 74455ee..15ef27e 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,12 +310,13 @@ 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 107a4fb..aa0b5b9 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,11 +17,10 @@ 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 abfff93..d105126 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,11 +17,12 @@ 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 0a0cbc2..3303b6b 100644 --- a/src/otko/core/catalog/generated/conditions/line_uniform_forces.py +++ b/src/otko/core/catalog/generated/conditions/line_uniform_forces.py @@ -15,12 +15,11 @@ 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 5d96720..600c99e 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,15 +17,14 @@ 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 b8403c9..8a52b87 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,10 +15,11 @@ 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 c51ad33..01c115d 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,12 +100,13 @@ 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 45073fd..e95eb4a 100644 --- a/src/otko/core/catalog/generated/conditions/point_displacements.py +++ b/src/otko/core/catalog/generated/conditions/point_displacements.py @@ -15,15 +15,14 @@ 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 b5166b0..5c8465e 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,10 +15,11 @@ 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 b5106af..969e08e 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,16 +17,17 @@ 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 599b530..2bf40d5 100644 --- a/src/otko/core/catalog/generated/conditions/point_forces.py +++ b/src/otko/core/catalog/generated/conditions/point_forces.py @@ -15,15 +15,14 @@ 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 da6ddef..9898b6d 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,11 +17,10 @@ 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 d4143f8..5afb059 100644 --- a/src/otko/core/catalog/generated/conditions/point_mass.py +++ b/src/otko/core/catalog/generated/conditions/point_mass.py @@ -17,16 +17,15 @@ 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 24dd864..70155a1 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,4 +25,3 @@ 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 ea4fbd6..4a45da1 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,12 +310,13 @@ 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 1996c5f..3bdb1f3 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,10 +15,9 @@ 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 115514d..bc69de3 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,11 +17,10 @@ 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 8c0bad6..cb56d02 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,10 +15,9 @@ 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 fac5ffd..6d9cdc9 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,11 +17,12 @@ 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 7be402d..aa82b6f 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,11 +60,10 @@ 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 c0aa22e..813ace9 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,16 +181,15 @@ 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 a2c2a1b..c9c7b25 100644 --- a/src/otko/core/catalog/generated/conditions/surface_displacements.py +++ b/src/otko/core/catalog/generated/conditions/surface_displacements.py @@ -15,15 +15,14 @@ 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 ac53c76..dbf8ea9 100644 --- a/src/otko/core/catalog/generated/conditions/surface_forces.py +++ b/src/otko/core/catalog/generated/conditions/surface_forces.py @@ -15,15 +15,14 @@ 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 b7c08ec..1001fb0 100644 --- a/src/otko/core/catalog/generated/conditions/surface_mass.py +++ b/src/otko/core/catalog/generated/conditions/surface_mass.py @@ -17,16 +17,15 @@ 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 e33f80f..d32d2aa 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,4 +25,3 @@ 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 d086691..f3ed8c9 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,4 +25,3 @@ 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 7c40c31..c88e9d1 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,12 +310,13 @@ 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 2281556..a527d58 100644 --- a/src/otko/core/catalog/generated/conditions/volume_mass.py +++ b/src/otko/core/catalog/generated/conditions/volume_mass.py @@ -17,16 +17,15 @@ 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 48a38d0..6f25b2f 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,4 +25,3 @@ 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 eb57cdd..c268826 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,4 +25,3 @@ 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 361c6c8..7b59d3b 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,16 +181,15 @@ 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 040344a..6e685b9 100644 --- a/src/otko/core/catalog/generated/contact.py +++ b/src/otko/core/catalog/generated/contact.py @@ -17,14 +17,13 @@ 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 2638cb1..04bdb57 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,21 +216,22 @@ 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 77e3c1a..58bf76b 100644 --- a/src/otko/core/catalog/generated/damage2p.py +++ b/src/otko/core/catalog/generated/damage2p.py @@ -17,21 +17,22 @@ 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 e23893a..1ccfce7 100644 --- a/src/otko/core/catalog/generated/displacement_based_beam_column.py +++ b/src/otko/core/catalog/generated/displacement_based_beam_column.py @@ -17,18 +17,19 @@ 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 79b6ce1..ad411c7 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,16 +213,32 @@ 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 4321f87..0e575f5 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,25 +306,24 @@ 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 8443902..86cbeb5 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,10 +60,24 @@ 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 a19ab57..af479db 100644 --- a/src/otko/core/catalog/generated/elastic_membrane_plate.py +++ b/src/otko/core/catalog/generated/elastic_membrane_plate.py @@ -17,14 +17,13 @@ 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 5a3f863..f1a225a 100644 --- a/src/otko/core/catalog/generated/elastic_orthotropic.py +++ b/src/otko/core/catalog/generated/elastic_orthotropic.py @@ -17,20 +17,19 @@ 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 4b36761..fee4bf3 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,26 +358,50 @@ 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 27221b1..e460af0 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,23 +333,45 @@ 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 2d98fb1..dc17b4f 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,26 +352,25 @@ 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 8e02988..ca8b6fc 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,27 +326,33 @@ 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 a047e2d..5ee6e3f 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,34 +1988,36 @@ 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 @@ -2024,14 +2026,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 @@ -2040,24 +2042,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 @@ -2073,13 +2075,12 @@ 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 bce9428..4850326 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,18 +50,17 @@ 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 193d671..9f9a812 100644 --- a/src/otko/core/catalog/generated/fiberint.py +++ b/src/otko/core/catalog/generated/fiberint.py @@ -17,33 +17,32 @@ 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 c0a6567..10de81b 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,19 +17,20 @@ 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 e26d416..183019a 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,17 +60,22 @@ 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 e41c545..b21d04d 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,11 +106,12 @@ 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 bed462d..809b4b7 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,12 +56,11 @@ 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 66b7f0e..1f6d3ad 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,49 +617,48 @@ 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 1ea1283..e5cc1e9 100644 --- a/src/otko/core/catalog/generated/initial_strain.py +++ b/src/otko/core/catalog/generated/initial_strain.py @@ -17,12 +17,11 @@ 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 cd31467..4938462 100644 --- a/src/otko/core/catalog/generated/initial_stress.py +++ b/src/otko/core/catalog/generated/initial_stress.py @@ -17,12 +17,11 @@ 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 9cea9ee..1dc77e9 100644 --- a/src/otko/core/catalog/generated/j2plasticity.py +++ b/src/otko/core/catalog/generated/j2plasticity.py @@ -17,16 +17,15 @@ 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 515478e..3b9f2d2 100644 --- a/src/otko/core/catalog/generated/layeredshell.py +++ b/src/otko/core/catalog/generated/layeredshell.py @@ -17,41 +17,40 @@ 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 343b55d..a066d64 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,15 +180,14 @@ 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 b613566..813e7e1 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,15 +154,14 @@ 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 45facaa..93c2073 100644 --- a/src/otko/core/catalog/generated/plate_fiber.py +++ b/src/otko/core/catalog/generated/plate_fiber.py @@ -17,13 +17,14 @@ 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 e62d0c3..e510fa8 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,30 +76,36 @@ 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 253454d..b60cd29 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,32 +76,33 @@ 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 89a9a68..d239293 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,18 +76,17 @@ 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 d2c6230..73e8724 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,12 +56,21 @@ 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 2b31ef5..c283988 100644 --- a/src/otko/core/catalog/generated/quad.py +++ b/src/otko/core/catalog/generated/quad.py @@ -17,20 +17,25 @@ 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 a1fb541..6393d90 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,18 +50,28 @@ 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 4641469..61fdbbc 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,12 +56,22 @@ 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 2b451c3..e029923 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,14 +56,13 @@ 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 0cfb1ec..cf6f384 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,16 +56,15 @@ 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 9a6b95a..dc5c4d3 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,21 +206,20 @@ 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 6eb399c..47f00ee 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,15 +154,14 @@ 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 1991283..e47d08a 100644 --- a/src/otko/core/catalog/generated/shell.py +++ b/src/otko/core/catalog/generated/shell.py @@ -17,13 +17,17 @@ 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 5fee640..030a9ea 100644 --- a/src/otko/core/catalog/generated/shelldkgq.py +++ b/src/otko/core/catalog/generated/shelldkgq.py @@ -17,13 +17,15 @@ 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 8e91dd5..927ada6 100644 --- a/src/otko/core/catalog/generated/standard_brick_element.py +++ b/src/otko/core/catalog/generated/standard_brick_element.py @@ -17,16 +17,18 @@ 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 a928a79..0f3c320 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,23 +182,27 @@ 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 9fe1c55..19b2c51 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,30 +227,34 @@ 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 ada28d8..418d78b 100644 --- a/src/otko/core/catalog/generated/tri31.py +++ b/src/otko/core/catalog/generated/tri31.py @@ -17,19 +17,20 @@ 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 c789e73..a337c69 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,21 +216,22 @@ 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 819052a..39cb493 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,11 +56,17 @@ 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 5f43e7d..70adc8c 100644 --- a/src/otko/core/catalog/generated/user_material.py +++ b/src/otko/core/catalog/generated/user_material.py @@ -17,12 +17,11 @@ 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 e659a9a..fd9c380 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,11 +56,10 @@ 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 c2c021d..1f53eac 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,14 +82,13 @@ 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" From 7093164f751ed607783dde8da55bf7a60d9c499d Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 12:02:55 -0400 Subject: [PATCH 03/28] test: relocate service-layer tests to tests/services/ Mirrors the source layout (core/services split) so unit/ holds core-only tests and services get their own home. --- tests/services/__init__.py | 1 + .../{unit => services}/test_element_forces.py | 45 ++++++----- tests/{unit => services}/test_export.py | 0 .../test_imposed_support_motion.py | 0 tests/{unit => services}/test_peer_record.py | 0 tests/{unit => services}/test_persistence.py | 16 +++- .../test_runner_translation.py | 65 ++++++++++------ tests/{unit => services}/test_section_bbox.py | 75 +++++++++++++------ .../test_transient_and_export.py | 21 ++---- 9 files changed, 144 insertions(+), 79 deletions(-) create mode 100644 tests/services/__init__.py rename tests/{unit => services}/test_element_forces.py (90%) rename tests/{unit => services}/test_export.py (100%) rename tests/{unit => services}/test_imposed_support_motion.py (100%) rename tests/{unit => services}/test_peer_record.py (100%) rename tests/{unit => services}/test_persistence.py (90%) rename tests/{unit => services}/test_runner_translation.py (89%) rename tests/{unit => services}/test_section_bbox.py (66%) rename tests/{unit => services}/test_transient_and_export.py (80%) diff --git a/tests/services/__init__.py b/tests/services/__init__.py new file mode 100644 index 0000000..c495081 --- /dev/null +++ b/tests/services/__init__.py @@ -0,0 +1 @@ +"""Service-layer tests: runner, persistence, export, results I/O.""" diff --git a/tests/unit/test_element_forces.py b/tests/services/test_element_forces.py similarity index 90% rename from tests/unit/test_element_forces.py rename to tests/services/test_element_forces.py index 3fc8dd8..ad03cda 100644 --- a/tests/unit/test_element_forces.py +++ b/tests/services/test_element_forces.py @@ -24,6 +24,7 @@ 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 +45,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,8 +59,10 @@ 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 @@ -86,22 +89,20 @@ 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,8 +113,10 @@ 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. @@ -127,7 +130,9 @@ 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 @@ -138,8 +143,10 @@ 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)), @@ -163,7 +170,9 @@ 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) @@ -178,7 +187,9 @@ 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_export.py b/tests/services/test_export.py similarity index 100% rename from tests/unit/test_export.py rename to tests/services/test_export.py diff --git a/tests/unit/test_imposed_support_motion.py b/tests/services/test_imposed_support_motion.py similarity index 100% rename from tests/unit/test_imposed_support_motion.py rename to tests/services/test_imposed_support_motion.py diff --git a/tests/unit/test_peer_record.py b/tests/services/test_peer_record.py similarity index 100% rename from tests/unit/test_peer_record.py rename to tests/services/test_peer_record.py diff --git a/tests/unit/test_persistence.py b/tests/services/test_persistence.py similarity index 90% rename from tests/unit/test_persistence.py rename to tests/services/test_persistence.py index 7416697..93319ff 100644 --- a/tests/unit/test_persistence.py +++ b/tests/services/test_persistence.py @@ -27,7 +27,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, + ndm=3, + ndf=6, nodes=[ Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6), Node(id=2, coords=(0, 0, 3.0)), @@ -35,8 +36,14 @@ 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)], @@ -47,7 +54,8 @@ 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_runner_translation.py b/tests/services/test_runner_translation.py similarity index 89% rename from tests/unit/test_runner_translation.py rename to tests/services/test_runner_translation.py index c4d4e15..1bbee24 100644 --- a/tests/unit/test_runner_translation.py +++ b/tests/services/test_runner_translation.py @@ -34,7 +34,8 @@ 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,7 +49,8 @@ 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))], ) ], @@ -57,7 +59,8 @@ 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)), @@ -72,27 +75,35 @@ 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, ), ], ) @@ -100,7 +111,8 @@ 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)), @@ -210,9 +222,18 @@ 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: @@ -220,7 +241,13 @@ 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, ) @@ -234,24 +261,18 @@ 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/unit/test_section_bbox.py b/tests/services/test_section_bbox.py similarity index 66% rename from tests/unit/test_section_bbox.py rename to tests/services/test_section_bbox.py index 68ee8cd..43ab0c0 100644 --- a/tests/unit/test_section_bbox.py +++ b/tests/services/test_section_bbox.py @@ -24,11 +24,17 @@ 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 @@ -41,10 +47,17 @@ 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 @@ -56,11 +69,16 @@ 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 @@ -72,15 +90,28 @@ 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/unit/test_transient_and_export.py b/tests/services/test_transient_and_export.py similarity index 80% rename from tests/unit/test_transient_and_export.py rename to tests/services/test_transient_and_export.py index 30985b1..31b9199 100644 --- a/tests/unit/test_transient_and_export.py +++ b/tests/services/test_transient_and_export.py @@ -22,20 +22,15 @@ 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)) @@ -52,8 +47,7 @@ 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: @@ -107,8 +101,7 @@ 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() From ea352a5de5f7d878fe0cf5e581759b458c696e31 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 12:03:07 -0400 Subject: [PATCH 04/28] feat: load combinations LoadCombination entity, combination service, manager dialog and unit/integration coverage. --- src/otko/commands/combinations.py | 82 ++++++ src/otko/services/combinations.py | 235 +++++++++++++++++ src/otko/views/dialogs/combination_dialog.py | 258 +++++++++++++++++++ tests/integration/test_combinations.py | 90 +++++++ tests/unit/test_combinations.py | 180 +++++++++++++ 5 files changed, 845 insertions(+) create mode 100644 src/otko/commands/combinations.py create mode 100644 src/otko/services/combinations.py create mode 100644 src/otko/views/dialogs/combination_dialog.py create mode 100644 tests/integration/test_combinations.py create mode 100644 tests/unit/test_combinations.py diff --git a/src/otko/commands/combinations.py b/src/otko/commands/combinations.py new file mode 100644 index 0000000..0af9a08 --- /dev/null +++ b/src/otko/commands/combinations.py @@ -0,0 +1,82 @@ +"""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/services/combinations.py b/src/otko/services/combinations.py new file mode 100644 index 0000000..20a0d3a --- /dev/null +++ b/src/otko/services/combinations.py @@ -0,0 +1,235 @@ +"""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/views/dialogs/combination_dialog.py b/src/otko/views/dialogs/combination_dialog.py new file mode 100644 index 0000000..882736d --- /dev/null +++ b/src/otko/views/dialogs/combination_dialog.py @@ -0,0 +1,258 @@ +"""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/tests/integration/test_combinations.py b/tests/integration/test_combinations.py new file mode 100644 index 0000000..8a4dc21 --- /dev/null +++ b/tests/integration/test_combinations.py @@ -0,0 +1,90 @@ +"""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 ( # noqa: E402 + ElasticBeamColumn, + ElasticSection, + LinearTimeSeries, + LoadCombination, + LoadCombinationItem, + NodalLoad, + Node, + PlainLoadPattern, + Project, + StaticCase, +) +from otko.services import OpenSeesRunner, evaluate_combination # noqa: E402 +from otko.services.results import StaticResults # noqa: E402 + +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/unit/test_combinations.py b/tests/unit/test_combinations.py new file mode 100644 index 0000000..6a5133a --- /dev/null +++ b/tests/unit/test_combinations.py @@ -0,0 +1,180 @@ +"""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) From 1d2100628186f526f83b3565e4be5c325de68ac9 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 12:03:07 -0400 Subject: [PATCH 05/28] feat: console log dock --- src/otko/views/docks/console.py | 108 ++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/otko/views/docks/console.py diff --git a/src/otko/views/docks/console.py b/src/otko/views/docks/console.py new file mode 100644 index 0000000..e00308c --- /dev/null +++ b/src/otko/views/docks/console.py @@ -0,0 +1,108 @@ +# 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 From f1d58273b7b9f448f7abacaeeb3526fdd7a64923 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 12:03:07 -0400 Subject: [PATCH 06/28] feat: shared unit-aware formatting helpers Pure presentation helpers (labels, number parse/format) shared by docks and dialogs; no Qt imports. --- src/otko/views/formatting.py | 97 +++++++++++++++++++++++++++++++++++ tests/unit/test_formatting.py | 51 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 src/otko/views/formatting.py create mode 100644 tests/unit/test_formatting.py diff --git a/src/otko/views/formatting.py b/src/otko/views/formatting.py new file mode 100644 index 0000000..1123a08 --- /dev/null +++ b/src/otko/views/formatting.py @@ -0,0 +1,97 @@ +# 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/tests/unit/test_formatting.py b/tests/unit/test_formatting.py new file mode 100644 index 0000000..bc1c6db --- /dev/null +++ b/tests/unit/test_formatting.py @@ -0,0 +1,51 @@ +"""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) From ff09e38b7c76e04a7c04fa51ae42b3e8152822fa Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 12:03:07 -0400 Subject: [PATCH 07/28] test: meta gates for architecture, CI, docs and packaging --- tests/unit/test_architecture.py | 197 ++++++++++++++++++++++++++++++++ tests/unit/test_ci_config.py | 39 +++++++ tests/unit/test_docs.py | 51 +++++++++ tests/unit/test_meta_gates.py | 29 +++++ tests/unit/test_packaging.py | 72 ++++++++++++ 5 files changed, 388 insertions(+) create mode 100644 tests/unit/test_architecture.py create mode 100644 tests/unit/test_ci_config.py create mode 100644 tests/unit/test_docs.py create mode 100644 tests/unit/test_meta_gates.py create mode 100644 tests/unit/test_packaging.py diff --git a/tests/unit/test_architecture.py b/tests/unit/test_architecture.py new file mode 100644 index 0000000..9281e67 --- /dev/null +++ b/tests/unit/test_architecture.py @@ -0,0 +1,197 @@ +"""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 backend is confined to +``views/canvas3d``. 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 = SRC / "views" / "canvas3d" + +# 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 views/canvas3d/ 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_dir: Path | None = None, +) -> 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 exclude_dir is not None and _is_within(path, exclude_dir): + 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 views/canvas3d/.""" + violations = _violations(SRC, VTK_ROOTS, exclude_dir=CANVAS) + assert violations == [], f"backend imports outside views/canvas3d/: {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 new file mode 100644 index 0000000..063f9b3 --- /dev/null +++ b/tests/unit/test_ci_config.py @@ -0,0 +1,39 @@ +"""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_docs.py b/tests/unit/test_docs.py new file mode 100644 index 0000000..f8c78e0 --- /dev/null +++ b/tests/unit/test_docs.py @@ -0,0 +1,51 @@ +"""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/unit/test_meta_gates.py b/tests/unit/test_meta_gates.py new file mode 100644 index 0000000..67ee070 --- /dev/null +++ b/tests/unit/test_meta_gates.py @@ -0,0 +1,29 @@ +"""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_packaging.py b/tests/unit/test_packaging.py new file mode 100644 index 0000000..7b16ea4 --- /dev/null +++ b/tests/unit/test_packaging.py @@ -0,0 +1,72 @@ +"""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 From cb4bf8fccd04dc84e89fabdeb0bb0fa0bfd27f97 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 12:03:07 -0400 Subject: [PATCH 08/28] docs: quick guide, docs README, specifications and NOTICE --- NOTICE | 70 ++++ docs/QUICK_GUIDE.md | 132 +++++++ docs/README.md | 26 ++ specifications/15-rebuild-adoption-plan.md | 383 +++++++++++++++++++++ specifications/README.md | 37 ++ 5 files changed, 648 insertions(+) create mode 100644 NOTICE create mode 100644 docs/QUICK_GUIDE.md create mode 100644 docs/README.md create mode 100644 specifications/15-rebuild-adoption-plan.md create mode 100644 specifications/README.md diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..10a928c --- /dev/null +++ b/NOTICE @@ -0,0 +1,70 @@ +# NOTICE + +OTKO +Copyright © 2026 Ozan and contributors. + +## OTKO license + +OTKO's own source code is licensed under the **GNU Affero General Public +License v3.0** (AGPL-3.0). The full text is in [`LICENSE`](LICENSE). This +`NOTICE` file does not replace or modify that license; where the two +disagree, `LICENSE` governs. + +OTKO is **not** an MIT-licensed project. Portions of the codebase were +ported or adapted from an earlier, MIT-licensed prototype called +`otko-development`, so the original MIT notice is reproduced below as +required by that license. + +## Ported / adapted code: `otko-development` + +Portions of OTKO were ported or adapted from the `otko-development` +project, which is distributed under the MIT License. The MIT +copyright and permission notice is reproduced verbatim below: + +``` +MIT License + +Copyright (c) 2026 OTKO contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## Runtime dependencies + +OTKO depends on third-party software that is not covered by OTKO's +AGPL-3.0 license. Each component remains under its own license, and its +license text ships with the corresponding package. The notes below are a +summary for attribution, not a substitute for those license texts. + +- **OpenSees / OpenSeesPy** — the finite-element solver invoked by + `otko.services.opensees_runner`. OpenSees is copyright The Regents of + the University of California and is distributed under a BSD-style + license; the notice is shipped inside the `openseespy` package. +- **PySide6** — the Qt 6 bindings used by the desktop GUI. PySide6 is + available under the GNU Lesser General Public License v3 (LGPLv3) or a + commercial license. OTKO links against it dynamically, which keeps the + LGPL relinking obligation satisfiable for redistributors. +- **numpy**, **pydantic**, **h5py**, **pyvista**, **VTK**, + **pyqtgraph**, and **imageio** — each is distributed under its own + license (for example BSD-3-Clause, MIT, and similar permissive terms). + See the license file bundled with each installed package for the exact + terms. + +When redistributing OTKO, keep `LICENSE`, this `NOTICE`, and the license +notices of the dependencies above. diff --git a/docs/QUICK_GUIDE.md b/docs/QUICK_GUIDE.md new file mode 100644 index 0000000..0bc4b8c --- /dev/null +++ b/docs/QUICK_GUIDE.md @@ -0,0 +1,132 @@ +# OTKO Quick Guide + +A practical, task-first guide to the OTKO desktop GUI. It assumes you +have already installed the desktop extras and can launch the app: + +```bash +pip install -e ".[gui,dev]" +python -m otko +``` + +Project files use the `.osmodel` extension — a single, Pydantic-validated +JSON document that diffs cleanly in Git. Analysis output is written +separately to `*.osresults.h5`. + +For the layer map and the OpenSeesPy command order, see +[`architecture.md`](architecture.md). For the feature-by-feature plan, see +[`roadmap.md`](roadmap.md). + +## 1. A cantilever walkthrough + +This follows the bundled `examples/cantilever.osmodel` model: a 5 m +horizontal beam, fixed at the left end, with a tip load. If you would +rather build it by hand, the steps are below. + +1. **Start a project.** **File → New (3D Frame)**. Pick display units in + the bottom-right **Units** combo before typing any values. +2. **Lay out a grid.** **Define → Coordinate System/Grids…** (Ctrl+G). + Define X lines at 0…5 m (say, every 1 m), Y = 0, Z = 0, and set the + grid as the active coordinate system. The 3D canvas will draw it as + reference geometry. +3. **Add nodes.** **Define → Add Node…** (Ctrl+N), or use the **Draw + Node** tool and click on grid intersections at (0,0,0) … (5,0,0). +4. **Define material and section.** **Define → Material Library…** + (Ctrl+Shift+M) then **Define → Section Library…** (Ctrl+Shift+S). The + example uses a steel `ElasticSection` named `W12x40`. +5. **Draw the element.** **Assign/Define → Draw Frame** (F2), then click + from the first node to the last. Assign the section with + **Assign → Frame → Section…**. +6. **Add the support.** Select the node at x = 0 and use + **Assign → Joint → Restraints…** (Ctrl+R); restrain all six DOF. The + support icon confirms the fixed end. +7. **Add the load.** Select the tip node and use **Assign → Joint → + Point Loads…** (Ctrl+L). The example applies -10 kN in Y. Alternatively + build the distributed case with **Assign → Frame → Distributed + Load…**. +8. **Set up and run the case.** **Analyze → Cases…** (Ctrl+Shift+A) to + create or review a Static case, then **Analyze → Run…** (F5). The + bundled file already contains `Tip-Load`, `Uniform-Load`, and a modal + `Modal-3` case. + +### Smoke check + +Open `examples/cantilever.osmodel`, run the `Tip-Load` static case, then +**Display → Show Force Diagram… → M3**. The moment diagram is linear and +peaks at **50 kN·m at the fixed end**. V2 is a constant -10 kN along the +span. If you see that, the model, runner, and post-processor are wired up +correctly. + +## 2. Running a modal analysis + +1. Open a model that has mass assigned (the bundled cantilever lumps mass + at every free node so modal works out of the box). +2. **Analyze → Cases…**, add or select a **Modal** case, and set the + number of modes `n_modes` (the example uses 3). +3. **Analyze → Run…** (F5). Results appear in the results/report panel: + periods, frequencies, and participation factors per mode. +4. **Display → Animate Mode Shape** to view each mode. Use the mode + selector and the animation controls, and **Export…** if you want a + video of the mode shape. +5. Modal results also feed the response-spectrum case: define a response + spectrum, then run the SRSS or CQC combination and open + **Display → Show Response Spectrum**. + +## 3. Reviewing results and exporting a report or script + +After a run, the results/report panel shows a summary for the active case +(static reactions and forces, modal periods, and so on). Use the display +actions to inspect the model visually: + +- **Display → Show Deformed Shape** — with a scale slider. +- **Display → Show Force Diagram…** — axial (P), shear (V2/V3), moment + (M2/M3) diagrams. +- **Display → Show Pushover Curve**, **Show Time-History**, **Show + Hysteresis** as applicable. + +To hand the analysis to someone else, or to archive exactly what was run, +export a script: + +- **File → Export OpenSeesPy (.py)…** — writes the full model, and + optionally a selected analysis case, as a runnable Python script. +- **File → Export Tcl (.tcl)…** — the same model as classic OpenSees Tcl. + +The export dialog lets you choose "Model only (no analysis case)" or one +of the configured cases. The generated script follows the runner's fixed +command order (`wipe → model → node → fix → … → analyze`), so it +reproduces the analysis outside the GUI. + +## 4. Changing display units + +Use either control, they are the same setting: + +- The **Units** combo in the bottom-right of the status bar, or +- **Options → Set Display Units…** + +Changing units updates how lengths, forces, and moments are formatted in +the UI and plots. It does **not** rescale the underlying model numbers — +pick the right unit system before you type values, and convert +deliberately if you switch later. A set of unit labels is available in the +unit-label tests under `tests/unit/test_unit_labels.py`. + +## 5. Undo and redo + +Every model mutation goes through the undo stack, so most edits are +reversible: + +- **Edit → Undo** (Ctrl+Z) +- **Edit → Redo** (Ctrl+Y / Ctrl+Shift+Z) + +Menu text is dynamic — it names the operation, for example "Undo Add 4 +Nodes". Compound operations such as drawing a frame (node + element) are +wrapped in a single macro, so one undo removes the whole step. File +loads, analysis runs, and display-only changes are not model mutations and +are not undoable. + +## Where to go next + +- [`architecture.md`](architecture.md) — MVVM layering and command order. +- [`roadmap.md`](roadmap.md) — what is done and what is planned. +- `examples/` — 20+ verified models, each generated from a checked-in + Python script. +- [`../CONTRIBUTING.md`](../CONTRIBUTING.md) — setup, rules, and verify + commands. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..9ef3bfb --- /dev/null +++ b/docs/README.md @@ -0,0 +1,26 @@ +# OTKO Documentation + +Index of the project documentation. Start with the quick guide if you +just want to build and run a model; read the architecture page if you are +changing code. + +| Document | What it covers | +| --- | --- | +| [QUICK_GUIDE.md](QUICK_GUIDE.md) | Task-first walkthrough: cantilever model, modal analysis, report and script export, display units, undo/redo. | +| [architecture.md](architecture.md) | MVVM layering, package responsibilities, threading, persistence, and the fixed OpenSeesPy command order. | +| [roadmap.md](roadmap.md) | Phase-by-phase feature status, from scaffolding through the earthquake-engineering primitives. | +| [adr/](adr/) | Architecture Decision Records — the "why" behind individual design choices. | +| [screenshots/](screenshots/) | Screenshots referenced by the docs and README. | + +## Architecture Decision Records + +- [ADR-0001 — GiD/OpenSees schema import](adr/ADR-0001-gidopensees-schema-import.md) +- [ADR-0002 — Headless / GUI dependency split](adr/ADR-0002-headless-gui-dep-split.md) + +## Related documentation + +- [`../README.md`](../README.md) — project overview, install, and quick start. +- [`../CONTRIBUTING.md`](../CONTRIBUTING.md) — dev setup, layering rules, commit style, and verify commands. +- [`../AGENTS.md`](../AGENTS.md) — condensed context for automated agents. +- [`gap-analysis-gidopensees.md`](gap-analysis-gidopensees.md) — gap analysis against the GiD/OpenSees reference. +- [`../examples/README.md`](../examples/README.md) — the bundled example models. diff --git a/specifications/15-rebuild-adoption-plan.md b/specifications/15-rebuild-adoption-plan.md new file mode 100644 index 0000000..f10ddfd --- /dev/null +++ b/specifications/15-rebuild-adoption-plan.md @@ -0,0 +1,383 @@ +# 15 — Rebuild Adoption Plan + +> **Status:** proposed. **Source of truth for intent:** the greenfield +> rebuild specs at `~/Sync/otko-development/specifications` (`00`–`14`). +> This document plans how to bring that rebuild's good ideas into **this** +> repo without breaking the working app. +> +> Requirement IDs are shown without brackets for brevity (e.g. `PKG-032` = +> `[PKG-032]` in the rebuild set) so they stay grep-able. + +--- + +## 1. Context + +| | This repo (`~/Sync/otko`) | Rebuild (`~/Sync/otko-development`) | +|---|---|---| +| Role | **Working app** — the primary deliverable | Greenfield spec-conformant attempt | +| VCS | Not a git repo in this checkout | `rebuild` branch, commit `725040e` | +| License | AGPL-3.0 | MIT (+ `NOTICE`) | +| Python | 3.10–3.12 | 3.12 only | +| Project file | `.osmodel` | `.otko` | +| Scope | Superset: static, modal, **transient, pushover, response-spectrum**, quad/shell-adjacent elements, HDF5 | Narrow v1: static + modal; reports, combinations, self-weight, units display | +| src LOC | ~37k | ~25k | +| Gate | deps not installed here; **57 collection errors** locally | `ruff` passes; `mypy`/`pytest` deps missing locally | + +The rebuild is *ahead* on: **reporting/Typst, load combinations, self-weight, +display-unit conversion, closed-form diagrams, modal mass participation, +progress/cancel, local-axis editing, viewmodel separation, and quality +gates**. It is *behind* on everything this repo already ships. Therefore the +plan is a **selective harvest**, not a merge. + +## 2. Goal and non-goals + +**Goal.** Adopt the rebuild's good capabilities into this repo, incrementally, +each phase independently shippable, with the existing app and tests staying +green. + +**Non-goals.** +- Do **not** rewrite this repo to the rebuild's architecture (`ProjectStore`, + frozen `Project`, `CommandFactory`, `services/scene.py`, + `services/diagram_data.py` substrates). +- Do **not** replace the runner/export/results surface — this repo's is a + strict superset. +- Do **not** change `.osmodel`, the AGPL license, or the Python 3.10–3.12 + support range as part of this plan. + +## 3. Principles + +1. **Additive, adapt, don't copy.** Port algorithms/patterns; re-type to this + repo's APIs (`StaticResults`/`ModalResults`, mutable `Project`, + 4-value `UnitSystem`, `element_forces.DiagramData`). +2. **Protect the working app.** Every phase keeps `pytest -m "not slow"` and + the GUI smoke tests green. New optional deps stay **lazy** so headless + installs are unaffected. +3. **One phase = one branch = one shippable PR**, with tests and a gate. +4. **Trace every item to a rebuild spec ID** so "is it done?" stays mechanical. +5. **License hygiene.** MIT→AGPL-3.0 is one-way compatible: preserve the MIT + copyright/permission notice for ported files (`NOTICE` + per-file header). + Never copy this repo's AGPL code back into the MIT rebuild. +6. **Establish version control first.** This checkout has **no `.git`**. Before + any work, create a repo/branch per `AGENTS.md` (`origin` is the self-hosted + Forgejo; work on `feat/`, PR against `main`). + +## 4. Adoption matrix + +Verdicts: **PORT** (near-verbatim), **ADAPT** (re-type/re-wire), **REIMPL** +(reimplement against this repo's APIs), **SKIP** (intentional divergence). +Size: **S** ≈ hours · **M** ≈ a few days · **L** ≈ 1–2+ weeks. + +### 4.1 Quality / foundations + +| # | Capability | Rebuild source | Verdict | Phase | Size | Spec IDs | +|---|---|---|---|---|---|---| +| Q1 | Single-source version + solver pin via `_const.py` | `src/otko/_const.py`, `pyproject.toml` `[tool.hatch.version]` | ADAPT | 0 | S | PKG-032/033, RUN-085 | +| Q2 | Layering/import-discipline AST tests | `tests/unit/test_architecture.py` (246) | ADAPT (paths) | 0 | M | ARC-001..003/006/011/060..064 | +| Q3 | CI split into 5 jobs incl. missing **`type` (mypy)** job; mark integration `slow` | `.github/workflows/ci.yml`, `tests/unit/test_ci_config.py`, `test_meta_gates.py` | ADAPT | 0 | M | TST-004/031/040/041 | +| Q4 | conftest hardening (offscreen default; `ops.wipe()` in teardown) | `tests/conftest.py` | ADAPT | 0 | S | TST §7 | +| Q5 | `tests/services/` split from `tests/unit/` | tests layout | ADAPT | 0 | M | TST-002 | +| Q6 | Standalone docs + docs test | `docs/QUICK_GUIDE.md`, `docs/README.md`, `tests/unit/test_docs.py` | ADAPT | 0 | M | — | +| Q7 | `NOTICE` + attribution for ported MIT code | `NOTICE` | REIMPL (AGPL wording) | 0 | S | PKG-042 | +| Q8 | Drop phantom `scipy`/`pandas` from `[gui]` | `pyproject.toml`, `tests/unit/test_packaging.py` | ADAPT | 0 | S | PKG-011 | +| Q9 | Scoped mypy overrides + coverage `fail_under` | `pyproject.toml` | ADAPT | 0 | S | TST-031/032 | +| Q10 | Packaging config tests subset (entrypoint, line length, single-source) | `tests/unit/test_packaging.py` (117) | ADAPT | 0 | S | PKG-030..033 | +| Q11 | Spec-ID traceability harness | `tests/unit/test_spec_traceability.py` (343) | REIMPL | 8 | L | TST-013/024/025 | +| Q12 | Performance probes | `tests/performance/*` | ADAPT | 8 | L | OVR-010..014 | + +### 4.2 Services / engine + +| # | Capability | Rebuild source | Verdict | Phase | Size | Spec IDs | +|---|---|---|---|---|---|---| +| S1 | `SolverSession` Protocol + `Real`/`Recording` sessions | `services/solver_session.py` (154) | PORT | 1 | S | RUN-003/010/011/012 | +| S2 | Persistence refuses to save an invalid model + resolved path | `services/persistence.py` (63) | ADAPT | 1 | S | PER-021/022 | +| S3 | Result fields: `StaticResults.time`, modal `ModeParticipation` record | `services/results.py` (138) | ADAPT | 1 | S | RES-001/003/005, ANL-012 | +| S4 | Component-less element forces = absent, not zeros | `runner/run.py` | ADAPT | 1 | S | RUN-053 | +| S5 | `progress(int)` + `cancelled` + `isInterruptionRequested()` + `cancel()`; real Cancel UX | `services/qt_workers.py`, `viewmodels/analysis_vm.py`, `views/dialogs/run.py` | ADAPT | 2 | M | ANL-031..036, ARC-032/033, UX-081 | +| S6 | Modal mass participation unified with response-spectrum path | `runner/modal.py` (466), `services/results.py` | ADAPT | 2 | M | RUN-062/070..072, ANL-013 | +| S7 | Display-unit layer + `GRAVITY` + `ProjectMeta.display` (adapter over 4-system enum) | `core/units.py` (298) | ADAPT | 3 | M | UNT-003/004/010/020..024/030..032 | +| S8 | Closed-form diagram math (`N/V/M/T` shapes + extrema) | `core/diagrams.py` (436) | PORT | 4 | S | RES-010/011/012 | +| S9 | Backend-neutral `diagram_data` service + `render_matplotlib` | `services/diagram_data.py` (400) | REIMPL | 4 | M | RES-020..024/030..032, CAN-084 | +| S10 | **Report pipeline** CSV / PNG(≥300 dpi) / SVG / Typst (+optional PDF) + case-report action | `services/report/*` (1141) | PORT + ADAPT deps | 5 | M | RES-040..047/050/051/061 | +| S11 | Local axis: `LocalAxis`, element field, geomTransf dedup by `(type,vecxz,roll)`, editor + triad | `core/geometry/local_axis.py`, `runner/emit.py` | ADAPT | 6 | M | GEO-060..064, DOM-040 | +| S12 | Load combinations entity + commands + runner materialisation | `core/combinations.py` (44), `commands/combinations.py` (146) | PORT + ADAPT | 7 | M | LOD-050..058, ANL-001, RUN-050 | +| S13 | Self-weight service + regenerate command | `services/self_weight.py` (364) | REIMPL | 7 | M | UNT-040..045, LOD-011/040..043 | +| S14 | Material `rho` coverage for self-weight | `core/materials` | ADAPT | 7 | S | UNT-041 | + +### 4.3 UI / UX + +| # | Capability | Rebuild source | Verdict | Phase | Size | Spec IDs | +|---|---|---|---|---|---|---| +| U1 | Pure formatting helpers (`DOF/MASS/RESTRAINT_LABELS`, number/float parse) | `views/formatting.py` (84) | PORT | 0 | S | UX-033/062 | +| U2 | Timestamped, severity-tagged console | `views/docks/console.py` (103) | PORT | 0 | S | UX-042 | +| U3 | `ValidatedDialog` base (help line, inline errors, OK-gating, unit suffix) | `views/dialogs/base.py` (191) | PORT + staged | 8 | M | UX-060..064 | +| U4 | `ResultsVM` per-case handle cache; `CanvasVM` selection ownership | `viewmodels/results_vm.py`, `canvas_vm.py` | REIMPL | 8 | M | RES-005, CAN-042/041 | +| U5 | Overlay helpers: ghost undeformed, unit-labelled diagram extremes, zero hint, pattern-filtered loads | `views/canvas/overlays.py` (684) | REIMPL | 8 | M | CAN-052/062/082/083, LOD-063 | +| U6 | `QSettings` layout persistence + `closeEvent` save prompt | `views/main_window.py` | PORT | 0 | S | UX-013, PER-032/033, ARC-050 | +| U7 | `ThemeManager` + dark icon set | `views/resources/theme.py`, `icons.py` | ADAPT (verify icon provenance) | 8 | S | UX-070/071/072 | +| U8 | Thicken `ProjectViewModel` (move `action_handlers` logic into VM methods) | `viewmodels/project_vm.py` (713) | REIMPL | 8 | L | UX-001/002/005, ARC-005/020..022 | +| U9 | Analysis VM status + Simple/Advanced control fields | `viewmodels/analysis_vm.py` | ADAPT | 8 | M | ANL-021/050..054, UX-037 | + +### 4.4 Explicitly deferred / skipped + +| Item | Verdict | Why | +|---|---|---| +| Polygon-first sections + `services/sections_mesh.py` (opstool GPLv3) | DEFER (Phase 9, conditional) | Schema-breaking redesign; adds GPLv3/runtime deps; not report-critical | +| Frozen `Project`/entities + `ProjectStore`/`CommandFactory` rewrite | DEFER (not recommended now) | Large coordinated rewrite of 43 commands + all call sites; low user-visible value vs risk | +| Canvas backend Protocol + `plotly_backend.py` + `services/scene.py` | DEFER (Phase 9) | Requires core + views surgery; plotly pulls QtWebEngine (known teardown SIGSEGV) | +| `.otko` file suffix / greenfield `Project` schema (PER-001/006) | SKIP | Breaks `.osmodel` corpus and compatibility; do only as an explicit user-approved migration | +| Rebuild `runner/export.py` | SKIP | This repo's `export.py` is a superset (5 case types, `.py`+`.tcl`) | +| Rebuild static/modal HDF5 | SKIP | Rebuild has none; this repo's transient HDF5 is already better | +| Rebuild slim `materials`/`analysis`/`loads` unions | SKIP | Dropping `HystereticSM`, Transient/Pushover/ResponseSpectrum, Path/Imposed patterns would regress shipped features | +| MIT license / py3.12-only / single-OS CI assertions | SKIP | Intentional differences (AGPL, 3.10–3.12, 3-OS matrix) | + +## 5. Phases + +### Phase 0 — Foundations, safety net, quick wins +**Why first:** makes later phases verifiable and cheap; all items are +additive and low risk. Also unblocks the mpy gate that currently does nothing. + +- **Prep:** initialise git and a `feat/rebuild-adoption` branch (§3.6). +- **Q1** Create `src/otko/_const.py` (`__version__`, `OPENSEESPY_VERSION`), + switch `pyproject.toml` to `dynamic = ["version"]` + `[tool.hatch.version]`, + and import the pin in `services/export.py` (currently duplicated at + `export.py:52`). Spec: PKG-032/033, RUN-085. +- **Q2** Port `tests/unit/test_architecture.py`; adapt `CANVAS = views/canvas3d` + path and `services/{_emit,_run}.py` paths. Expect it to flag three existing + offenders — decide per item: `OSS_PICK_DEBUG` env var in + `views/canvas3d/model_canvas.py:29` (ARC-052), `QUndoStack` outside VMs, and + the canvas path. Either fix or add a documented, narrow allowlist. +- **Q3** Split CI: `lint` (+`ruff format --check`), **`type` (mypy scopes)**, + `test-headless` (`pytest tests/unit tests/services -m "not slow"`), + `test-gui` (`xvfb-run pytest tests/gui`), `test-integration` + (`pytest tests/integration -m slow`). Add `pytestmark = pytest.mark.slow` to + integration modules. Keep the existing 3-OS × 3.10–3.12 matrix and the long + Linux Qt apt list. +- **Q4** `tests/conftest.py`: `QT_QPA_PLATFORM=offscreen` default; move + `ops.wipe()` to teardown (this repo currently wipes before each test). +- **Q5** Move service-level tests (`runner_translation`, `persistence`, + `results`, `export`, …) into `tests/services/`; update CI and `AGENTS.md`. +- **Q6** Add `docs/QUICK_GUIDE.md` + `docs/README.md` (rewrite `.otko` → + `.osmodel`, rebuild-only APIs → this repo's), update `CONTRIBUTING.md` with + the 5-layer table + install/run/test commands; port `test_docs.py`. +- **Q7** Add `NOTICE` crediting MIT `otko-development` for ported files; + add a per-file header to ported files (`# Ported from otko-development + (MIT), (c) 2026 OTKO contributors`). +- **Q8/Q9/Q10** `pyproject.toml`: drop `scipy`/`pandas` from `[gui]`; convert + mypy to scoped `strict` overrides (core/services/viewmodels); add + `[tool.coverage.run] fail_under`; port the `test_packaging.py` subset that + applies (entrypoint, version single-source, line length). +- **U1/U2/U6** Port `views/formatting.py`; timestamp the console + (`views/docks/console.py` into the existing dock); add `save_layout`/ + `restore_layout` (`QSettings`) and a `closeEvent` unsaved-changes prompt. + +**Gate:** `ruff check src tests`, `ruff format --check src tests`, +`mypy src/otko/core src/otko/services src/otko/viewmodels`, +`pytest -m "not slow"`, arch test, and the new packaging/docs tests pass. + +### Phase 1 — Cheap, safe services wins +- **S1** Port `services/solver_session.py` (`SolverSession`, `RealSolverSession`, + `RecordingSolverSession`). Inject into `OpenSeesRunner` while keeping the + existing `ops_module=` shim; consolidate the two wipe paths + (`_emit.py:105` → `session.reset()`). Tests: + `tests/services/test_solver_session.py`. +- **S2** In `services/persistence.py::save_project`, call + `validate_references()` first and refuse to write an invalid model, + reporting the problem list (PER-022); return the resolved path (PER-021). +- **S3** Extend `StaticResults` with `time` and add a `ModeParticipation` + record to `ModalResults` (keep the existing dataclass names — consumers in + `views/docks/results_panel.py` dispatch on them). +- **S4** `RUN-053`: return an absent (not zero-filled) force entry when a + component is missing; guard the static `eleForce` fallback. + +**Gate:** new service tests + existing runner/persistence tests green; runner +integration suite unchanged. + +### Phase 2 — Progress & cancellation +- **S5** Add to `services/qt_workers.py`: `progress(int)`, `cancelled`, + an `_interruption_requested()` check between steps, and a bounded teardown + wait. Add `AnalysisRunner.cancel()` (calls `QThread.requestInterruption()`). + Wire progress callbacks through static (`RUN-054`) and modal (`RUN-063`) + runs. Replace the indeterminate bar in `views/dialogs/run_analysis.py` with + a real progress bar + working Cancel (UX-081). Add per-case status to the + case manager (ANL-050). +- **S6** Unify modal mass participation: put the rebuild's participation + math into one service and have **both** modal results and the existing + response-spectrum path (`services/spectrum.py`) consume it. Add the ANL-013 + no-mass warning. + +**Gate:** cancel leaves no partial handle; progress reaches 100; participation +sums to 1.0 against a hand-checked example. + +### Phase 3 — Units display layer + GRAVITY +- **S7** Port the display machinery from `core/units.py` **as an adapter**: + keep this repo's 4-value `UnitSystem` and map families + (`SI_M_N|SI_MM_N → METRIC`, `US_FT_KIP|US_IN_KIP → IMPERIAL`); add + `GRAVITY`, `display(value, system, quantity, pref)`, `DisplayPrefs`; add + `ProjectMeta.display` (persisted). Retarget the status-bar/menu unit picker + so it writes **display prefs**, not the stored `meta.units` (fixes UNT-031). +- Watch-outs: rebuild uses `StrEnum` (3.11+) — do **not** adopt it (this repo + targets 3.10). `export.py` currently embeds `meta.units.value`; keep that + but source labels via the new API. + +**Gate:** switching display units changes every label and leaves +`project.model_dump()` byte-identical; old 4-system `.osmodel` files still load. + +### Phase 4 — Closed-form diagrams +- **S8** Port `core/diagrams.py` near-verbatim (pure numpy, self-contained). +- **S9** Build a new `services/diagram_data.py` (REIMPL) on top of this repo's + `StaticResults` + load patterns, reusing `core/diagrams` for interior + shapes/extrema. Run it **alongside** `services/element_forces.py`, migrate + `views/canvas3d/diagram_renderer.py` and `views/dock_manager.py` behind a + flag, then retire the old extractor once the on-screen output matches. + +**Gate:** port `test_diagram_data.py` (end-values, Vz/My plane, axial, +all-six components, shared data, auto-scale); on-screen diagrams unchanged. + +### Phase 5 — Reporting pipeline ★ (the headline v1 capability) +- **S10** Port `services/report/{__init__,csv,figures,typst,pdf}.py`. Add a + `[reports]` extra (`matplotlib`, `imageio-ffmpeg`; Typst CLI optional, probed + via `shutil.which`, graceful degradation per RES-046). Expose a "case report" + action that produces the full set: CSV tables, PNG (≥300 dpi), SVG, a Typst + document (PDF optional), conditional static/modal sections. +- Dependencies: **Phase 3** (units) and **Phase 4** (diagrams) must land first; + reuse **Phase 1** result fields. + +**Gate:** the reference test — cantilever moment **50 kN·m at the fixed end** +(RES-061) — plus report generation off the GUI thread and preservation of the +original model file. Port `test_report_{csv,figures,typst,modal}.py` and the +`test_full_report.py` / `test_report_reference.py` integration tests. + +### Phase 6 — Local axis (user-editable) +- **S11** Port `core/geometry/local_axis.py`; add + `local_axis: LocalAxis` to the frame elements (`ElasticBeamColumn`, + `ForceBeamColumn`, `DispBeamColumn`, `BeamWithHinges`). Change + `services/_emit.py` geomTransf to read the explicit axis and dedup by + `(type, vecxz, roll_deg)` instead of auto-deriving. Update the renderer + triad (its current `getattr(el, "vecxz")` at `model_renderer.py:727` is dead + code) and add an editor (property dock / dialog). +- **Critical migration risk:** a blind default `vecxz=(0,0,1)` is parallel to + a vertical member's axis and would break existing vertical models. Use a + **safe default rule** (e.g. derive the default from geometry exactly as the + runner does today, then only override when the user sets it) so existing + `.osmodel` files keep loading and producing identical results. + +**Gate:** existing frame/eigen integration tests unchanged; new +`test_local_axis_parallel_rejected` + `test_geom_transf_dedup_by_combination`. + +### Phase 7 — Load combinations + self-weight +- **S12** Port `core/combinations.py` and `commands/combinations.py`; add + `Project.combinations`; extend `validate_references`; add + `StaticCase.combination_id` with the XOR invariant (combination **or** + patterns+factors, ANL-001); materialise scaled loads in the runner + (LOD-054) without regressing the existing per-pattern factor path. +- **S13** Reimplement `services/self_weight.py` against this repo's types: + `w = ρ·A·g` distributed local for frames, equivalent nodal loads for trusses + (UNT-042/043), a `ConstantTimeSeries` + `PlainLoadPattern`, regeneration that + replaces the prior pattern (LOD-041), no-density/unresolved reporting + (LOD-042), and the gravity constant recorded in the description (UNT-045). + Add a `RegenerateSelfWeightCommand` (undoable). +- **S14** `ElasticIsotropic` currently is the only material with `rho` + (`materials/__init__.py:28`); add `rho` to the other self-weight-capable + materials, or report them as missing density. +- Depends on **Phase 3** (`GRAVITY`) and **Phase 6** (correct local projection). + +**Gate:** port `test_loads_combinations.py` and `test_self_weight.py` (minus +the combination assertion until S12 lands); verify superposition matches a +hand calculation. + +### Phase 8 — UI/UX adoption (larger, optional but valuable) +- **U3** `ValidatedDialog` base + migrate dialogs incrementally (unit suffix + via the Phase 3 API). +- **U4** Extract `ResultsVM` (per-case handle cache) and `CanvasVM` (selection + ownership) as supersets of the current state holders so existing call sites + keep working. +- **U5** Reimplement overlay **pure helpers** against this repo's + `deformation`/`element_forces`: ghost undeformed (CAN-062), unit-labelled + diagram extremes (CAN-082), zero-component hint (CAN-083), and + pattern-filtered load display (CAN-052/LOD-063). Unit-test them display-free. +- **U7** Adopt `ThemeManager` + a dark icon set — **verify SVG provenance and + license first** before copying any icon assets. +- **U8/U9** Thicken `ProjectViewModel` (move `action_handlers.py` logic into VM + methods, one command family at a time) and add analysis-case status / + Simple-vs-Advanced controls. +- **Q11/Q12** Spec-ID traceability harness and performance probes (only if the + team commits to the `[AREA-NNN]` convention and a perf budget). + +**Gate:** GUI tests under `xvfb-run`; VM logic unit-tested without a display. + +### Phase 9 — Deferred, only on explicit decision +- Polygon-first sections + `sections_mesh` (opstool **GPLv3**, lazy import, + optional `[sections]` extra; ensure AGPL compatibility and attribution). +- Frozen `Project`/`ProjectStore`/`CommandFactory` migration. +- Canvas backend Protocol + plotly backend + `services/scene.py`. + +Revisit only after Phases 0–8 are stable and if the value justifies the blast +radius documented in the dossiers. + +## 6. Dependency graph + +``` +Phase 0 (foundations) + │ + ├─► Phase 1 (sessions, persistence, result fields) + │ │ + │ ├─► Phase 2 (progress/cancel, mass participation) + │ │ + │ └─► Phase 3 (units display + GRAVITY) ──┐ + │ │ + │ Phase 4 (diagrams) ────────────────┤ + │ ▼ + │ Phase 5 (REPORTING) ★ + │ + ├─► Phase 6 (local axis) ──┐ + │ ▼ + └─► Phase 7 (combinations + self-weight) + │ + ▼ + Phase 8 (UI/UX) ──► Phase 9 (deferred) +``` + +**Critical path to the v1 promise (a printable case report):** +`Phase 3 → Phase 4 → Phase 5`, with `Phase 1` as a cheap prerequisite and +`Phase 2` interleavable. + +## 7. Licensing & attribution + +- This repo is **AGPL-3.0**; the rebuild is **MIT**. MIT code may be + incorporated into an AGPL work — **one-way**. Preserve the MIT notice: + add a `NOTICE` (Phase 0) and a short provenance header to each ported file. +- **Never** copy this repo's AGPL code back into the MIT rebuild. +- `opstool` is **GPLv3** (only relevant to deferred `sections_mesh`); GPLv3 ↔ + AGPLv3 are compatible. Keep it lazy/optional and do not vendor its source. +- Verify the provenance/license of any copied **icon/SVG assets** before + adopting the dark icon set (U7). + +## 8. Risks and mitigations + +| Risk | Mitigation | +|---|---| +| Big-bang port destabilises the working app | Strict additive phases; feature flags for diagram/report migration; keep old paths until parity is proven | +| Freezing/command rewrite stalls progress | Explicitly deferred (Phase 9); combinations/self-weight do **not** require it | +| Units enum change corrupts stored models | Keep 4-value enum; add a family mapping + `DisplayPrefs`; byte-identical dump test | +| Local-axis default breaks vertical members | Derive default per-element as today; override only on explicit user set; regression tests on vertical frames | +| opstool private-API coupling / GPL | Deferred; pin version + thin adapter; lazy import; attribution | +| Report pipeline pulls GUI deps into headless | Keep `matplotlib`/Typst optional and imported lazily; headless CI must pass without `[reports]` | +| `StrEnum` / 3.11-only syntax | Target py3.10: avoid `StrEnum`, `zip(strict=)`, etc. | +| No VCS in this checkout | Initialise git + branch before any code changes | +| Tests can't run locally (deps missing) | Phase 0 fixes env expectations; run gates in CI | + +## 9. Tracking + +Adopt the rebuild's `.otko-build/tasks.json` **pattern** (schema, gates, +`{id, phase, layer, specs, title, tests, status, deps}`), seeded from this +plan. Each task names the phase, the spec IDs it implements, and its tests +(the rebuild's `tasks.json` is a working reference). Keep build state out of +versioned source (`.gitignore` it) or commit it deliberately — team choice. + +## 10. Recommended first step + +Execute **Phase 0** in one branch. It is almost entirely additive, it turns +the currently-inert mypy/CI gate into a real one, and it establishes the +attribution and docs baseline the later ports depend on. Then proceed +`1 → 2 → 3 → 4 → 5`, pulling Phase 6/7 forward only if self-weight is needed +before reporting. diff --git a/specifications/README.md b/specifications/README.md new file mode 100644 index 0000000..3c8f8d4 --- /dev/null +++ b/specifications/README.md @@ -0,0 +1,37 @@ +# OTKO — specifications (this repo) + +This folder holds **OTKO's own planning documents**. It is deliberately +separate from the greenfield rebuild spec set at +`~/Sync/otko-development/specifications` (docs `00`–`14`), which stays the +reference for *what a conformant rebuild looks like*. Requirement IDs of +the form `[AREA-NNN]` cited in this folder refer to that rebuild set. + +## Why this folder exists + +`~/Sync/otko` (this repo) is the **working, feature-rich app** and is the +primary deliverable. `~/Sync/otko-development` is a **greenfield rebuild** +that conforms closely to the rebuild specs but is less capable in several +areas (transient/pushover/response-spectrum, richer element/material +coverage, `.osmodel` corpus, HDF5). The goal of the plan below is to +**harvest the rebuild's good, spec-conformant capabilities into this app +without breaking what already works** — not to rewrite this app to the +rebuild's architecture. + +## Documents + +| # | Document | Purpose | +|---|---|---| +| 15 | [15-rebuild-adoption-plan.md](15-rebuild-adoption-plan.md) | Dependency-ordered plan for porting rebuild capabilities into this repo, with adoption matrix, phases, gates, risks and explicit skips. | + +## Ground rules (summary — see doc 15 §3) + +- **Additive, never replacement.** This repo is a superset of the rebuild + in the solver/runner/export surface; protect that. +- **Keep the working app working:** `.osmodel` format, AGPL-3.0 license, + Python 3.10–3.12 support, existing tests green. +- **Adapt, don't copy:** re-type ported code to this repo's APIs + (`StaticResults`/`ModalResults`, mutable `Project`, 4-value `UnitSystem`). +- **One phase = one branch = independently shippable**, each with its own + tests and gate. +- **License hygiene:** the rebuild is MIT, this repo is AGPL-3.0; MIT→AGPL + is compatible, but ported files need attribution (see doc 15 §7). From ba783718d4ee4cdfea4229fa929d9595a0766dba Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 12:03:22 -0400 Subject: [PATCH 09/28] 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. --- CONTRIBUTING.md | 88 +++++- README.md | 7 + src/otko/commands/__init__.py | 60 +++- src/otko/commands/analysis.py | 12 +- src/otko/commands/base.py | 4 +- src/otko/commands/constraints.py | 3 +- src/otko/commands/loads.py | 17 +- src/otko/commands/materials.py | 7 +- src/otko/commands/nodes.py | 18 +- src/otko/commands/sections.py | 7 +- src/otko/commands/series_and_patterns.py | 4 +- src/otko/commands/transforms.py | 66 +++-- src/otko/core/__init__.py | 4 + src/otko/core/_base.py | 8 +- src/otko/core/analysis/__init__.py | 30 +- src/otko/core/defaults.py | 8 +- src/otko/core/geometry/elements.py | 11 +- src/otko/core/geometry/grid.py | 26 +- src/otko/core/loads/__init__.py | 68 ++++- src/otko/core/materials/__init__.py | 28 +- src/otko/core/project.py | 46 ++- src/otko/core/sections/__init__.py | 11 +- src/otko/core/units.py | 44 ++- src/otko/services/__init__.py | 10 + src/otko/services/_emit.py | 35 ++- src/otko/services/animation_export.py | 6 +- src/otko/services/deformation.py | 37 ++- src/otko/services/element_forces.py | 46 +-- src/otko/services/export.py | 6 +- src/otko/services/material_tester.py | 60 +++- src/otko/services/peer_record.py | 5 +- src/otko/services/qt_workers.py | 10 +- src/otko/services/results.py | 6 +- src/otko/services/section_bbox.py | 7 +- src/otko/services/section_properties.py | 18 +- src/otko/services/spectrum.py | 53 ++-- src/otko/viewmodels/analysis_runner.py | 2 +- src/otko/viewmodels/project_vm.py | 4 +- src/otko/views/action_handlers.py | 11 +- src/otko/views/canvas3d/diagram_renderer.py | 53 ++-- src/otko/views/canvas3d/model_canvas.py | 15 +- src/otko/views/canvas3d/style.py | 8 +- src/otko/views/dialogs/__init__.py | 2 + src/otko/views/dialogs/add_node.py | 31 +- src/otko/views/dialogs/assign_equal_dof.py | 17 +- src/otko/views/dialogs/assign_hinge.py | 55 ++-- src/otko/views/dialogs/assign_load.py | 4 +- src/otko/views/dialogs/assign_masses.py | 29 +- src/otko/views/dialogs/assign_property.py | 18 +- src/otko/views/dialogs/assign_support.py | 8 +- src/otko/views/dialogs/assign_zls.py | 22 +- src/otko/views/dialogs/case_forms.py | 119 +++++--- src/otko/views/dialogs/case_manager.py | 56 ++-- src/otko/views/dialogs/coord_grid_systems.py | 67 +++-- src/otko/views/dialogs/define_grid_data.py | 69 +++-- src/otko/views/dialogs/distributed_load.py | 8 +- src/otko/views/dialogs/grid_system.py | 15 +- src/otko/views/dialogs/locate_origin.py | 31 +- src/otko/views/dialogs/material_forms.py | 144 ++++++--- src/otko/views/dialogs/material_library.py | 12 +- src/otko/views/dialogs/mirror.py | 12 +- src/otko/views/dialogs/path_time_series.py | 40 +-- src/otko/views/dialogs/quick_grid_lines.py | 49 +++- src/otko/views/dialogs/quick_guide.py | 2 +- src/otko/views/dialogs/replicate.py | 12 +- src/otko/views/dialogs/run_analysis.py | 93 +++++- src/otko/views/dialogs/section_editor.py | 190 +++++++----- src/otko/views/dialogs/section_forms.py | 45 +-- src/otko/views/dialogs/section_library.py | 14 +- src/otko/views/dialogs/uniform_excitation.py | 17 +- src/otko/views/dock_manager.py | 15 +- src/otko/views/docks/__init__.py | 16 +- src/otko/views/docks/deformed_shape.py | 13 +- src/otko/views/docks/force_diagram.py | 9 +- src/otko/views/docks/property_editor.py | 80 +++-- src/otko/views/docks/response_spectrum.py | 40 ++- src/otko/views/docks/results_panel.py | 60 +++- src/otko/views/docks/table_dock.py | 64 ++++ src/otko/views/main_window.py | 57 +++- src/otko/views/menu_builder.py | 14 +- src/otko/views/render_controls.py | 2 +- src/otko/views/tools/__init__.py | 10 +- src/otko/views/tools/base.py | 7 +- src/otko/views/tools/draw_frame.py | 4 +- src/otko/views/tools/draw_node.py | 41 +-- tests/conftest.py | 30 +- tests/gui/test_analysis_commands.py | 51 +++- tests/gui/test_analysis_runner.py | 49 +++- tests/gui/test_assign_load_pattern_name.py | 64 ++-- tests/gui/test_assign_masses.py | 4 +- tests/gui/test_assign_zls.py | 22 +- tests/gui/test_canvas_grid_snap.py | 71 +++-- tests/gui/test_commands.py | 41 ++- tests/gui/test_convert_element_type.py | 159 +++++++--- tests/gui/test_define_dialogs.py | 10 +- tests/gui/test_draw_frame_tool.py | 48 +-- tests/gui/test_draw_node_tool.py | 13 +- tests/gui/test_draw_truss_tool.py | 28 +- tests/gui/test_grid_dialog.py | 19 +- tests/gui/test_main_window.py | 19 ++ tests/gui/test_material_dialog_concrete04.py | 17 +- tests/gui/test_material_form_ranges.py | 7 +- tests/gui/test_path_ts_dialogs.py | 54 +++- tests/gui/test_property_commands.py | 118 +++++--- tests/gui/test_property_forms.py | 24 +- tests/gui/test_pushover_curve_units.py | 18 +- tests/gui/test_run_analysis.py | 22 +- tests/gui/test_section_extrusion.py | 25 +- tests/gui/test_section_forms.py | 33 ++- tests/gui/test_transforms.py | 49 ++-- tests/gui/test_tree_multi_select.py | 14 +- tests/gui/test_units_status_bar.py | 4 + tests/gui/test_update_element_fields.py | 55 +++- tests/gui/test_working_plane.py | 13 +- tests/integration/test_basic_truss.py | 2 + tests/integration/test_beam_quad_2d.py | 9 +- tests/integration/test_concrete04_runner.py | 74 +++-- tests/integration/test_dof_coverage.py | 22 +- .../test_eigen_two_storey_one_bay_frame.py | 2 + .../test_eigen_two_storey_shear_frame.py | 2 + tests/integration/test_elastic_frame.py | 39 ++- tests/integration/test_ex1a_canti2d.py | 2 + tests/integration/test_ex1a_canti2d_eq.py | 2 + tests/integration/test_ex1b_portal2d.py | 2 + .../test_ex2a_canti2d_elastic_element.py | 2 + .../test_ex2b_canti2d_inelastic_section.py | 2 + ...st_ex2c_canti2d_inelastic_fiber_section.py | 2 + .../integration/test_ex3_canti2d_variants.py | 25 +- .../integration/test_ex4_portal2d_variants.py | 25 +- tests/integration/test_material_tester.py | 88 +++--- tests/integration/test_moment_curvature.py | 276 +++++++++++------- .../test_moment_curvature_example.py | 13 +- tests/integration/test_pattern_factors.py | 2 + tests/integration/test_rc_frame_earthquake.py | 11 +- tests/integration/test_rc_frame_gravity.py | 9 +- tests/integration/test_rc_frame_pushover.py | 11 +- .../integration/test_runner_imposed_motion.py | 3 + tests/integration/test_runner_modal.py | 20 +- tests/integration/test_runner_static.py | 18 +- tests/integration/test_runner_transient.py | 29 +- tests/tools/test_parse_schemas.py | 24 +- tests/unit/core/materials/test_concrete04.py | 44 ++- tests/unit/test_diagram_renderer.py | 20 +- tests/unit/test_grid_system.py | 16 +- tests/unit/test_materials.py | 23 +- tests/unit/test_node.py | 5 +- tests/unit/test_phase8a.py | 85 ++++-- tests/unit/test_phase8b.py | 160 ++++++---- tests/unit/test_phase8c.py | 96 +++--- tests/unit/test_phase9.py | 134 ++++++--- tests/unit/test_project.py | 54 +++- tests/unit/test_zerolength_section.py | 29 +- 152 files changed, 3394 insertions(+), 1651 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f911d7..ab9023e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,37 +1,97 @@ # Contributing -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. +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. ## Dev setup ```bash python -m venv .venv -source .venv/bin/activate -pip install -e ".[dev]" +source .venv/bin/activate # Linux / macOS +# .venv\Scripts\activate # Windows +pip install -e ".[gui,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 +ruff format src tests # line-length 100, E501 ignored mypy src/otko/core src/otko/services -pytest -m "not slow" +pytest -m "not slow" # CI gate: lint → this, on 3.10/3.11/3.12 × ubuntu/windows/macos ``` +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) -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. +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. ## Commit style Conventional Commits — `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, -`chore:`, `ci:`. +`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. diff --git a/README.md b/README.md index 38f1fd9..c291d67 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,13 @@ 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/src/otko/commands/__init__.py b/src/otko/commands/__init__.py index 4d6647e..82ccef5 100644 --- a/src/otko/commands/__init__.py +++ b/src/otko/commands/__init__.py @@ -6,6 +6,11 @@ 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, @@ -67,25 +72,50 @@ 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", + "AddMaterialsCommand", + "DeleteMaterialsCommand", + "UpdateMaterialCommand", + "AddSectionsCommand", + "DeleteSectionsCommand", + "UpdateSectionCommand", + "AddNodalLoadsCommand", + "AddElementLoadsCommand", + "UpdateNodalLoadCommand", + "DeleteNodalLoadsCommand", + "UpdateElementLoadCommand", + "DeleteElementLoadsCommand", + "MoveNodesCommand", + "ReplicateCommand", + "MirrorCommand", + "Plane", + "AddAnalysisCasesCommand", + "DeleteAnalysisCasesCommand", + "UpdateAnalysisCaseCommand", + "AddCombinationsCommand", + "DeleteCombinationsCommand", + "UpdateCombinationCommand", "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 c26dc22..5950b40 100644 --- a/src/otko/commands/analysis.py +++ b/src/otko/commands/analysis.py @@ -13,8 +13,9 @@ 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) @@ -42,12 +43,9 @@ class DeleteAnalysisCasesCommand(ProjectCommand): def redo(self) -> None: self._removed = [ - (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 + (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._notify() def undo(self) -> None: diff --git a/src/otko/commands/base.py b/src/otko/commands/base.py index c740057..15f616f 100644 --- a/src/otko/commands/base.py +++ b/src/otko/commands/base.py @@ -38,9 +38,7 @@ class ProjectCommand(QUndoCommand): @property 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/constraints.py b/src/otko/commands/constraints.py index 2fb051b..e3df65a 100644 --- a/src/otko/commands/constraints.py +++ b/src/otko/commands/constraints.py @@ -34,8 +34,7 @@ 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() diff --git a/src/otko/commands/loads.py b/src/otko/commands/loads.py index 7822caf..d24fabf 100644 --- a/src/otko/commands/loads.py +++ b/src/otko/commands/loads.py @@ -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,7 +101,10 @@ 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: @@ -157,7 +160,10 @@ 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)) @@ -171,7 +177,10 @@ 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 bce4088..836ade9 100644 --- a/src/otko/commands/materials.py +++ b/src/otko/commands/materials.py @@ -13,7 +13,9 @@ 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) @@ -41,8 +43,7 @@ class DeleteMaterialsCommand(ProjectCommand): 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 diff --git a/src/otko/commands/nodes.py b/src/otko/commands/nodes.py index b285159..b737265 100644 --- a/src/otko/commands/nodes.py +++ b/src/otko/commands/nodes.py @@ -14,7 +14,9 @@ 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) @@ -42,14 +44,15 @@ class DeleteNodesCommand(ProjectCommand): 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} @@ -58,7 +61,8 @@ 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} @@ -69,9 +73,7 @@ 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: diff --git a/src/otko/commands/sections.py b/src/otko/commands/sections.py index ecd6054..e75a54f 100644 --- a/src/otko/commands/sections.py +++ b/src/otko/commands/sections.py @@ -13,7 +13,9 @@ 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) @@ -41,8 +43,7 @@ class DeleteSectionsCommand(ProjectCommand): 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 diff --git a/src/otko/commands/series_and_patterns.py b/src/otko/commands/series_and_patterns.py index 8e11b6b..1d1062c 100644 --- a/src/otko/commands/series_and_patterns.py +++ b/src/otko/commands/series_and_patterns.py @@ -31,9 +31,7 @@ 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() diff --git a/src/otko/commands/transforms.py b/src/otko/commands/transforms.py index b8d60cc..3e48448 100644 --- a/src/otko/commands/transforms.py +++ b/src/otko/commands/transforms.py @@ -45,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: @@ -84,7 +84,8 @@ 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,30 +96,34 @@ 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 ] @@ -166,7 +171,8 @@ 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() @@ -176,28 +182,30 @@ 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 9443696..34e7482 100644 --- a/src/otko/core/__init__.py +++ b/src/otko/core/__init__.py @@ -50,6 +50,8 @@ from otko.core.loads import ( ConstantTimeSeries, ImposedSupportMotionPattern, LinearTimeSeries, + LoadCombination, + LoadCombinationItem, LoadPattern, NodalLoad, PathTimeSeries, @@ -165,6 +167,8 @@ __all__ = [ "PlainLoadPattern", "UniformExcitationPattern", "ImposedSupportMotionPattern", + "LoadCombination", + "LoadCombinationItem", "NodalLoad", "UniformElementLoad", "ResponseSpectrum", diff --git a/src/otko/core/_base.py b/src/otko/core/_base.py index 72e9d2f..da3e9b5 100644 --- a/src/otko/core/_base.py +++ b/src/otko/core/_base.py @@ -16,11 +16,13 @@ class Entity(BaseModel): """Base class for every persisted domain object.""" model_config = ConfigDict( - frozen=False, # individual setters allowed; we lock at the Project boundary - extra="forbid", # unknown JSON keys are an error, not a silent ignore + frozen=False, # individual setters allowed; we lock at the Project boundary + extra="forbid", # unknown JSON keys are an error, not a silent ignore validate_assignment=True, populate_by_name=True, ) - id: PositiveInt = Field(..., description="Unique tag within its kind. Used as the OpenSees tag.") + id: PositiveInt = Field( + ..., description="Unique tag within its kind. Used as the OpenSees tag." + ) name: str = Field(default="", description="Optional human-readable label.") diff --git a/src/otko/core/analysis/__init__.py b/src/otko/core/analysis/__init__.py index eba5330..b809bad 100644 --- a/src/otko/core/analysis/__init__.py +++ b/src/otko/core/analysis/__init__.py @@ -83,7 +83,8 @@ 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" @@ -100,7 +101,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, @@ -114,11 +115,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." @@ -195,7 +196,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" @@ -244,19 +245,26 @@ 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.", ) diff --git a/src/otko/core/defaults.py b/src/otko/core/defaults.py index 9742d21..0980d6b 100644 --- a/src/otko/core/defaults.py +++ b/src/otko/core/defaults.py @@ -159,12 +159,8 @@ 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/elements.py b/src/otko/core/geometry/elements.py index f78588f..f79ae39 100644 --- a/src/otko/core/geometry/elements.py +++ b/src/otko/core/geometry/elements.py @@ -91,7 +91,9 @@ class ZeroLengthElement(Entity): type: Literal["ZeroLength"] = "ZeroLength" nodes: tuple[PositiveInt, PositiveInt] material_ids: tuple[PositiveInt, ...] = Field(..., min_length=1) - dofs: tuple[int, ...] = Field(..., min_length=1, description="DOF directions, 1-indexed (1..6).") + dofs: tuple[int, ...] = Field( + ..., min_length=1, description="DOF directions, 1-indexed (1..6)." + ) do_rayleigh: bool = Field( default=False, description=( @@ -121,7 +123,9 @@ class ZeroLengthSectionElement(Entity): type: Literal["ZeroLengthSection"] = "ZeroLengthSection" nodes: tuple[PositiveInt, PositiveInt] - section_id: PositiveInt = Field(..., description="Section attached to the two coincident nodes.") + section_id: PositiveInt = Field( + ..., description="Section attached to the two coincident nodes." + ) class BeamWithHingesElement(Entity): @@ -179,7 +183,8 @@ class QuadElement(Entity): description="Surface pressure applied over the element (force / area).", ) rho: float = Field( - default=0.0, ge=0.0, + default=0.0, + ge=0.0, description="Mass density override (kip·s²/in⁴). Leave 0 to use material rho.", ) b1: float = Field( diff --git a/src/otko/core/geometry/grid.py b/src/otko/core/geometry/grid.py index d726f68..cf6ecc7 100644 --- a/src/otko/core/geometry/grid.py +++ b/src/otko/core/geometry/grid.py @@ -87,10 +87,12 @@ 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 @@ -121,21 +123,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): @@ -160,9 +162,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/loads/__init__.py b/src/otko/core/loads/__init__.py index 77429fc..283dfad 100644 --- a/src/otko/core/loads/__init__.py +++ b/src/otko/core/loads/__init__.py @@ -14,7 +14,7 @@ from __future__ import annotations from typing import Annotated, Literal, Union -from pydantic import BaseModel, ConfigDict, Field, PositiveInt +from pydantic import BaseModel, ConfigDict, Field, PositiveInt, model_validator from otko.core._base import Entity @@ -77,15 +77,19 @@ 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.", ) @@ -185,12 +189,66 @@ 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[ Union[PlainLoadPattern, UniformExcitationPattern, ImposedSupportMotionPattern], Field(discriminator="type"), diff --git a/src/otko/core/materials/__init__.py b/src/otko/core/materials/__init__.py index a12d75e..be71418 100644 --- a/src/otko/core/materials/__init__.py +++ b/src/otko/core/materials/__init__.py @@ -58,7 +58,9 @@ 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) @@ -83,7 +85,10 @@ 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.") @@ -108,15 +113,19 @@ 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.", ) @@ -138,7 +147,8 @@ 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.") @@ -176,7 +186,8 @@ 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).", ) @@ -206,7 +217,8 @@ 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( diff --git a/src/otko/core/project.py b/src/otko/core/project.py index 5ed9046..175f7a4 100644 --- a/src/otko/core/project.py +++ b/src/otko/core/project.py @@ -30,7 +30,7 @@ from otko.core.geometry import ( Node, default_global_system, ) -from otko.core.loads import LoadPattern, ResponseSpectrum, TimeSeries +from otko.core.loads import LoadCombination, LoadPattern, ResponseSpectrum, TimeSeries from otko.core.materials import Material from otko.core.sections import Section from otko.core.units import UnitSystem @@ -110,6 +110,7 @@ _INDEX_KINDS: tuple[str, ...] = ( "load_patterns", "spectra", "analyses", + "combinations", ) @@ -155,11 +156,13 @@ class Project(BaseModel): """ if isinstance(data, dict) and "grid_system" in data and "coord_systems" not in data: legacy = data.pop("grid_system") - data["coord_systems"] = [{ - "name": "Global", - "coord": {}, - "grid": legacy, - }] + data["coord_systems"] = [ + { + "name": "Global", + "coord": {}, + "grid": legacy, + } + ] return data @model_validator(mode="after") @@ -203,10 +206,12 @@ class Project(BaseModel): return # No Global system yet — create one with this grid. from otko.core.geometry import CoordinateGridSystem + self.coord_systems.insert( 0, CoordinateGridSystem(name="Global", grid=new_grid), ) + sections: list[Section] = Field(default_factory=list) elements: list[Element] = Field(default_factory=list) mp_constraints: list[EqualDOFConstraint] = Field(default_factory=list) @@ -214,6 +219,7 @@ 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") @@ -236,6 +242,7 @@ 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)): @@ -270,6 +277,10 @@ 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")) @@ -283,6 +294,20 @@ 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. @@ -395,5 +420,14 @@ 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 5e8ce0e..595b09b 100644 --- a/src/otko/core/sections/__init__.py +++ b/src/otko/core/sections/__init__.py @@ -75,8 +75,12 @@ 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=( @@ -198,7 +202,8 @@ 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).", ) diff --git a/src/otko/core/units.py b/src/otko/core/units.py index 426ccd2..63b40cc 100644 --- a/src/otko/core/units.py +++ b/src/otko/core/units.py @@ -42,30 +42,46 @@ class UnitLabels: label axes without hard-coding any particular set of units. """ - length: str # "m", "mm", "in", "ft" - force: str # "N", "kip" - moment: str # "N·m", "kip·in" - stress: str # "Pa", "MPa", "ksi", "ksf" - curvature: str # "1/m", "1/in", … - rotation: str # "rad" (always, no unit variants in practice) + length: str # "m", "mm", "in", "ft" + force: str # "N", "kip" + moment: str # "N·m", "kip·in" + stress: str # "Pa", "MPa", "ksi", "ksf" + curvature: str # "1/m", "1/in", … + rotation: str # "rad" (always, no unit variants in practice) _LABELS: dict[UnitSystem, UnitLabels] = { UnitSystem.SI_M_N: UnitLabels( - length="m", force="N", moment="N·m", - stress="Pa", curvature="1/m", rotation="rad", + length="m", + force="N", + moment="N·m", + stress="Pa", + curvature="1/m", + rotation="rad", ), UnitSystem.SI_MM_N: UnitLabels( - length="mm", force="N", moment="N·mm", - stress="MPa", curvature="1/mm", rotation="rad", + 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", + length="ft", + force="kip", + moment="kip·ft", + stress="ksf", + curvature="1/ft", + rotation="rad", ), UnitSystem.US_IN_KIP: UnitLabels( - length="in", force="kip", moment="kip·in", - stress="ksi", curvature="1/in", rotation="rad", + length="in", + force="kip", + moment="kip·in", + stress="ksi", + curvature="1/in", + rotation="rad", ), } diff --git a/src/otko/services/__init__.py b/src/otko/services/__init__.py index b14f725..0c14bb3 100644 --- a/src/otko/services/__init__.py +++ b/src/otko/services/__init__.py @@ -6,6 +6,12 @@ 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, @@ -28,12 +34,16 @@ 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 61e7530..ea6507a 100644 --- a/src/otko/services/_emit.py +++ b/src/otko/services/_emit.py @@ -409,7 +409,10 @@ 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] @@ -430,9 +433,13 @@ 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 @@ -647,15 +654,27 @@ 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 c23f5ea..20c3f53 100644 --- a/src/otko/services/animation_export.py +++ b/src/otko/services/animation_export.py @@ -55,8 +55,7 @@ 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,5 +93,4 @@ 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/deformation.py b/src/otko/services/deformation.py index a93340f..8c60f7b 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,8 +38,11 @@ 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. @@ -57,13 +60,16 @@ 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. @@ -78,10 +84,9 @@ 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 @@ -101,12 +106,15 @@ 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. @@ -139,8 +147,7 @@ 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 36de591..d802579 100644 --- a/src/otko/services/element_forces.py +++ b/src/otko/services/element_forces.py @@ -41,27 +41,36 @@ 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: @@ -69,10 +78,12 @@ _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, } @@ -122,6 +133,7 @@ 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 1ae5710..0f3a22c 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 import __version__ +from otko._const import OPENSEESPY_VERSION, __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 = "openseespy==3.8.0.0" +PINNED_OPENSEESPY = f"openseespy=={OPENSEESPY_VERSION}" #: Recorded call triple: (ops method name, positional args, keyword args). RecordedCall = tuple[str, tuple[Any, ...], dict[str, Any]] @@ -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 ``openseespy==3.8.0.0``, + The script source. The header pins the solver version, 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 fe70fbc..e494521 100644 --- a/src/otko/services/material_tester.py +++ b/src/otko/services/material_tester.py @@ -63,7 +63,9 @@ 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.") @@ -160,17 +162,35 @@ 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] @@ -187,10 +207,25 @@ 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__}") @@ -239,10 +274,13 @@ 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 618c2e5..b6f6e6c 100644 --- a/src/otko/services/peer_record.py +++ b/src/otko/services/peer_record.py @@ -46,7 +46,8 @@ 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)) @@ -66,7 +67,7 @@ def parse_peer_record(path: str | Path) -> tuple[float, int, list[float]]: try: values.append(float(tok)) except ValueError: - pass # skip stray tokens + pass # skip stray tokens if not values: raise ValueError("Header parsed but no numeric data lines found.") return dt, npts, values diff --git a/src/otko/services/qt_workers.py b/src/otko/services/qt_workers.py index 4419a1b..aeaf199 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,8 +52,10 @@ 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 43b608f..3163ed5 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 edabcc6..b3438b8 100644 --- a/src/otko/services/section_bbox.py +++ b/src/otko/services/section_bbox.py @@ -52,8 +52,10 @@ 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 @@ -69,6 +71,7 @@ def bbox_for_section(section: object, project: "Project | None" = None, 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 a041721..a38d4e4 100644 --- a/src/otko/services/section_properties.py +++ b/src/otko/services/section_properties.py @@ -35,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: @@ -65,9 +65,11 @@ 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: @@ -102,8 +104,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 a242dbf..9cde026 100644 --- a/src/otko/services/spectrum.py +++ b/src/otko/services/spectrum.py @@ -24,20 +24,22 @@ 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. @@ -79,21 +81,23 @@ 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 @@ -137,8 +141,7 @@ 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) @@ -156,7 +159,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 @@ -169,8 +172,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 7046e0c..5773307 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 148371d..6ed5dbd 100644 --- a/src/otko/viewmodels/project_vm.py +++ b/src/otko/viewmodels/project_vm.py @@ -23,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: diff --git a/src/otko/views/action_handlers.py b/src/otko/views/action_handlers.py index e0f3651..bcd9305 100644 --- a/src/otko/views/action_handlers.py +++ b/src/otko/views/action_handlers.py @@ -47,6 +47,7 @@ from otko.views.dialogs import ( AssignSectionDialog, AssignSupportDialog, AssignZeroLengthSectionDialog, + LoadCombinationManagerDialog, CoordinateGridSystemsDialog, LinearTimeSeriesDialog, MaterialLibraryDialog, @@ -917,6 +918,11 @@ 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.") @@ -961,10 +967,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)) diff --git a/src/otko/views/canvas3d/diagram_renderer.py b/src/otko/views/canvas3d/diagram_renderer.py index 949fa35..28b6f82 100644 --- a/src/otko/views/canvas3d/diagram_renderer.py +++ b/src/otko/views/canvas3d/diagram_renderer.py @@ -34,14 +34,21 @@ from otko.services.element_forces import DiagramData, ForceComponent _LOG = logging.getLogger("otko.diagram") # Components that draw perpendicular to the element axis vs along it. -_PERPENDICULAR = {ForceComponent.V2, ForceComponent.V3, - ForceComponent.M2, ForceComponent.M3, ForceComponent.T} +_PERPENDICULAR = { + ForceComponent.V2, + ForceComponent.V3, + ForceComponent.M2, + ForceComponent.M3, + ForceComponent.T, +} # Which local axis the value is plotted along (2 = local y, 3 = local z). _LOCAL_AXIS = { - ForceComponent.V2: 2, ForceComponent.M3: 2, - ForceComponent.V3: 3, ForceComponent.M2: 3, - ForceComponent.T: 2, + ForceComponent.V2: 2, + ForceComponent.M3: 2, + ForceComponent.V3: 3, + ForceComponent.M2: 3, + ForceComponent.T: 2, } @@ -69,8 +76,10 @@ class DiagramRenderer: # All values are zero → no diagram to draw. This is normal — # 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.", data.component.name) + _LOG.info( + "All '%s' values are zero for this analysis step " "— nothing to draw.", + data.component.name, + ) return node_pos = {n.id: np.asarray(n.coords, dtype=float) for n in project.nodes} @@ -79,8 +88,8 @@ class DiagramRenderer: is_perpendicular = data.component in _PERPENDICULAR axis_id = _LOCAL_AXIS.get(data.component, 2) - polys: list[np.ndarray] = [] # vertex arrays for each polygon - scalars: list[float] = [] # one value per polygon (avg of end values) + polys: list[np.ndarray] = [] # vertex arrays for each polygon + scalars: list[float] = [] # one value per polygon (avg of end values) for k, eid in enumerate(data.element_ids): elem = elem_lookup.get(int(eid)) if elem is None: @@ -106,15 +115,19 @@ class DiagramRenderer: perp = self._local_perp(pi, pj, 2) if perp is None: continue - width = abs(v_i) * 0.5 # half-width fall-off + width = abs(v_i) * 0.5 # half-width fall-off if width == 0.0: width = abs(v_j) * 0.5 if width == 0.0: continue - quad = np.vstack([ - pi - perp * width, pj - perp * width, - pj + perp * width, pi + perp * width, - ]) + quad = np.vstack( + [ + pi - perp * width, + pj - perp * width, + pj + perp * width, + pi + perp * width, + ] + ) polys.append(quad) scalars.append(0.5 * (data.values_i[k] + data.values_j[k])) @@ -138,7 +151,9 @@ class DiagramRenderer: # Symmetric color range so zero stays at the colormap mid-point. vmax = float(np.max(np.abs(scalars))) or 1.0 # Defensive: accept either ForceComponent enum or its name string. - comp_label = data.component.value if hasattr(data.component, "value") else str(data.component) + comp_label = ( + data.component.value if hasattr(data.component, "value") else str(data.component) + ) self._actor = self._plotter.add_mesh( mesh, scalars="value", @@ -157,8 +172,7 @@ class DiagramRenderer: # ── Numerical labels at the global min and max element ends. ── self._label_actor = self._add_value_labels(project, data, scale) - def _add_value_labels(self, project: Project, data: DiagramData, - scale: float) -> Any: + def _add_value_labels(self, project: Project, data: DiagramData, scale: float) -> Any: """Annotate the diagram's extreme ends with their numerical values. Avoids visual clutter by labelling only the two ends carrying the @@ -203,9 +217,10 @@ class DiagramRenderer: try: return self._plotter.add_point_labels( - positions, labels, + positions, + labels, font_size=14, - point_size=0, # don't draw the underlying points + point_size=0, # don't draw the underlying points shape=None, always_visible=True, pickable=False, diff --git a/src/otko/views/canvas3d/model_canvas.py b/src/otko/views/canvas3d/model_canvas.py index b761d62..b1b3417 100644 --- a/src/otko/views/canvas3d/model_canvas.py +++ b/src/otko/views/canvas3d/model_canvas.py @@ -12,7 +12,6 @@ needing a reference to the renderer. from __future__ import annotations -import os from typing import Any import numpy as np @@ -26,7 +25,7 @@ from otko.views.canvas3d.model_renderer import ModelRenderer from otko.views.canvas3d.selection import SelectionState from otko.views.canvas3d.style import RenderStyle -_PICK_DEBUG = os.environ.get("OSS_PICK_DEBUG") == "1" +PICK_DEBUG = False class ModelCanvas(QtInteractor): # type: ignore[misc] @@ -127,7 +126,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}" @@ -148,7 +147,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 " @@ -181,7 +180,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 " @@ -191,7 +190,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. ── @@ -202,7 +201,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: @@ -260,7 +259,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 " diff --git a/src/otko/views/canvas3d/style.py b/src/otko/views/canvas3d/style.py index 21b7a34..f25d674 100644 --- a/src/otko/views/canvas3d/style.py +++ b/src/otko/views/canvas3d/style.py @@ -19,15 +19,15 @@ class RenderStyle: background_top: str = "#dbe2ef" background_bottom: str = "#f5f7fb" - node_color: str = "#f0a500" # warm gold - node_selected_color: str = "#00d4ff" # bright cyan + node_color: str = "#f0a500" # warm gold + node_selected_color: str = "#00d4ff" # bright cyan frame_color: str = "#1f1f1f" truss_color: str = "#2e5cb8" zerolength_color: str = "#a020f0" selected_color: str = "#00d4ff" - fix_color: str = "#c0392b" # firebrick + fix_color: str = "#c0392b" # firebrick pin_color: str = "#c0392b" roller_color: str = "#e67e22" custom_support_color: str = "#7f8c8d" @@ -45,7 +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 diff --git a/src/otko/views/dialogs/__init__.py b/src/otko/views/dialogs/__init__.py index 73305a1..d2a3b45 100644 --- a/src/otko/views/dialogs/__init__.py +++ b/src/otko/views/dialogs/__init__.py @@ -12,6 +12,7 @@ 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, @@ -60,6 +61,7 @@ __all__ = [ "DisplayOptionsDialog", "GridSystemDialog", "LinearTimeSeriesDialog", + "LoadCombinationManagerDialog", "MaterialLibraryDialog", "MaterialTesterDialog", "MirrorDialog", diff --git a/src/otko/views/dialogs/add_node.py b/src/otko/views/dialogs/add_node.py index d58c577..1c5ddc5 100644 --- a/src/otko/views/dialogs/add_node.py +++ b/src/otko/views/dialogs/add_node.py @@ -40,12 +40,15 @@ class AddNodeDialog(QDialog): the node lands on the active plane by default. """ - def __init__(self, next_node_id: int, - grid: GridSystem, - ndm: int = 3, - parent: QWidget | None = None, - *, - plane_elevation: float | None = None) -> None: + def __init__( + self, + next_node_id: int, + grid: GridSystem, + ndm: int = 3, + parent: QWidget | None = None, + *, + plane_elevation: float | None = None, + ) -> None: super().__init__(parent) self.setWindowTitle("Add Node") self._next_id = next_node_id @@ -71,16 +74,18 @@ class AddNodeDialog(QDialog): 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}).", - )) + form.addRow( + QLabel( + f"Suggested Z = active working-plane level " + f"({self._plane_elevation:g}).", + ) + ) root.addLayout(form) self._snap_cb = QCheckBox("Snap to nearest grid intersection") - self._snap_cb.setChecked(bool( - self._grid.x_lines or self._grid.y_lines or self._grid.z_lines - )) + self._snap_cb.setChecked( + bool(self._grid.x_lines or self._grid.y_lines or self._grid.z_lines) + ) root.addWidget(self._snap_cb) buttons = QDialogButtonBox( diff --git a/src/otko/views/dialogs/assign_equal_dof.py b/src/otko/views/dialogs/assign_equal_dof.py index 38f5094..9ebf0ec 100644 --- a/src/otko/views/dialogs/assign_equal_dof.py +++ b/src/otko/views/dialogs/assign_equal_dof.py @@ -42,10 +42,12 @@ 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() @@ -64,7 +66,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: @@ -79,7 +81,10 @@ 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 bdf993d..7e5f4e3 100644 --- a/src/otko/views/dialogs/assign_hinge.py +++ b/src/otko/views/dialogs/assign_hinge.py @@ -64,9 +64,7 @@ class AssignHingeDialog(QDialog): def _build_ui(self, n_selected: int) -> None: layout = QVBoxLayout(self) - header = ( - f"Convert {n_selected} selected element(s) to BeamWithHinges." - ) + header = f"Convert {n_selected} selected element(s) to BeamWithHinges." prefill = self._selection_prefill() if prefill is None and self._element_ids: header += " Selection is mixed — unit-scaled defaults shown." @@ -160,18 +158,20 @@ class AssignHingeDialog(QDialog): if el is None: return None if getattr(el, "type", None) == "BeamWithHinges": - collected.append({ - "section_i_id": getattr(el, "section_i_id", None), - "section_j_id": getattr(el, "section_j_id", None), - "lp_i": getattr(el, "lp_i", None), - "lp_j": getattr(el, "lp_j", None), - "E": getattr(el, "E", None), - "A": getattr(el, "A", None), - "Iz": getattr(el, "Iz", None), - "Iy": getattr(el, "Iy", None), - "G": getattr(el, "G", None), - "J": getattr(el, "J", None), - }) + collected.append( + { + "section_i_id": getattr(el, "section_i_id", None), + "section_j_id": getattr(el, "section_j_id", None), + "lp_i": getattr(el, "lp_i", None), + "lp_j": getattr(el, "lp_j", None), + "E": getattr(el, "E", None), + "A": getattr(el, "A", None), + "Iz": getattr(el, "Iz", None), + "Iy": getattr(el, "Iy", None), + "G": getattr(el, "G", None), + "J": getattr(el, "J", None), + } + ) continue sec_id = getattr(el, "section_id", None) if sec_id is None: @@ -185,21 +185,22 @@ class AssignHingeDialog(QDialog): # prefill the hinge pickers with the section itself. collected.append({"section_i_id": sec_id, "section_j_id": sec_id}) continue - collected.append({ - "section_i_id": sec_id, - "section_j_id": sec_id, - "E": getattr(sec, "E", None), - "A": getattr(sec, "A", None), - "Iz": getattr(sec, "Iz", None), - "Iy": getattr(sec, "Iy", None), - "G": getattr(sec, "G", None), - "J": getattr(sec, "J", None), - }) + collected.append( + { + "section_i_id": sec_id, + "section_j_id": sec_id, + "E": getattr(sec, "E", None), + "A": getattr(sec, "A", None), + "Iz": getattr(sec, "Iz", None), + "Iy": getattr(sec, "Iy", None), + "G": getattr(sec, "G", None), + "J": getattr(sec, "J", None), + } + ) if not collected: return None merged: dict = {} - for key in ("section_i_id", "section_j_id", "lp_i", "lp_j", - "E", "A", "Iz", "Iy", "G", "J"): + for key in ("section_i_id", "section_j_id", "lp_i", "lp_j", "E", "A", "Iz", "Iy", "G", "J"): vals = {c.get(key) for c in collected if c.get(key) is not None} if len(vals) == 1: merged[key] = next(iter(vals)) diff --git a/src/otko/views/dialogs/assign_load.py b/src/otko/views/dialogs/assign_load.py index c77d2ad..9ab8c3c 100644 --- a/src/otko/views/dialogs/assign_load.py +++ b/src/otko/views/dialogs/assign_load.py @@ -80,9 +80,7 @@ class AssignLoadDialog(QDialog): pf.addRow("Active:", self._active_pattern_label) self._new_name_edit = QLineEdit("Pattern") - self._new_name_edit.setPlaceholderText( - "Name for the new pattern (e.g. RefMoment)" - ) + self._new_name_edit.setPlaceholderText("Name for the new pattern (e.g. RefMoment)") pf.addRow("New name:", self._new_name_edit) # TimeSeries type selector — only used when creating a new diff --git a/src/otko/views/dialogs/assign_masses.py b/src/otko/views/dialogs/assign_masses.py index 5a8f59c..6592e44 100644 --- a/src/otko/views/dialogs/assign_masses.py +++ b/src/otko/views/dialogs/assign_masses.py @@ -56,16 +56,22 @@ class AssignMassesDialog(QDialog): 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) - self._my = self._spin(cur[1]); form.addRow("Translation Y:", self._my) + self._mx = self._spin(cur[0]) + form.addRow("Translation X:", self._mx) + self._my = self._spin(cur[1]) + form.addRow("Translation Y:", self._my) if self._ndf >= 3: - self._mz = self._spin(cur[2]); form.addRow("Translation Z:", self._mz) + self._mz = self._spin(cur[2]) + form.addRow("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) - self._myy = self._spin(cur[4]); form.addRow("Rotation Y (Iyy):", self._myy) - self._mzz = self._spin(cur[5]); form.addRow("Rotation Z (Izz):", self._mzz) + self._mxx = self._spin(cur[3]) + form.addRow("Rotation X (Ixx):", self._mxx) + self._myy = self._spin(cur[4]) + form.addRow("Rotation Y (Iyy):", self._myy) + self._mzz = self._spin(cur[5]) + form.addRow("Rotation Z (Izz):", self._mzz) else: self._mxx = self._spin(cur[3]) self._myy = self._spin(cur[4]) @@ -77,8 +83,7 @@ class AssignMassesDialog(QDialog): root.addWidget(self._xy_link) buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok - | QDialogButtonBox.StandardButton.Cancel, + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, ) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) @@ -106,6 +111,10 @@ class AssignMassesDialog(QDialog): def mass_vector(self) -> tuple[float, float, float, float, float, float]: """Return the 6-tuple (Mx, My, Mz, Mxx, Myy, Mzz).""" return ( - self._mx.value(), self._my.value(), self._mz.value(), - self._mxx.value(), self._myy.value(), self._mzz.value(), + self._mx.value(), + self._my.value(), + self._mz.value(), + self._mxx.value(), + self._myy.value(), + self._mzz.value(), ) diff --git a/src/otko/views/dialogs/assign_property.py b/src/otko/views/dialogs/assign_property.py index 6127e99..5d62f4a 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 61f4872..b742ebe 100644 --- a/src/otko/views/dialogs/assign_support.py +++ b/src/otko/views/dialogs/assign_support.py @@ -25,10 +25,10 @@ from PySide6.QtWidgets import ( # 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 c1d23f0..b2c580c 100644 --- a/src/otko/views/dialogs/assign_zls.py +++ b/src/otko/views/dialogs/assign_zls.py @@ -25,8 +25,9 @@ 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 @@ -35,12 +36,14 @@ 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() @@ -53,8 +56,7 @@ 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 be1309c..b8f7b24 100644 --- a/src/otko/views/dialogs/case_forms.py +++ b/src/otko/views/dialogs/case_forms.py @@ -37,9 +37,14 @@ from otko.core import ( # ─────────────────────────── helpers ─────────────────────────── -def _spin(default: float = 0.0, *, decimals: int = 6, - minimum: float = -1e15, maximum: float = 1e15, - step: float = 1.0) -> QDoubleSpinBox: +def _spin( + default: float = 0.0, + *, + decimals: int = 6, + minimum: float = -1e15, + maximum: float = 1e15, + step: float = 1.0, +) -> QDoubleSpinBox: sb = QDoubleSpinBox() sb.setRange(minimum, maximum) sb.setDecimals(decimals) @@ -227,7 +232,14 @@ class CaseFormBase(QWidget): # ─────────────────────────── Static ─────────────────────────── -_STATIC_SYSTEMS = ["BandGeneral", "BandSPD", "ProfileSPD", "SparseGeneral", "UmfPack", "FullGeneral"] +_STATIC_SYSTEMS = [ + "BandGeneral", + "BandSPD", + "ProfileSPD", + "SparseGeneral", + "UmfPack", + "FullGeneral", +] _CONSTRAINTS = ["Plain", "Lagrange", "Penalty", "Transformation"] _INTEGRATORS_STATIC = ["LoadControl", "DisplacementControl", "ArcLength"] _ALGORITHMS = ["Linear", "Newton", "ModifiedNewton", "KrylovNewton", "BFGS", "Broyden"] @@ -291,7 +303,8 @@ class StaticCaseForm(CaseFormBase): def _read_specific(self, cid: int) -> StaticCase: return StaticCase( - id=cid, name=self._name_edit.text(), + id=cid, + name=self._name_edit.text(), pattern_ids=_require_pattern_ids(self._patterns_picker), **_factor_kwargs(StaticCase, self._patterns_picker, self._pattern_factor_spins), n_steps=self._n_steps.value(), @@ -322,9 +335,9 @@ class ModalCaseForm(CaseFormBase): self._solver.addItems(["genBandArpack", "fullGenLapack", "symmBandLapack"]) self._layout.addRow("Number of modes:", self._n_modes) self._layout.addRow("Solver:", self._solver) - self._layout.addRow(QLabel( - "The runner auto-falls back to fullGenLapack for very small models." - )) + self._layout.addRow( + QLabel("The runner auto-falls back to fullGenLapack for very small models.") + ) def _populate_specific(self, c: ModalCase) -> None: self._n_modes.setValue(c.n_modes) @@ -332,7 +345,8 @@ class ModalCaseForm(CaseFormBase): def _read_specific(self, cid: int) -> ModalCase: return ModalCase( - id=cid, name=self._name_edit.text(), + id=cid, + name=self._name_edit.text(), n_modes=self._n_modes.value(), solver=self._solver.currentText(), ) @@ -359,13 +373,19 @@ class TransientCaseForm(CaseFormBase): self._remove_patterns_picker = _make_pattern_picker(patterns) self._dt = _spin(0.01, decimals=8, minimum=1e-12, step=1e-3) self._n_steps = _int_spin(1000, minimum=1, maximum=10_000_000) - self._system = QComboBox(); self._system.addItems(_STATIC_SYSTEMS) - self._constraints = QComboBox(); self._constraints.addItems(_CONSTRAINTS) - self._integrator = QComboBox(); self._integrator.addItems(_INTEGRATORS_TRANSIENT) + self._system = QComboBox() + self._system.addItems(_STATIC_SYSTEMS) + self._constraints = QComboBox() + self._constraints.addItems(_CONSTRAINTS) + self._integrator = QComboBox() + self._integrator.addItems(_INTEGRATORS_TRANSIENT) self._gamma = _spin(0.5, decimals=4, minimum=0.0, maximum=1.0, step=0.01) self._beta = _spin(0.25, decimals=4, minimum=0.0, maximum=1.0, step=0.01) - self._algorithm = QComboBox(); self._algorithm.addItems(_ALGORITHMS); self._algorithm.setCurrentText("Newton") - self._test = QComboBox(); self._test.addItems(_TESTS) + self._algorithm = QComboBox() + self._algorithm.addItems(_ALGORITHMS) + self._algorithm.setCurrentText("Newton") + self._test = QComboBox() + self._test.addItems(_TESTS) self._tol = _spin(1e-6, decimals=12, minimum=1e-15, step=1e-7) self._max_iter = _int_spin(25) self._alpha_m = _spin(0.0, decimals=8, minimum=0.0, maximum=1e12, step=1e-4) @@ -376,10 +396,12 @@ class TransientCaseForm(CaseFormBase): self._layout.addRow(self._patterns_picker) self._layout.addRow(QLabel("Preload static cases (optional):")) self._layout.addRow(self._preload_picker) - self._layout.addRow(QLabel( - "Run these Static cases first, then hold them constant via " - "loadConst -time 0.0 before the transient starts." - )) + self._layout.addRow( + QLabel( + "Run these Static cases first, then hold them constant via " + "loadConst -time 0.0 before the transient starts." + ) + ) self._layout.addRow(QLabel("Patterns to remove after preload (optional):")) self._layout.addRow(self._remove_patterns_picker) self._layout.addRow("dt:", self._dt) @@ -396,17 +418,20 @@ class TransientCaseForm(CaseFormBase): self._layout.addRow("Rayleigh αM:", self._alpha_m) self._layout.addRow("Rayleigh βK:", self._beta_k) self._layout.addRow("Mode-1 damping ratio:", self._mode1_damping) - self._layout.addRow(QLabel( - "If mode-1 damping is > 0, the runner computes βK = 2ζ/√λ1 " - "after preload and uses it instead of the manual βK value." - )) + self._layout.addRow( + QLabel( + "If mode-1 damping is > 0, the runner computes βK = 2ζ/√λ1 " + "after preload and uses it instead of the manual βK value." + ) + ) def _populate_specific(self, c: TransientCase) -> None: _select_pattern_ids(self._patterns_picker, c.pattern_ids) _set_factor_spins(self._pattern_factor_spins, getattr(c, "pattern_factors", {}) or {}) _select_case_ids(self._preload_picker, c.preload_case_ids) _select_pattern_ids(self._remove_patterns_picker, c.remove_patterns) - self._dt.setValue(c.dt); self._n_steps.setValue(c.n_steps) + self._dt.setValue(c.dt) + self._n_steps.setValue(c.n_steps) self._system.setCurrentText(c.system) self._constraints.setCurrentText(c.constraints) self._integrator.setCurrentText(c.integrator) @@ -423,12 +448,14 @@ class TransientCaseForm(CaseFormBase): def _read_specific(self, cid: int) -> TransientCase: mode1_damping = self._mode1_damping.value() return TransientCase( - id=cid, name=self._name_edit.text(), + id=cid, + name=self._name_edit.text(), pattern_ids=_require_pattern_ids(self._patterns_picker), **_factor_kwargs(TransientCase, self._patterns_picker, self._pattern_factor_spins), preload_case_ids=_selected_case_ids(self._preload_picker), remove_patterns=_selected_pattern_ids(self._remove_patterns_picker), - dt=self._dt.value(), n_steps=self._n_steps.value(), + dt=self._dt.value(), + n_steps=self._n_steps.value(), system=self._system.currentText(), constraints=self._constraints.currentText(), integrator=self._integrator.currentText(), @@ -462,12 +489,18 @@ class PushoverCaseForm(CaseFormBase): self._target = _spin(0.1, decimals=6, minimum=-1e6, maximum=1e6, step=0.001) self._step = _spin(0.001, decimals=8, minimum=1e-12, step=1e-4) self._base_nodes = QLineEdit() - self._base_nodes.setPlaceholderText("comma-separated node ids (leave blank for all supports)") - self._system = QComboBox(); self._system.addItems(_STATIC_SYSTEMS) - self._constraints = QComboBox(); self._constraints.addItems(_CONSTRAINTS) - self._algorithm = QComboBox(); self._algorithm.addItems(_ALGORITHMS) + self._base_nodes.setPlaceholderText( + "comma-separated node ids (leave blank for all supports)" + ) + self._system = QComboBox() + self._system.addItems(_STATIC_SYSTEMS) + self._constraints = QComboBox() + self._constraints.addItems(_CONSTRAINTS) + self._algorithm = QComboBox() + self._algorithm.addItems(_ALGORITHMS) self._algorithm.setCurrentText("Newton") - self._test = QComboBox(); self._test.addItems(_TESTS) + self._test = QComboBox() + self._test.addItems(_TESTS) self._tol = _spin(1e-6, decimals=12, minimum=1e-15, step=1e-7) self._max_iter = _int_spin(25) @@ -475,10 +508,12 @@ class PushoverCaseForm(CaseFormBase): self._layout.addRow(self._patterns_picker) self._layout.addRow(QLabel("Preload static cases (optional):")) self._layout.addRow(self._preload_picker) - self._layout.addRow(QLabel( - "Run these Static cases first, then hold them constant via " - "loadConst -time 0.0 before the pushover starts." - )) + self._layout.addRow( + QLabel( + "Run these Static cases first, then hold them constant via " + "loadConst -time 0.0 before the pushover starts." + ) + ) self._layout.addRow("Control node:", self._control_node) self._layout.addRow("Control DOF:", self._control_dof) self._layout.addRow("Target displacement:", self._target) @@ -511,7 +546,8 @@ class PushoverCaseForm(CaseFormBase): txt = self._base_nodes.text().strip() base_ids = [int(x) for x in txt.replace(",", " ").split() if x] if txt else [] return PushoverCase( - id=cid, name=self._name_edit.text(), + id=cid, + name=self._name_edit.text(), pattern_ids=_require_pattern_ids(self._patterns_picker), **_factor_kwargs(PushoverCase, self._patterns_picker, self._pattern_factor_spins), preload_case_ids=_selected_case_ids(self._preload_picker), @@ -552,10 +588,12 @@ class ResponseSpectrumCaseForm(CaseFormBase): self._layout.addRow("Direction (DOF):", self._direction) self._layout.addRow("Combination:", self._combination) self._layout.addRow("Damping (CQC override):", self._damping) - self._layout.addRow(QLabel( - "Damping is used by CQC modal correlation only; " - "leave at 0 to use the spectrum's own damping ratio.", - )) + self._layout.addRow( + QLabel( + "Damping is used by CQC modal correlation only; " + "leave at 0 to use the spectrum's own damping ratio.", + ) + ) def _populate_specific(self, c: ResponseSpectrumCase) -> None: self._modal_case.setValue(c.modal_case_id) @@ -568,7 +606,8 @@ class ResponseSpectrumCaseForm(CaseFormBase): def _read_specific(self, cid: int) -> ResponseSpectrumCase: damp_val = self._damping.value() return ResponseSpectrumCase( - id=cid, name=self._name_edit.text(), + id=cid, + name=self._name_edit.text(), modal_case_id=self._modal_case.value(), spectrum_id=self._spectrum_id.value(), direction=self._direction.value(), diff --git a/src/otko/views/dialogs/case_manager.py b/src/otko/views/dialogs/case_manager.py index d451d2b..000a90c 100644 --- a/src/otko/views/dialogs/case_manager.py +++ b/src/otko/views/dialogs/case_manager.py @@ -27,7 +27,11 @@ from otko.commands import ( UpdateAnalysisCaseCommand, ) from otko.core import ( - ModalCase, PushoverCase, ResponseSpectrumCase, StaticCase, TransientCase, + ModalCase, + PushoverCase, + ResponseSpectrumCase, + StaticCase, + TransientCase, ) from otko.viewmodels import ProjectViewModel from otko.views.dialogs.case_forms import FORM_REGISTRY, form_for @@ -56,16 +60,23 @@ def _default_case(kind: str, cid: int, project: Project): # type: ignore[no-unt return ModalCase(id=cid, name="Modal", n_modes=3) if kind == "Transient": return TransientCase( - id=cid, name="Transient", pattern_ids=[_first_pattern_id(project)], - dt=0.01, n_steps=1000, + id=cid, + name="Transient", + pattern_ids=[_first_pattern_id(project)], + dt=0.01, + n_steps=1000, ) if kind == "Pushover": if not project.nodes: raise ValueError("Define at least one node before adding a Pushover case.") return PushoverCase( - id=cid, name="Pushover", pattern_ids=[_first_pattern_id(project)], - control_node=project.nodes[0].id, control_dof=1, - target_disp=0.1, step_size=0.001, + id=cid, + name="Pushover", + pattern_ids=[_first_pattern_id(project)], + control_node=project.nodes[0].id, + control_dof=1, + target_disp=0.1, + step_size=0.001, ) if kind == "ResponseSpectrum": modal_ids = [c.id for c in project.analyses if isinstance(c, ModalCase)] @@ -73,13 +84,15 @@ 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, name="ResponseSpectrum", - modal_case_id=modal_ids[0], spectrum_id=project.spectra[0].id, - direction=1, combination="SRSS", + id=cid, + name="ResponseSpectrum", + modal_case_id=modal_ids[0], + spectrum_id=project.spectra[0].id, + direction=1, + combination="SRSS", ) raise ValueError(f"Unknown analysis type: {kind}.") @@ -150,7 +163,7 @@ class AnalysisCaseManagerDialog(QDialog): 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.analyses if self._vm.project else []): + for c in self._vm.project.analyses if self._vm.project else []: label = f"#{c.id} {c.name or '(unnamed)'} [{c.type}]" item = QListWidgetItem(label) item.setData(Qt.ItemDataRole.UserRole, c.id) @@ -201,22 +214,31 @@ class AnalysisCaseManagerDialog(QDialog): # patterns). Do NOT fall through to the general type picker — # Static/Transient/Pushover would get dangling pattern refs. QMessageBox.information( - self, "No patterns", + self, + "No patterns", "No load patterns defined yet — only a Modal case can be " "added (Modal works without patterns). Define a pattern " "first for Static, Transient, or Pushover cases.", ) kind, ok = QInputDialog.getItem( - self, "Add analysis case", "Type:", - ["Modal"], current=0, editable=False, + self, + "Add analysis case", + "Type:", + ["Modal"], + current=0, + editable=False, ) if not ok: return self._create_case_of_kind("Modal") return kind, ok = QInputDialog.getItem( - self, "Add analysis case", "Type:", - list(_DEFAULTS.keys()), current=0, editable=False, + self, + "Add analysis case", + "Type:", + list(_DEFAULTS.keys()), + current=0, + editable=False, ) if not ok: return diff --git a/src/otko/views/dialogs/coord_grid_systems.py b/src/otko/views/dialogs/coord_grid_systems.py index 78459a1..595464e 100644 --- a/src/otko/views/dialogs/coord_grid_systems.py +++ b/src/otko/views/dialogs/coord_grid_systems.py @@ -61,8 +61,7 @@ class CoordSystemDataDialog(QDialog): ) -> None: super().__init__(parent) self.setWindowTitle( - "Edit Coordinate/Grid System" - if existing is not None else "Add Coordinate/Grid System" + "Edit Coordinate/Grid System" if existing is not None else "Add Coordinate/Grid System" ) self._is_global = is_global self._existing = existing @@ -88,28 +87,33 @@ class CoordSystemDataDialog(QDialog): loc_box = QGroupBox("Location and Orientation (relative to Global)") loc_form = QFormLayout(loc_box) - self._ox = self._spin(); self._oy = self._spin(); self._oz = self._spin() + self._ox = self._spin() + self._oy = self._spin() + self._oz = self._spin() 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(w) - origin_wrap = QWidget(); origin_wrap.setLayout(origin_row) + origin_wrap = QWidget() + origin_wrap.setLayout(origin_row) loc_form.addRow("Origin:", origin_wrap) - self._rx = self._rot_spin(); self._ry = self._rot_spin(); self._rz = self._rot_spin() + self._rx = self._rot_spin() + self._ry = self._rot_spin() + self._rz = self._rot_spin() rot_row = QHBoxLayout() for label, w in (("about X", self._rx), ("about Y", self._ry), ("about Z", self._rz)): rot_row.addWidget(QLabel(f"{label}:")) rot_row.addWidget(w) - rot_wrap = QWidget(); rot_wrap.setLayout(rot_row) + rot_wrap = QWidget() + rot_wrap.setLayout(rot_row) loc_form.addRow("Rotation (deg):", rot_wrap) if self._is_global: 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) @@ -160,8 +164,7 @@ class CoordSystemDataDialog(QDialog): # ── Buttons ────────────────────────────────────────────── 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) @@ -189,9 +192,13 @@ class CoordSystemDataDialog(QDialog): def _load_existing(self, cs: CoordinateGridSystem) -> None: self._name_edit.setText(cs.name) ox, oy, oz = cs.coord.origin - self._ox.setValue(ox); self._oy.setValue(oy); self._oz.setValue(oz) + self._ox.setValue(ox) + self._oy.setValue(oy) + self._oz.setValue(oz) rx, ry, rz = cs.coord.rotation_deg - self._rx.setValue(rx); self._ry.setValue(ry); self._rz.setValue(rz) + self._rx.setValue(rx) + self._ry.setValue(ry) + self._rz.setValue(rz) self._x_edit.setText(_spacings_text(cs.grid.x_lines)) self._y_edit.setText(_spacings_text(cs.grid.y_lines)) self._z_edit.setText(_spacings_text(cs.grid.z_lines)) @@ -245,9 +252,7 @@ class CoordinateGridSystemsDialog(QDialog): super().__init__(parent) self.setWindowTitle("Coordinate/Grid Systems") self.resize(600, 420) - self._systems: list[CoordinateGridSystem] = [ - s.model_copy(deep=True) for s in systems - ] + self._systems: list[CoordinateGridSystem] = [s.model_copy(deep=True) for s in systems] self._build_ui() self._refresh_list() @@ -290,8 +295,7 @@ class CoordinateGridSystemsDialog(QDialog): right.addStretch(1) buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok - | QDialogButtonBox.StandardButton.Cancel, + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, ) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) @@ -332,9 +336,7 @@ class CoordinateGridSystemsDialog(QDialog): self._btn_modify.setEnabled(cs is not None) self._cb_general.blockSignals(True) self._cb_general.setChecked(cs.grid.is_general if cs is not None else False) - self._cb_general.setEnabled( - cs is not None and not cs.grid.is_general - ) + self._cb_general.setEnabled(cs is not None and not cs.grid.is_general) self._cb_general.blockSignals(False) # ── actions ────────────────────────────────────────────────── @@ -351,14 +353,17 @@ class CoordinateGridSystemsDialog(QDialog): from otko.views.dialogs.define_grid_data import ( DefineGridSystemDataDialog, ) + dlg = DefineGridSystemDataDialog(parent=self) if dlg.exec() != QDialog.DialogCode.Accepted: return new_sys = dlg.system() if any(s.name == new_sys.name for s in self._systems): - new_sys = new_sys.model_copy(update={ - "name": self._unique_name(new_sys.name), - }) + new_sys = new_sys.model_copy( + update={ + "name": self._unique_name(new_sys.name), + } + ) self._systems.append(new_sys) self._refresh_list(select_name=new_sys.name) @@ -380,8 +385,11 @@ class CoordinateGridSystemsDialog(QDialog): from otko.views.dialogs.define_grid_data import ( DefineGridSystemDataDialog, ) + dlg = DefineGridSystemDataDialog( - existing=cs, is_global=cs.is_global(), parent=self, + existing=cs, + is_global=cs.is_global(), + parent=self, ) if dlg.exec() != QDialog.DialogCode.Accepted: return @@ -392,9 +400,11 @@ class CoordinateGridSystemsDialog(QDialog): # Enforce unique names against siblings. others = [s for s in self._systems if s is not cs] if any(s.name == updated.name for s in others): - updated = updated.model_copy(update={ - "name": self._unique_name(updated.name), - }) + updated = updated.model_copy( + update={ + "name": self._unique_name(updated.name), + } + ) row = self._list.currentRow() self._systems[row] = updated self._refresh_list(select_name=updated.name) @@ -414,7 +424,8 @@ class CoordinateGridSystemsDialog(QDialog): if cs.grid.is_general and not checked: # SAP2000: once converted to General, can't convert back. QMessageBox.information( - self, "Convert to General", + self, + "Convert to General", "Once a system is converted to General, it cannot be " "converted back to a regular Cartesian system.", ) diff --git a/src/otko/views/dialogs/define_grid_data.py b/src/otko/views/dialogs/define_grid_data.py index 5ff378a..8230650 100644 --- a/src/otko/views/dialogs/define_grid_data.py +++ b/src/otko/views/dialogs/define_grid_data.py @@ -127,9 +127,9 @@ class _AxisGridTable(QWidget): prev = 0.0 for i, v in enumerate(ords): if i == 0: - out.append(v) # first row: absolute position + out.append(v) # first row: absolute position else: - out.append(v - prev) # subsequent rows: spacing from previous + out.append(v - prev) # subsequent rows: spacing from previous prev = v return out @@ -169,23 +169,28 @@ class _AxisGridTable(QWidget): visible_widget = self._table.cellWidget(row, 3) bubble_widget = self._table.cellWidget(row, 4) color_item = self._table.item(row, 5) - out.append(GridLine( - id=(id_item.text().strip() if id_item else f"{self.axis}{row + 1}"), - ordinate=ords[row] if row < len(ords) else 0.0, - line_type=( - line_type_widget.currentText() # type: ignore[union-attr] - if isinstance(line_type_widget, QComboBox) else "Primary" - ), - visible=( - visible_widget.isChecked() # type: ignore[union-attr] - if isinstance(visible_widget, QCheckBox) else True - ), - bubble_loc=( - bubble_widget.currentText() # type: ignore[union-attr] - if isinstance(bubble_widget, QComboBox) else "End" - ), - color=(color_item.text() if color_item else "#808080"), - )) + out.append( + GridLine( + id=(id_item.text().strip() if id_item else f"{self.axis}{row + 1}"), + ordinate=ords[row] if row < len(ords) else 0.0, + line_type=( + line_type_widget.currentText() # type: ignore[union-attr] + if isinstance(line_type_widget, QComboBox) + else "Primary" + ), + visible=( + visible_widget.isChecked() # type: ignore[union-attr] + if isinstance(visible_widget, QCheckBox) + else True + ), + bubble_loc=( + bubble_widget.currentText() # type: ignore[union-attr] + if isinstance(bubble_widget, QComboBox) + else "End" + ), + color=(color_item.text() if color_item else "#808080"), + ) + ) return out # ── row ops ─────────────────────────────────────────────────── @@ -218,7 +223,8 @@ class _AxisGridTable(QWidget): vis = QCheckBox() vis.setChecked(ln.visible) vis_wrap = QWidget() - lay = QHBoxLayout(vis_wrap); lay.setContentsMargins(0, 0, 0, 0) + lay = QHBoxLayout(vis_wrap) + lay.setContentsMargins(0, 0, 0, 0) lay.addWidget(vis) lay.setAlignment(Qt.AlignmentFlag.AlignCenter) # We keep the QCheckBox directly; the wrap is optional visual @@ -250,13 +256,13 @@ class _AxisGridTable(QWidget): lines = self.collect_lines() lines.sort(key=lambda ln: ln.ordinate) self._table.setRowCount(0) - self._show_spacing = False # reset to ordinate view after sort + self._show_spacing = False # reset to ordinate view after sort self._table.setHorizontalHeaderLabels(self.COLUMNS) for ln in lines: self._append_row(ln) def _on_cell_double_clicked(self, row: int, col: int) -> None: - if col == 5: # Color picker + if col == 5: # Color picker item = self._table.item(row, col) current = QColor(item.text() if item else "#808080") new = QColorDialog.getColor(current, self, "Grid Line Color") @@ -278,8 +284,7 @@ class DefineGridSystemDataDialog(QDialog): ) -> None: super().__init__(parent) self.setWindowTitle( - "Define Grid System Data" if existing is not None - else "Add Grid System" + "Define Grid System Data" if existing is not None else "Add Grid System" ) self.resize(780, 620) self._is_global = is_global @@ -328,9 +333,7 @@ class DefineGridSystemDataDialog(QDialog): self._btn_locate.clicked.connect(self._on_locate_origin) if self._is_global: self._btn_locate.setEnabled(False) - self._btn_locate.setToolTip( - "Global system is anchored at the world origin." - ) + self._btn_locate.setToolTip("Global system is anchored at the world origin.") head.addWidget(self._btn_locate) root.addLayout(head) @@ -375,8 +378,7 @@ class DefineGridSystemDataDialog(QDialog): bottom.addWidget(self._btn_quick) bottom.addStretch(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) @@ -388,8 +390,7 @@ class DefineGridSystemDataDialog(QDialog): ox, oy, oz = self._origin rx, ry, rz = self._rotation_deg self._origin_label.setText( - f"Origin: ({ox:g}, {oy:g}, {oz:g}) " - f"Rotation: ({rx:g}°, {ry:g}°, {rz:g}°)" + f"Origin: ({ox:g}, {oy:g}, {oz:g}) " f"Rotation: ({rx:g}°, {ry:g}°, {rz:g}°)" ) def _load_existing(self, cs: CoordinateGridSystem) -> None: @@ -415,6 +416,7 @@ class DefineGridSystemDataDialog(QDialog): from otko.views.dialogs.locate_origin import ( CoordSystemLocationOrientationDialog, ) + dlg = CoordSystemLocationOrientationDialog( origin=self._origin, rotation_deg=self._rotation_deg, @@ -429,11 +431,13 @@ class DefineGridSystemDataDialog(QDialog): from otko.views.dialogs.quick_grid_lines import ( QuickGridLinesDialog, ) + dlg = QuickGridLinesDialog(parent=self) if dlg.exec() != QDialog.DialogCode.Accepted: return xs, ys, zs = dlg.ordinates() from otko.core.geometry import make_grid_lines + # Revert to ordinate display mode before replacing rows. self._rb_ordinates.setChecked(True) self._tab_x.load_lines(make_grid_lines("X", xs)) @@ -465,7 +469,8 @@ class DefineGridSystemDataDialog(QDialog): return CoordinateGridSystem( name=self._name_edit.text().strip(), coord=CoordinateSystem( - origin=self._origin, rotation_deg=self._rotation_deg, + origin=self._origin, + rotation_deg=self._rotation_deg, ), grid=grid, ) diff --git a/src/otko/views/dialogs/distributed_load.py b/src/otko/views/dialogs/distributed_load.py index 19945d4..306a678 100644 --- a/src/otko/views/dialogs/distributed_load.py +++ b/src/otko/views/dialogs/distributed_load.py @@ -141,9 +141,11 @@ class AssignDistributedLoadDialog(QDialog): def values(self) -> tuple[float, float, float]: """Return (wy, wz, wx).""" - return (self._spinboxes["wy"].value(), - self._spinboxes["wz"].value(), - self._spinboxes["wx"].value()) + return ( + self._spinboxes["wy"].value(), + self._spinboxes["wz"].value(), + self._spinboxes["wx"].value(), + ) def selected_pattern_id(self) -> int | None: """Return the chosen pattern id, or ``None`` if the user picked diff --git a/src/otko/views/dialogs/grid_system.py b/src/otko/views/dialogs/grid_system.py index 4f94f71..3f39cf8 100644 --- a/src/otko/views/dialogs/grid_system.py +++ b/src/otko/views/dialogs/grid_system.py @@ -63,9 +63,9 @@ def _coords_from_spacings(spacings: list[float], origin: float = 0.0) -> list[fl 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) -> None: + def __init__( + self, next_node_id: int, existing: GridSystem | None = None, parent: QWidget | None = None + ) -> None: super().__init__(parent) self.setWindowTitle("Grid System") self._next_id = next_node_id @@ -104,9 +104,7 @@ class GridSystemDialog(QDialog): self._visible_cb.setChecked(True) layout.addWidget(self._visible_cb) - self._generate_nodes_cb = QCheckBox( - "Also create nodes at every intersection" - ) + self._generate_nodes_cb = QCheckBox("Also create nodes at every intersection") self._generate_nodes_cb.setChecked(False) layout.addWidget(self._generate_nodes_cb) @@ -128,6 +126,7 @@ class GridSystemDialog(QDialog): def _load_existing(self, grid: GridSystem) -> None: """Pre-fill editors from an existing GridSystem (as spacings).""" + def to_spacings_text(coords: list[float]) -> str: if not coords: return "" @@ -136,6 +135,7 @@ class GridSystemDialog(QDialog): # Single line at nonzero origin — degenerate, show empty. return "" return ", ".join(f"{s:g}" for s in spacings) + self._x_edit.setText(to_spacings_text(grid.x_lines)) self._y_edit.setText(to_spacings_text(grid.y_lines)) self._z_edit.setText(to_spacings_text(grid.z_lines)) @@ -152,8 +152,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." f"{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 5121c0e..21708e7 100644 --- a/src/otko/views/dialogs/locate_origin.py +++ b/src/otko/views/dialogs/locate_origin.py @@ -36,28 +36,35 @@ class CoordSystemLocationOrientationDialog(QDialog): def _build_ui(self) -> None: root = QVBoxLayout(self) - root.addWidget(QLabel( - "Enter the system's origin and orientation relative to Global. " - "Rotations are XYZ Euler angles in degrees." - )) + root.addWidget( + QLabel( + "Enter the system's origin and orientation relative to Global. " + "Rotations are XYZ Euler angles in degrees." + ) + ) origin_box = QGroupBox("Origin (world units)") of = QFormLayout(origin_box) - self._ox = self._spin(); of.addRow("X:", self._ox) - self._oy = self._spin(); of.addRow("Y:", self._oy) - self._oz = self._spin(); of.addRow("Z:", self._oz) + self._ox = self._spin() + of.addRow("X:", self._ox) + self._oy = self._spin() + of.addRow("Y:", self._oy) + self._oz = self._spin() + of.addRow("Z:", self._oz) root.addWidget(origin_box) rot_box = QGroupBox("Rotation about axes (degrees)") rf = QFormLayout(rot_box) - self._rx = self._rot_spin(); rf.addRow("about X:", self._rx) - self._ry = self._rot_spin(); rf.addRow("about Y:", self._ry) - self._rz = self._rot_spin(); rf.addRow("about Z:", self._rz) + self._rx = self._rot_spin() + rf.addRow("about X:", self._rx) + self._ry = self._rot_spin() + rf.addRow("about Y:", self._ry) + self._rz = self._rot_spin() + rf.addRow("about Z:", self._rz) root.addWidget(rot_box) 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/material_forms.py b/src/otko/views/dialogs/material_forms.py index 2c3a155..e151f59 100644 --- a/src/otko/views/dialogs/material_forms.py +++ b/src/otko/views/dialogs/material_forms.py @@ -33,10 +33,16 @@ from otko.core import ( # ─────────────────────────── helpers ─────────────────────────── -def _spin(default: float = 0.0, *, decimals: int = 6, - minimum: float = -1e15, maximum: float = 1e15, - step: float = 1.0) -> QDoubleSpinBox: +def _spin( + default: float = 0.0, + *, + decimals: int = 6, + minimum: float = -1e15, + maximum: float = 1e15, + step: float = 1.0, +) -> QDoubleSpinBox: from PySide6.QtCore import QLocale + sb = QDoubleSpinBox() # Force C locale so "." is always the decimal separator. sb.setLocale(QLocale(QLocale.Language.C)) @@ -110,8 +116,11 @@ class Steel01Form(MaterialFormBase): def _read_specific(self, mid: int) -> Steel01: return Steel01( - id=mid, name=self._name_edit.text(), - Fy=self._fy.value(), E0=self._e0.value(), b=self._b.value(), + id=mid, + name=self._name_edit.text(), + Fy=self._fy.value(), + E0=self._e0.value(), + b=self._b.value(), ) @@ -127,19 +136,34 @@ class Steel02Form(MaterialFormBase): 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) - for label, w in (("Fy:", self._fy), ("E0:", self._e0), ("b:", self._b), - ("R0:", self._r0), ("cR1:", self._cR1), ("cR2:", self._cR2)): + for label, w in ( + ("Fy:", self._fy), + ("E0:", self._e0), + ("b:", self._b), + ("R0:", self._r0), + ("cR1:", self._cR1), + ("cR2:", self._cR2), + ): self._layout.addRow(label, w) def _populate_specific(self, m: Steel02) -> None: - self._fy.setValue(m.Fy); self._e0.setValue(m.E0); self._b.setValue(m.b) - self._r0.setValue(m.R0); self._cR1.setValue(m.cR1); self._cR2.setValue(m.cR2) + self._fy.setValue(m.Fy) + self._e0.setValue(m.E0) + self._b.setValue(m.b) + self._r0.setValue(m.R0) + self._cR1.setValue(m.cR1) + self._cR2.setValue(m.cR2) def _read_specific(self, mid: int) -> Steel02: return Steel02( - id=mid, name=self._name_edit.text(), - Fy=self._fy.value(), E0=self._e0.value(), b=self._b.value(), - R0=self._r0.value(), cR1=self._cR1.value(), cR2=self._cR2.value(), + id=mid, + name=self._name_edit.text(), + Fy=self._fy.value(), + E0=self._e0.value(), + b=self._b.value(), + R0=self._r0.value(), + cR1=self._cR1.value(), + cR2=self._cR2.value(), ) @@ -156,19 +180,28 @@ 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) - for label, w in (("f'c (-):", self._fpc), ("ε_c0 (-):", self._epsc0), - ("f'cu (-):", self._fpcu), ("ε_U (-):", self._epsU)): + for label, w in ( + ("f'c (-):", self._fpc), + ("ε_c0 (-):", self._epsc0), + ("f'cu (-):", self._fpcu), + ("ε_U (-):", self._epsU), + ): self._layout.addRow(label, w) def _populate_specific(self, m: Concrete01) -> None: - self._fpc.setValue(m.fpc); self._epsc0.setValue(m.epsc0) - self._fpcu.setValue(m.fpcu); self._epsU.setValue(m.epsU) + self._fpc.setValue(m.fpc) + self._epsc0.setValue(m.epsc0) + self._fpcu.setValue(m.fpcu) + self._epsU.setValue(m.epsU) def _read_specific(self, mid: int) -> Concrete01: return Concrete01( - id=mid, name=self._name_edit.text(), - fpc=self._fpc.value(), epsc0=self._epsc0.value(), - fpcu=self._fpcu.value(), epsU=self._epsU.value(), + id=mid, + name=self._name_edit.text(), + fpc=self._fpc.value(), + epsc0=self._epsc0.value(), + fpcu=self._fpcu.value(), + epsU=self._epsU.value(), ) @@ -187,23 +220,36 @@ 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) - for label, w in (("f'c (-):", self._fpc), ("ε_c0 (-):", self._epsc0), - ("f'cu (-):", self._fpcu), ("ε_U (-):", self._epsU), - ("λ (unload ratio):", self._lambda), - ("ft (tensile):", self._ft), ("Ets (soften):", self._ets)): + for label, w in ( + ("f'c (-):", self._fpc), + ("ε_c0 (-):", self._epsc0), + ("f'cu (-):", self._fpcu), + ("ε_U (-):", self._epsU), + ("λ (unload ratio):", self._lambda), + ("ft (tensile):", self._ft), + ("Ets (soften):", self._ets), + ): self._layout.addRow(label, w) def _populate_specific(self, m: Concrete02) -> None: - self._fpc.setValue(m.fpc); self._epsc0.setValue(m.epsc0) - self._fpcu.setValue(m.fpcu); self._epsU.setValue(m.epsU) - self._lambda.setValue(m.lambda_); self._ft.setValue(m.ft); self._ets.setValue(m.Ets) + self._fpc.setValue(m.fpc) + self._epsc0.setValue(m.epsc0) + self._fpcu.setValue(m.fpcu) + self._epsU.setValue(m.epsU) + self._lambda.setValue(m.lambda_) + self._ft.setValue(m.ft) + self._ets.setValue(m.Ets) def _read_specific(self, mid: int) -> Concrete02: return Concrete02( - id=mid, name=self._name_edit.text(), - fpc=self._fpc.value(), epsc0=self._epsc0.value(), - fpcu=self._fpcu.value(), epsU=self._epsU.value(), - ft=self._ft.value(), Ets=self._ets.value(), + id=mid, + name=self._name_edit.text(), + fpc=self._fpc.value(), + epsc0=self._epsc0.value(), + fpcu=self._fpcu.value(), + epsU=self._epsU.value(), + ft=self._ft.value(), + Ets=self._ets.value(), **{"lambda": self._lambda.value()}, ) @@ -243,9 +289,12 @@ class Concrete04Form(MaterialFormBase): fct_val = self._fct.value() et_val = self._et.value() return Concrete04( - id=mid, name=self._name_edit.text(), - fpc=self._fpc.value(), epsc0=self._epsc0.value(), - epscu=self._epscu.value(), Ec=self._Ec.value(), + id=mid, + name=self._name_edit.text(), + fpc=self._fpc.value(), + epsc0=self._epsc0.value(), + epscu=self._epscu.value(), + Ec=self._Ec.value(), fct=fct_val if fct_val > 0.0 else None, et=et_val if et_val > 0.0 else None, ) @@ -263,12 +312,15 @@ class ElasticUniaxialForm(MaterialFormBase): self._layout.addRow("η (damping):", self._eta) def _populate_specific(self, m: ElasticUniaxial) -> None: - self._e.setValue(m.E); self._eta.setValue(m.eta) + self._e.setValue(m.E) + self._eta.setValue(m.eta) def _read_specific(self, mid: int) -> ElasticUniaxial: return ElasticUniaxial( - id=mid, name=self._name_edit.text(), - E=self._e.value(), eta=self._eta.value(), + id=mid, + name=self._name_edit.text(), + E=self._e.value(), + eta=self._eta.value(), ) @@ -286,12 +338,17 @@ class ElasticIsotropicForm(MaterialFormBase): self._layout.addRow("ρ (density):", self._rho) def _populate_specific(self, m: ElasticIsotropic) -> None: - self._e.setValue(m.E); self._nu.setValue(m.nu); self._rho.setValue(m.rho) + self._e.setValue(m.E) + self._nu.setValue(m.nu) + self._rho.setValue(m.rho) def _read_specific(self, mid: int) -> ElasticIsotropic: return ElasticIsotropic( - id=mid, name=self._name_edit.text(), - E=self._e.value(), nu=self._nu.value(), rho=self._rho.value(), + id=mid, + name=self._name_edit.text(), + E=self._e.value(), + nu=self._nu.value(), + rho=self._rho.value(), ) @@ -307,12 +364,15 @@ class ElasticPPForm(MaterialFormBase): self._layout.addRow("ε_y (yield strain):", self._epsy) def _populate_specific(self, m: ElasticPP) -> None: - self._e.setValue(m.E); self._epsy.setValue(m.epsy_pos) + self._e.setValue(m.E) + self._epsy.setValue(m.epsy_pos) def _read_specific(self, mid: int) -> ElasticPP: return ElasticPP( - id=mid, name=self._name_edit.text(), - E=self._e.value(), epsy_pos=self._epsy.value(), + id=mid, + name=self._name_edit.text(), + E=self._e.value(), + epsy_pos=self._epsy.value(), ) diff --git a/src/otko/views/dialogs/material_library.py b/src/otko/views/dialogs/material_library.py index 93ad07a..ace776e 100644 --- a/src/otko/views/dialogs/material_library.py +++ b/src/otko/views/dialogs/material_library.py @@ -150,7 +150,12 @@ class MaterialLibraryDialog(QDialog): return kinds = list(FORM_REGISTRY.keys()) kind, ok = QInputDialog.getItem( - self, "Add material", "Type:", kinds, current=0, editable=False, + self, + "Add material", + "Type:", + kinds, + current=0, + editable=False, ) if not ok: return @@ -179,9 +184,10 @@ class MaterialLibraryDialog(QDialog): if material is None: return reply = QMessageBox.question( - self, "Delete material", + self, + "Delete material", f"Delete material #{material.id} ({material.type})?\n" - "Elements that reference it will be invalid until reassigned." + "Elements that reference it will be invalid until reassigned.", ) if reply != QMessageBox.StandardButton.Yes: return diff --git a/src/otko/views/dialogs/mirror.py b/src/otko/views/dialogs/mirror.py index d5131e7..7be07f6 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/path_time_series.py b/src/otko/views/dialogs/path_time_series.py index af5df29..476b5d4 100644 --- a/src/otko/views/dialogs/path_time_series.py +++ b/src/otko/views/dialogs/path_time_series.py @@ -44,11 +44,13 @@ 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") @@ -65,7 +67,7 @@ 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 " @@ -84,9 +86,7 @@ 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,8 +102,7 @@ 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) @@ -112,8 +111,10 @@ 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 @@ -132,8 +133,10 @@ 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 @@ -144,8 +147,7 @@ class PathTimeSeriesDialog(QDialog): 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() @@ -167,7 +169,9 @@ 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/quick_grid_lines.py b/src/otko/views/dialogs/quick_grid_lines.py index 876a2f3..23d3b61 100644 --- a/src/otko/views/dialogs/quick_grid_lines.py +++ b/src/otko/views/dialogs/quick_grid_lines.py @@ -31,39 +31,56 @@ class QuickGridLinesDialog(QDialog): def _build_ui(self) -> None: root = QVBoxLayout(self) - root.addWidget(QLabel( - "Quick-define a regular (Cartesian) grid.
" - "Each axis: number of lines, equal spacing, first line coordinate." - )) + root.addWidget( + QLabel( + "Quick-define a regular (Cartesian) grid.
" + "Each axis: number of lines, equal spacing, first line coordinate." + ) + ) self._x_n, self._x_s, self._x_f = self._axis_group("X Grid Data", root) self._y_n, self._y_s, self._y_f = self._axis_group("Y Grid Data", root) self._z_n, self._z_s, self._z_f = self._axis_group("Z Grid Data", root) # Sensible defaults. - self._x_n.setValue(3); self._x_s.setValue(6.0); self._x_f.setValue(0.0) - self._y_n.setValue(3); self._y_s.setValue(6.0); self._y_f.setValue(0.0) - self._z_n.setValue(2); self._z_s.setValue(3.0); self._z_f.setValue(0.0) + self._x_n.setValue(3) + self._x_s.setValue(6.0) + self._x_f.setValue(0.0) + self._y_n.setValue(3) + self._y_s.setValue(6.0) + self._y_f.setValue(0.0) + self._z_n.setValue(2) + self._z_s.setValue(3.0) + self._z_f.setValue(0.0) buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok - | QDialogButtonBox.StandardButton.Cancel, + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, ) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) root.addWidget(buttons) def _axis_group( - self, title: str, parent_layout: QVBoxLayout, + self, + title: str, + parent_layout: QVBoxLayout, ) -> tuple[QSpinBox, QDoubleSpinBox, QDoubleSpinBox]: box = QGroupBox(title) row = QHBoxLayout(box) - n = QSpinBox(); n.setRange(0, 200); n.setValue(3) - s = QDoubleSpinBox(); s.setRange(0.0, 1e6); s.setDecimals(4) - s.setSingleStep(0.5); s.setValue(1.0) - f = QDoubleSpinBox(); f.setRange(-1e6, 1e6); f.setDecimals(4) - f.setSingleStep(0.5); f.setValue(0.0) + n = QSpinBox() + n.setRange(0, 200) + n.setValue(3) + s = QDoubleSpinBox() + s.setRange(0.0, 1e6) + s.setDecimals(4) + s.setSingleStep(0.5) + s.setValue(1.0) + f = QDoubleSpinBox() + f.setRange(-1e6, 1e6) + f.setDecimals(4) + f.setSingleStep(0.5) + f.setValue(0.0) row.addWidget(QLabel("Number of lines:")) row.addWidget(n) @@ -76,8 +93,10 @@ class QuickGridLinesDialog(QDialog): def ordinates(self) -> tuple[list[float], list[float], list[float]]: """Return (xs, ys, zs) — flat lists of ordinate values.""" + def axis(n: QSpinBox, s: QDoubleSpinBox, f: QDoubleSpinBox) -> list[float]: return [f.value() + i * s.value() for i in range(n.value())] + return ( axis(self._x_n, self._x_s, self._x_f), axis(self._y_n, self._y_s, self._y_f), diff --git a/src/otko/views/dialogs/quick_guide.py b/src/otko/views/dialogs/quick_guide.py index 85daf78..9b0c424 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 (left): Select, Draw Node (F1), +

Tools toolbar (top): 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 2c63cef..f627ec3 100644 --- a/src/otko/views/dialogs/replicate.py +++ b/src/otko/views/dialogs/replicate.py @@ -42,12 +42,12 @@ class ReplicateDialog(QDialog): def _build_ui(self, n_nodes: int, n_elements: int) -> None: layout = QVBoxLayout(self) - layout.addWidget(QLabel( - f"Replicate {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"Replicate {n_nodes} node(s) and {n_elements} element(s).") + ) + layout.addWidget( + QLabel("Only elements whose endpoints are both in the selection are copied.") + ) form = QFormLayout() diff --git a/src/otko/views/dialogs/run_analysis.py b/src/otko/views/dialogs/run_analysis.py index 4833400..f31ec6f 100644 --- a/src/otko/views/dialogs/run_analysis.py +++ b/src/otko/views/dialogs/run_analysis.py @@ -57,6 +57,18 @@ 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) @@ -79,12 +91,14 @@ class RunAnalysisDialog(QDialog): self._mode1_damping.setSingleStep(0.01) form.addRow("Mode-1 damping zeta:", self._mode1_damping) - form.addRow(QLabel( - "Values are applied for this run only via a copy of the case — " - "the stored case is not modified. If mode-1 damping is " - "greater than zero, the runner computes betaK from the first " - "mode after preload and overrides the manual betaK value.", - )) + form.addRow( + QLabel( + "Values are applied for this run only via a copy of the case — " + "the stored case is not modified. If mode-1 damping is " + "greater than zero, the runner computes betaK from the first " + "mode after preload and overrides the manual betaK value.", + ) + ) layout.addWidget(self._damping_box) self._progress = QProgressBar() @@ -98,7 +112,8 @@ class RunAnalysisDialog(QDialog): layout.addWidget(self._log, stretch=1) self._buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Close, parent=self, + QDialogButtonBox.StandardButton.Close, + parent=self, ) self._run_btn = QPushButton("Run") self._buttons.addButton(self._run_btn, QDialogButtonBox.ButtonRole.ActionRole) @@ -106,6 +121,7 @@ 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) @@ -133,15 +149,21 @@ 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. @@ -157,11 +179,13 @@ class RunAnalysisDialog(QDialog): return if case.type == "Transient": mode1 = float(self._mode1_damping.value()) - case = case.model_copy(update={ - "rayleigh_alpha_m": float(self._alpha_m.value()), - "rayleigh_beta_k": float(self._beta_k.value()), - "rayleigh_mode1_damping": mode1 if mode1 > 0.0 else None, - }) + case = case.model_copy( + update={ + "rayleigh_alpha_m": float(self._alpha_m.value()), + "rayleigh_beta_k": float(self._beta_k.value()), + "rayleigh_mode1_damping": mode1 if mode1 > 0.0 else None, + } + ) self._results = None self._log.clear() results_dir: Path | None = None @@ -172,6 +196,49 @@ class RunAnalysisDialog(QDialog): 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: # noqa: BLE001 + 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 ---") @@ -191,4 +258,6 @@ 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 ba2937c..4bb185f 100644 --- a/src/otko/views/dialogs/section_editor.py +++ b/src/otko/views/dialogs/section_editor.py @@ -45,22 +45,31 @@ from otko.services.section_properties import ( ) _PATCH_COLORS = [ - "#4e79a7", "#f28e2b", "#e15759", "#76b7b2", - "#59a14f", "#edc948", "#b07aa1", "#ff9da7", + "#4e79a7", + "#f28e2b", + "#e15759", + "#76b7b2", + "#59a14f", + "#edc948", + "#b07aa1", + "#ff9da7", ] class FiberSectionEditor(QDialog): """Modal dialog: build a FiberSection from patches + layers.""" - def __init__(self, material_ids: list[int], - existing: FiberSection | None = None, - parent: QWidget | None = None) -> None: + def __init__( + self, + material_ids: list[int], + existing: FiberSection | None = None, + parent: QWidget | None = None, + ) -> None: super().__init__(parent) self.setWindowTitle("Fiber Section Editor") self.resize(1000, 650) self._material_ids = material_ids - self._patches: list[Any] = [] # RectangularPatch | CircularPatch + 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 "" @@ -106,25 +115,37 @@ class FiberSectionEditor(QDialog): pf.addRow("Material:", self._patch_mat) # Rect fields - self._rect_yi = self._spin(-0.15); self._rect_zi = self._spin(-0.15) - self._rect_yj = self._spin(0.15); self._rect_zj = self._spin(0.15) - self._rect_ny = self._ispin(8); self._rect_nz = self._ispin(8) + self._rect_yi = self._spin(-0.15) + self._rect_zi = self._spin(-0.15) + self._rect_yj = self._spin(0.15) + self._rect_zj = self._spin(0.15) + self._rect_ny = self._ispin(8) + self._rect_nz = self._ispin(8) self._rect_rows = [ - ("y_i:", self._rect_yi), ("z_i:", self._rect_zi), - ("y_j:", self._rect_yj), ("z_j:", self._rect_zj), - ("n_fib_y:", self._rect_ny), ("n_fib_z:", self._rect_nz), + ("y_i:", self._rect_yi), + ("z_i:", self._rect_zi), + ("y_j:", self._rect_yj), + ("z_j:", self._rect_zj), + ("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); self._circ_zc = self._spin(0.0) - self._circ_ri = self._spin(0.0); self._circ_ro = self._spin(0.15) - self._circ_nc = self._ispin(16); self._circ_nr = self._ispin(4) + self._circ_yc = self._spin(0.0) + self._circ_zc = self._spin(0.0) + self._circ_ri = self._spin(0.0) + self._circ_ro = self._spin(0.15) + self._circ_nc = self._ispin(16) + self._circ_nr = self._ispin(4) self._circ_rows = [ - ("y_center:", self._circ_yc), ("z_center:", self._circ_zc), - ("r_inner:", self._circ_ri), ("r_outer:", self._circ_ro), - ("n_circ:", self._circ_nc), ("n_rad:", self._circ_nr), + ("y_center:", self._circ_yc), + ("z_center:", self._circ_zc), + ("r_inner:", self._circ_ri), + ("r_outer:", self._circ_ro), + ("n_circ:", self._circ_nc), + ("n_rad:", self._circ_nr), ] for label, widget in self._circ_rows: pf.addRow(label, widget) @@ -145,12 +166,16 @@ class FiberSectionEditor(QDialog): lf.addRow("Material:", self._layer_mat) self._layer_nbars = self._ispin(4) self._layer_area = self._spin(0.0005, step=0.0001, minimum=1e-12) - self._layer_ys = self._spin(-0.12); self._layer_zs = self._spin(-0.12) - self._layer_ye = self._spin(0.12); self._layer_ze = self._spin(-0.12) + self._layer_ys = self._spin(-0.12) + self._layer_zs = self._spin(-0.12) + 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) + 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_layer_btn = QPushButton("Add layer") self._add_layer_btn.clicked.connect(self._on_add_layer) lf.addRow(self._add_layer_btn) @@ -197,8 +222,7 @@ class FiberSectionEditor(QDialog): # ── helpers ───────────────────────────────────────────────────── @staticmethod - def _spin(default: float = 0.0, *, step: float = 0.01, - minimum: float = -1e6) -> QDoubleSpinBox: + def _spin(default: float = 0.0, *, step: float = 0.01, minimum: float = -1e6) -> QDoubleSpinBox: sb = QDoubleSpinBox() sb.setRange(minimum, 1e6) sb.setDecimals(6) @@ -214,7 +238,7 @@ class FiberSectionEditor(QDialog): return sb def _on_patch_type_changed(self, idx: int) -> None: - is_rect = (idx == 0) + is_rect = idx == 0 for _, w in self._rect_rows: w.setVisible(is_rect) for _, w in self._circ_rows: @@ -226,28 +250,38 @@ class FiberSectionEditor(QDialog): if self._patch_type.currentIndex() == 0: p = RectangularPatch( material_id=mid, - n_fib_y=self._rect_ny.value(), n_fib_z=self._rect_nz.value(), - y_i=self._rect_yi.value(), z_i=self._rect_zi.value(), - y_j=self._rect_yj.value(), z_j=self._rect_zj.value(), + n_fib_y=self._rect_ny.value(), + n_fib_z=self._rect_nz.value(), + y_i=self._rect_yi.value(), + z_i=self._rect_zi.value(), + y_j=self._rect_yj.value(), + z_j=self._rect_zj.value(), ) self._patches.append(p) - self._item_list.addItem(QListWidgetItem( - f"Rect patch mat={mid} " - f"({p.y_i:.3f},{p.z_i:.3f})→({p.y_j:.3f},{p.z_j:.3f}) " - f"{p.n_fib_y}×{p.n_fib_z}", - )) + self._item_list.addItem( + QListWidgetItem( + f"Rect patch mat={mid} " + f"({p.y_i:.3f},{p.z_i:.3f})→({p.y_j:.3f},{p.z_j:.3f}) " + f"{p.n_fib_y}×{p.n_fib_z}", + ) + ) else: p = CircularPatch( material_id=mid, - n_fib_circ=self._circ_nc.value(), n_fib_rad=self._circ_nr.value(), - y_center=self._circ_yc.value(), z_center=self._circ_zc.value(), - r_inner=self._circ_ri.value(), r_outer=self._circ_ro.value(), + n_fib_circ=self._circ_nc.value(), + n_fib_rad=self._circ_nr.value(), + y_center=self._circ_yc.value(), + z_center=self._circ_zc.value(), + r_inner=self._circ_ri.value(), + r_outer=self._circ_ro.value(), ) self._patches.append(p) - self._item_list.addItem(QListWidgetItem( - f"Circ patch mat={mid} " - f"r={p.r_inner:.3f}→{p.r_outer:.3f} {p.n_fib_circ}×{p.n_fib_rad}", - )) + self._item_list.addItem( + QListWidgetItem( + f"Circ patch mat={mid} " + f"r={p.r_inner:.3f}→{p.r_outer:.3f} {p.n_fib_circ}×{p.n_fib_rad}", + ) + ) self._refresh_preview() def _on_add_layer(self) -> None: @@ -256,13 +290,17 @@ class FiberSectionEditor(QDialog): material_id=mid, n_bars=self._layer_nbars.value(), bar_area=self._layer_area.value(), - y_start=self._layer_ys.value(), z_start=self._layer_zs.value(), - y_end=self._layer_ye.value(), z_end=self._layer_ze.value(), + y_start=self._layer_ys.value(), + z_start=self._layer_zs.value(), + y_end=self._layer_ye.value(), + z_end=self._layer_ze.value(), ) self._layers.append(lay) - self._item_list.addItem(QListWidgetItem( - f"Layer mat={mid} {lay.n_bars} bars A={lay.bar_area:.4g}", - )) + self._item_list.addItem( + QListWidgetItem( + f"Layer mat={mid} {lay.n_bars} bars A={lay.bar_area:.4g}", + ) + ) self._refresh_preview() def _on_remove(self) -> None: @@ -280,8 +318,7 @@ class FiberSectionEditor(QDialog): # ── preview ───────────────────────────────────────────────────── def _refresh_preview(self) -> None: self._preview.clear() - sec = FiberSection(id=999999, patches=list(self._patches), - layers=list(self._layers)) + sec = FiberSection(id=999999, patches=list(self._patches), layers=list(self._layers)) props = compute_section_props(sec) if props.n_fibres == 0: self._props_label.setText("Add patches or layers to see the preview.") @@ -295,8 +332,9 @@ class FiberSectionEditor(QDialog): for lay in self._layers: mat_ids.append(lay.material_id) unique_mats = sorted(set(mat_ids)) if mat_ids else [1] - mat_to_color = {mid: _PATCH_COLORS[i % len(_PATCH_COLORS)] - for i, mid in enumerate(unique_mats)} + mat_to_color = { + mid: _PATCH_COLORS[i % len(_PATCH_COLORS)] for i, mid in enumerate(unique_mats) + } # Draw patch fibres as squares, layer fibres as circles. # Expand per-patch for color assignment. @@ -307,9 +345,13 @@ class FiberSectionEditor(QDialog): continue color = mat_to_color.get(p.material_id, "#888888") self._preview.plot( - sub[:, 0], sub[:, 1], - pen=None, symbol="s", symbolSize=6, - symbolBrush=color, symbolPen=None, + sub[:, 0], + sub[:, 1], + pen=None, + symbol="s", + symbolSize=6, + symbolBrush=color, + symbolPen=None, ) for lay in self._layers: sub_sec = FiberSection(id=999999, layers=[lay]) @@ -318,16 +360,24 @@ class FiberSectionEditor(QDialog): continue color = mat_to_color.get(lay.material_id, "#ff0000") self._preview.plot( - sub[:, 0], sub[:, 1], - pen=None, symbol="o", symbolSize=8, - symbolBrush=color, symbolPen=pg.mkPen("#ffffff", width=1), + sub[:, 0], + sub[:, 1], + pen=None, + symbol="o", + symbolSize=8, + symbolBrush=color, + symbolPen=pg.mkPen("#ffffff", width=1), ) # Centroid marker self._preview.plot( - [props.centroid_y], [props.centroid_z], - pen=None, symbol="+", symbolSize=16, - symbolBrush=None, symbolPen=pg.mkPen("#ff0000", width=2), + [props.centroid_y], + [props.centroid_z], + pen=None, + symbol="+", + symbolSize=16, + symbolBrush=None, + symbolPen=pg.mkPen("#ff0000", width=2), ) self._props_label.setText( @@ -341,16 +391,22 @@ class FiberSectionEditor(QDialog): for p in sec.patches: self._patches.append(p) if isinstance(p, RectangularPatch): - self._item_list.addItem(QListWidgetItem( - f"Rect patch mat={p.material_id} {p.n_fib_y}×{p.n_fib_z}", - )) + self._item_list.addItem( + QListWidgetItem( + f"Rect patch mat={p.material_id} {p.n_fib_y}×{p.n_fib_z}", + ) + ) elif isinstance(p, CircularPatch): - self._item_list.addItem(QListWidgetItem( - f"Circ patch mat={p.material_id} {p.n_fib_circ}×{p.n_fib_rad}", - )) + self._item_list.addItem( + QListWidgetItem( + f"Circ patch mat={p.material_id} {p.n_fib_circ}×{p.n_fib_rad}", + ) + ) for lay in sec.layers: if isinstance(lay, StraightLayer): self._layers.append(lay) - self._item_list.addItem(QListWidgetItem( - f"Layer mat={lay.material_id} {lay.n_bars} bars", - )) + self._item_list.addItem( + QListWidgetItem( + f"Layer mat={lay.material_id} {lay.n_bars} bars", + ) + ) diff --git a/src/otko/views/dialogs/section_forms.py b/src/otko/views/dialogs/section_forms.py index 7276efe..c824db7 100644 --- a/src/otko/views/dialogs/section_forms.py +++ b/src/otko/views/dialogs/section_forms.py @@ -25,9 +25,14 @@ from otko.core import ( ) -def _spin(default: float = 0.0, *, decimals: int = 8, - minimum: float = 1e-12, maximum: float = 1e15, - step: float = 1.0) -> QDoubleSpinBox: +def _spin( + default: float = 0.0, + *, + decimals: int = 8, + minimum: float = 1e-12, + maximum: float = 1e15, + step: float = 1.0, +) -> QDoubleSpinBox: sb = QDoubleSpinBox() sb.setRange(minimum, maximum) sb.setDecimals(decimals) @@ -107,9 +112,14 @@ class ElasticSectionForm(SectionFormBase): self._iy = _spin(8.33e-6, step=1e-7) self._g = _spin(80e9, step=1e9) self._j = _spin(1e-6, step=1e-7) - for label, w in (("E:", self._e), ("A:", self._a), - ("Iz:", self._iz), ("Iy:", self._iy), - ("G:", self._g), ("J:", self._j)): + for label, w in ( + ("E:", self._e), + ("A:", self._a), + ("Iz:", self._iz), + ("Iy:", self._iy), + ("G:", self._g), + ("J:", self._j), + ): self._layout.addRow(label, w) self._layout.addRow(QLabel("Iy, G, J required for 3D models.")) @@ -196,10 +206,12 @@ class FiberSectionSummaryForm(SectionFormBase): self._summary.setWordWrap(True) self._summary.setStyleSheet("color: #555;") self._layout.addRow(self._summary) - self._layout.addRow(QLabel( - "Edit this fiber section from the Section Library list — " - "Add / Modify uses the visual Fiber Section Editor." - )) + self._layout.addRow( + QLabel( + "Edit this fiber section from the Section Library list — " + "Add / Modify uses the visual Fiber Section Editor." + ) + ) self._cached: FiberSection | None = None def _populate_specific(self, s: FiberSection) -> None: @@ -240,9 +252,9 @@ class SectionAggregatorSummaryForm(SectionFormBase): def _populate_specific(self, s: SectionAggregator) -> None: self._cached = s - pairings = "
".join( - f" mat #{p.material_id} on DOF {p.dof}" for p in s.pairings - ) or "(none)" + pairings = ( + "
".join(f" mat #{p.material_id} on DOF {p.dof}" for p in s.pairings) or "(none)" + ) self._summary.setText( f"{s.name or 'Section Aggregator'}
" f"Wraps section: {s.section_id}
" @@ -268,10 +280,9 @@ def form_for(section: Any) -> SectionFormBase: # Graceful fallback — unknown section types display a minimal # placeholder instead of crashing the entire dialog. form = SectionFormBase() - form._layout.addRow(QLabel( - f"No form registered for section type " - f"{section.type} yet." - )) + form._layout.addRow( + QLabel(f"No form registered for section type " f"{section.type} yet.") + ) form._section_id = section.id form._name_edit.setText(getattr(section, "name", "") or "") form._name_edit.setEnabled(False) diff --git a/src/otko/views/dialogs/section_library.py b/src/otko/views/dialogs/section_library.py index 0647580..c525a50 100644 --- a/src/otko/views/dialogs/section_library.py +++ b/src/otko/views/dialogs/section_library.py @@ -113,6 +113,7 @@ class SectionLibraryDialog(QDialog): """ from otko.core import FiberSection from otko.views.dialogs.section_editor import FiberSectionEditor + sec = self._selected_section() if not isinstance(sec, FiberSection) or self._vm.project is None: return @@ -156,7 +157,12 @@ class SectionLibraryDialog(QDialog): summary_kinds = {"FiberSection", "SectionAggregator"} kinds = [k for k in FORM_REGISTRY if k not in summary_kinds] kind, ok = QInputDialog.getItem( - self, "Add section", "Type:", kinds, current=0, editable=False, + self, + "Add section", + "Type:", + kinds, + current=0, + editable=False, ) if not ok: return @@ -176,6 +182,7 @@ class SectionLibraryDialog(QDialog): if self._vm.project is None: return from otko.views.dialogs.section_editor import FiberSectionEditor + 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) @@ -196,9 +203,10 @@ class SectionLibraryDialog(QDialog): if section is None: return reply = QMessageBox.question( - self, "Delete section", + self, + "Delete section", f"Delete section #{section.id} ({section.type})?\n" - "Frame elements that reference it will be invalid until reassigned." + "Frame elements that reference it will be invalid until reassigned.", ) if reply != QMessageBox.StandardButton.Yes: return diff --git a/src/otko/views/dialogs/uniform_excitation.py b/src/otko/views/dialogs/uniform_excitation.py index f5c7cd7..6bdc94b 100644 --- a/src/otko/views/dialogs/uniform_excitation.py +++ b/src/otko/views/dialogs/uniform_excitation.py @@ -51,10 +51,12 @@ 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") @@ -84,16 +86,13 @@ 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 b8876ae..df4b70e 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, QPlainTextEdit, QTreeWidget, QTreeWidgetItem +from PySide6.QtWidgets import QDockWidget, QMessageBox, QTreeWidget, QTreeWidgetItem from otko.services.deformation import ( linear_static_auto_scale, @@ -34,6 +34,7 @@ from otko.services.results import ( ) from otko.views.canvas3d.model_renderer import RendererMode from otko.views.docks import ( + ConsoleDock, DeformedShapeView, ForceDiagramView, HysteresisView, @@ -103,13 +104,9 @@ class DockManager: self.tabifyDockWidget(props_dock, tree_dock) tree_dock.raise_() - 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._console_dock = ConsoleDock(self) + self._console = self._console_dock.view + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self._console_dock) self._table = TableDock() table_dock = QDockWidget("Table", self) @@ -122,7 +119,7 @@ class DockManager: results_dock.setWidget(self._results_panel) self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, results_dock) self._results_dock = results_dock - self.tabifyDockWidget(console_dock, table_dock) + self.tabifyDockWidget(self._console_dock, table_dock) self.tabifyDockWidget(table_dock, results_dock) table_dock.raise_() diff --git a/src/otko/views/docks/__init__.py b/src/otko/views/docks/__init__.py index 5b292c3..c479a97 100644 --- a/src/otko/views/docks/__init__.py +++ b/src/otko/views/docks/__init__.py @@ -1,5 +1,6 @@ """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 @@ -12,8 +13,15 @@ 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", + "PropertyEditorDock", + "ResultsPanel", + "TableDock", + "DeformedShapeView", + "ModeShapeAnimator", + "ForceDiagramView", + "TimeHistoryView", + "HysteresisView", + "PushoverCurveView", + "ResponseSpectrumView", + "ConsoleDock", ] diff --git a/src/otko/views/docks/deformed_shape.py b/src/otko/views/docks/deformed_shape.py index 3745c64..497dc85 100644 --- a/src/otko/views/docks/deformed_shape.py +++ b/src/otko/views/docks/deformed_shape.py @@ -31,8 +31,7 @@ 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() @@ -41,9 +40,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() @@ -53,8 +52,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 855f822..2580e36 100644 --- a/src/otko/views/docks/force_diagram.py +++ b/src/otko/views/docks/force_diagram.py @@ -31,12 +31,11 @@ from otko.services.element_forces import ForceComponent class ForceDiagramView(QWidget): """Compact controls for live-updating an element-force diagram.""" - componentChanged = Signal(object) # ForceComponent - changed = Signal(object, float) # (ForceComponent, scale) + componentChanged = Signal(object) # ForceComponent + 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) -> None: super().__init__(parent) self._scale_base = max(suggested_scale, 1e-12) self._pending_component_change = False @@ -87,7 +86,7 @@ class ForceDiagramView(QWidget): self._slider = QSlider(Qt.Orientation.Horizontal) self._slider.setRange(1, 1000) - self._slider.setValue(500) # midpoint = suggested scale + self._slider.setValue(500) # midpoint = suggested scale form.addRow("", self._slider) root.addWidget(group) diff --git a/src/otko/views/docks/property_editor.py b/src/otko/views/docks/property_editor.py index 5d42684..10cb7f7 100644 --- a/src/otko/views/docks/property_editor.py +++ b/src/otko/views/docks/property_editor.py @@ -32,8 +32,11 @@ from otko.core import Project # uniaxial material + area. The Convert command drops/adds fields to # bridge between them. _CONVERTIBLE_ELEMENT_TYPES = [ - "Truss", "CorotTruss", - "ElasticBeamColumn", "ForceBeamColumn", "DispBeamColumn", + "Truss", + "CorotTruss", + "ElasticBeamColumn", + "ForceBeamColumn", + "DispBeamColumn", ] @@ -53,7 +56,9 @@ class PropertyEditorDock(QScrollArea): self._layout.setContentsMargins(8, 8, 8, 8) self.setWidget(self._inner) self._project: Project | None = None - self.on_apply_mass: Callable[[int, tuple[float, float, float, float, float, float]], None] | None = None + self.on_apply_mass: ( + Callable[[int, tuple[float, float, float, float, float, float]], None] | None + ) = None # Element callbacks — all optional; wired by MainWindow. self.on_change_element_type: Callable[[int, str], None] | None = None self.on_change_element_material: Callable[[int, int], None] | None = None @@ -121,9 +126,12 @@ class PropertyEditorDock(QScrollArea): self._layout.addWidget(QLabel(f"

Node #{node.id}

")) form = QFormLayout() form.addRow("Name:", QLabel(node.name or "—")) - form.addRow("X, Y, Z:", QLabel( - f"{node.coords[0]:.4f}, {node.coords[1]:.4f}, {node.coords[2]:.4f}", - )) + form.addRow( + "X, Y, Z:", + QLabel( + f"{node.coords[0]:.4f}, {node.coords[1]:.4f}, {node.coords[2]:.4f}", + ), + ) form.addRow("Restraint:", QLabel(self._fmt_restraint(node.restraint))) self._layout.addLayout(form) @@ -179,6 +187,7 @@ class PropertyEditorDock(QScrollArea): def _on_type_changed(new_type: str, _eid: int = el.id) -> None: if new_type != el.type and self.on_change_element_type is not None: self.on_change_element_type(_eid, new_type) + type_cb.currentTextChanged.connect(_on_type_changed) form.addRow("Type:", type_cb) else: @@ -188,16 +197,19 @@ class PropertyEditorDock(QScrollArea): form.addRow("Nodes:", QLabel(", ".join(str(n) for n in el.nodes))) # ── Section picker for frame elements. ── - if (hasattr(el, "section_id") - and self._project is not None - and self._project.sections - and self.on_change_element_section is not None): + if ( + hasattr(el, "section_id") + and self._project is not None + and self._project.sections + and self.on_change_element_section is not None + ): sec_cb = QComboBox() for s in self._project.sections: sec_cb.addItem( - f"#{s.id} {s.name or s.type}", s.id, + f"#{s.id} {s.name or s.type}", + s.id, ) - idx = sec_cb.findData(el.section_id) # type: ignore[attr-defined] + idx = sec_cb.findData(el.section_id) # type: ignore[attr-defined] if idx >= 0: sec_cb.setCurrentIndex(idx) @@ -205,20 +217,23 @@ class PropertyEditorDock(QScrollArea): sid = sec_cb.currentData() if sid is not None and self.on_change_element_section is not None: self.on_change_element_section(_eid, int(sid)) + sec_cb.currentIndexChanged.connect(_on_section_changed) form.addRow("Section:", sec_cb) elif hasattr(el, "section_id"): form.addRow("Section id:", QLabel(str(el.section_id))) # ── Material picker for truss / uniaxial-material elements. ── - if (hasattr(el, "material_id") - and self._project is not None - and self._project.materials - and self.on_change_element_material is not None): + if ( + hasattr(el, "material_id") + and self._project is not None + and self._project.materials + and self.on_change_element_material is not None + ): mat_cb = QComboBox() for m in self._project.materials: mat_cb.addItem(f"#{m.id} {m.name or m.type}", m.id) - idx = mat_cb.findData(el.material_id) # type: ignore[attr-defined] + idx = mat_cb.findData(el.material_id) # type: ignore[attr-defined] if idx >= 0: mat_cb.setCurrentIndex(idx) @@ -226,6 +241,7 @@ class PropertyEditorDock(QScrollArea): mid = mat_cb.currentData() if mid is not None and self.on_change_element_material is not None: self.on_change_element_material(_eid, int(mid)) + mat_cb.currentIndexChanged.connect(_on_material_changed) form.addRow("Material:", mat_cb) elif hasattr(el, "material_id"): @@ -236,9 +252,12 @@ class PropertyEditorDock(QScrollArea): area_spin = QDoubleSpinBox() area_spin.setRange(1e-12, 1e6) area_spin.setDecimals(8) - area_spin.setValue(float(el.area)) # type: ignore[attr-defined] - area_spin.setSingleStep(float(el.area) * 0.1 # type: ignore[attr-defined] - if el.area else 0.001) # type: ignore[attr-defined] + area_spin.setValue(float(el.area)) # type: ignore[attr-defined] + area_spin.setSingleStep( + float(el.area) * 0.1 # type: ignore[attr-defined] + if el.area + else 0.001 + ) # type: ignore[attr-defined] # Commit on editingFinished so we don't dispatch a command on # every keystroke (which would spam the undo stack). @@ -255,6 +274,7 @@ class PropertyEditorDock(QScrollArea): return if self.on_change_element_fields is not None: self.on_change_element_fields(_eid, {"area": new_val}) + area_spin.editingFinished.connect(_on_area_edited) form.addRow("Area:", area_spin) elif hasattr(el, "area"): @@ -268,13 +288,17 @@ class PropertyEditorDock(QScrollArea): def _show_multi(self, node_ids: frozenset[int], element_ids: frozenset[int]) -> None: self._layout.addWidget(QLabel("

Multi-selection

")) if node_ids: - self._layout.addWidget(QLabel( - f"{len(node_ids)} node(s) selected: {self._fmt_id_list(node_ids)}", - )) + self._layout.addWidget( + QLabel( + f"{len(node_ids)} node(s) selected: {self._fmt_id_list(node_ids)}", + ) + ) if element_ids: - self._layout.addWidget(QLabel( - f"{len(element_ids)} element(s) selected: {self._fmt_id_list(element_ids)}", - )) + self._layout.addWidget( + QLabel( + f"{len(element_ids)} element(s) selected: {self._fmt_id_list(element_ids)}", + ) + ) self._layout.addStretch(1) @staticmethod @@ -288,4 +312,6 @@ class PropertyEditorDock(QScrollArea): sorted_ids = sorted(ids) if len(sorted_ids) <= limit: return ", ".join(str(i) for i in sorted_ids) - return ", ".join(str(i) for i in sorted_ids[:limit]) + f", … (+{len(sorted_ids) - limit} more)" + return ( + ", ".join(str(i) for i in sorted_ids[:limit]) + f", … (+{len(sorted_ids) - limit} more)" + ) diff --git a/src/otko/views/docks/response_spectrum.py b/src/otko/views/docks/response_spectrum.py index 56d8d6e..42cb523 100644 --- a/src/otko/views/docks/response_spectrum.py +++ b/src/otko/views/docks/response_spectrum.py @@ -36,7 +36,8 @@ class ResponseSpectrumView(QWidget): self._build_ui() def set_results( - self, results: ResponseSpectrumResults | None, + self, + results: ResponseSpectrumResults | None, spectrum: ResponseSpectrum | None, ) -> None: self._plot.clear() @@ -59,9 +60,13 @@ class ResponseSpectrumView(QWidget): self._plot.plot(p_dense, a_dense, pen=pen, name="Sa(T)") # Original control points. self._plot.plot( - list(spectrum.periods), list(spectrum.accelerations), - pen=None, symbol="s", symbolSize=7, - symbolBrush="#1f77b4", symbolPen=None, + list(spectrum.periods), + list(spectrum.accelerations), + pen=None, + symbol="s", + symbolSize=7, + symbolBrush="#1f77b4", + symbolPen=None, name="Control pts", ) @@ -77,14 +82,19 @@ class ResponseSpectrumView(QWidget): # Slight vertical jitter for duplicate periods y_offset = count * m.sa_at_period * 0.04 self._plot.plot( - [m.period], [m.sa_at_period + y_offset], - pen=None, symbol="o", symbolSize=12, - symbolBrush="#d62728", symbolPen=pg.mkPen("#ffffff", width=1), + [m.period], + [m.sa_at_period + y_offset], + pen=None, + symbol="o", + symbolSize=12, + symbolBrush="#d62728", + symbolPen=pg.mkPen("#ffffff", width=1), name=f"Mode {m.mode_number}" if count == 0 else None, ) # Small text label right next to the marker. txt = pg.TextItem( - f" M{m.mode_number}", color="#d62728", + f" M{m.mode_number}", + color="#d62728", anchor=(0.0, 0.5), ) txt.setPos(m.period, m.sa_at_period + y_offset) @@ -133,9 +143,17 @@ 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._table.setHorizontalHeaderLabels( + [ + "Mode", + "T (s)", + "f (Hz)", + "Γ", + "M_eff", + "Mass %", + "Sa(T)", + ] + ) 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 65d0866..9384e9a 100644 --- a/src/otko/views/docks/results_panel.py +++ b/src/otko/views/docks/results_panel.py @@ -20,6 +20,7 @@ from PySide6.QtWidgets import ( QWidget, ) +from otko.services.combinations import EnvelopeResults from otko.services.results import ( ModalResults, StaticResults, @@ -59,7 +60,18 @@ class ResultsPanel(QWidget): """ self._ndm, self._ndf = ndm, ndf self._tabs.clear() - if isinstance(results, StaticResults): + 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): self._title.setText( f"Static — case #{results.case_id} '{results.case_name}' " f"({results.n_steps} step(s))" @@ -67,9 +79,7 @@ class ResultsPanel(QWidget): self._tabs.addTab(self._build_static_disp_table(results), "Displacements") self._tabs.addTab(self._build_static_reaction_table(results), "Reactions") elif isinstance(results, ModalResults): - self._title.setText( - f"Modal — case #{results.case_id} '{results.case_name}'" - ) + self._title.setText(f"Modal — case #{results.case_id} '{results.case_name}'") self._tabs.addTab(self._build_modal_table(results), "Frequencies") elif isinstance(results, TransientResults): self._title.setText( @@ -109,6 +119,28 @@ class ResultsPanel(QWidget): 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" + ) + return self._wrap(table, caption) + def _build_modal_table(self, r: ModalResults) -> QWidget: n = len(r.eigenvalues) headers = ["Mode", "Eigenvalue (rad²/s²)", "ω (rad/s)", "f (Hz)", "T (s)"] @@ -124,15 +156,17 @@ class ResultsPanel(QWidget): def _transient_summary(self, r: TransientResults) -> QWidget: w = QWidget() layout = QVBoxLayout(w) - layout.addWidget(QLabel( - f"Steps: {r.n_steps}
" - f"dt: {r.dt:g}
" - f"Total time: {r.n_steps * r.dt:g}
" - f"HDF5 file: {r.h5_path}" - )) - layout.addWidget(QLabel( - "Time-history plots and animation will appear here in Phase 7." - )) + layout.addWidget( + QLabel( + f"Steps: {r.n_steps}
" + f"dt: {r.dt:g}
" + f"Total time: {r.n_steps * r.dt:g}
" + f"HDF5 file: {r.h5_path}" + ) + ) + layout.addWidget( + QLabel("Time-history plots and animation will appear here in Phase 7.") + ) layout.addStretch(1) return w diff --git a/src/otko/views/docks/table_dock.py b/src/otko/views/docks/table_dock.py index 23a2592..c8d3342 100644 --- a/src/otko/views/docks/table_dock.py +++ b/src/otko/views/docks/table_dock.py @@ -35,6 +35,7 @@ from PySide6.QtWidgets import ( from otko.commands import ( AddAnalysisCasesCommand, + AddCombinationsCommand, AddElementLoadsCommand, AddElementsCommand, AddEqualDOFConstraintCommand, @@ -47,6 +48,7 @@ from otko.commands import ( AssignMaterialCommand, AssignSectionCommand, DeleteAnalysisCasesCommand, + DeleteCombinationsCommand, DeleteElementLoadsCommand, DeleteElementsCommand, DeleteEqualDOFConstraintCommand, @@ -59,6 +61,7 @@ from otko.commands import ( SetMassCommand, SetRestraintCommand, UpdateAnalysisCaseCommand, + UpdateCombinationCommand, UpdateElementFieldsCommand, UpdateElementLoadCommand, UpdateEqualDOFConstraintCommand, @@ -75,6 +78,8 @@ from otko.core import ( ElasticUniaxial, EqualDOFConstraint, LinearTimeSeries, + LoadCombination, + LoadCombinationItem, NodalLoad, Node, PlainLoadPattern, @@ -169,6 +174,7 @@ _ANALYSIS_ATTRS = [ "tolerance", "max_iter", ] +_COMBO_HEADERS = ["id", "name", "kind", "cases", "factors"] _TAB_ORDER = ( "Nodes", @@ -181,6 +187,7 @@ _TAB_ORDER = ( "Element Loads", "Constraints", "Analyses", + "Combinations", ) @@ -270,6 +277,7 @@ 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() @@ -417,6 +425,7 @@ 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: @@ -1095,6 +1104,49 @@ 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: @@ -1192,6 +1244,16 @@ 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}") @@ -1264,6 +1326,8 @@ 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/main_window.py b/src/otko/views/main_window.py index 5577d8b..ec9f487 100644 --- a/src/otko/views/main_window.py +++ b/src/otko/views/main_window.py @@ -15,8 +15,9 @@ from __future__ import annotations from pathlib import Path -from PySide6.QtGui import QIcon -from PySide6.QtWidgets import QMainWindow +from PySide6.QtCore import QSettings +from PySide6.QtGui import QCloseEvent, QIcon +from PySide6.QtWidgets import QMainWindow, QMessageBox from otko.viewmodels import AnalysisRunner, ProjectViewModel from otko.views.action_handlers import ActionHandlers @@ -74,6 +75,7 @@ class MainWindow( self._build_status_bar() self._wire() self._refresh_action_enablement() + self.restore_layout() # ── construction ───────────────────────────────────────────────── def _build_central_canvas(self) -> None: @@ -82,6 +84,54 @@ class MainWindow( # Diagram overlay paints onto the same plotter as the model. self._diagram_renderer = DiagramRenderer(self._canvas) + # ── layout persistence ─────────────────────────────────────────── + def save_layout(self) -> None: + """Persist window geometry and dock/toolbar state for next launch.""" + settings = QSettings("OTKO", "OTKO") + settings.setValue("geometry", self.saveGeometry()) + 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. + """ + settings = QSettings("OTKO", "OTKO") + geometry = settings.value("geometry") + if geometry is not None: + self.restoreGeometry(geometry) + state = settings.value("windowState") + if state is not None: + self.restoreState(state) + + def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 (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() + # ── wiring ──────────────────────────────────────────────────────── def _wire(self) -> None: # File @@ -141,6 +191,7 @@ 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 @@ -156,7 +207,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.appendPlainText) + self._runner.log.connect(self._console_dock.log) 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 30450f9..bd8af6a 100644 --- a/src/otko/views/menu_builder.py +++ b/src/otko/views/menu_builder.py @@ -155,6 +155,7 @@ 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 @@ -285,6 +286,11 @@ 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", @@ -420,6 +426,7 @@ 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) @@ -482,7 +489,7 @@ class MenuBuilder: tb.setMovable(True) tb.setIconSize(QSize(24, 24)) tb.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) - # Top edge; Tools stays in the left area independently (built next). + # Top edge; Tools shares the top area (built next). self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb) self._view_toolbar = tb tb.addAction(self._act_zoom_extents) @@ -528,9 +535,8 @@ class MenuBuilder: tb.setMovable(True) tb.setIconSize(QSize(24, 24)) tb.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) - # View lives on the top edge, so Tools stays left independently - # in its own left-area column. - self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, tb) + # Top edge alongside the View toolbar (built just before). + self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb) self._tools_toolbar = tb tb.addAction(self._act_tool_select) tb.addAction(self._act_tool_draw_node) diff --git a/src/otko/views/render_controls.py b/src/otko/views/render_controls.py index fecf2d7..1d574c1 100644 --- a/src/otko/views/render_controls.py +++ b/src/otko/views/render_controls.py @@ -496,5 +496,5 @@ class RenderControls: self._act_clear_display.setEnabled(has_project) def _log(self, message: str) -> None: - self._console.appendPlainText(message) + self._console_dock.log(message) self.statusBar().showMessage(message, 5000) diff --git a/src/otko/views/tools/__init__.py b/src/otko/views/tools/__init__.py index 992e462..8c593d9 100644 --- a/src/otko/views/tools/__init__.py +++ b/src/otko/views/tools/__init__.py @@ -5,5 +5,11 @@ 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 2ffe593..04abc8e 100644 --- a/src/otko/views/tools/base.py +++ b/src/otko/views/tools/base.py @@ -84,10 +84,11 @@ 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 d0bd47a..958abe3 100644 --- a/src/otko/views/tools/draw_frame.py +++ b/src/otko/views/tools/draw_frame.py @@ -67,7 +67,9 @@ 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 b82660a..5e7ee0f 100644 --- a/src/otko/views/tools/draw_node.py +++ b/src/otko/views/tools/draw_node.py @@ -27,14 +27,20 @@ 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) @@ -70,16 +76,17 @@ 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: @@ -124,13 +131,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. @@ -153,9 +160,5 @@ 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/tests/conftest.py b/tests/conftest.py index 930c400..82dd86a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,22 +1,30 @@ -"""Shared pytest fixtures. +"""Shared pytest configuration. -`pytest-qt` automatically provides a `qtbot` fixture and a QApplication -instance. We add convenience fixtures here as the suite grows. +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. """ from __future__ import annotations +import os +from collections.abc import Iterator + import pytest +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + @pytest.fixture(autouse=True) -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. - """ +def _wipe_opensees_domain() -> Iterator[None]: + """Wipe the OpenseesPy domain after a test if the solver was imported.""" + yield try: - import openseespy.opensees as ops - except ImportError: + from openseespy.opensees import wipe + except Exception: # pragma: no cover - solver not installed / import side effects return - ops.wipe() + wipe() diff --git a/tests/gui/test_analysis_commands.py b/tests/gui/test_analysis_commands.py index ca5ecf8..9750537 100644 --- a/tests/gui/test_analysis_commands.py +++ b/tests/gui/test_analysis_commands.py @@ -29,8 +29,9 @@ 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 @@ -57,9 +58,14 @@ 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 @@ -74,9 +80,14 @@ 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 @@ -87,9 +98,14 @@ 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) @@ -109,10 +125,15 @@ 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 b2f982c..2d19389 100644 --- a/tests/gui/test_analysis_runner.py +++ b/tests/gui/test_analysis_runner.py @@ -35,21 +35,42 @@ 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 151b1d2..0f95da2 100644 --- a/tests/gui/test_assign_load_pattern_name.py +++ b/tests/gui/test_assign_load_pattern_name.py @@ -48,10 +48,11 @@ 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,14 +69,23 @@ 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 @@ -90,13 +100,20 @@ 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] @@ -111,11 +128,20 @@ 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 9501620..87a3a7a 100644 --- a/tests/gui/test_assign_masses.py +++ b/tests/gui/test_assign_masses.py @@ -23,7 +23,9 @@ 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 183e173..850e0a8 100644 --- a/tests/gui/test_assign_zls.py +++ b/tests/gui/test_assign_zls.py @@ -18,15 +18,24 @@ from otko.views.dialogs.assign_zls import ( # noqa: E402 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, + ) + ], ) @@ -43,7 +52,8 @@ 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_grid_snap.py b/tests/gui/test_canvas_grid_snap.py index 81e8bd3..8410edf 100644 --- a/tests/gui/test_canvas_grid_snap.py +++ b/tests/gui/test_canvas_grid_snap.py @@ -23,7 +23,8 @@ from otko.core import ( # noqa: E402 # ────────────────────── 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, @@ -33,7 +34,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 @@ -65,6 +66,7 @@ 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) @@ -101,15 +103,17 @@ 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) @@ -134,20 +138,23 @@ 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)) @@ -158,20 +165,23 @@ 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 @@ -183,7 +193,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 @@ -204,21 +214,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] @@ -226,6 +236,7 @@ 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_commands.py b/tests/gui/test_commands.py index a02bdc0..4584ed9 100644 --- a/tests/gui/test_commands.py +++ b/tests/gui/test_commands.py @@ -77,17 +77,27 @@ 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} @@ -102,10 +112,15 @@ 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 fa14511..4fa11ff 100644 --- a/tests/gui/test_convert_element_type.py +++ b/tests/gui/test_convert_element_type.py @@ -26,32 +26,62 @@ 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) @@ -62,13 +92,22 @@ 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) @@ -78,12 +117,22 @@ 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() @@ -93,13 +142,23 @@ 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 @@ -107,10 +166,20 @@ 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 c671a1e..57c1bfc 100644 --- a/tests/gui/test_define_dialogs.py +++ b/tests/gui/test_define_dialogs.py @@ -27,10 +27,12 @@ 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 b98bb40..6161eb2 100644 --- a/tests/gui/test_draw_frame_tool.py +++ b/tests/gui/test_draw_frame_tool.py @@ -35,10 +35,15 @@ 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 @@ -86,7 +91,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 @@ -150,6 +155,7 @@ 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] @@ -165,9 +171,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] @@ -179,18 +185,21 @@ 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} @@ -198,19 +207,22 @@ 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 26a9edb..d7d0023 100644 --- a/tests/gui/test_draw_node_tool.py +++ b/tests/gui/test_draw_node_tool.py @@ -46,8 +46,12 @@ 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) @@ -99,10 +103,11 @@ 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 3cc5d52..4c67164 100644 --- a/tests/gui/test_draw_truss_tool.py +++ b/tests/gui/test_draw_truss_tool.py @@ -34,10 +34,12 @@ 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", @@ -60,13 +62,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) @@ -78,9 +80,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 @@ -104,8 +106,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) @@ -115,5 +117,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 489d64b..c73e852 100644 --- a/tests/gui/test_grid_dialog.py +++ b/tests/gui/test_grid_dialog.py @@ -19,11 +19,18 @@ from otko.core import ( # noqa: E402 @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] @@ -33,6 +40,7 @@ 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) @@ -48,6 +56,7 @@ 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), @@ -65,6 +74,7 @@ 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)), @@ -96,6 +106,7 @@ 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])), @@ -117,6 +128,7 @@ 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" @@ -130,6 +142,7 @@ 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 8ce4d0b..a57a00f 100644 --- a/tests/gui/test_main_window.py +++ b/tests/gui/test_main_window.py @@ -20,3 +20,22 @@ 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 4671343..4dd34d5 100644 --- a/tests/gui/test_material_dialog_concrete04.py +++ b/tests/gui/test_material_dialog_concrete04.py @@ -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,9 +77,14 @@ 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() @@ -102,8 +107,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 13a6eb3..cb47096 100644 --- a/tests/gui/test_material_form_ranges.py +++ b/tests/gui/test_material_form_ranges.py @@ -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,6 +83,7 @@ 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 1571fc3..7245068 100644 --- a/tests/gui/test_path_ts_dialogs.py +++ b/tests/gui/test_path_ts_dialogs.py @@ -28,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] @@ -39,9 +39,14 @@ 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)) @@ -51,22 +56,31 @@ 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 @@ -87,20 +101,24 @@ 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) @@ -114,6 +132,7 @@ 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) @@ -123,9 +142,12 @@ 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_property_commands.py b/tests/gui/test_property_commands.py index 117c445..a325b87 100644 --- a/tests/gui/test_property_commands.py +++ b/tests/gui/test_property_commands.py @@ -30,18 +30,34 @@ 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 @@ -91,8 +107,7 @@ 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 @@ -106,14 +121,23 @@ 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 @@ -126,10 +150,15 @@ 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 @@ -140,13 +169,23 @@ 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 @@ -157,10 +196,15 @@ 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 7a5d74c..3943498 100644 --- a/tests/gui/test_property_forms.py +++ b/tests/gui/test_property_forms.py @@ -37,8 +37,7 @@ 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() @@ -50,8 +49,15 @@ 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) @@ -75,8 +81,14 @@ 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) diff --git a/tests/gui/test_pushover_curve_units.py b/tests/gui/test_pushover_curve_units.py index 09c03b6..03af4bb 100644 --- a/tests/gui/test_pushover_curve_units.py +++ b/tests/gui/test_pushover_curve_units.py @@ -17,8 +17,11 @@ from otko.views.docks.pushover_curve import ( # noqa: E402 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]), ) @@ -34,10 +37,10 @@ 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 @@ -96,8 +99,7 @@ def test_no_auto_scaling_applied_to_values(qtbot) -> None: # type: ignore[no-un 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 0e40752..03319e5 100644 --- a/tests/gui/test_run_analysis.py +++ b/tests/gui/test_run_analysis.py @@ -41,16 +41,18 @@ 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 449b24e..ad21da0 100644 --- a/tests/gui/test_section_extrusion.py +++ b/tests/gui/test_section_extrusion.py @@ -18,18 +18,25 @@ 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), ], @@ -39,6 +46,7 @@ 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()) @@ -58,6 +66,7 @@ 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 9e68a75..3c52f4d 100644 --- a/tests/gui/test_section_forms.py +++ b/tests/gui/test_section_forms.py @@ -31,7 +31,14 @@ 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) @@ -41,11 +48,19 @@ 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) @@ -59,7 +74,9 @@ 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) @@ -71,10 +88,12 @@ 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_transforms.py b/tests/gui/test_transforms.py index 120cc64..4ad8716 100644 --- a/tests/gui/test_transforms.py +++ b/tests/gui/test_transforms.py @@ -21,19 +21,29 @@ 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 @@ -44,7 +54,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 @@ -68,8 +78,9 @@ 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 @@ -81,8 +92,9 @@ 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 @@ -94,8 +106,7 @@ 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 9b5e288..9048600 100644 --- a/tests/gui/test_tree_multi_select.py +++ b/tests/gui/test_tree_multi_select.py @@ -16,6 +16,7 @@ from otko.core import Node # noqa: E402 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,14 +28,17 @@ 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 b3c27e7..b052ad4 100644 --- a/tests/gui/test_units_status_bar.py +++ b/tests/gui/test_units_status_bar.py @@ -12,6 +12,7 @@ from otko.core import UnitSystem # noqa: E402 @pytest.mark.gui def test_status_bar_combo_lists_all_unit_systems(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.main_window import MainWindow + mw = MainWindow() qtbot.addWidget(mw) assert mw._units_combo.count() == len(list(UnitSystem)) @@ -20,6 +21,7 @@ def test_status_bar_combo_lists_all_unit_systems(qtbot) -> None: # type: ignore @pytest.mark.gui def test_status_bar_combo_reflects_project_units(qtbot) -> None: # type: ignore[no-untyped-def] from otko.views.main_window import MainWindow + mw = MainWindow() qtbot.addWidget(mw) mw._vm.new_project() @@ -32,6 +34,7 @@ def test_status_bar_combo_reflects_project_units(qtbot) -> None: # type: ignore def test_status_bar_combo_write_updates_project(qtbot) -> None: # type: ignore[no-untyped-def] """Changing the combo writes through to project.meta.units.""" from otko.views.main_window import MainWindow + mw = MainWindow() qtbot.addWidget(mw) mw._vm.new_project() @@ -46,6 +49,7 @@ def test_status_bar_combo_write_updates_project(qtbot) -> None: # type: ignore[ def test_status_bar_combo_no_project_noop(qtbot) -> None: # type: ignore[no-untyped-def] """Before a project is loaded, changing the combo is a no-op.""" from otko.views.main_window import MainWindow + mw = MainWindow() qtbot.addWidget(mw) # No crash even with no project. diff --git a/tests/gui/test_update_element_fields.py b/tests/gui/test_update_element_fields.py index e684e5a..bdecc06 100644 --- a/tests/gui/test_update_element_fields.py +++ b/tests/gui/test_update_element_fields.py @@ -23,16 +23,31 @@ 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 @@ -57,9 +72,13 @@ 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 @@ -68,9 +87,13 @@ 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 b0fc2c8..a0b59f0 100644 --- a/tests/gui/test_working_plane.py +++ b/tests/gui/test_working_plane.py @@ -33,6 +33,7 @@ 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()) @@ -45,6 +46,7 @@ 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()) @@ -61,6 +63,7 @@ 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()) @@ -74,6 +77,7 @@ 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()) @@ -87,6 +91,7 @@ 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()) @@ -100,6 +105,7 @@ 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): @@ -112,10 +118,11 @@ 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 @@ -126,6 +133,7 @@ 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()) @@ -133,7 +141,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) @@ -162,6 +170,7 @@ 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 db4b471..72ffaa3 100644 --- a/tests/integration/test_basic_truss.py +++ b/tests/integration/test_basic_truss.py @@ -13,6 +13,8 @@ pytest.importorskip("openseespy") from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + def test_basic_truss_matches_opensees_tcl_reference() -> None: """Crown displacement must match the OpenSees Tcl Example-1 reference. diff --git a/tests/integration/test_beam_quad_2d.py b/tests/integration/test_beam_quad_2d.py index 6ca31a5..6c52b5b 100644 --- a/tests/integration/test_beam_quad_2d.py +++ b/tests/integration/test_beam_quad_2d.py @@ -12,6 +12,8 @@ pytest.importorskip("openseespy") from otko.services import load_project, save_project # noqa: E402 from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + def test_beam_quad_2d_midspan_deflection(tmp_path) -> None: # type: ignore[no-untyped-def] """10-step LoadControl on a 16×4 plane-stress quad mesh — reference @@ -23,6 +25,7 @@ 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() @@ -49,10 +52,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) @@ -69,6 +72,7 @@ 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() @@ -81,6 +85,7 @@ 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_concrete04_runner.py b/tests/integration/test_concrete04_runner.py index a3aa8e1..054f462 100644 --- a/tests/integration/test_concrete04_runner.py +++ b/tests/integration/test_concrete04_runner.py @@ -34,22 +34,25 @@ from otko.core import ( # noqa: E402 ) 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²] +pytestmark = pytest.mark.slow -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] + +# ── Model constants ──────────────────────────────────────────────────────────── +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] # 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: @@ -61,32 +64,45 @@ def _build_project() -> Project: 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, @@ -96,7 +112,9 @@ 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)), ], @@ -104,7 +122,8 @@ 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, @@ -139,9 +158,14 @@ 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 087b461..cb20abb 100644 --- a/tests/integration/test_dof_coverage.py +++ b/tests/integration/test_dof_coverage.py @@ -23,15 +23,16 @@ from otko.core import ( # noqa: E402 ) from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + 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)], @@ -40,10 +41,13 @@ 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 5f76282..49eb4cd 100644 --- a/tests/integration/test_eigen_two_storey_one_bay_frame.py +++ b/tests/integration/test_eigen_two_storey_one_bay_frame.py @@ -12,6 +12,8 @@ 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 +pytestmark = pytest.mark.slow + def _reload(proj, tmp_path): # type: ignore[no-untyped-def] path = tmp_path / "two_storey_one_bay.osmodel" diff --git a/tests/integration/test_eigen_two_storey_shear_frame.py b/tests/integration/test_eigen_two_storey_shear_frame.py index c0cc4db..724ba40 100644 --- a/tests/integration/test_eigen_two_storey_shear_frame.py +++ b/tests/integration/test_eigen_two_storey_shear_frame.py @@ -12,6 +12,8 @@ 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 +pytestmark = pytest.mark.slow + def _reload(proj, tmp_path): # type: ignore[no-untyped-def] path = tmp_path / "two_storey_shear.osmodel" diff --git a/tests/integration/test_elastic_frame.py b/tests/integration/test_elastic_frame.py index 54edec4..03aa14c 100644 --- a/tests/integration/test_elastic_frame.py +++ b/tests/integration/test_elastic_frame.py @@ -12,6 +12,8 @@ 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 +pytestmark = pytest.mark.slow + BASE_NODES = (1, 2, 3, 4) @@ -27,13 +29,18 @@ 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 ( - BAY, 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) @@ -51,24 +58,33 @@ 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 ( - BAY, 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) @@ -87,6 +103,7 @@ 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)) @@ -104,6 +121,4 @@ def test_elastic_frame_modal_periods(tmp_path) -> None: # type: ignore[no-untyp 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), start=1): - assert T == pytest.approx(T_ref, rel=0.02), ( - f"T{i} = {T:.4f} s, reference {T_ref:.4f} s" - ) + 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 660810a..e0d0a1d 100644 --- a/tests/integration/test_ex1a_canti2d.py +++ b/tests/integration/test_ex1a_canti2d.py @@ -13,6 +13,8 @@ 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 +pytestmark = pytest.mark.slow + def _reload(proj, tmp_path): # type: ignore[no-untyped-def] path = tmp_path / "ex1a_canti2d.osmodel" diff --git a/tests/integration/test_ex1a_canti2d_eq.py b/tests/integration/test_ex1a_canti2d_eq.py index 246af57..69ad31c 100644 --- a/tests/integration/test_ex1a_canti2d_eq.py +++ b/tests/integration/test_ex1a_canti2d_eq.py @@ -12,6 +12,8 @@ pytest.importorskip("openseespy") from otko.services import load_project, save_project # noqa: E402 from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + def test_ex1a_canti2d_eq_runs_and_oscillates(tmp_path) -> None: # type: ignore[no-untyped-def] from examples.ex1a_canti2d_eq import ( diff --git a/tests/integration/test_ex1b_portal2d.py b/tests/integration/test_ex1b_portal2d.py index 8e3f2a4..324837e 100644 --- a/tests/integration/test_ex1b_portal2d.py +++ b/tests/integration/test_ex1b_portal2d.py @@ -13,6 +13,8 @@ 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 +pytestmark = pytest.mark.slow + def _reload(proj, tmp_path): # type: ignore[no-untyped-def] path = tmp_path / "ex1b_portal2d.osmodel" diff --git a/tests/integration/test_ex2a_canti2d_elastic_element.py b/tests/integration/test_ex2a_canti2d_elastic_element.py index 5a79a65..3218683 100644 --- a/tests/integration/test_ex2a_canti2d_elastic_element.py +++ b/tests/integration/test_ex2a_canti2d_elastic_element.py @@ -13,6 +13,8 @@ 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 +pytestmark = pytest.mark.slow + def _reload(proj, tmp_path): # type: ignore[no-untyped-def] path = tmp_path / "ex2a_canti2d_elastic_element.osmodel" diff --git a/tests/integration/test_ex2b_canti2d_inelastic_section.py b/tests/integration/test_ex2b_canti2d_inelastic_section.py index 4afae36..2909e21 100644 --- a/tests/integration/test_ex2b_canti2d_inelastic_section.py +++ b/tests/integration/test_ex2b_canti2d_inelastic_section.py @@ -13,6 +13,8 @@ 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 +pytestmark = pytest.mark.slow + def _reload(proj, tmp_path): # type: ignore[no-untyped-def] path = tmp_path / "ex2b_canti2d_inelastic_section.osmodel" diff --git a/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py b/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py index a9caf9c..86d6fa8 100644 --- a/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py +++ b/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py @@ -13,6 +13,8 @@ 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 +pytestmark = pytest.mark.slow + def _reload(proj, tmp_path): # type: ignore[no-untyped-def] path = tmp_path / "ex2c_canti2d_inelastic_fiber_section.osmodel" diff --git a/tests/integration/test_ex3_canti2d_variants.py b/tests/integration/test_ex3_canti2d_variants.py index 273610d..8007be3 100644 --- a/tests/integration/test_ex3_canti2d_variants.py +++ b/tests/integration/test_ex3_canti2d_variants.py @@ -13,6 +13,8 @@ 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 +pytestmark = pytest.mark.slow + def _reload(proj, tmp_path, stem: str): # type: ignore[no-untyped-def] path = tmp_path / f"{stem}.osmodel" @@ -27,10 +29,16 @@ 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)) @@ -41,8 +49,12 @@ def test_ex3_variant_pushover_runs(tmp_path, builder_name: str, module_name: str 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: @@ -54,7 +66,10 @@ def test_ex3_variant_pushover_runs(tmp_path, builder_name: str, module_name: str [ ("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 875942f..e3abe5a 100644 --- a/tests/integration/test_ex4_portal2d_variants.py +++ b/tests/integration/test_ex4_portal2d_variants.py @@ -13,6 +13,8 @@ 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 +pytestmark = pytest.mark.slow + def _reload(proj, tmp_path, stem: str): # type: ignore[no-untyped-def] path = tmp_path / f"{stem}.osmodel" @@ -27,10 +29,16 @@ 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)) @@ -45,8 +53,12 @@ def test_ex4_variant_pushover_runs(tmp_path, builder_name: str, module_name: str 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: @@ -58,7 +70,10 @@ def test_ex4_variant_pushover_runs(tmp_path, builder_name: str, module_name: str [ ("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 0711df4..6463b21 100644 --- a/tests/integration/test_material_tester.py +++ b/tests/integration/test_material_tester.py @@ -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}" diff --git a/tests/integration/test_moment_curvature.py b/tests/integration/test_moment_curvature.py index 8d17046..a73c4ec 100644 --- a/tests/integration/test_moment_curvature.py +++ b/tests/integration/test_moment_curvature.py @@ -28,6 +28,8 @@ from otko.core import ( # noqa: E402 ) from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + def _moment_curvature_project(moment: float) -> Project: """Two coincident nodes + a rectangular fibre section + one moment step. @@ -36,32 +38,44 @@ 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)], ) @@ -72,16 +86,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: @@ -94,23 +108,28 @@ 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 @@ -119,9 +138,9 @@ def test_pushover_drives_rotation_for_moment_curvature() -> None: 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) @@ -145,6 +164,7 @@ def test_moment_curvature_with_constant_axial_preload() -> None: Steel01, StraightLayer, ) + colWidth = 15.0 colDepth = 24.0 cover = 1.5 @@ -153,58 +173,104 @@ 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"), @@ -212,14 +278,16 @@ 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=[], @@ -227,25 +295,29 @@ 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 1c27358..f0bed1f 100644 --- a/tests/integration/test_moment_curvature_example.py +++ b/tests/integration/test_moment_curvature_example.py @@ -9,6 +9,8 @@ pytest.importorskip("openseespy") from otko.services import load_project # noqa: E402 from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + 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.""" @@ -21,11 +23,13 @@ def test_moment_curvature_example_round_trips_and_converges(tmp_path) -> None: MU, NUM_INCR, ) + 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) @@ -39,15 +43,14 @@ 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 f67c645..58d58a1 100644 --- a/tests/integration/test_pattern_factors.py +++ b/tests/integration/test_pattern_factors.py @@ -20,6 +20,8 @@ from otko.core import ( # noqa: E402 ) from otko.services import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + def _cantilever() -> Project: length = 5.0 diff --git a/tests/integration/test_rc_frame_earthquake.py b/tests/integration/test_rc_frame_earthquake.py index 8f2a7a8..1d4d3dc 100644 --- a/tests/integration/test_rc_frame_earthquake.py +++ b/tests/integration/test_rc_frame_earthquake.py @@ -12,6 +12,8 @@ pytest.importorskip("openseespy") from otko.services import load_project, save_project # noqa: E402 from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + 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.""" @@ -20,6 +22,7 @@ def test_rc_frame_earthquake_runs_and_has_oscillatory_response(tmp_path) -> None DT, N_PTS, ) + proj = build_rc_frame_earthquake() proj.validate_references() @@ -33,9 +36,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 @@ -50,4 +53,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" \ No newline at end of file + assert max(abs(uy)) < 1.0, f"max |Uy| = {max(abs(uy)):.4f} in too large" diff --git a/tests/integration/test_rc_frame_gravity.py b/tests/integration/test_rc_frame_gravity.py index a213681..5250108 100644 --- a/tests/integration/test_rc_frame_gravity.py +++ b/tests/integration/test_rc_frame_gravity.py @@ -9,6 +9,8 @@ pytest.importorskip("openseespy") from otko.services import load_project, save_project # noqa: E402 from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + def test_rc_frame_gravity_matches_opensees_reference(tmp_path) -> None: # type: ignore[no-untyped-def] """Build → save → load → run → compare to OpenSees Tcl output. @@ -21,6 +23,7 @@ def test_rc_frame_gravity_matches_opensees_reference(tmp_path) -> None: # type: load stepped onto each top node). """ from examples.rc_frame_gravity import build_rc_frame_gravity, P_LOAD + proj = build_rc_frame_gravity() proj.validate_references() @@ -54,7 +57,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 d7d6df5..89518c3 100644 --- a/tests/integration/test_rc_frame_pushover.py +++ b/tests/integration/test_rc_frame_pushover.py @@ -9,6 +9,8 @@ pytest.importorskip("openseespy") from otko.services import load_project, save_project # noqa: E402 from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + def test_rc_frame_pushover_reaches_target_with_fallback(tmp_path) -> None: # type: ignore[no-untyped-def] """The 15-in pushover requires the ModifiedNewton convergence @@ -22,6 +24,7 @@ def test_rc_frame_pushover_reaches_target_with_fallback(tmp_path) -> None: # ty D_TARGET, ) from otko.core import PushoverCase + proj = build_rc_frame_pushover() proj.validate_references() @@ -35,7 +38,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." @@ -64,6 +67,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 3565fb7..54daa94 100644 --- a/tests/integration/test_runner_imposed_motion.py +++ b/tests/integration/test_runner_imposed_motion.py @@ -35,6 +35,9 @@ from otko.core import ( # noqa: E402 ) from otko.services import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + + W = 1.57 # drive (rad/s) DT_SERIES = 0.01 NPTS = 1601 # 16 s diff --git a/tests/integration/test_runner_modal.py b/tests/integration/test_runner_modal.py index 753210b..670e682 100644 --- a/tests/integration/test_runner_modal.py +++ b/tests/integration/test_runner_modal.py @@ -26,6 +26,8 @@ from otko.core import ( # noqa: E402 ) from otko.services import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + def test_sdof_pole_first_frequency_matches_kspring_over_m() -> None: """Vertical pole, mass at top, fixed base. ω₁ = √(3EI/(mL³)).""" @@ -36,11 +38,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)], @@ -54,9 +56,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: @@ -64,11 +66,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 da5aa9d..5722c0b 100644 --- a/tests/integration/test_runner_static.py +++ b/tests/integration/test_runner_static.py @@ -24,6 +24,8 @@ from otko.core import ( # noqa: E402 ) from otko.services import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + def test_cantilever_tip_deflection_matches_closed_form() -> None: """Horizontal cantilever in 2D: P at tip, expect δ = P L³ / (3 E I).""" @@ -34,7 +36,8 @@ 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)), @@ -44,7 +47,8 @@ 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))], ) @@ -54,12 +58,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 69c5f3b..4715153 100644 --- a/tests/integration/test_runner_transient.py +++ b/tests/integration/test_runner_transient.py @@ -29,6 +29,8 @@ from otko.core import ( # noqa: E402 ) from otko.services import OpenSeesRunner # noqa: E402 +pytestmark = pytest.mark.slow + def test_sdof_free_vibration_matches_cosine(tmp_path) -> None: # type: ignore[no-untyped-def] """Initial displacement, no external load, no damping → u(t) = u₀ cos(ωt).""" @@ -52,25 +54,27 @@ def test_sdof_free_vibration_matches_cosine(tmp_path) -> None: # type: ignore[n 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))], ), ], @@ -85,8 +89,11 @@ 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), ) @@ -111,7 +118,8 @@ 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), @@ -121,7 +129,8 @@ 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/tools/test_parse_schemas.py b/tests/tools/test_parse_schemas.py index cf9dc38..e1159aa 100644 --- a/tests/tools/test_parse_schemas.py +++ b/tests/tools/test_parse_schemas.py @@ -297,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") @@ -316,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") @@ -354,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 ccec7eb..52142a0 100644 --- a/tests/unit/core/materials/test_concrete04.py +++ b/tests/unit/core/materials/test_concrete04.py @@ -24,9 +24,14 @@ 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 @@ -34,9 +39,15 @@ 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) @@ -83,8 +94,14 @@ 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, ) @@ -101,9 +118,14 @@ 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) diff --git a/tests/unit/test_diagram_renderer.py b/tests/unit/test_diagram_renderer.py index a1cd1d1..cf6ae38 100644 --- a/tests/unit/test_diagram_renderer.py +++ b/tests/unit/test_diagram_renderer.py @@ -59,12 +59,9 @@ 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] @@ -93,14 +90,17 @@ 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,7 +108,9 @@ 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_grid_system.py b/tests/unit/test_grid_system.py index 5461d9b..adcbb55 100644 --- a/tests/unit/test_grid_system.py +++ b/tests/unit/test_grid_system.py @@ -86,6 +86,7 @@ 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,12 +103,13 @@ 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) @@ -116,10 +118,13 @@ 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)) @@ -205,7 +210,8 @@ 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/unit/test_materials.py b/tests/unit/test_materials.py index 36086a5..43bde52 100644 --- a/tests/unit/test_materials.py +++ b/tests/unit/test_materials.py @@ -37,8 +37,14 @@ 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. @@ -49,8 +55,14 @@ 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}, ) @@ -66,7 +78,8 @@ 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_node.py b/tests/unit/test_node.py index fbfbe22..a2e9c74 100644 --- a/tests/unit/test_node.py +++ b/tests/unit/test_node.py @@ -19,8 +19,9 @@ 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_phase8a.py b/tests/unit/test_phase8a.py index 033ef00..0e67780 100644 --- a/tests/unit/test_phase8a.py +++ b/tests/unit/test_phase8a.py @@ -67,7 +67,8 @@ 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,10 +78,13 @@ 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) @@ -121,7 +125,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 @@ -136,16 +140,21 @@ 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) @@ -165,16 +174,20 @@ 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) @@ -187,14 +200,13 @@ 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) @@ -217,6 +229,7 @@ class _RecordingOps: def rec(*args, **kwargs): # type: ignore[no-untyped-def] self.calls.append((name, args, kwargs)) return None + return rec @@ -224,8 +237,10 @@ 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")], @@ -242,7 +257,10 @@ 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) @@ -253,7 +271,11 @@ 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 @@ -273,14 +295,13 @@ 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 c398b3f..41d50fb 100644 --- a/tests/unit/test_phase8b.py +++ b/tests/unit/test_phase8b.py @@ -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, ), ], ) diff --git a/tests/unit/test_phase8c.py b/tests/unit/test_phase8c.py index c49f764..557ed34 100644 --- a/tests/unit/test_phase8c.py +++ b/tests/unit/test_phase8c.py @@ -27,7 +27,8 @@ 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, @@ -39,7 +40,8 @@ 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], ) @@ -47,7 +49,8 @@ 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], ) @@ -55,7 +58,8 @@ 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], ) @@ -63,16 +67,18 @@ 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 @@ -88,29 +94,26 @@ def _two_dof_modal(masses: list[float]) -> tuple[Project, ModalResults]: """ p = Project( meta=ProjectMeta(name="2dof", 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, 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 @@ -124,7 +127,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) @@ -144,10 +147,8 @@ 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) @@ -157,12 +158,9 @@ 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) @@ -171,21 +169,29 @@ def test_cqc_equals_srss_for_well_separated_modes() -> None: 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)], + 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 1b6413e..04af175 100644 --- a/tests/unit/test_phase9.py +++ b/tests/unit/test_phase9.py @@ -29,10 +29,17 @@ 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) @@ -43,10 +50,17 @@ 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) @@ -58,13 +72,18 @@ 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) @@ -75,11 +94,17 @@ 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) @@ -91,7 +116,8 @@ 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")], ) @@ -102,20 +128,27 @@ 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) @@ -123,29 +156,50 @@ def test_Iz_of_rect_matches_analytical() -> None: # ── Round-trip persistence ─────────────────────────────────────────── def test_fiber_section_with_patches_round_trips(tmp_path) -> None: # type: ignore[no-untyped-def] 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_project.py b/tests/unit/test_project.py index 6df0846..fe36fd5 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -126,7 +126,8 @@ def test_validate_references_catches_missing_time_series_in_pattern() -> None: nodes=[Node(id=1, coords=(0, 0, 0))], load_patterns=[ PlainLoadPattern( - id=1, time_series_id=99, + id=1, + time_series_id=99, nodal_loads=[NodalLoad(node_id=1, forces=(0, 0, -10, 0, 0, 0))], ) ], @@ -143,8 +144,13 @@ def test_validate_references_catches_missing_material_in_fiber_patch() -> None: id=1, patches=[ RectangularPatch( - material_id=99, n_fib_y=2, n_fib_z=2, - y_i=-0.1, z_i=-0.1, y_j=0.1, z_j=0.1, + material_id=99, + n_fib_y=2, + n_fib_z=2, + y_i=-0.1, + z_i=-0.1, + y_j=0.1, + z_j=0.1, ) ], ) @@ -162,8 +168,13 @@ def test_validate_references_catches_missing_material_in_fiber_layer() -> None: id=1, layers=[ StraightLayer( - material_id=99, n_bars=3, bar_area=1e-4, - y_start=-0.1, z_start=-0.1, y_end=0.1, z_end=-0.1, + material_id=99, + n_bars=3, + bar_area=1e-4, + y_start=-0.1, + z_start=-0.1, + y_end=0.1, + z_end=-0.1, ) ], ) @@ -179,7 +190,8 @@ def test_validate_references_catches_missing_material_in_aggregator_pairing() -> sections=[ ElasticSection(id=1, E=200e9, A=0.01, Iz=8.33e-6), SectionAggregator( - id=2, section_id=1, + id=2, + section_id=1, pairings=[AggregatorDOF(material_id=99, dof="T")], ), ], @@ -194,7 +206,8 @@ def test_validate_references_catches_missing_base_section_in_aggregator() -> Non materials=[Steel01(id=1, Fy=420e6, E0=200e9, b=0.01)], sections=[ SectionAggregator( - id=2, section_id=42, + id=2, + section_id=42, pairings=[AggregatorDOF(material_id=1, dof="T")], ), ], @@ -214,19 +227,30 @@ def test_validate_references_passes_on_valid_fiber_and_aggregator() -> None: id=1, patches=[ RectangularPatch( - material_id=1, n_fib_y=2, n_fib_z=2, - y_i=-0.1, z_i=-0.1, y_j=0.1, z_j=0.1, + material_id=1, + n_fib_y=2, + n_fib_z=2, + y_i=-0.1, + z_i=-0.1, + y_j=0.1, + z_j=0.1, ) ], layers=[ StraightLayer( - material_id=2, n_bars=3, bar_area=1e-4, - y_start=-0.1, z_start=-0.1, y_end=0.1, z_end=-0.1, + material_id=2, + n_bars=3, + bar_area=1e-4, + y_start=-0.1, + z_start=-0.1, + y_end=0.1, + z_end=-0.1, ) ], ), SectionAggregator( - id=2, section_id=1, + id=2, + section_id=1, pairings=[AggregatorDOF(material_id=1, dof="T")], ), ], @@ -237,7 +261,8 @@ def test_validate_references_passes_on_valid_fiber_and_aggregator() -> None: # ────────────────────────── small smoke build ────────────────────────── def test_full_truss_project_builds_and_validates() -> None: p = 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)), @@ -253,7 +278,8 @@ def test_full_truss_project_builds_and_validates() -> None: 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))], ) ], diff --git a/tests/unit/test_zerolength_section.py b/tests/unit/test_zerolength_section.py index 72d3409..696b83e 100644 --- a/tests/unit/test_zerolength_section.py +++ b/tests/unit/test_zerolength_section.py @@ -23,7 +23,9 @@ def test_zero_length_section_schema_defaults() -> None: def test_zero_length_section_rejects_extra_fields() -> None: 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] ) @@ -31,17 +33,24 @@ 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)), + 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, + ) ], - 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), ], From ba3cad4eff50c261c7ca977af1cd825f4d995411 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 13:08:49 -0400 Subject: [PATCH 10/28] fix: ElasticSectionForm round-trips the SectionShape union The form still passed flat b/h/d/tw/tf/diameter fields, which ElasticSection (extra='forbid') rejects, so reading any elastic section crashed. Rebuild the display-only hint as a PipeShape / AngleShape / RectShape member instead; an incomplete shape degrades to None rather than raising. --- src/otko/views/dialogs/section_forms.py | 61 +++++++++++++++++-------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/src/otko/views/dialogs/section_forms.py b/src/otko/views/dialogs/section_forms.py index c824db7..ec386e4 100644 --- a/src/otko/views/dialogs/section_forms.py +++ b/src/otko/views/dialogs/section_forms.py @@ -19,8 +19,11 @@ from PySide6.QtWidgets import ( ) from otko.core import ( + AngleShape, ElasticSection, FiberSection, + PipeShape, + RectShape, SectionAggregator, ) @@ -94,14 +97,13 @@ class SectionFormBase(QWidget): class ElasticSectionForm(SectionFormBase): type_label = "Elastic Section" - #: Shape → visible display-only dimensions. ``None`` hides them all. + #: Shape kind → visible display-only dimensions (see the core + #: ``SectionShape`` union). ``None`` hides them all. _SHAPE_DIMS: ClassVar[dict[str | None, tuple[str, ...]]] = { None: (), - "rect": ("b", "h"), - "I": ("b", "d", "tw", "tf"), - "H": ("b", "d", "tw", "tf"), - "pipe": ("diameter",), - "circle": ("diameter",), + "rect": ("b", "d"), + "angle": ("b", "d", "t"), + "pipe": ("od", "t"), } def __init__(self, parent: QWidget | None = None) -> None: @@ -127,14 +129,14 @@ class ElasticSectionForm(SectionFormBase): self._layout.addRow(QLabel("Extruded-view shape (display only).")) self._shape = QComboBox() self._shape.addItem("(none)", None) - for key in ("rect", "I", "H", "pipe", "circle"): + for key in ("rect", "angle", "pipe"): 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", "h", "d", "tw", "tf", "diameter"): + for key in ("b", "d", "t", "od"): spin = _opt_spin() lbl = QLabel(f"{key}:") self._dim_spins[key] = spin @@ -160,20 +162,15 @@ class ElasticSectionForm(SectionFormBase): self._g.setValue(s.G) if s.J is not None: self._j.setValue(s.J) - idx = self._shape.findData(s.shape) + kind = getattr(s.shape, "kind", None) if s.shape is not None else None + idx = self._shape.findData(kind) self._shape.setCurrentIndex(idx if idx >= 0 else 0) for key, spin in self._dim_spins.items(): - v = getattr(s, key, None) + v = getattr(s.shape, key, None) if s.shape is not None else 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(), @@ -183,10 +180,38 @@ class ElasticSectionForm(SectionFormBase): Iy=self._iy.value(), G=self._g.value(), J=self._j.value(), - shape=shape, - **dims, # type: ignore[arg-type] + shape=self._read_shape(), ) + 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`. From 1f39cff5b0f7eb83a32eecd609df7088a44e09d4 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 13:08:49 -0400 Subject: [PATCH 11/28] ci: split the workflow into lint/type/headless/gui/integration jobs Reconstructs the workflow stripped during the tree copy to match the meta-gates: separate type job, headless command over tests/unit+tests/services, and dedicated GUI / integration jobs. Tool versions pinned to the pre-commit revisions (ruff 0.4.4, mypy 1.10.0) so CI and local hooks agree. --- .github/workflows/ci.yml | 53 ++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebc3520..f0a7165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,12 +14,25 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" - - run: pip install ruff mypy + # Pinned to the pre-commit revisions so CI and local hooks agree. + - run: pip install ruff==0.4.4 - run: ruff check src tests - run: ruff format --check src tests - test: - needs: lint + 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] strategy: fail-fast: false matrix: @@ -31,8 +44,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 \ @@ -40,9 +63,19 @@ jobs: libxcb-shape0 libxcb-sync1 libxcb-xfixes0 libxcb-xinerama0 \ libxcb-cursor0 libdbus-1-3 libgl1 libxkbcommon0 xvfb - run: pip install -e ".[dev]" - - 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" + - 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 From 184bb4662bd6e1b74d7e5c7bc7b64f0a927ec3d6 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 13:20:36 -0400 Subject: [PATCH 12/28] chore: scope ruff ignores to tests, Qt and engineering notation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RUF001-003 are ignored repo-wide: × / ≈ / ² are deliberate in the engineering docs and formulas, which also makes the scattered inline noqa suppressions redundant. Tests use engineering symbols (A, I, L, E), `importorskip` before imports and capitalised test names; Qt requires camelCase signal names and PascalCase event overrides; OpenSees material fields (cR1, epsU) mirror the solver verbatim. N806 stays exempt only for the numeric modules that use L / Iz / Iy as symbols. --- pyproject.toml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5e1e4bf..ffab17d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,25 @@ select = [ "SIM", # simplify "RUF", # ruff-specific ] -ignore = ["E501"] # line length handled by formatter +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"] [tool.ruff.format] quote-style = "double" From 61b850bf46f2e7164f711974289415e0956583bc Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 13:20:36 -0400 Subject: [PATCH 13/28] style: resolve the remaining ruff findings Auto-fixes (UP037, UP038, F401, I001, SIM105, RUF100) plus hand-fixes: drop the now-unused `Union` imports, annotate mutable class attributes with ClassVar, raise ValidationError instead of blind Exception in the zero-length test, delete two dead locals, and bind the per-iteration grid-builder closures to their loop variables (B023). --- src/otko/commands/__init__.py | 16 ++-- src/otko/commands/analysis.py | 8 +- src/otko/commands/base.py | 6 +- src/otko/commands/combinations.py | 8 +- src/otko/commands/constraints.py | 6 +- src/otko/commands/elements.py | 14 ++-- src/otko/commands/grid.py | 4 +- src/otko/commands/loads.py | 4 +- src/otko/commands/materials.py | 6 +- src/otko/commands/nodes.py | 12 ++- src/otko/commands/project.py | 2 +- src/otko/commands/sections.py | 6 +- src/otko/commands/series_and_patterns.py | 12 +-- src/otko/commands/transforms.py | 9 +- src/otko/core/analysis/__init__.py | 4 +- src/otko/core/geometry/elements.py | 22 +++-- src/otko/core/geometry/grid.py | 2 +- src/otko/core/loads/__init__.py | 8 +- src/otko/core/materials/__init__.py | 26 +++--- src/otko/core/project.py | 8 +- src/otko/core/sections/__init__.py | 10 +-- src/otko/services/animation_export.py | 3 +- src/otko/services/export.py | 2 +- src/otko/services/peer_record.py | 10 +-- src/otko/services/section_bbox.py | 2 +- src/otko/services/section_properties.py | 1 - src/otko/viewmodels/project_vm.py | 5 +- src/otko/views/action_handlers.py | 2 +- src/otko/views/canvas3d/diagram_renderer.py | 5 +- src/otko/views/canvas3d/model_renderer.py | 84 ++++++++++--------- src/otko/views/dialogs/assign_load.py | 1 - src/otko/views/dialogs/assign_masses.py | 6 +- src/otko/views/dialogs/assign_support.py | 2 - src/otko/views/dialogs/case_forms.py | 1 - src/otko/views/dialogs/case_manager.py | 4 +- src/otko/views/dialogs/define_grid_data.py | 13 ++- src/otko/views/dialogs/grid_system.py | 2 +- src/otko/views/dialogs/locate_origin.py | 1 - src/otko/views/dialogs/material_library.py | 3 +- src/otko/views/dialogs/path_time_series.py | 4 +- src/otko/views/dialogs/quick_grid_lines.py | 1 - src/otko/views/dialogs/run_analysis.py | 4 +- src/otko/views/dialogs/section_editor.py | 2 - src/otko/views/dialogs/section_library.py | 2 +- src/otko/views/dialogs/uniform_excitation.py | 1 - src/otko/views/docks/deformed_shape.py | 2 +- src/otko/views/docks/force_diagram.py | 2 - src/otko/views/docks/hysteresis.py | 5 +- src/otko/views/docks/property_editor.py | 4 +- src/otko/views/docks/pushover_curve.py | 2 +- src/otko/views/docks/response_spectrum.py | 3 +- src/otko/views/docks/table_dock.py | 4 +- src/otko/views/docks/time_history.py | 6 +- src/otko/views/main_window.py | 2 +- src/otko/views/tools/base.py | 6 +- src/otko/views/tools/draw_frame.py | 6 +- src/otko/views/tools/draw_node.py | 6 +- src/otko/views/tools/draw_truss.py | 6 +- tests/gui/test_analysis_commands.py | 6 +- tests/gui/test_analysis_runner.py | 8 +- tests/gui/test_assign_load_pattern_name.py | 9 +- tests/gui/test_assign_masses.py | 2 +- tests/gui/test_assign_zls.py | 4 +- tests/gui/test_canvas_grid_snap.py | 2 +- tests/gui/test_case_forms.py | 4 +- tests/gui/test_commands.py | 7 +- tests/gui/test_convert_element_type.py | 6 +- tests/gui/test_define_dialogs.py | 6 +- tests/gui/test_draw_frame_tool.py | 10 +-- tests/gui/test_draw_node_tool.py | 8 +- tests/gui/test_draw_truss_tool.py | 9 +- tests/gui/test_grid_dialog.py | 2 +- tests/gui/test_material_dialog_concrete04.py | 6 +- tests/gui/test_material_form_ranges.py | 2 +- tests/gui/test_path_ts_dialogs.py | 7 +- tests/gui/test_property_commands.py | 6 +- tests/gui/test_property_forms.py | 9 +- tests/gui/test_pushover_curve_units.py | 6 +- tests/gui/test_run_analysis.py | 8 +- tests/gui/test_section_extrusion.py | 2 +- tests/gui/test_section_forms.py | 4 +- tests/gui/test_selection.py | 2 +- tests/gui/test_transforms.py | 6 +- tests/gui/test_tree_multi_select.py | 5 +- tests/gui/test_units_status_bar.py | 2 +- tests/gui/test_update_element_fields.py | 6 +- tests/gui/test_working_plane.py | 2 +- tests/integration/test_basic_truss.py | 5 +- tests/integration/test_beam_quad_2d.py | 4 +- tests/integration/test_combinations.py | 6 +- tests/integration/test_concrete04_runner.py | 4 +- tests/integration/test_dof_coverage.py | 4 +- .../test_eigen_two_storey_one_bay_frame.py | 6 +- .../test_eigen_two_storey_shear_frame.py | 6 +- tests/integration/test_elastic_frame.py | 12 ++- tests/integration/test_ex1a_canti2d.py | 6 +- tests/integration/test_ex1a_canti2d_eq.py | 4 +- tests/integration/test_ex1b_portal2d.py | 6 +- .../test_ex2a_canti2d_elastic_element.py | 6 +- .../test_ex2b_canti2d_inelastic_section.py | 6 +- ...st_ex2c_canti2d_inelastic_fiber_section.py | 6 +- .../integration/test_ex3_canti2d_variants.py | 6 +- .../integration/test_ex4_portal2d_variants.py | 6 +- tests/integration/test_moment_curvature.py | 8 +- .../test_moment_curvature_example.py | 6 +- tests/integration/test_pattern_factors.py | 4 +- tests/integration/test_rc_frame_earthquake.py | 7 +- tests/integration/test_rc_frame_gravity.py | 6 +- tests/integration/test_rc_frame_pushover.py | 7 +- .../integration/test_runner_imposed_motion.py | 4 +- tests/integration/test_runner_modal.py | 6 +- tests/integration/test_runner_static.py | 4 +- tests/integration/test_runner_transient.py | 9 +- tests/services/test_element_forces.py | 1 - tests/services/test_runner_translation.py | 1 - tests/services/test_section_bbox.py | 1 - tests/tools/test_parse_schemas.py | 1 + tests/unit/core/materials/test_concrete04.py | 1 + tests/unit/test_diagram_renderer.py | 10 +-- tests/unit/test_elements.py | 2 +- tests/unit/test_grid_system.py | 3 +- tests/unit/test_phase8a.py | 1 - tests/unit/test_phase9.py | 4 +- tests/unit/test_renderer.py | 8 +- tests/unit/test_unit_labels.py | 2 - tests/unit/test_zerolength_section.py | 3 +- 126 files changed, 359 insertions(+), 397 deletions(-) diff --git a/src/otko/commands/__init__.py b/src/otko/commands/__init__.py index 82ccef5..eddf86f 100644 --- a/src/otko/commands/__init__.py +++ b/src/otko/commands/__init__.py @@ -29,14 +29,6 @@ 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, @@ -63,6 +55,14 @@ 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, diff --git a/src/otko/commands/analysis.py b/src/otko/commands/analysis.py index 5950b40..444f34c 100644 --- a/src/otko/commands/analysis.py +++ b/src/otko/commands/analysis.py @@ -13,9 +13,7 @@ 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) @@ -36,7 +34,7 @@ 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]] = [] @@ -58,7 +56,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 15f616f..0e8b3dd 100644 --- a/src/otko/commands/base.py +++ b/src/otko/commands/base.py @@ -27,16 +27,16 @@ 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.") return self._vm.project diff --git a/src/otko/commands/combinations.py b/src/otko/commands/combinations.py index 0af9a08..23f1aa8 100644 --- a/src/otko/commands/combinations.py +++ b/src/otko/commands/combinations.py @@ -13,9 +13,7 @@ if TYPE_CHECKING: class AddCombinationsCommand(ProjectCommand): """Add one or more load combinations.""" - def __init__( - self, vm: "ProjectViewModel", combos: list[Any], *, text: str | None = None - ) -> None: + 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) @@ -36,7 +34,7 @@ class AddCombinationsCommand(ProjectCommand): class DeleteCombinationsCommand(ProjectCommand): """Remove a set of load combinations.""" - def __init__(self, vm: "ProjectViewModel", combo_ids: set[int]) -> None: + 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]] = [] @@ -60,7 +58,7 @@ class DeleteCombinationsCommand(ProjectCommand): class UpdateCombinationCommand(ProjectCommand): """Replace a load combination at a given id.""" - def __init__(self, vm: "ProjectViewModel", new_combo: Any) -> None: + 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 diff --git a/src/otko/commands/constraints.py b/src/otko/commands/constraints.py index e3df65a..3f94205 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}", @@ -48,7 +48,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 +71,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 6487a9d..f1967ba 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 894c56b..e271746 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 d24fabf..789b6c8 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, @@ -122,7 +122,7 @@ class AddElementLoadsCommand(ProjectCommand): def __init__( self, - vm: "ProjectViewModel", + vm: ProjectViewModel, element_ids: set[int], wy: float = 0.0, wz: float = 0.0, diff --git a/src/otko/commands/materials.py b/src/otko/commands/materials.py index 836ade9..9729ced 100644 --- a/src/otko/commands/materials.py +++ b/src/otko/commands/materials.py @@ -14,7 +14,7 @@ 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 + 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,7 +36,7 @@ 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]] = [] @@ -64,7 +64,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 b737265..e24840c 100644 --- a/src/otko/commands/nodes.py +++ b/src/otko/commands/nodes.py @@ -14,9 +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) @@ -41,7 +39,7 @@ 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) @@ -95,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: @@ -130,7 +128,7 @@ class UpdateNodeFieldsCommand(ProjectCommand): def __init__( self, - vm: "ProjectViewModel", + vm: ProjectViewModel, node_id: int, fields: dict[str, Any], ) -> None: @@ -168,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 362e2a6..3fd0065 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 e75a54f..94142a6 100644 --- a/src/otko/commands/sections.py +++ b/src/otko/commands/sections.py @@ -14,7 +14,7 @@ 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 + 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,7 +36,7 @@ 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]] = [] @@ -60,7 +60,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 1d1062c..1d03ab5 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 @@ -41,7 +41,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 +67,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 +91,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 +117,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 +142,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 3e48448..07c335d 100644 --- a/src/otko/commands/transforms.py +++ b/src/otko/commands/transforms.py @@ -15,10 +15,9 @@ All copies inherit the source node's restraint, mass, and name. from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Literal from otko.commands.base import ProjectCommand -from otko.core import Node if TYPE_CHECKING: from otko.viewmodels import ProjectViewModel @@ -30,7 +29,7 @@ class MoveNodesCommand(ProjectCommand): def __init__( self, - vm: "ProjectViewModel", + vm: ProjectViewModel, node_ids: set[int], offset: tuple[float, float, float], ) -> None: @@ -63,7 +62,7 @@ class ReplicateCommand(ProjectCommand): def __init__( self, - vm: "ProjectViewModel", + vm: ProjectViewModel, node_ids: set[int], element_ids: set[int], offset: tuple[float, float, float], @@ -147,7 +146,7 @@ class MirrorCommand(ProjectCommand): def __init__( self, - vm: "ProjectViewModel", + vm: ProjectViewModel, node_ids: set[int], element_ids: set[int], plane: Plane, diff --git a/src/otko/core/analysis/__init__.py b/src/otko/core/analysis/__init__.py index b809bad..e8835b7 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, Union +from typing import Annotated, Literal from pydantic import Field, PositiveFloat, PositiveInt, model_validator @@ -269,6 +269,6 @@ class ResponseSpectrumCase(Entity): AnalysisCase = Annotated[ - Union[StaticCase, ModalCase, TransientCase, PushoverCase, ResponseSpectrumCase], + StaticCase | ModalCase | TransientCase | PushoverCase | ResponseSpectrumCase, Field(discriminator="type"), ] diff --git a/src/otko/core/geometry/elements.py b/src/otko/core/geometry/elements.py index f79ae39..35a2026 100644 --- a/src/otko/core/geometry/elements.py +++ b/src/otko/core/geometry/elements.py @@ -10,7 +10,7 @@ Conventions follow OpenSeesPy ``element ...`` commands. from __future__ import annotations -from typing import Annotated, Literal, Union +from typing import Annotated, Literal from pydantic import Field, PositiveFloat, PositiveInt @@ -198,16 +198,14 @@ class QuadElement(Entity): Element = Annotated[ - Union[ - TrussElement, - CorotTrussElement, - ElasticBeamColumn, - ForceBeamColumn, - DispBeamColumn, - ZeroLengthElement, - ZeroLengthSectionElement, - BeamWithHingesElement, - QuadElement, - ], + 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 cf6ecc7..f7d61ea 100644 --- a/src/otko/core/geometry/grid.py +++ b/src/otko/core/geometry/grid.py @@ -97,7 +97,7 @@ class GridSystem(BaseModel): 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) diff --git a/src/otko/core/loads/__init__.py b/src/otko/core/loads/__init__.py index 283dfad..68a1557 100644 --- a/src/otko/core/loads/__init__.py +++ b/src/otko/core/loads/__init__.py @@ -12,7 +12,7 @@ pattern is deleted, its loads go with it. from __future__ import annotations -from typing import Annotated, Literal, Union +from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field, PositiveInt, model_validator @@ -110,7 +110,7 @@ class ResponseSpectrum(Entity): TimeSeries = Annotated[ - Union[LinearTimeSeries, ConstantTimeSeries, PathTimeSeries], + LinearTimeSeries | ConstantTimeSeries | PathTimeSeries, Field(discriminator="type"), ] @@ -229,7 +229,7 @@ class LoadCombination(Entity): ) @model_validator(mode="after") - def _check_items(self) -> "LoadCombination": + def _check_items(self) -> LoadCombination: """Reject duplicate case ids and non-finite factors.""" import math @@ -250,6 +250,6 @@ class LoadCombination(Entity): LoadPattern = Annotated[ - Union[PlainLoadPattern, UniformExcitationPattern, ImposedSupportMotionPattern], + PlainLoadPattern | UniformExcitationPattern | ImposedSupportMotionPattern, Field(discriminator="type"), ] diff --git a/src/otko/core/materials/__init__.py b/src/otko/core/materials/__init__.py index be71418..daa84f6 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, Union +from typing import Annotated, Literal from pydantic import Field, PositiveFloat, model_validator @@ -130,7 +130,7 @@ class Concrete04(Entity): ) @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: @@ -229,18 +229,16 @@ class HystereticSM(Entity): # ──────────────────────────── Discriminated union ──────────────────────────── Material = Annotated[ - Union[ - ElasticIsotropic, - ElasticUniaxial, - Steel01, - Steel02, - Concrete01, - Concrete02, - Concrete04, - ElasticPP, - HystereticMaterial, - HystereticSM, - ], + 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 175f7a4..f2d030a 100644 --- a/src/otko/core/project.py +++ b/src/otko/core/project.py @@ -166,7 +166,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 +174,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``, @@ -223,7 +223,7 @@ class Project(BaseModel): # ─────────────────── 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 +233,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), diff --git a/src/otko/core/sections/__init__.py b/src/otko/core/sections/__init__.py index 595b09b..530e0ca 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, Union +from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field, PositiveFloat, PositiveInt @@ -61,7 +61,7 @@ class RectShape(BaseModel): SectionShape = Annotated[ - Union[PipeShape, AngleShape, RectShape], + PipeShape | AngleShape | RectShape, Field(discriminator="kind"), ] @@ -165,12 +165,12 @@ class StraightLayer(BaseModel): Patch = Annotated[ - Union[RectangularPatch, CircularPatch], + RectangularPatch | CircularPatch, Field(discriminator="kind"), ] Layer = Annotated[ - Union[StraightLayer], + StraightLayer, Field(discriminator="kind"), ] @@ -225,6 +225,6 @@ class SectionAggregator(Entity): Section = Annotated[ - Union[ElasticSection, FiberSection, SectionAggregator], + ElasticSection | FiberSection | SectionAggregator, Field(discriminator="type"), ] diff --git a/src/otko/services/animation_export.py b/src/otko/services/animation_export.py index 20c3f53..fa80790 100644 --- a/src/otko/services/animation_export.py +++ b/src/otko/services/animation_export.py @@ -12,8 +12,9 @@ the main thread. from __future__ import annotations import math +from collections.abc import Callable from pathlib import Path -from typing import Any, Callable +from typing import Any import numpy as np diff --git a/src/otko/services/export.py b/src/otko/services/export.py index 0f3a22c..5886360 100644 --- a/src/otko/services/export.py +++ b/src/otko/services/export.py @@ -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) diff --git a/src/otko/services/peer_record.py b/src/otko/services/peer_record.py index b6f6e6c..ce99a54 100644 --- a/src/otko/services/peer_record.py +++ b/src/otko/services/peer_record.py @@ -11,6 +11,7 @@ building the PathTimeSeries. from __future__ import annotations +import contextlib import re from pathlib import Path @@ -64,10 +65,9 @@ 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(): - try: + # Skip stray non-numeric tokens. + with contextlib.suppress(ValueError): 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,10 +83,8 @@ def parse_plain_values(path: str | Path) -> list[float]: vals: list[float] = [] for line in text.splitlines(): for tok in line.split(): - try: + with contextlib.suppress(ValueError): 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/section_bbox.py b/src/otko/services/section_bbox.py index b3438b8..18f5a20 100644 --- a/src/otko/services/section_bbox.py +++ b/src/otko/services/section_bbox.py @@ -54,7 +54,7 @@ def _angle_in_arc(angle: float, start: float, span: float) -> bool: def bbox_for_section( section: object, - project: "Project | None" = None, + project: Project | None = None, ) -> tuple[float, float] | None: """Return ``(width_y, height_z)`` of the section's local bounding box. diff --git a/src/otko/services/section_properties.py b/src/otko/services/section_properties.py index a38d4e4..a637428 100644 --- a/src/otko/services/section_properties.py +++ b/src/otko/services/section_properties.py @@ -21,7 +21,6 @@ import numpy as np from otko.core.sections import ( CircularPatch, FiberSection, - Fibre, RectangularPatch, StraightLayer, ) diff --git a/src/otko/viewmodels/project_vm.py b/src/otko/viewmodels/project_vm.py index 6ed5dbd..9ea06d1 100644 --- a/src/otko/viewmodels/project_vm.py +++ b/src/otko/viewmodels/project_vm.py @@ -11,6 +11,7 @@ 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 @@ -40,10 +41,8 @@ class ProjectViewModel(QObject): @Slot(bool) def _on_stack_clean_changed(self, clean: bool) -> None: # Guard against the late-fire that Qt sends during destruction. - try: + with contextlib.suppress(RuntimeError): 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 bcd9305..4352b00 100644 --- a/src/otko/views/action_handlers.py +++ b/src/otko/views/action_handlers.py @@ -47,9 +47,9 @@ from otko.views.dialogs import ( AssignSectionDialog, AssignSupportDialog, AssignZeroLengthSectionDialog, - LoadCombinationManagerDialog, CoordinateGridSystemsDialog, LinearTimeSeriesDialog, + LoadCombinationManagerDialog, MaterialLibraryDialog, MaterialTesterDialog, MirrorDialog, diff --git a/src/otko/views/canvas3d/diagram_renderer.py b/src/otko/views/canvas3d/diagram_renderer.py index 28b6f82..05c5f2c 100644 --- a/src/otko/views/canvas3d/diagram_renderer.py +++ b/src/otko/views/canvas3d/diagram_renderer.py @@ -22,6 +22,7 @@ Convention: from __future__ import annotations +import contextlib import logging from typing import Any @@ -254,10 +255,8 @@ class DiagramRenderer: pass self._actor = None if self._label_actor is not None: - try: + with contextlib.suppress(Exception): 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_renderer.py b/src/otko/views/canvas3d/model_renderer.py index 568c079..9eb9274 100644 --- a/src/otko/views/canvas3d/model_renderer.py +++ b/src/otko/views/canvas3d/model_renderer.py @@ -10,9 +10,10 @@ 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 +from typing import Any, ClassVar import numpy as np import pyvista as pv @@ -24,11 +25,11 @@ from otko.core import ( ElasticBeamColumn, ForceBeamColumn, NodalLoad, - QuadElement, - UniformElementLoad, PlainLoadPattern, Project, + QuadElement, TrussElement, + UniformElementLoad, ZeroLengthElement, ZeroLengthSectionElement, ) @@ -184,8 +185,8 @@ 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 + _NODE_LUT: ClassVar[list[str]] = ["#d9d9d9", "#00ffff"] # gray normal, cyan selected + _FRAME_LUT: ClassVar[list[str]] = ["#338cd9", "#00ffff"] # blue normal, cyan selected def __init__(self, plotter: Any, style: RenderStyle | None = None) -> None: self._plotter = plotter @@ -219,10 +220,8 @@ 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 - try: + with contextlib.suppress(Exception): self._plotter.enable_anti_aliasing("ssaa") - except Exception: - pass # ── public API ─────────────────────────────────────────────────── def render(self, project: Project | None) -> None: @@ -329,10 +328,8 @@ class ModelRenderer: """ # Remove any previous marker. if self._hover_actor is not None: - try: + with contextlib.suppress(Exception): self._plotter.remove_actor(self._hover_actor, render=False) - except Exception: - pass self._hover_actor = None if world_point is None: @@ -376,10 +373,8 @@ 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: - try: + with contextlib.suppress(Exception): self._plotter.remove_actor(self._node_actor, render=False) - except Exception: - pass self._node_glyph = glyph self._node_actor = self._plotter.add_mesh( glyph, @@ -484,10 +479,17 @@ class ModelRenderer: plane_axis = axis_idx plane_offset_local = plane_off - cs_shift - def _on_active_plane(local_pt: tuple[float, float, float]) -> bool: - if plane_axis is None: + # 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: return True - return abs(local_pt[plane_axis] - plane_offset_local) < 1e-6 # type: ignore[operator] + return abs(local_pt[_axis] - _offset) < 1e-6 # Collect active + dim segments separately so they get their # own polydata + actor (different opacity / color). @@ -496,13 +498,21 @@ 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]) -> None: + 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: 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] @@ -614,7 +624,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]) @@ -665,18 +675,16 @@ 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]) @@ -1094,15 +1102,11 @@ class ModelRenderer: self._clear_label_actors() for a in (self._node_actor, self._frame_actor): if a is not None: - try: + with contextlib.suppress(Exception): self._plotter.remove_actor(a, render=False) - except Exception: - pass for a in self._aux_actors: - try: + with contextlib.suppress(Exception): self._plotter.remove_actor(a, render=False) - except Exception: - pass self._node_actor = None self._frame_actor = None self._aux_actors.clear() @@ -1120,10 +1124,8 @@ class ModelRenderer: def _clear_label_actors(self) -> None: for actor in (self._node_label_actor, self._element_label_actor): if actor is not None: - try: + with contextlib.suppress(Exception): self._plotter.remove_actor(actor, render=False) - except Exception: - pass self._node_label_actor = None self._element_label_actor = None diff --git a/src/otko/views/dialogs/assign_load.py b/src/otko/views/dialogs/assign_load.py index 9ab8c3c..534ba69 100644 --- a/src/otko/views/dialogs/assign_load.py +++ b/src/otko/views/dialogs/assign_load.py @@ -8,7 +8,6 @@ from PySide6.QtWidgets import ( QDialogButtonBox, QDoubleSpinBox, QFormLayout, - QInputDialog, QLabel, QLineEdit, QVBoxLayout, diff --git a/src/otko/views/dialogs/assign_masses.py b/src/otko/views/dialogs/assign_masses.py index 6592e44..f87a710 100644 --- a/src/otko/views/dialogs/assign_masses.py +++ b/src/otko/views/dialogs/assign_masses.py @@ -8,6 +8,8 @@ it maps cleanly onto our existing :class:`SetMassCommand`. from __future__ import annotations +import contextlib + from PySide6.QtWidgets import ( QCheckBox, QDialog, @@ -103,10 +105,8 @@ class AssignMassesDialog(QDialog): self._my.setValue(self._mx.value()) self._mx.valueChanged.connect(self._my.setValue) else: - try: + with contextlib.suppress(RuntimeError, TypeError): 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_support.py b/src/otko/views/dialogs/assign_support.py index b742ebe..8b3a5bc 100644 --- a/src/otko/views/dialogs/assign_support.py +++ b/src/otko/views/dialogs/assign_support.py @@ -8,7 +8,6 @@ model's actual ``ndf`` at translation time. from __future__ import annotations -from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QButtonGroup, QCheckBox, @@ -22,7 +21,6 @@ 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), diff --git a/src/otko/views/dialogs/case_forms.py b/src/otko/views/dialogs/case_forms.py index b8f7b24..d608992 100644 --- a/src/otko/views/dialogs/case_forms.py +++ b/src/otko/views/dialogs/case_forms.py @@ -21,7 +21,6 @@ from PySide6.QtWidgets import ( QListWidget, QListWidgetItem, QSpinBox, - QVBoxLayout, QWidget, ) diff --git a/src/otko/views/dialogs/case_manager.py b/src/otko/views/dialogs/case_manager.py index 000a90c..3ba5f29 100644 --- a/src/otko/views/dialogs/case_manager.py +++ b/src/otko/views/dialogs/case_manager.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import TYPE_CHECKING +from pydantic import ValidationError from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QDialog, @@ -19,7 +20,6 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from pydantic import ValidationError from otko.commands import ( AddAnalysisCasesCommand, @@ -34,7 +34,7 @@ from otko.core import ( TransientCase, ) from otko.viewmodels import ProjectViewModel -from otko.views.dialogs.case_forms import FORM_REGISTRY, form_for +from otko.views.dialogs.case_forms import form_for if TYPE_CHECKING: from otko.core import Project diff --git a/src/otko/views/dialogs/define_grid_data.py b/src/otko/views/dialogs/define_grid_data.py index 8230650..7440b04 100644 --- a/src/otko/views/dialogs/define_grid_data.py +++ b/src/otko/views/dialogs/define_grid_data.py @@ -18,6 +18,8 @@ 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 ( @@ -27,7 +29,6 @@ from PySide6.QtWidgets import ( QComboBox, QDialog, QDialogButtonBox, - QDoubleSpinBox, QFormLayout, QGroupBox, QHBoxLayout, @@ -52,7 +53,6 @@ from otko.core.geometry import ( GridSystem, ) - _LINE_TYPE_CHOICES = ["Primary", "Secondary"] _BUBBLE_LOC_CHOICES = ["Start", "End"] @@ -60,7 +60,14 @@ _BUBBLE_LOC_CHOICES = ["Start", "End"] class _AxisGridTable(QWidget): """A single X/Y/Z spreadsheet — one row per GridLine.""" - COLUMNS = ["Grid ID", "Ordinate", "Line Type", "Visibility", "Bubble Loc", "Color"] + COLUMNS: ClassVar[list[str]] = [ + "Grid ID", + "Ordinate", + "Line Type", + "Visibility", + "Bubble Loc", + "Color", + ] def __init__(self, axis: str, parent: QWidget | None = None) -> None: super().__init__(parent) diff --git a/src/otko/views/dialogs/grid_system.py b/src/otko/views/dialogs/grid_system.py index 3f39cf8..c1ac05c 100644 --- a/src/otko/views/dialogs/grid_system.py +++ b/src/otko/views/dialogs/grid_system.py @@ -17,8 +17,8 @@ from PySide6.QtWidgets import ( QDialog, QDialogButtonBox, QFormLayout, - QLineEdit, QLabel, + QLineEdit, QVBoxLayout, QWidget, ) diff --git a/src/otko/views/dialogs/locate_origin.py b/src/otko/views/dialogs/locate_origin.py index 21708e7..28a8205 100644 --- a/src/otko/views/dialogs/locate_origin.py +++ b/src/otko/views/dialogs/locate_origin.py @@ -13,7 +13,6 @@ from PySide6.QtWidgets import ( QDoubleSpinBox, QFormLayout, QGroupBox, - QHBoxLayout, QLabel, QVBoxLayout, QWidget, diff --git a/src/otko/views/dialogs/material_library.py b/src/otko/views/dialogs/material_library.py index ace776e..371a507 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,7 +23,6 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from pydantic import ValidationError from otko.commands import ( AddMaterialsCommand, diff --git a/src/otko/views/dialogs/path_time_series.py b/src/otko/views/dialogs/path_time_series.py index 476b5d4..66a6b34 100644 --- a/src/otko/views/dialogs/path_time_series.py +++ b/src/otko/views/dialogs/path_time_series.py @@ -120,7 +120,7 @@ class PathTimeSeriesDialog(QDialog): return try: dt, npts, vals = parse_peer_record(fname) - except Exception as exc: # noqa: BLE001 + except Exception as exc: QMessageBox.critical(self, "PEER import failed", str(exc)) return self._values = vals @@ -142,7 +142,7 @@ class PathTimeSeriesDialog(QDialog): return try: vals = parse_plain_values(fname) - except Exception as exc: # noqa: BLE001 + except Exception as exc: QMessageBox.critical(self, "Import failed", str(exc)) return self._values = vals diff --git a/src/otko/views/dialogs/quick_grid_lines.py b/src/otko/views/dialogs/quick_grid_lines.py index 23d3b61..7928d60 100644 --- a/src/otko/views/dialogs/quick_grid_lines.py +++ b/src/otko/views/dialogs/quick_grid_lines.py @@ -11,7 +11,6 @@ from PySide6.QtWidgets import ( QDialog, QDialogButtonBox, QDoubleSpinBox, - QFormLayout, QGroupBox, QHBoxLayout, QLabel, diff --git a/src/otko/views/dialogs/run_analysis.py b/src/otko/views/dialogs/run_analysis.py index f31ec6f..b1beb12 100644 --- a/src/otko/views/dialogs/run_analysis.py +++ b/src/otko/views/dialogs/run_analysis.py @@ -193,7 +193,7 @@ 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: # noqa: BLE001 + except Exception as exc: self._log.appendPlainText(f"Could not start: {exc}") def _on_evaluate_combo(self) -> None: @@ -233,7 +233,7 @@ class RunAnalysisDialog(QDialog): self._results = combined self._log.appendPlainText(f"--- Done. Returned {type(combined).__name__}. ---") self._runner.finished.emit(combined) - except Exception as exc: # noqa: BLE001 + except Exception as exc: import traceback self._log.appendPlainText("--- FAILED ---") diff --git a/src/otko/views/dialogs/section_editor.py b/src/otko/views/dialogs/section_editor.py index 4bb185f..fdb9031 100644 --- a/src/otko/views/dialogs/section_editor.py +++ b/src/otko/views/dialogs/section_editor.py @@ -12,7 +12,6 @@ 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 ( @@ -324,7 +323,6 @@ 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: diff --git a/src/otko/views/dialogs/section_library.py b/src/otko/views/dialogs/section_library.py index c525a50..198c821 100644 --- a/src/otko/views/dialogs/section_library.py +++ b/src/otko/views/dialogs/section_library.py @@ -2,6 +2,7 @@ from __future__ import annotations +from pydantic import ValidationError from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QDialog, @@ -17,7 +18,6 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from pydantic import ValidationError from otko.commands import ( AddSectionsCommand, diff --git a/src/otko/views/dialogs/uniform_excitation.py b/src/otko/views/dialogs/uniform_excitation.py index 6bdc94b..b7cc471 100644 --- a/src/otko/views/dialogs/uniform_excitation.py +++ b/src/otko/views/dialogs/uniform_excitation.py @@ -23,7 +23,6 @@ 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)"), diff --git a/src/otko/views/docks/deformed_shape.py b/src/otko/views/docks/deformed_shape.py index 497dc85..de89664 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, Callable +from typing import TYPE_CHECKING from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( diff --git a/src/otko/views/docks/force_diagram.py b/src/otko/views/docks/force_diagram.py index 2580e36..a19e99d 100644 --- a/src/otko/views/docks/force_diagram.py +++ b/src/otko/views/docks/force_diagram.py @@ -10,8 +10,6 @@ Exposes two signals: from __future__ import annotations -import math - from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( QComboBox, diff --git a/src/otko/views/docks/hysteresis.py b/src/otko/views/docks/hysteresis.py index cc56c3e..c8f0a79 100644 --- a/src/otko/views/docks/hysteresis.py +++ b/src/otko/views/docks/hysteresis.py @@ -15,6 +15,7 @@ For 2D it's [N1, Vy1, Mz1, N2, Vy2, Mz2]. from __future__ import annotations +import contextlib from typing import Any import pyqtgraph as pg @@ -337,8 +338,6 @@ class HysteresisView(QWidget): def _clear_curve(self) -> None: if self._curve is not None: - try: + with contextlib.suppress(Exception): 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 10cb7f7..b6c5ed0 100644 --- a/src/otko/views/docks/property_editor.py +++ b/src/otko/views/docks/property_editor.py @@ -8,9 +8,8 @@ dumb: no direct Project mutation, no Qt ↔ OpenSees coupling. from __future__ import annotations -from typing import Callable +from collections.abc import Callable -from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QComboBox, QDoubleSpinBox, @@ -26,7 +25,6 @@ from PySide6.QtWidgets import ( from otko.core import Project - # Element types the Properties dock lets the user switch between. # "ElasticBeamColumn" needs a section; "Truss" / "CorotTruss" need a # uniaxial material + area. The Convert command drops/adds fields to diff --git a/src/otko/views/docks/pushover_curve.py b/src/otko/views/docks/pushover_curve.py index 3dc0e11..86beff2 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 UnitLabels, UnitSystem, labels_for +from otko.core import UnitSystem, labels_for from otko.services.results import PushoverResults if TYPE_CHECKING: diff --git a/src/otko/views/docks/response_spectrum.py b/src/otko/views/docks/response_spectrum.py index 42cb523..7664171 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 Signal +from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( QHBoxLayout, QHeaderView, @@ -20,7 +20,6 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from PySide6.QtCore import Qt from otko.core import ResponseSpectrum from otko.services.results import ResponseSpectrumResults diff --git a/src/otko/views/docks/table_dock.py b/src/otko/views/docks/table_dock.py index c8d3342..3af8383 100644 --- a/src/otko/views/docks/table_dock.py +++ b/src/otko/views/docks/table_dock.py @@ -197,7 +197,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) @@ -1035,7 +1035,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}") # noqa: RUF001 + parts.append(f"{label}×{float(factor):g}") except (TypeError, ValueError): continue return ", ".join(parts) if parts else "—" diff --git a/src/otko/views/docks/time_history.py b/src/otko/views/docks/time_history.py index ba285fe..560dba0 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 ( @@ -208,9 +208,7 @@ class TimeHistoryView(QWidget): def _clear_traces(self) -> None: for _, _, item in self._traces: - try: + with contextlib.suppress(Exception): self._plot.removeItem(item) - except Exception: - pass self._traces.clear() self._trace_list.clear() diff --git a/src/otko/views/main_window.py b/src/otko/views/main_window.py index ec9f487..958459e 100644 --- a/src/otko/views/main_window.py +++ b/src/otko/views/main_window.py @@ -105,7 +105,7 @@ class MainWindow( if state is not None: self.restoreState(state) - def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 (Qt override) + 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 diff --git a/src/otko/views/tools/base.py b/src/otko/views/tools/base.py index 04abc8e..bfb6072 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) @@ -87,7 +87,7 @@ class ToolController(QObject): toolChanged = Signal(object) # emits the new CanvasTool (or None for default) def __init__( - self, canvas: "ModelCanvas", vm: "ProjectViewModel", parent: QObject | None = None + self, canvas: ModelCanvas, vm: ProjectViewModel, parent: QObject | None = None ) -> None: super().__init__(parent) self._canvas = canvas diff --git a/src/otko/views/tools/draw_frame.py b/src/otko/views/tools/draw_frame.py index 958abe3..980716a 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) diff --git a/src/otko/views/tools/draw_node.py b/src/otko/views/tools/draw_node.py index 5e7ee0f..0f286be 100644 --- a/src/otko/views/tools/draw_node.py +++ b/src/otko/views/tools/draw_node.py @@ -115,9 +115,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) diff --git a/src/otko/views/tools/draw_truss.py b/src/otko/views/tools/draw_truss.py index 1eeb69d..4ee8254 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/gui/test_analysis_commands.py b/tests/gui/test_analysis_commands.py index 9750537..fed67e3 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 ( # noqa: E402 +from otko.commands import ( AddAnalysisCasesCommand, DeleteAnalysisCasesCommand, UpdateAnalysisCaseCommand, ) -from otko.core import ( # noqa: E402 +from otko.core import ( LinearTimeSeries, ModalCase, NodalLoad, @@ -20,7 +20,7 @@ from otko.core import ( # noqa: E402 StaticCase, TransientCase, ) -from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.viewmodels import ProjectViewModel def _vm_with_pattern() -> ProjectViewModel: diff --git a/tests/gui/test_analysis_runner.py b/tests/gui/test_analysis_runner.py index 2d19389..14c100a 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 ( # noqa: E402 +from otko.commands import ( AddAnalysisCasesCommand, AddElementsCommand, AddNodalLoadsCommand, AddNodesCommand, AddSectionsCommand, ) -from otko.core import ( # noqa: E402 +from otko.core import ( ElasticBeamColumn, ElasticSection, Node, StaticCase, ) -from otko.services.results import StaticResults # noqa: E402 -from otko.viewmodels import AnalysisRunner, ProjectViewModel # noqa: E402 +from otko.services.results import StaticResults +from otko.viewmodels import AnalysisRunner, ProjectViewModel @pytest.fixture diff --git a/tests/gui/test_assign_load_pattern_name.py b/tests/gui/test_assign_load_pattern_name.py index 0f95da2..b0b4fba 100644 --- a/tests/gui/test_assign_load_pattern_name.py +++ b/tests/gui/test_assign_load_pattern_name.py @@ -6,15 +6,14 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import AddNodalLoadsCommand # noqa: E402 -from otko.core import ( # noqa: E402 +from otko.commands import AddNodalLoadsCommand +from otko.core import ( LinearTimeSeries, Node, PlainLoadPattern, - Project, ) -from otko.viewmodels import ProjectViewModel # noqa: E402 -from otko.views.dialogs.assign_load import AssignLoadDialog # noqa: E402 +from otko.viewmodels import ProjectViewModel +from otko.views.dialogs.assign_load import AssignLoadDialog @pytest.mark.gui diff --git a/tests/gui/test_assign_masses.py b/tests/gui/test_assign_masses.py index 87a3a7a..3ba0dda 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 # noqa: E402 +from otko.views.dialogs.assign_masses import AssignMassesDialog @pytest.mark.gui diff --git a/tests/gui/test_assign_zls.py b/tests/gui/test_assign_zls.py index 850e0a8..9a8cb66 100644 --- a/tests/gui/test_assign_zls.py +++ b/tests/gui/test_assign_zls.py @@ -6,12 +6,12 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( # noqa: E402 +from otko.core import ( ElasticSection, Node, Project, ) -from otko.views.dialogs.assign_zls import ( # noqa: E402 +from otko.views.dialogs.assign_zls import ( AssignZeroLengthSectionDialog, ) diff --git a/tests/gui/test_canvas_grid_snap.py b/tests/gui/test_canvas_grid_snap.py index 8410edf..02ddb4b 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 ( # noqa: E402 +from otko.core import ( CoordinateGridSystem, CoordinateSystem, GridSystem, diff --git a/tests/gui/test_case_forms.py b/tests/gui/test_case_forms.py index c911141..6672806 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 ( # noqa: E402 +from otko.core import ( NodalLoad, PlainLoadPattern, StaticCase, TransientCase, ) -from otko.views.dialogs.case_forms import TransientCaseForm # noqa: E402 +from otko.views.dialogs.case_forms import TransientCaseForm def _patterns(): # type: ignore[no-untyped-def] diff --git a/tests/gui/test_commands.py b/tests/gui/test_commands.py index 4584ed9..0457390 100644 --- a/tests/gui/test_commands.py +++ b/tests/gui/test_commands.py @@ -11,15 +11,14 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( # noqa: E402 +from otko.commands import ( AddElementsCommand, AddNodalLoadsCommand, AddNodesCommand, - DeleteElementsCommand, DeleteNodesCommand, SetRestraintCommand, ) -from otko.core import ( # noqa: E402 +from otko.core import ( LinearTimeSeries, NodalLoad, Node, @@ -27,7 +26,7 @@ from otko.core import ( # noqa: E402 Steel01, TrussElement, ) -from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.viewmodels import ProjectViewModel # ─────────────────────────── helpers ──────────────────────────────── diff --git a/tests/gui/test_convert_element_type.py b/tests/gui/test_convert_element_type.py index 4fa11ff..426c23b 100644 --- a/tests/gui/test_convert_element_type.py +++ b/tests/gui/test_convert_element_type.py @@ -6,21 +6,21 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( # noqa: E402 +from otko.commands import ( AddElementsCommand, AddMaterialsCommand, AddNodesCommand, AddSectionsCommand, ConvertElementTypeCommand, ) -from otko.core import ( # noqa: E402 +from otko.core import ( ElasticBeamColumn, ElasticSection, ElasticUniaxial, Node, TrussElement, ) -from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.viewmodels import ProjectViewModel def _vm_setup() -> ProjectViewModel: diff --git a/tests/gui/test_define_dialogs.py b/tests/gui/test_define_dialogs.py index 57c1bfc..4b6f9b3 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 # noqa: E402 -from otko.views.dialogs.linear_time_series import LinearTimeSeriesDialog # noqa: E402 -from otko.views.dialogs.plain_pattern import PlainPatternDialog # noqa: E402 +from otko.core import LinearTimeSeries, Project +from otko.views.dialogs.linear_time_series import LinearTimeSeriesDialog +from otko.views.dialogs.plain_pattern import PlainPatternDialog @pytest.mark.gui diff --git a/tests/gui/test_draw_frame_tool.py b/tests/gui/test_draw_frame_tool.py index 6161eb2..ff6370c 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 # 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 +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 class _CanvasStub: diff --git a/tests/gui/test_draw_node_tool.py b/tests/gui/test_draw_node_tool.py index d7d0023..aab389d 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, 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 +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 ( DrawNodeTool, _snap_to_grid, ) diff --git a/tests/gui/test_draw_truss_tool.py b/tests/gui/test_draw_truss_tool.py index 4c67164..bcc2879 100644 --- a/tests/gui/test_draw_truss_tool.py +++ b/tests/gui/test_draw_truss_tool.py @@ -6,18 +6,17 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( # noqa: E402 +from otko.core import ( CoordinateGridSystem, - CoordinateSystem, ElasticUniaxial, GridSystem, Node, TrussElement, make_grid_lines, ) -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 +from otko.viewmodels import ProjectViewModel +from otko.views.canvas3d.selection import SelectionState +from otko.views.tools.draw_truss import DrawTrussTool class _CanvasStub: diff --git a/tests/gui/test_grid_dialog.py b/tests/gui/test_grid_dialog.py index c73e852..0013ad4 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 ( # noqa: E402 +from otko.core import ( CoordinateGridSystem, CoordinateSystem, GridLine, diff --git a/tests/gui/test_material_dialog_concrete04.py b/tests/gui/test_material_dialog_concrete04.py index 4dd34d5..23523bc 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 # noqa: E402 -from otko.views.dialogs.material_forms import ( # noqa: E402 - Concrete04Form, +from otko.core import Concrete04 +from otko.views.dialogs.material_forms import ( FORM_REGISTRY, + Concrete04Form, ) diff --git a/tests/gui/test_material_form_ranges.py b/tests/gui/test_material_form_ranges.py index cb47096..54c1899 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 ( # noqa: E402 +from otko.views.dialogs.material_forms import ( Concrete01Form, Concrete02Form, Steel01Form, diff --git a/tests/gui/test_path_ts_dialogs.py b/tests/gui/test_path_ts_dialogs.py index 7245068..5f1e132 100644 --- a/tests/gui/test_path_ts_dialogs.py +++ b/tests/gui/test_path_ts_dialogs.py @@ -8,17 +8,16 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( # noqa: E402 +from otko.commands import ( AddLoadPatternCommand, AddTimeSeriesCommand, ) -from otko.core import ( # noqa: E402 - Node, +from otko.core import ( PathTimeSeries, Project, UniformExcitationPattern, ) -from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.viewmodels import ProjectViewModel # ─────────────── AddTimeSeriesCommand / AddLoadPatternCommand ────────────── diff --git a/tests/gui/test_property_commands.py b/tests/gui/test_property_commands.py index a325b87..8a49e26 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 ( # noqa: E402 +from otko.commands import ( AddElementsCommand, AddMaterialsCommand, AddNodesCommand, @@ -16,7 +16,7 @@ from otko.commands import ( # noqa: E402 UpdateMaterialCommand, UpdateSectionCommand, ) -from otko.core import ( # noqa: E402 +from otko.core import ( ElasticBeamColumn, ElasticSection, Node, @@ -24,7 +24,7 @@ from otko.core import ( # noqa: E402 Steel02, TrussElement, ) -from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.viewmodels import ProjectViewModel def _vm_with_steel() -> ProjectViewModel: diff --git a/tests/gui/test_property_forms.py b/tests/gui/test_property_forms.py index 3943498..aed5ece 100644 --- a/tests/gui/test_property_forms.py +++ b/tests/gui/test_property_forms.py @@ -10,13 +10,10 @@ import pytest pytest.importorskip("PySide6") -from otko.core import ( # noqa: E402 - Concrete01, +from otko.core import ( Concrete02, ElasticIsotropic, - ElasticPP, ElasticSection, - ElasticUniaxial, Steel01, Steel02, ) @@ -100,10 +97,14 @@ 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 03af4bb..9aee331 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 # noqa: E402 -from otko.services.results import PushoverResults # noqa: E402 -from otko.views.docks.pushover_curve import ( # noqa: E402 +from otko.core import UnitSystem +from otko.services.results import PushoverResults +from otko.views.docks.pushover_curve import ( PushoverCurveView, _is_rotation_dof, ) diff --git a/tests/gui/test_run_analysis.py b/tests/gui/test_run_analysis.py index 03319e5..5281fd8 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 # noqa: E402 +from PySide6.QtCore import QObject, Signal -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 +from otko.core import LinearTimeSeries, PlainLoadPattern, TransientCase +from otko.viewmodels import ProjectViewModel +from otko.views.dialogs.run_analysis import RunAnalysisDialog class _FakeRunner(QObject): diff --git a/tests/gui/test_section_extrusion.py b/tests/gui/test_section_extrusion.py index ad21da0..3f66eb4 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 ( # noqa: E402 +from otko.core import ( ElasticBeamColumn, ElasticSection, Node, diff --git a/tests/gui/test_section_forms.py b/tests/gui/test_section_forms.py index 3c52f4d..4747cec 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 ( # noqa: E402 +from otko.core import ( AggregatorDOF, ElasticSection, FiberSection, RectangularPatch, SectionAggregator, ) -from otko.views.dialogs.section_forms import ( # noqa: E402 +from otko.views.dialogs.section_forms import ( FORM_REGISTRY, form_for, ) diff --git a/tests/gui/test_selection.py b/tests/gui/test_selection.py index c795493..365c03c 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 # noqa: E402 +from otko.views.canvas3d import SelectionState @pytest.mark.gui diff --git a/tests/gui/test_transforms.py b/tests/gui/test_transforms.py index 4ad8716..fe13b70 100644 --- a/tests/gui/test_transforms.py +++ b/tests/gui/test_transforms.py @@ -6,15 +6,15 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( # noqa: E402 +from otko.commands import ( AddElementsCommand, AddNodesCommand, MirrorCommand, MoveNodesCommand, ReplicateCommand, ) -from otko.core import Node, Steel01, TrussElement # noqa: E402 -from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.core import Node, Steel01, TrussElement +from otko.viewmodels import ProjectViewModel def _populated_vm() -> ProjectViewModel: diff --git a/tests/gui/test_tree_multi_select.py b/tests/gui/test_tree_multi_select.py index 9048600..235ee7e 100644 --- a/tests/gui/test_tree_multi_select.py +++ b/tests/gui/test_tree_multi_select.py @@ -6,10 +6,9 @@ import pytest pytest.importorskip("PySide6") -from PySide6.QtCore import Qt # noqa: E402 -from PySide6.QtWidgets import QTreeWidget # noqa: E402 +from PySide6.QtWidgets import QTreeWidget -from otko.core import Node # noqa: E402 +from otko.core import Node @pytest.mark.gui diff --git a/tests/gui/test_units_status_bar.py b/tests/gui/test_units_status_bar.py index b052ad4..4eaaae1 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 # noqa: E402 +from otko.core import UnitSystem @pytest.mark.gui diff --git a/tests/gui/test_update_element_fields.py b/tests/gui/test_update_element_fields.py index bdecc06..f83cd67 100644 --- a/tests/gui/test_update_element_fields.py +++ b/tests/gui/test_update_element_fields.py @@ -6,18 +6,18 @@ import pytest pytest.importorskip("PySide6") -from otko.commands import ( # noqa: E402 +from otko.commands import ( AddElementsCommand, AddMaterialsCommand, AddNodesCommand, UpdateElementFieldsCommand, ) -from otko.core import ( # noqa: E402 +from otko.core import ( ElasticUniaxial, Node, TrussElement, ) -from otko.viewmodels import ProjectViewModel # noqa: E402 +from otko.viewmodels import ProjectViewModel def _vm_with_truss() -> ProjectViewModel: diff --git a/tests/gui/test_working_plane.py b/tests/gui/test_working_plane.py index a0b59f0..012c1c0 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 ( # noqa: E402 +from otko.core import ( CoordinateGridSystem, GridSystem, Project, diff --git a/tests/integration/test_basic_truss.py b/tests/integration/test_basic_truss.py index 72ffaa3..20be03c 100644 --- a/tests/integration/test_basic_truss.py +++ b/tests/integration/test_basic_truss.py @@ -11,7 +11,7 @@ import pytest pytest.importorskip("openseespy") -from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow @@ -25,7 +25,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 build_basic_truss, IN_TO_M + from examples.basic_truss import IN_TO_M, build_basic_truss proj = build_basic_truss() runner = OpenSeesRunner(proj) @@ -45,6 +45,7 @@ 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 6c52b5b..4dc4a60 100644 --- a/tests/integration/test_beam_quad_2d.py +++ b/tests/integration/test_beam_quad_2d.py @@ -9,8 +9,8 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project, save_project # noqa: E402 -from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +from otko.services import load_project, save_project +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow diff --git a/tests/integration/test_combinations.py b/tests/integration/test_combinations.py index 8a4dc21..8ba5ca7 100644 --- a/tests/integration/test_combinations.py +++ b/tests/integration/test_combinations.py @@ -8,7 +8,7 @@ import pytest ops = pytest.importorskip("openseespy.opensees") # skip if OpenSeesPy not installed -from otko.core import ( # noqa: E402 +from otko.core import ( ElasticBeamColumn, ElasticSection, LinearTimeSeries, @@ -20,8 +20,8 @@ from otko.core import ( # noqa: E402 Project, StaticCase, ) -from otko.services import OpenSeesRunner, evaluate_combination # noqa: E402 -from otko.services.results import StaticResults # noqa: E402 +from otko.services import OpenSeesRunner, evaluate_combination +from otko.services.results import StaticResults pytestmark = pytest.mark.slow diff --git a/tests/integration/test_concrete04_runner.py b/tests/integration/test_concrete04_runner.py index 054f462..4293dea 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 ( # noqa: E402 +from otko.core import ( Concrete04, FiberSection, ForceBeamColumn, @@ -32,7 +32,7 @@ from otko.core import ( # noqa: E402 StaticCase, UnitSystem, ) -from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow diff --git a/tests/integration/test_dof_coverage.py b/tests/integration/test_dof_coverage.py index cb20abb..7e9e37e 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 ( # noqa: E402 +from otko.core import ( ElasticUniaxial, LinearTimeSeries, NodalLoad, @@ -21,7 +21,7 @@ from otko.core import ( # noqa: E402 StaticCase, TrussElement, ) -from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow 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 49eb4cd..83cd67e 100644 --- a/tests/integration/test_eigen_two_storey_one_bay_frame.py +++ b/tests/integration/test_eigen_two_storey_one_bay_frame.py @@ -8,9 +8,9 @@ import pytest pytest.importorskip("openseespy") -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 +from otko.core import ModalCase +from otko.services import load_project, save_project +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow diff --git a/tests/integration/test_eigen_two_storey_shear_frame.py b/tests/integration/test_eigen_two_storey_shear_frame.py index 724ba40..793f98d 100644 --- a/tests/integration/test_eigen_two_storey_shear_frame.py +++ b/tests/integration/test_eigen_two_storey_shear_frame.py @@ -8,9 +8,9 @@ import pytest pytest.importorskip("openseespy") -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 +from otko.core import ModalCase +from otko.services import load_project, save_project +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow diff --git a/tests/integration/test_elastic_frame.py b/tests/integration/test_elastic_frame.py index 03aa14c..6f04163 100644 --- a/tests/integration/test_elastic_frame.py +++ b/tests/integration/test_elastic_frame.py @@ -8,9 +8,9 @@ import pytest pytest.importorskip("openseespy") -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 +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 @@ -29,7 +29,6 @@ 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 ( - BAY, LOAD_F1, LOAD_F2, LOAD_F3, @@ -70,7 +69,6 @@ def test_elastic_frame_gravity_reactions(tmp_path) -> None: # type: ignore[no-u 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 ( - BAY, LOAD_F1, LOAD_F2, LOAD_F3, @@ -120,5 +118,5 @@ 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), start=1): - assert T == pytest.approx(T_ref, rel=0.02), f"T{i} = {T:.4f} s, reference {T_ref:.4f} s" + 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" diff --git a/tests/integration/test_ex1a_canti2d.py b/tests/integration/test_ex1a_canti2d.py index e0d0a1d..a20d589 100644 --- a/tests/integration/test_ex1a_canti2d.py +++ b/tests/integration/test_ex1a_canti2d.py @@ -9,9 +9,9 @@ import pytest pytest.importorskip("openseespy") -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 +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 diff --git a/tests/integration/test_ex1a_canti2d_eq.py b/tests/integration/test_ex1a_canti2d_eq.py index 69ad31c..264d30c 100644 --- a/tests/integration/test_ex1a_canti2d_eq.py +++ b/tests/integration/test_ex1a_canti2d_eq.py @@ -9,8 +9,8 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project, save_project # noqa: E402 -from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +from otko.services import load_project, save_project +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow diff --git a/tests/integration/test_ex1b_portal2d.py b/tests/integration/test_ex1b_portal2d.py index 324837e..f47bdd2 100644 --- a/tests/integration/test_ex1b_portal2d.py +++ b/tests/integration/test_ex1b_portal2d.py @@ -9,9 +9,9 @@ import pytest pytest.importorskip("openseespy") -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 +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 diff --git a/tests/integration/test_ex2a_canti2d_elastic_element.py b/tests/integration/test_ex2a_canti2d_elastic_element.py index 3218683..71bfb6d 100644 --- a/tests/integration/test_ex2a_canti2d_elastic_element.py +++ b/tests/integration/test_ex2a_canti2d_elastic_element.py @@ -9,9 +9,9 @@ import pytest pytest.importorskip("openseespy") -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 +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 diff --git a/tests/integration/test_ex2b_canti2d_inelastic_section.py b/tests/integration/test_ex2b_canti2d_inelastic_section.py index 2909e21..3766581 100644 --- a/tests/integration/test_ex2b_canti2d_inelastic_section.py +++ b/tests/integration/test_ex2b_canti2d_inelastic_section.py @@ -9,9 +9,9 @@ import pytest pytest.importorskip("openseespy") -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 +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 diff --git a/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py b/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py index 86d6fa8..1cae060 100644 --- a/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py +++ b/tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py @@ -9,9 +9,9 @@ import pytest pytest.importorskip("openseespy") -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 +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 diff --git a/tests/integration/test_ex3_canti2d_variants.py b/tests/integration/test_ex3_canti2d_variants.py index 8007be3..2486519 100644 --- a/tests/integration/test_ex3_canti2d_variants.py +++ b/tests/integration/test_ex3_canti2d_variants.py @@ -9,9 +9,9 @@ import pytest pytest.importorskip("openseespy") -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 +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 diff --git a/tests/integration/test_ex4_portal2d_variants.py b/tests/integration/test_ex4_portal2d_variants.py index e3abe5a..d4943cb 100644 --- a/tests/integration/test_ex4_portal2d_variants.py +++ b/tests/integration/test_ex4_portal2d_variants.py @@ -9,9 +9,9 @@ import pytest pytest.importorskip("openseespy") -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 +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 diff --git a/tests/integration/test_moment_curvature.py b/tests/integration/test_moment_curvature.py index a73c4ec..12439d4 100644 --- a/tests/integration/test_moment_curvature.py +++ b/tests/integration/test_moment_curvature.py @@ -8,13 +8,11 @@ behaviour is exercised by the Phase 9 pushover tests. from __future__ import annotations -import math - import pytest pytest.importorskip("openseespy") -from otko.core import ( # noqa: E402 +from otko.core import ( ElasticUniaxial, FiberSection, LinearTimeSeries, @@ -26,7 +24,7 @@ from otko.core import ( # noqa: E402 StaticCase, ZeroLengthSectionElement, ) -from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow @@ -134,7 +132,7 @@ def test_pushover_drives_rotation_for_moment_curvature() -> None: # 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): + for kappa, moment in zip(result.control_disp, result.base_shear, strict=False): if abs(kappa) < 1e-12: continue expected_M = E * I * kappa diff --git a/tests/integration/test_moment_curvature_example.py b/tests/integration/test_moment_curvature_example.py index f0bed1f..7648e51 100644 --- a/tests/integration/test_moment_curvature_example.py +++ b/tests/integration/test_moment_curvature_example.py @@ -6,8 +6,8 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project # noqa: E402 -from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +from otko.services import load_project +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow @@ -15,13 +15,13 @@ pytestmark = pytest.mark.slow 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() diff --git a/tests/integration/test_pattern_factors.py b/tests/integration/test_pattern_factors.py index 58d58a1..3c05c3e 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 ( # noqa: E402 +from otko.core import ( ElasticBeamColumn, ElasticSection, LinearTimeSeries, @@ -18,7 +18,7 @@ from otko.core import ( # noqa: E402 Project, StaticCase, ) -from otko.services import OpenSeesRunner # noqa: E402 +from otko.services import OpenSeesRunner pytestmark = pytest.mark.slow diff --git a/tests/integration/test_rc_frame_earthquake.py b/tests/integration/test_rc_frame_earthquake.py index 1d4d3dc..132535f 100644 --- a/tests/integration/test_rc_frame_earthquake.py +++ b/tests/integration/test_rc_frame_earthquake.py @@ -9,8 +9,8 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project, save_project # noqa: E402 -from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +from otko.services import load_project, save_project +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow @@ -18,9 +18,8 @@ pytestmark = pytest.mark.slow 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 ( - build_rc_frame_earthquake, - DT, N_PTS, + build_rc_frame_earthquake, ) proj = build_rc_frame_earthquake() diff --git a/tests/integration/test_rc_frame_gravity.py b/tests/integration/test_rc_frame_gravity.py index 5250108..0c91190 100644 --- a/tests/integration/test_rc_frame_gravity.py +++ b/tests/integration/test_rc_frame_gravity.py @@ -6,8 +6,8 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project, save_project # noqa: E402 -from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +from otko.services import load_project, save_project +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow @@ -22,7 +22,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 build_rc_frame_gravity, P_LOAD + from examples.rc_frame_gravity import P_LOAD, build_rc_frame_gravity proj = build_rc_frame_gravity() proj.validate_references() diff --git a/tests/integration/test_rc_frame_pushover.py b/tests/integration/test_rc_frame_pushover.py index 89518c3..5bd9ed0 100644 --- a/tests/integration/test_rc_frame_pushover.py +++ b/tests/integration/test_rc_frame_pushover.py @@ -6,8 +6,8 @@ import pytest pytest.importorskip("openseespy") -from otko.services import load_project, save_project # noqa: E402 -from otko.services.opensees_runner import OpenSeesRunner # noqa: E402 +from otko.services import load_project, save_project +from otko.services.opensees_runner import OpenSeesRunner pytestmark = pytest.mark.slow @@ -19,10 +19,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() diff --git a/tests/integration/test_runner_imposed_motion.py b/tests/integration/test_runner_imposed_motion.py index 54daa94..33c352b 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 ( # noqa: E402 +from otko.core import ( ElasticUniaxial, ImposedSupportMotionPattern, Node, @@ -33,7 +33,7 @@ from otko.core import ( # noqa: E402 UniformExcitationPattern, ZeroLengthElement, ) -from otko.services import OpenSeesRunner # noqa: E402 +from otko.services import OpenSeesRunner pytestmark = pytest.mark.slow diff --git a/tests/integration/test_runner_modal.py b/tests/integration/test_runner_modal.py index 670e682..bf2a843 100644 --- a/tests/integration/test_runner_modal.py +++ b/tests/integration/test_runner_modal.py @@ -15,16 +15,16 @@ import math import pytest -ops = pytest.importorskip("openseespy.opensees") # noqa: F401 +ops = pytest.importorskip("openseespy.opensees") -from otko.core import ( # noqa: E402 +from otko.core import ( ElasticBeamColumn, ElasticSection, ModalCase, Node, Project, ) -from otko.services import OpenSeesRunner # noqa: E402 +from otko.services import OpenSeesRunner pytestmark = pytest.mark.slow diff --git a/tests/integration/test_runner_static.py b/tests/integration/test_runner_static.py index 5722c0b..bd9cac1 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 ( # noqa: E402 +from otko.core import ( ElasticBeamColumn, ElasticSection, LinearTimeSeries, @@ -22,7 +22,7 @@ from otko.core import ( # noqa: E402 Project, StaticCase, ) -from otko.services import OpenSeesRunner # noqa: E402 +from otko.services import OpenSeesRunner pytestmark = pytest.mark.slow diff --git a/tests/integration/test_runner_transient.py b/tests/integration/test_runner_transient.py index 4715153..a2be365 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") # noqa: F401 -h5py = pytest.importorskip("h5py") # noqa: F401 +ops = pytest.importorskip("openseespy.opensees") +h5py = pytest.importorskip("h5py") -from otko.core import ( # noqa: E402 +from otko.core import ( ConstantTimeSeries, ElasticBeamColumn, ElasticSection, @@ -27,7 +27,7 @@ from otko.core import ( # noqa: E402 StaticCase, TransientCase, ) -from otko.services import OpenSeesRunner # noqa: E402 +from otko.services import OpenSeesRunner pytestmark = pytest.mark.slow @@ -51,7 +51,6 @@ 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, diff --git a/tests/services/test_element_forces.py b/tests/services/test_element_forces.py index ad03cda..2f57387 100644 --- a/tests/services/test_element_forces.py +++ b/tests/services/test_element_forces.py @@ -19,7 +19,6 @@ from otko.services.element_forces import ( ) from otko.services.results import StaticResults - # ────────────────────────────────────────────────────────────────────── # Fixtures: a tiny 3D project with 2 elements + canned force results. # ────────────────────────────────────────────────────────────────────── diff --git a/tests/services/test_runner_translation.py b/tests/services/test_runner_translation.py index 1bbee24..1903183 100644 --- a/tests/services/test_runner_translation.py +++ b/tests/services/test_runner_translation.py @@ -23,7 +23,6 @@ from otko.core import ( Node, PlainLoadPattern, Project, - StaticCase, Steel01, TrussElement, ZeroLengthElement, diff --git a/tests/services/test_section_bbox.py b/tests/services/test_section_bbox.py index 43ab0c0..25f6b24 100644 --- a/tests/services/test_section_bbox.py +++ b/tests/services/test_section_bbox.py @@ -10,7 +10,6 @@ from otko.core import ( CircularPatch, ElasticSection, FiberSection, - Project, RectangularPatch, StraightLayer, ) diff --git a/tests/tools/test_parse_schemas.py b/tests/tools/test_parse_schemas.py index e1159aa..506496c 100644 --- a/tests/tools/test_parse_schemas.py +++ b/tests/tools/test_parse_schemas.py @@ -13,6 +13,7 @@ import json from pathlib import Path import pytest + from tools.gidopensees_import.parse_schemas import ( ParseError, _parse_dependencies, diff --git a/tests/unit/core/materials/test_concrete04.py b/tests/unit/core/materials/test_concrete04.py index 52142a0..5ea8311 100644 --- a/tests/unit/core/materials/test_concrete04.py +++ b/tests/unit/core/materials/test_concrete04.py @@ -136,6 +136,7 @@ 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_diagram_renderer.py b/tests/unit/test_diagram_renderer.py index cf6ae38..425606b 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 # noqa: E402 +import pyvista as pv -from otko.core import ( # noqa: E402 +from otko.core import ( ElasticBeamColumn, ElasticSection, Node, Project, ) -from otko.services.element_forces import ( # noqa: E402 +from otko.services.element_forces import ( DiagramData, ForceComponent, extract_diagram_data, ) -from otko.services.results import StaticResults # noqa: E402 -from otko.views.canvas3d.diagram_renderer import DiagramRenderer # noqa: E402 +from otko.services.results import StaticResults +from otko.views.canvas3d.diagram_renderer import DiagramRenderer pv.OFF_SCREEN = True diff --git a/tests/unit/test_elements.py b/tests/unit/test_elements.py index 11987c6..e3c971a 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 ( - Element, ElasticBeamColumn, + Element, ForceBeamColumn, TrussElement, ZeroLengthElement, diff --git a/tests/unit/test_grid_system.py b/tests/unit/test_grid_system.py index adcbb55..6688dce 100644 --- a/tests/unit/test_grid_system.py +++ b/tests/unit/test_grid_system.py @@ -2,7 +2,6 @@ from __future__ import annotations -import math from pathlib import Path import pytest @@ -174,7 +173,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): + for a, b in zip(p_local, p_back, strict=False): assert a == pytest.approx(b, abs=1e-9) diff --git a/tests/unit/test_phase8a.py b/tests/unit/test_phase8a.py index 0e67780..c9a3842 100644 --- a/tests/unit/test_phase8a.py +++ b/tests/unit/test_phase8a.py @@ -8,7 +8,6 @@ from __future__ import annotations -import numpy as np import pytest from otko.commands.nodes import SetMassCommand diff --git a/tests/unit/test_phase9.py b/tests/unit/test_phase9.py index 04af175..24b38b9 100644 --- a/tests/unit/test_phase9.py +++ b/tests/unit/test_phase9.py @@ -10,9 +10,7 @@ import pytest from otko.core import ( AggregatorDOF, CircularPatch, - ElasticSection, FiberSection, - Fibre, Project, RectangularPatch, SectionAggregator, @@ -155,7 +153,7 @@ def test_Iz_of_rect_matches_analytical() -> None: # ── Round-trip persistence ─────────────────────────────────────────── def test_fiber_section_with_patches_round_trips(tmp_path) -> None: # type: ignore[no-untyped-def] - from otko.core import Node, ElasticBeamColumn + from otko.core import ElasticBeamColumn, Node p = Project( nodes=[Node(id=1, coords=(0, 0, 0)), Node(id=2, coords=(1, 0, 0))], diff --git a/tests/unit/test_renderer.py b/tests/unit/test_renderer.py index e1c8fb0..1586ca2 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 # noqa: E402 +import numpy as np -from otko.core import ( # noqa: E402 +from otko.core import ( ElasticBeamColumn, ElasticSection, LinearTimeSeries, @@ -18,8 +18,8 @@ from otko.core import ( # noqa: E402 Steel01, TrussElement, ) -from otko.services.deformation import DeformationSource # noqa: E402 -from otko.views.canvas3d.model_renderer import ( # noqa: E402 +from otko.services.deformation import DeformationSource +from otko.views.canvas3d.model_renderer import ( ModelRenderer, RendererMode, _classify_support, diff --git a/tests/unit/test_unit_labels.py b/tests/unit/test_unit_labels.py index 1fe1e3e..999f0e4 100644 --- a/tests/unit/test_unit_labels.py +++ b/tests/unit/test_unit_labels.py @@ -2,8 +2,6 @@ from __future__ import annotations -import pytest - from otko.core import UnitSystem, labels_for diff --git a/tests/unit/test_zerolength_section.py b/tests/unit/test_zerolength_section.py index 696b83e..69ef334 100644 --- a/tests/unit/test_zerolength_section.py +++ b/tests/unit/test_zerolength_section.py @@ -3,6 +3,7 @@ from __future__ import annotations import pytest +from pydantic import ValidationError from otko.core import ( ElasticSection, @@ -21,7 +22,7 @@ def test_zero_length_section_schema_defaults() -> None: def test_zero_length_section_rejects_extra_fields() -> None: - with pytest.raises(Exception): + with pytest.raises(ValidationError): ZeroLengthSectionElement( id=1, nodes=(1, 2), From f99b3a3efd07a80aecdb692a17fb1ef1e5453716 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 18:43:53 -0400 Subject: [PATCH 14/28] build: add plotly to the GUI extra Supplies the offline plotly.js bundle for the new canvas backend. The GUI CI job now installs the gui extra so the Qt/WebEngine tests run instead of being import-skipped. --- .github/workflows/ci.yml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0a7165..2c834e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: 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 ".[dev]" + - run: pip install -e ".[gui,dev]" - run: xvfb-run -a pytest tests/gui -m "not slow" test-integration: diff --git a/pyproject.toml b/pyproject.toml index ffab17d..d67c45f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ gui = [ "vtk>=9.3", "pyqtgraph>=0.13", "imageio[ffmpeg]>=2.34", + "plotly>=5.18", ] dev = [ "pytest>=7.4", From fc5971ad3dd02d60149ea4f562d0a940dc1b7310 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 18:43:53 -0400 Subject: [PATCH 15/28] feat: pure Plotly trace builder for the 3D canvas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Project → plotly.js figure dicts with no Qt/pyvista/plotly import, so it is unit-tested in the headless job. Mirrors the PyVista renderer's geometry (grid, nodes, frames, supports, loads, extrusions, local axes, labels, deformation) while respecting plotly.js's medium: pixel-sized markers, None-separated line segments, selection as a second trace (per-segment line colours are impossible), cone traces for arrows. Pickable traces carry meta.kind + customdata. --- src/otko/views/canvas_plotly/__init__.py | 26 + src/otko/views/canvas_plotly/trace_builder.py | 930 ++++++++++++++++++ tests/unit/test_plotly_trace_builder.py | 176 ++++ 3 files changed, 1132 insertions(+) create mode 100644 src/otko/views/canvas_plotly/__init__.py create mode 100644 src/otko/views/canvas_plotly/trace_builder.py create mode 100644 tests/unit/test_plotly_trace_builder.py diff --git a/src/otko/views/canvas_plotly/__init__.py b/src/otko/views/canvas_plotly/__init__.py new file mode 100644 index 0000000..7fabb6d --- /dev/null +++ b/src/otko/views/canvas_plotly/__init__.py @@ -0,0 +1,26 @@ +"""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/trace_builder.py b/src/otko/views/canvas_plotly/trace_builder.py new file mode 100644 index 0000000..7c6d782 --- /dev/null +++ b/src/otko/views/canvas_plotly/trace_builder.py @@ -0,0 +1,930 @@ +"""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 + +_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 +_LABEL_FONT_SIZE = 11 + + +@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 + + 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} + + +@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 + + +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() + + # ── 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) + return Scene( + data=data, + layout=self._layout(), + center=(float(center[0]), float(center[1]), float(center[2])), + diagonal=_diag_of_points(all_pts), + hover_trace=hover_trace, + ) + + # ── layout ─────────────────────────────────────────────────────── + def _layout(self) -> dict[str, Any]: + style = self._style + + def axis(color: str, title: str) -> dict[str, Any]: + return { + "title": {"text": title, "font": {"color": color, "size": 12}}, + "showgrid": False, + "showbackground": False, + "zeroline": False, + "showticklabels": False, + "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 = ["#00ffff" if nid in opts.selection_nodes else "#d9d9d9" 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"}, + "hoverinfo": "skip", + "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: + if not len(points): + return + normal: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = [] + selected: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = [] + normal_ids: list[Any] = [] + selected_ids: 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 + seg = (tuple(points[i]), tuple(points[j])) + if el.id in opts.selection_elements: + selected.append(seg) + selected_ids.extend([el.id, el.id, None]) + else: + normal.append(seg) + normal_ids.extend([el.id, el.id, None]) + if not normal and not selected: + return + if normal: + data.append( + _line_trace( + normal, + color="#338cd9", + width=4, + name="elements", + meta={"kind": "element"}, + customdata=normal_ids, + ) + ) + if selected: + data.append( + _line_trace( + selected, + color="#00ffff", + width=6, + name="elements-selected", + meta={"kind": "element"}, + customdata=selected_ids, + ) + ) + + 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": "#ff8019", + "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="#33d933", + 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="#ff8c33", + 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="#598cff", opacity=0.22, 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")) + 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")) + + @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) -> 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": _LABEL_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/tests/unit/test_plotly_trace_builder.py b/tests/unit/test_plotly_trace_builder.py new file mode 100644 index 0000000..92104be --- /dev/null +++ b/tests/unit/test_plotly_trace_builder.py @@ -0,0 +1,176 @@ +"""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.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 test_selected_element_gets_its_own_trace() -> None: + project = _load("cantilever") + scene = PlotlyTraceBuilder().build(project, SceneOptions(selection_elements=frozenset({2}))) + normal = _traces(scene, "elements") + selected = _traces(scene, "elements-selected") + assert len(normal) == 1 and len(selected) == 1 + # Plotly cannot colour one segment of a single line trace, so the + # selected element moves to the highlight trace. + assert 2 in selected[0]["customdata"] + assert 2 not in normal[0]["customdata"] + + +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"]) From 804bf22f61c8b98fc9a2797d7ace8cf1833d6b32 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 18:43:59 -0400 Subject: [PATCH 16/28] feat: plotly.js canvas widget (WebEngine + QWebChannel) PlotlyCanvas hosts plotly.js in a QWebEngineView driven over QWebChannel: figures update with Plotly.react (the camera survives unless a view preset asks for it), and clicks round-trip as node/element picks or grid-snap clicks. Assets are written to a temp dir and loaded from file:// because the ~5 MB bundle is past setHtml's data-URL limit. render() forwards QWidget's overload so grab() and painting keep working. --- src/otko/views/canvas_plotly/bridge.py | 28 ++ src/otko/views/canvas_plotly/html.py | 175 +++++++++ src/otko/views/canvas_plotly/plotly_canvas.py | 353 ++++++++++++++++++ 3 files changed, 556 insertions(+) create mode 100644 src/otko/views/canvas_plotly/bridge.py create mode 100644 src/otko/views/canvas_plotly/html.py create mode 100644 src/otko/views/canvas_plotly/plotly_canvas.py diff --git a/src/otko/views/canvas_plotly/bridge.py b/src/otko/views/canvas_plotly/bridge.py new file mode 100644 index 0000000..9dafbce --- /dev/null +++ b/src/otko/views/canvas_plotly/bridge.py @@ -0,0 +1,28 @@ +"""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 new file mode 100644 index 0000000..278abba --- /dev/null +++ b/src/otko/views/canvas_plotly/html.py @@ -0,0 +1,175 @@ +"""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 new file mode 100644 index 0000000..e5af025 --- /dev/null +++ b/src/otko/views/canvas_plotly/plotly_canvas.py @@ -0,0 +1,353 @@ +"""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: ``Plotly.react`` diffs client-side, and the layout only +carries ``scene.camera`` when a view preset or the projection toggle asks for +it — so re-rendering on a model edit or selection change never yanks the +camera the user is orbiting. +""" + +from __future__ import annotations + +import json +import math +from dataclasses import replace +from typing import Any + +from PySide6.QtCore import QUrl, Signal +from PySide6.QtWebChannel import QWebChannel +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 _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, + ) -> None: + super().__init__(parent) + 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._camera_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) + 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._camera_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._camera_dirty = True + + def reset_camera(self) -> None: + self._view_preset = "iso" + self._camera_dirty = True + self.render() + + def view_isometric(self) -> None: + self._view_preset = "iso" + self._camera_dirty = True + self.render() + + def view_xy(self) -> None: + """Top view: the eye sits on +Z looking down.""" + self._view_preset = "xy" + self._camera_dirty = True + self.render() + + def view_xz(self) -> None: + """Front view: the eye sits on -Y.""" + self._view_preset = "xz" + self._camera_dirty = True + self.render() + + def view_yz(self) -> None: + """Right view: the eye sits on +X.""" + self._view_preset = "yz" + self._camera_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_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: + self._scene = self._builder.build(self._project, self._options) + layout = dict(self._scene.layout) + if self._camera_dirty: + layout["scene"] = { + **layout["scene"], + "camera": self._camera_dict(self._scene), + } + self._camera_dirty = False + if not self._ready: + return + payload = json.dumps({"data": self._scene.data, "layout": layout}) + self._web.page().runJavaScript(f"window.otkoUpdate({json.dumps(payload)})") + + 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" From 9a1baa07c2b0f0a1570e4e6fcae672f4dc859668 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 18:43:59 -0400 Subject: [PATCH 17/28] =?UTF-8?q?feat:=20live=20canvas=20backend=20switchi?= =?UTF-8?q?ng=20(Options=20=E2=86=92=20Canvas=20Backend)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both backends share one SelectionState owned by MainWindow and live side by side in a QStackedWidget — switching is setCurrentWidget, so no widget is destroyed mid-session (tearing a VTK window down leaves dangling make-current callbacks). The choice persists in QSettings. CanvasCapabilities declares per-backend gaps (force diagrams and video export stay PyVista-only, both are documented and greyed out) and the UI gates on capabilities rather than the backend name. The architecture gate now allows the canvas_plotly package. --- AGENTS.md | 3 +- src/otko/views/canvas3d/model_canvas.py | 4 + src/otko/views/canvas_base.py | 92 +++++++++++++ src/otko/views/dock_manager.py | 24 ++++ src/otko/views/main_window.py | 163 ++++++++++++++++++++---- src/otko/views/menu_builder.py | 37 ++++++ src/otko/views/render_controls.py | 7 +- tests/gui/test_canvas_backend_switch.py | 122 ++++++++++++++++++ tests/unit/test_architecture.py | 28 ++-- 9 files changed, 442 insertions(+), 38 deletions(-) create mode 100644 src/otko/views/canvas_base.py create mode 100644 tests/gui/test_canvas_backend_switch.py diff --git a/AGENTS.md b/AGENTS.md index bdc5f2e..53e073c 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 + dev tools +pip install -e ".[gui,dev]" # desktop: Qt + PyVista + plotly.js backend + 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,6 +19,7 @@ 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. - `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). diff --git a/src/otko/views/canvas3d/model_canvas.py b/src/otko/views/canvas3d/model_canvas.py index b1b3417..2b69352 100644 --- a/src/otko/views/canvas3d/model_canvas.py +++ b/src/otko/views/canvas3d/model_canvas.py @@ -24,6 +24,7 @@ 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 @@ -31,6 +32,9 @@ PICK_DEBUG = False 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) diff --git a/src/otko/views/canvas_base.py b/src/otko/views/canvas_base.py new file mode 100644 index 0000000..0352961 --- /dev/null +++ b/src/otko/views/canvas_base.py @@ -0,0 +1,92 @@ +"""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/dock_manager.py b/src/otko/views/dock_manager.py index df4b70e..cd34449 100644 --- a/src/otko/views/dock_manager.py +++ b/src/otko/views/dock_manager.py @@ -210,6 +210,14 @@ 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( @@ -260,6 +268,14 @@ 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 @@ -364,6 +380,14 @@ 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 diff --git a/src/otko/views/main_window.py b/src/otko/views/main_window.py index 958459e..cbd21e3 100644 --- a/src/otko/views/main_window.py +++ b/src/otko/views/main_window.py @@ -14,15 +14,18 @@ The ``projectChanged`` / ``modelMutated`` signals live on the from __future__ import annotations 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 +from PySide6.QtWidgets import QMainWindow, QMessageBox, QStackedWidget 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.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 @@ -34,6 +37,9 @@ from otko.views.tools import ( ToolController, ) +#: Canvas backends, in menu order. +CANVAS_BACKENDS = ("pyvista", "plotly") + class MainWindow( QMainWindow, @@ -44,7 +50,7 @@ class MainWindow( ): """Top-level application shell.""" - def __init__(self) -> None: + def __init__(self, settings: QSettings | None = None) -> None: super().__init__() self.setWindowTitle("OTKO") self.setWindowIcon( @@ -60,12 +66,20 @@ class MainWindow( self._show_node_labels = False self._show_element_labels = False - 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 + # 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() + 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_docks() self._build_actions() @@ -77,19 +91,120 @@ class MainWindow( self._refresh_action_enablement() self.restore_layout() - # ── 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) + # ── 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, selection=self._selection) + else: + canvas = ModelCanvas(self, 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 _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.""" - settings = QSettings("OTKO", "OTKO") - settings.setValue("geometry", self.saveGeometry()) - settings.setValue("windowState", self.saveState()) + 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. @@ -97,11 +212,10 @@ class MainWindow( On first run no values are stored, so the defaults from ``__init__`` (``resize`` + dock construction) stay in effect. """ - settings = QSettings("OTKO", "OTKO") - geometry = settings.value("geometry") + geometry = self._settings.value("geometry") if geometry is not None: self.restoreGeometry(geometry) - state = settings.value("windowState") + state = self._settings.value("windowState") if state is not None: self.restoreState(state) @@ -161,9 +275,12 @@ 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) - 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) + # 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")) # Define self._act_grid.triggered.connect(self._on_grid_system) diff --git a/src/otko/views/menu_builder.py b/src/otko/views/menu_builder.py index bd8af6a..0aa0b2e 100644 --- a/src/otko/views/menu_builder.py +++ b/src/otko/views/menu_builder.py @@ -193,8 +193,41 @@ class MenuBuilder: self._act_quick_guide = QAction("&Quick Guide", self) self._act_set_units = QAction("Set Display &Units…", self) + # 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. @@ -479,6 +512,10 @@ class MenuBuilder: m_options = mb.addMenu("&Options") m_options.addAction(self._act_set_units) + 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) diff --git a/src/otko/views/render_controls.py b/src/otko/views/render_controls.py index 1d574c1..63d1f0f 100644 --- a/src/otko/views/render_controls.py +++ b/src/otko/views/render_controls.py @@ -459,6 +459,7 @@ 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) @@ -482,9 +483,11 @@ 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) - self._act_show_force_diagram.setEnabled(has_static) + # 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_time_history.setEnabled(has_transient) - self._act_export_th_animation.setEnabled(has_transient) + self._act_export_th_animation.setEnabled(has_transient and caps.animation_export) self._act_show_hysteresis.setEnabled(has_transient) self._act_show_pushover.setEnabled(has_pushover) self._act_show_response_spectrum.setEnabled(has_rs) diff --git a/tests/gui/test_canvas_backend_switch.py b/tests/gui/test_canvas_backend_switch.py new file mode 100644 index 0000000..ed77be1 --- /dev/null +++ b/tests/gui/test_canvas_backend_switch.py @@ -0,0 +1,122 @@ +"""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/unit/test_architecture.py b/tests/unit/test_architecture.py index 9281e67..7b3f95c 100644 --- a/tests/unit/test_architecture.py +++ b/tests/unit/test_architecture.py @@ -2,11 +2,12 @@ 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 backend is confined to -``views/canvas3d``. 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. +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 @@ -16,7 +17,10 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] SRC = ROOT / "src" / "otko" -CANVAS = SRC / "views" / "canvas3d" +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"} @@ -32,7 +36,7 @@ CORE_FORBIDDEN = QT_ROOTS | { "matplotlib", } -# Only views/canvas3d/ may touch a 3D or plotting backend. +# Only the canvas packages may touch a 3D or plotting backend. VTK_ROOTS = {"pyvista", "pyvistaqt", "vtk", "plotly"} @@ -75,14 +79,14 @@ def _violations( forbidden: set[str], *, skip: tuple[Path, ...] = (), - exclude_dir: Path | None = None, + 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 exclude_dir is not None and _is_within(path, exclude_dir): + 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}") @@ -112,9 +116,9 @@ def test_architecture_views_no_solver() -> None: def test_no_vtk_import_outside_canvas() -> None: - """3D/plot backends stay under views/canvas3d/.""" - violations = _violations(SRC, VTK_ROOTS, exclude_dir=CANVAS) - assert violations == [], f"backend imports outside views/canvas3d/: {violations}" + """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 From d09e72e95dddfc0b189adb6c62f8072471485074 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 19:15:48 -0400 Subject: [PATCH 18/28] chore: clone opstool for visualization reference opstool v1.0.26 (GPL-3.0) is cloned read-only under .slim/clonedeps/repos/ to inspect its PyVista/Plotly visualization settings. GPLv3 section 13 permits combining it with this AGPL-3.0 project. The clone itself is git-ignored; the manifest and the AGENTS.md pointer are committed. --- .gitignore | 4 ++++ .ignore | 9 +++++++++ .slim/clonedeps.json | 15 +++++++++++++++ AGENTS.md | 14 ++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 .ignore create mode 100644 .slim/clonedeps.json diff --git a/.gitignore b/.gitignore index c82640e..eac3bc1 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,7 @@ 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 new file mode 100644 index 0000000..32dbf4f --- /dev/null +++ b/.ignore @@ -0,0 +1,9 @@ +# 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 new file mode 100644 index 0000000..ddddcab --- /dev/null +++ b/.slim/clonedeps.json @@ -0,0 +1,15 @@ +{ + "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 53e073c..6b63da2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,3 +57,17 @@ git remote add origin ssh://git@smill-home.ddns.net/smill/otko.git - `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. + From 40a673c5624d139502a8756ba662e8c2a2699573 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 19:15:48 -0400 Subject: [PATCH 19/28] test: isolate QSettings so user preferences cannot fail GUI tests MainWindow persists preferences (window layout, canvas backend) through QSettings("OTKO", "OTKO"), so tests constructing it were reading the developer's real settings: after switching to the Plotly backend the PyVista-specific viewport-axis tests failed with "PlotlyCanvas has no attribute renderer". A session fixture now redirects QSettings into a temp dir and is skipped when Qt is not installed, keeping the headless job Qt-free. --- tests/conftest.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 82dd86a..c91c064 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ domain after every test. The import is inside the teardown so that from __future__ import annotations +import importlib.util import os from collections.abc import Iterator @@ -19,6 +20,27 @@ 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.""" From 17d2ed21d3b2f43277ce2f2ffb7481ea66307119 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 19:15:55 -0400 Subject: [PATCH 20/28] feat: opstool-derived element palette and single-trace frame colours Ports opstool's per-family element colours and its diverging response scale; RenderStyle.response_scale_colors now also drives the PyVista force-diagram colouring instead of a hard-coded "coolwarm". The frame renderer drops the two-trace normal/selected workaround: my earlier assumption that plotly cannot colour segments individually was wrong. Scatter3d.line.color accepts an array mapped through a colorscale, so one trace now carries per-element colours (family + selection) and is ready to be coloured by response value later. Attribution recorded in NOTICE per GPLv3 section 5(a)/(b). --- NOTICE | 15 +++ src/otko/views/canvas3d/diagram_renderer.py | 8 +- src/otko/views/canvas3d/style.py | 35 ++++++ src/otko/views/canvas_plotly/trace_builder.py | 101 ++++++++++++------ tests/unit/test_plotly_trace_builder.py | 50 +++++++-- 5 files changed, 164 insertions(+), 45 deletions(-) diff --git a/NOTICE b/NOTICE index 10a928c..24b2dd1 100644 --- a/NOTICE +++ b/NOTICE @@ -45,6 +45,21 @@ 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 diff --git a/src/otko/views/canvas3d/diagram_renderer.py b/src/otko/views/canvas3d/diagram_renderer.py index 05c5f2c..57c0a70 100644 --- a/src/otko/views/canvas3d/diagram_renderer.py +++ b/src/otko/views/canvas3d/diagram_renderer.py @@ -31,6 +31,7 @@ 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") @@ -62,8 +63,9 @@ class DiagramRenderer: - ``clear()`` removes the overlay. """ - def __init__(self, plotter: Any) -> None: + def __init__(self, plotter: Any, style: RenderStyle | None = None) -> None: self._plotter = plotter + self._style = style or RenderStyle() self._actor: Any = None self._label_actor: Any = None @@ -158,7 +160,9 @@ class DiagramRenderer: self._actor = self._plotter.add_mesh( mesh, scalars="value", - cmap="coolwarm", + # Shared response palette (opstool's default diverging scale), so + # the PyVista and Plotly backends stay visually consistent. + cmap=list(self._style.response_scale_colors), clim=(-vmax, vmax), show_scalar_bar=True, scalar_bar_args={"title": comp_label, "n_labels": 5}, diff --git a/src/otko/views/canvas3d/style.py b/src/otko/views/canvas3d/style.py index f25d674..3e1de73 100644 --- a/src/otko/views/canvas3d/style.py +++ b/src/otko/views/canvas3d/style.py @@ -27,6 +27,31 @@ class RenderStyle: zerolength_color: str = "#a020f0" 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 + + #: 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 pin_color: str = "#c0392b" roller_color: str = "#e67e22" @@ -49,3 +74,13 @@ class RenderStyle: load_min_length: float = 0.5 selection_thickness_factor: float = 1.6 # multiplier for selected actors + + # ── 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) + ] diff --git a/src/otko/views/canvas_plotly/trace_builder.py b/src/otko/views/canvas_plotly/trace_builder.py index 7c6d782..8c1538b 100644 --- a/src/otko/views/canvas_plotly/trace_builder.py +++ b/src/otko/views/canvas_plotly/trace_builder.py @@ -56,6 +56,19 @@ _FRAME_CLASSES = ( ZeroLengthElement, ) +#: Element families that get their own palette entry (see ``RenderStyle``). +_TRUSS_FAMILY = (TrussElement, CorotTrussElement) + + +def _family_index(el: Any) -> int: + """Palette slot for a frame element: 0 beam, 1 truss, 2 link/zero-length.""" + if isinstance(el, _TRUSS_FAMILY): + return 1 + if isinstance(el, ZeroLengthElement): + return 2 + return 0 + + #: Triangle indices for the 8-corner box sweep built by :meth:`_SceneBuilder._box_corners`. _BOX_TRIS = ( (0, 1, 2), @@ -504,12 +517,30 @@ class PlotlyTraceBuilder: 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 - normal: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = [] - selected: list[tuple[tuple[float, float, float], tuple[float, float, float]]] = [] - normal_ids: list[Any] = [] - selected_ids: list[Any] = [] + scale_colors = [ + self._style.element_beam_color, + self._style.element_truss_color, + self._style.element_link_color, + self._style.selected_color, + ] + 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 @@ -517,37 +548,39 @@ class PlotlyTraceBuilder: j = node_row.get(el.nodes[1]) if i is None or j is None: continue - seg = (tuple(points[i]), tuple(points[j])) - if el.id in opts.selection_elements: - selected.append(seg) - selected_ids.extend([el.id, el.id, None]) - else: - normal.append(seg) - normal_ids.extend([el.id, el.id, None]) - if not normal and not selected: + index = float(selected_index if el.id in opts.selection_elements else _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 - if normal: - data.append( - _line_trace( - normal, - color="#338cd9", - width=4, - name="elements", - meta={"kind": "element"}, - customdata=normal_ids, - ) - ) - if selected: - data.append( - _line_trace( - selected, - color="#00ffff", - width=6, - name="elements-selected", - meta={"kind": "element"}, - customdata=selected_ids, - ) - ) + + 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"}, + "hoverinfo": "skip", + "name": "elements", + "showlegend": False, + } + ) def _build_supports( self, project: Project, data: list[dict[str, Any]], opts: SceneOptions diff --git a/tests/unit/test_plotly_trace_builder.py b/tests/unit/test_plotly_trace_builder.py index 92104be..ae960a9 100644 --- a/tests/unit/test_plotly_trace_builder.py +++ b/tests/unit/test_plotly_trace_builder.py @@ -9,6 +9,7 @@ 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, @@ -49,16 +50,47 @@ def test_nodes_carry_ids_as_customdata() -> None: assert len(nodes["marker"]["color"]) == len(project.nodes) -def test_selected_element_gets_its_own_trace() -> None: +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") - scene = PlotlyTraceBuilder().build(project, SceneOptions(selection_elements=frozenset({2}))) - normal = _traces(scene, "elements") - selected = _traces(scene, "elements-selected") - assert len(normal) == 1 and len(selected) == 1 - # Plotly cannot colour one segment of a single line trace, so the - # selected element moves to the highlight trace. - assert 2 in selected[0]["customdata"] - assert 2 not in normal[0]["customdata"] + 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: From 56f1f6c08b28abc52a055d1e7518fae71c68c8bd Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 19:31:30 -0400 Subject: [PATCH 21/28] feat: route both canvas backends through RenderStyle Colours that were hard-coded in the renderers now come from the shared style: nodes, supports, nodal/element loads, section extrusions (+ opacity) and label font size. The PyVista frame LUT becomes a four-slot palette [beam, truss, link, selected] with the cell scalar carrying the family slot, so VTK matches the Plotly backend's per-family colouring that landed earlier. Adds style helpers shared by both backends (element_family_index, family_palette) and an immutable with_updates(). The style also gains the editable field table (STYLE_FIELDS) the Plot Properties dialog is built from. --- src/otko/views/canvas3d/model_renderer.py | 56 ++++++--- src/otko/views/canvas3d/style.py | 83 ++++++++++++- src/otko/views/canvas_plotly/trace_builder.py | 77 +++++++----- tests/unit/test_render_style.py | 117 ++++++++++++++++++ tests/unit/test_renderer.py | 9 +- 5 files changed, 285 insertions(+), 57 deletions(-) create mode 100644 tests/unit/test_render_style.py diff --git a/src/otko/views/canvas3d/model_renderer.py b/src/otko/views/canvas3d/model_renderer.py index 9eb9274..12c88cb 100644 --- a/src/otko/views/canvas3d/model_renderer.py +++ b/src/otko/views/canvas3d/model_renderer.py @@ -13,7 +13,7 @@ from __future__ import annotations import contextlib import enum from dataclasses import dataclass -from typing import Any, ClassVar +from typing import Any import numpy as np import pyvista as pv @@ -33,7 +33,12 @@ from otko.core import ( ZeroLengthElement, ZeroLengthSectionElement, ) -from otko.views.canvas3d.style import RenderStyle +from otko.views.canvas3d.style import ( + SELECTED_STATE, + RenderStyle, + element_family_index, + family_palette, +) class RendererMode(enum.Enum): @@ -185,9 +190,6 @@ class ModelRenderer: r, g, b = (int(round(x * 255)) for x in rgb) return f"#{r:02x}{g:02x}{b:02x}" - _NODE_LUT: ClassVar[list[str]] = ["#d9d9d9", "#00ffff"] # gray normal, cyan selected - _FRAME_LUT: ClassVar[list[str]] = ["#338cd9", "#00ffff"] # blue normal, cyan selected - def __init__(self, plotter: Any, style: RenderStyle | None = None) -> None: self._plotter = plotter self._style = style or RenderStyle() @@ -206,6 +208,8 @@ 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 @@ -278,6 +282,16 @@ 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. @@ -306,11 +320,12 @@ class ModelRenderer: self._reglyph_nodes() if self._frame_pd is not None and self._frame_ids_ordered: - states = np.zeros(len(self._frame_ids_ordered), dtype=np.int8) + # Restore each cell to its family slot, then flag the selected ones. + states = np.array(self._frame_family, dtype=np.int8) for eid in element_ids: row = self._frame_id_to_row.get(eid) if row is not None: - states[row] = 1 + states[row] = SELECTED_STATE self._frame_pd.cell_data["_oss_state"] = states self._frame_pd.Modified() @@ -379,7 +394,7 @@ class ModelRenderer: self._node_actor = self._plotter.add_mesh( glyph, scalars="_oss_state", - cmap=self._NODE_LUT, + cmap=[self._style.node_color, self._style.node_selected_color], clim=[0, 1], show_scalar_bar=False, pickable=True, @@ -392,6 +407,7 @@ 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]] @@ -400,6 +416,7 @@ 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() @@ -407,16 +424,18 @@ 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) - pd.cell_data["_oss_state"] = np.zeros(len(ids), dtype=np.int8) + # Cell scalar carries the palette slot (family, or SELECTED_STATE). + pd.cell_data["_oss_state"] = np.array(families, 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=self._FRAME_LUT, - clim=[0, 1], + cmap=family_palette(self._style), + clim=[0, SELECTED_STATE], show_scalar_bar=False, line_width=3.0, pickable=True, @@ -761,8 +780,8 @@ class ModelRenderer: """Add one semi-transparent steel-blue non-pickable aux actor.""" actor = self._plotter.add_mesh( mesh, - color=(0.35, 0.60, 0.85), # cool steel-blue - opacity=0.22, + color=self._style.extrusion_color, + opacity=self._style.extrusion_opacity, show_edges=True, edge_color=(0.15, 0.25, 0.45), line_width=1.0, @@ -982,7 +1001,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 = (1.0, 0.5, 0.1) + support_color = self._style.support_color for node in project.nodes: if not any(node.restraint[i] for i in dof_idx): continue @@ -996,7 +1015,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 = (0.2, 0.85, 0.2) + load_color = self._style.nodal_load_color 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: @@ -1027,7 +1046,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 = (1.0, 0.55, 0.2) # orange + elem_load_color = self._style.element_load_color # distributed loads n_arrows_per_elem = 5 for eload in pattern.element_loads: if not isinstance(eload, UniformElementLoad): @@ -1116,6 +1135,7 @@ 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 @@ -1148,7 +1168,7 @@ class ModelRenderer: return self._plotter.add_point_labels( pts, labels, - font_size=12, + font_size=self._style.label_font_size, shape_opacity=0.15, text_color="black", point_color="white", @@ -1178,7 +1198,7 @@ class ModelRenderer: return self._plotter.add_point_labels( np.asarray(centers), labels, - font_size=12, + font_size=self._style.label_font_size, 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 3e1de73..6051fd9 100644 --- a/src/otko/views/canvas3d/style.py +++ b/src/otko/views/canvas3d/style.py @@ -4,11 +4,63 @@ 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 +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"), + ("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) @dataclass(frozen=True, slots=True) @@ -19,13 +71,13 @@ class RenderStyle: background_top: str = "#dbe2ef" background_bottom: str = "#f5f7fb" - node_color: str = "#f0a500" # warm gold - node_selected_color: str = "#00d4ff" # bright cyan + node_color: str = "#d9d9d9" # normal node sphere / marker + node_selected_color: str = "#00ffff" # selected node frame_color: str = "#1f1f1f" - truss_color: str = "#2e5cb8" + truss_color: str = "#2e5cb8" # also the Z-axis colour of the view triad zerolength_color: str = "#a020f0" - selected_color: str = "#00d4ff" + selected_color: str = "#00ffff" # selected element highlight # ── element palette (adapted from opstool v1.0.26, GPL-3.0) ─────── # opstool colours wireframe elements by *family* rather than painting @@ -36,6 +88,14 @@ class RenderStyle: 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 + #: 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). @@ -57,7 +117,7 @@ class RenderStyle: roller_color: str = "#e67e22" custom_support_color: str = "#7f8c8d" - load_color: str = "#27ae60" + load_color: str = "#27ae60" # Y-axis colour of the view triad mass_color: str = "#9b59b6" # ── sizes (relative to bbox diagonal unless absolute) ──────────── @@ -75,6 +135,9 @@ class RenderStyle: 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`.""" @@ -84,3 +147,11 @@ class RenderStyle: 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_plotly/trace_builder.py b/src/otko/views/canvas_plotly/trace_builder.py index 8c1538b..40ae710 100644 --- a/src/otko/views/canvas_plotly/trace_builder.py +++ b/src/otko/views/canvas_plotly/trace_builder.py @@ -44,7 +44,11 @@ from otko.core import ( ZeroLengthElement, ZeroLengthSectionElement, ) -from otko.views.canvas3d.style import RenderStyle +from otko.views.canvas3d.style import ( + RenderStyle, + element_family_index, + family_palette, +) _FRAME_CLASSES = ( ElasticBeamColumn, @@ -56,19 +60,6 @@ _FRAME_CLASSES = ( ZeroLengthElement, ) -#: Element families that get their own palette entry (see ``RenderStyle``). -_TRUSS_FAMILY = (TrussElement, CorotTrussElement) - - -def _family_index(el: Any) -> int: - """Palette slot for a frame element: 0 beam, 1 truss, 2 link/zero-length.""" - if isinstance(el, _TRUSS_FAMILY): - return 1 - if isinstance(el, ZeroLengthElement): - return 2 - return 0 - - #: Triangle indices for the 8-corner box sweep built by :meth:`_SceneBuilder._box_corners`. _BOX_TRIS = ( (0, 1, 2), @@ -96,7 +87,6 @@ _SUPPORT_SYMBOLS = { _NODE_MARKER_SIZE = 7.0 _SUPPORT_MARKER_SIZE = 11.0 _SNAP_MARKER_SIZE = 8.0 -_LABEL_FONT_SIZE = 11 @dataclass(frozen=True) @@ -275,6 +265,10 @@ class PlotlyTraceBuilder: 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() @@ -488,7 +482,12 @@ class PlotlyTraceBuilder: ) -> None: if not len(points): return - colors = ["#00ffff" if nid in opts.selection_nodes else "#d9d9d9" for nid in node_ids] + 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", @@ -528,12 +527,7 @@ class PlotlyTraceBuilder: """ if not len(points): return - scale_colors = [ - self._style.element_beam_color, - self._style.element_truss_color, - self._style.element_link_color, - self._style.selected_color, - ] + scale_colors = family_palette(self._style) selected_index = len(scale_colors) - 1 x: list[float | None] = [] @@ -548,7 +542,9 @@ class PlotlyTraceBuilder: 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 _family_index(el)) + 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]) @@ -603,7 +599,7 @@ class PlotlyTraceBuilder: "y": [float(n.coords[1]) for n in nodes], "z": [float(n.coords[2]) for n in nodes], "marker": { - "color": "#ff8019", + "color": self._style.support_color, "size": _SUPPORT_MARKER_SIZE, "symbol": _SUPPORT_SYMBOLS[kind], "line": {"color": "#7f3f00", "width": 1}, @@ -708,7 +704,7 @@ class PlotlyTraceBuilder: nodal_u, nodal_v, nodal_w, - color="#33d933", + color=self._style.nodal_load_color, name="nodal-loads", size=scale, ) @@ -722,7 +718,7 @@ class PlotlyTraceBuilder: dist_u, dist_v, dist_w, - color="#ff8c33", + color=self._style.element_load_color, name="element-loads", size=0.6 * scale, ) @@ -833,7 +829,13 @@ class PlotlyTraceBuilder: 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="#598cff", opacity=0.22, name="extrusions")) + data.append( + mesh.as_trace( + color=self._style.extrusion_color, + opacity=self._style.extrusion_opacity, + name="extrusions", + ) + ) @staticmethod def _box_corners( @@ -872,7 +874,11 @@ class PlotlyTraceBuilder: ) -> 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")) + 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] = [] @@ -886,7 +892,14 @@ class PlotlyTraceBuilder: 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")) + 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: @@ -942,7 +955,9 @@ def _cone_trace( } -def _text_trace(points: np.ndarray, labels: list[str], *, name: str) -> dict[str, Any]: +def _text_trace( + points: np.ndarray, labels: list[str], *, name: str, font_size: int +) -> dict[str, Any]: return { "type": "scatter3d", "mode": "text", @@ -951,7 +966,7 @@ def _text_trace(points: np.ndarray, labels: list[str], *, name: str) -> dict[str "z": [float(p[2]) for p in points], "text": labels, "textposition": "top center", - "textfont": {"size": _LABEL_FONT_SIZE, "color": "#111111"}, + "textfont": {"size": font_size, "color": "#111111"}, "hoverinfo": "skip", "name": name, "showlegend": False, diff --git a/tests/unit/test_render_style.py b/tests/unit/test_render_style.py new file mode 100644 index 0000000..32b027b --- /dev/null +++ b/tests/unit/test_render_style.py @@ -0,0 +1,117 @@ +"""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"} + + +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 1586ca2..04529d6 100644 --- a/tests/unit/test_renderer.py +++ b/tests/unit/test_renderer.py @@ -24,6 +24,7 @@ from otko.views.canvas3d.model_renderer import ( RendererMode, _classify_support, ) +from otko.views.canvas3d.style import SELECTED_STATE # ──────────────────────────── support classification ──────────────────────────── @@ -125,9 +126,13 @@ 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] - # Element 2 selected → it's the second frame (index 1 in frame_ids_ordered) + # Frame cells carry the palette slot: the element family normally, and + # SELECTED_STATE when selected. selected_frame_idx = r._frame_id_to_row[2] - assert frame_states[selected_frame_idx] == 1 + 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 def test_clear_selection(offscreen_plotter, small_3d_project) -> None: # type: ignore[no-untyped-def] From bd365be77d82dd267452ed916d87c344d1da15e3 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 19:31:38 -0400 Subject: [PATCH 22/28] =?UTF-8?q?feat:=20Options=20=E2=86=92=20Plot=20Prop?= =?UTF-8?q?erties=20dialog=20with=20live=20preview=20and=20persistence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a table-driven dialog over RenderStyle.STYLE_FIELDS (colour swatches, an opacity spin and a label font size), reachable from Options. Edits preview immediately on every canvas, Cancel restores the style the dialog opened with, and only OK persists — as JSON under QSettings plot/props, reloaded on the next launch. Both canvases gain set_style(); the dialog deliberately exposes plot_style() rather than style() so QWidget.style() keeps its Qt meaning. --- AGENTS.md | 1 + src/otko/views/canvas3d/model_canvas.py | 11 ++ src/otko/views/canvas_plotly/plotly_canvas.py | 6 + src/otko/views/dialogs/__init__.py | 2 + src/otko/views/dialogs/plot_properties.py | 139 ++++++++++++++++++ src/otko/views/main_window.py | 38 ++++- src/otko/views/menu_builder.py | 5 + src/otko/views/render_controls.py | 17 +++ tests/gui/test_plot_properties.py | 131 +++++++++++++++++ 9 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 src/otko/views/dialogs/plot_properties.py create mode 100644 tests/gui/test_plot_properties.py diff --git a/AGENTS.md b/AGENTS.md index 6b63da2..bbe6d81 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,7 @@ Strict one-way MVVM + services: `views → viewmodels → services → core`. - `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. +- `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). diff --git a/src/otko/views/canvas3d/model_canvas.py b/src/otko/views/canvas3d/model_canvas.py index 2b69352..3e00425 100644 --- a/src/otko/views/canvas3d/model_canvas.py +++ b/src/otko/views/canvas3d/model_canvas.py @@ -342,6 +342,17 @@ 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/canvas_plotly/plotly_canvas.py b/src/otko/views/canvas_plotly/plotly_canvas.py index e5af025..1e771d5 100644 --- a/src/otko/views/canvas_plotly/plotly_canvas.py +++ b/src/otko/views/canvas_plotly/plotly_canvas.py @@ -271,6 +271,12 @@ class PlotlyCanvas(QWidget): 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.""" + 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, diff --git a/src/otko/views/dialogs/__init__.py b/src/otko/views/dialogs/__init__.py index d2a3b45..e4dff02 100644 --- a/src/otko/views/dialogs/__init__.py +++ b/src/otko/views/dialogs/__init__.py @@ -34,6 +34,7 @@ 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 @@ -69,6 +70,7 @@ __all__ = [ "PathTimeSeriesDialog", "PatternLoadsDialog", "PlainPatternDialog", + "PlotPropertiesDialog", "QuickGridLinesDialog", "QuickGuideDialog", "ReplicateDialog", diff --git a/src/otko/views/dialogs/plot_properties.py b/src/otko/views/dialogs/plot_properties.py new file mode 100644 index 0000000..5240546 --- /dev/null +++ b/src/otko/views/dialogs/plot_properties.py @@ -0,0 +1,139 @@ +"""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 ( + 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] = {} + + 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()}) + 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 + 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)) + self._emit_changed() diff --git a/src/otko/views/main_window.py b/src/otko/views/main_window.py index cbd21e3..cca1d7e 100644 --- a/src/otko/views/main_window.py +++ b/src/otko/views/main_window.py @@ -13,6 +13,7 @@ The ``projectChanged`` / ``modelMutated`` signals live on the from __future__ import annotations +import json from pathlib import Path from typing import Any @@ -25,6 +26,7 @@ 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 @@ -72,6 +74,8 @@ class MainWindow( 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 @@ -115,9 +119,9 @@ class MainWindow( # Imported lazily so a plotly-less install still boots PyVista. from otko.views.canvas_plotly import PlotlyCanvas - canvas = PlotlyCanvas(self, selection=self._selection) + canvas = PlotlyCanvas(self, style=self._style, selection=self._selection) else: - canvas = ModelCanvas(self, selection=self._selection) + canvas = ModelCanvas(self, style=self._style, selection=self._selection) self._canvases[backend] = canvas return canvas @@ -145,6 +149,35 @@ class MainWindow( """Name of the active canvas backend (``"pyvista"`` or ``"plotly"``).""" return self._canvas_backend + # ── 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) @@ -261,6 +294,7 @@ 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) diff --git a/src/otko/views/menu_builder.py b/src/otko/views/menu_builder.py index 0aa0b2e..5e7a271 100644 --- a/src/otko/views/menu_builder.py +++ b/src/otko/views/menu_builder.py @@ -192,6 +192,10 @@ 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 @@ -512,6 +516,7 @@ 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) diff --git a/src/otko/views/render_controls.py b/src/otko/views/render_controls.py index 63d1f0f..06db479 100644 --- a/src/otko/views/render_controls.py +++ b/src/otko/views/render_controls.py @@ -50,6 +50,23 @@ 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 diff --git a/tests/gui/test_plot_properties.py b/tests/gui/test_plot_properties.py new file mode 100644 index 0000000..3ef3601 --- /dev/null +++ b/tests/gui/test_plot_properties.py @@ -0,0 +1,131 @@ +"""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() From 5f977e9df0805ac6d894ce5d1216341330f345d1 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 20:05:26 -0400 Subject: [PATCH 23/28] feat: opstool-style scene recipe for the Plotly canvas Frames the view deterministically: Scene now carries padded per-axis bounds (opstool's pad_ratio 0.15, with unit slack on degenerate axes) plus axis_overrides() pinning range + autorange=False, so the model is never flush against the viewport edge. Adds entity hover templates (Node #n / Element #n with an empty extra tag), and an optional axis outline (grid + ticks) - off by default so the SAP2000-like clean viewport is unchanged; the coloured X/Y/Z axis lines always stay as the orientation cue. The outline is exposed as a bool in the Plot Properties table, so the table-driven dialog now builds a checkbox for kind=bool. --- src/otko/views/canvas3d/style.py | 6 ++ src/otko/views/canvas_plotly/trace_builder.py | 62 +++++++++++++++++-- src/otko/views/dialogs/plot_properties.py | 11 ++++ tests/unit/test_plotly_trace_builder.py | 54 ++++++++++++++++ tests/unit/test_render_style.py | 2 +- 5 files changed, 128 insertions(+), 7 deletions(-) diff --git a/src/otko/views/canvas3d/style.py b/src/otko/views/canvas3d/style.py index 6051fd9..a55d43b 100644 --- a/src/otko/views/canvas3d/style.py +++ b/src/otko/views/canvas3d/style.py @@ -56,6 +56,7 @@ STYLE_FIELDS: tuple[tuple[str, str, str], ...] = ( ("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"), ) @@ -96,6 +97,11 @@ class RenderStyle: 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). diff --git a/src/otko/views/canvas_plotly/trace_builder.py b/src/otko/views/canvas_plotly/trace_builder.py index 40ae710..fa5fc94 100644 --- a/src/otko/views/canvas_plotly/trace_builder.py +++ b/src/otko/views/canvas_plotly/trace_builder.py @@ -114,11 +114,25 @@ class Scene: 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: @@ -173,6 +187,34 @@ def _diag_of_points(pts: np.ndarray | None) -> float: 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)) @@ -298,25 +340,31 @@ class PlotlyTraceBuilder: 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_points(all_pts), + 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": False, + "showgrid": outline, "showbackground": False, - "zeroline": False, - "showticklabels": False, + "zeroline": outline, + "showticklabels": outline, "showspikes": False, "visible": True, "linecolor": color, @@ -502,7 +550,9 @@ class PlotlyTraceBuilder: }, "customdata": list(node_ids), "meta": {"kind": "node"}, - "hoverinfo": "skip", + # 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, } @@ -572,7 +622,7 @@ class PlotlyTraceBuilder: }, "customdata": customdata, "meta": {"kind": "element"}, - "hoverinfo": "skip", + "hovertemplate": "Element #%{customdata}", "name": "elements", "showlegend": False, } diff --git a/src/otko/views/dialogs/plot_properties.py b/src/otko/views/dialogs/plot_properties.py index 5240546..ae249ef 100644 --- a/src/otko/views/dialogs/plot_properties.py +++ b/src/otko/views/dialogs/plot_properties.py @@ -11,6 +11,7 @@ from __future__ import annotations from PySide6.QtCore import Signal from PySide6.QtGui import QColor from PySide6.QtWidgets import ( + QCheckBox, QColorDialog, QDialog, QDialogButtonBox, @@ -75,6 +76,7 @@ class PlotPropertiesDialog(QDialog): self._base = style self._colors: dict[str, _ColorButton] = {} self._numbers: dict[str, QDoubleSpinBox | QSpinBox] = {} + self._checks: dict[str, QCheckBox] = {} layout = QVBoxLayout(self) form = QFormLayout() @@ -102,6 +104,7 @@ class PlotPropertiesDialog(QDialog): """ 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 ─────────────────────────────────────────────────── @@ -120,6 +123,12 @@ class PlotPropertiesDialog(QDialog): 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] @@ -136,4 +145,6 @@ class PlotPropertiesDialog(QDialog): 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/tests/unit/test_plotly_trace_builder.py b/tests/unit/test_plotly_trace_builder.py index ae960a9..45b2b21 100644 --- a/tests/unit/test_plotly_trace_builder.py +++ b/tests/unit/test_plotly_trace_builder.py @@ -206,3 +206,57 @@ def test_frame_trace_meta_marks_elements_pickable() -> None: # 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_render_style.py b/tests/unit/test_render_style.py index 32b027b..c179eb7 100644 --- a/tests/unit/test_render_style.py +++ b/tests/unit/test_render_style.py @@ -49,7 +49,7 @@ def test_editable_subset_matches_the_field_table() -> None: 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"} + assert kinds[name] in {"color", "float", "int", "bool"} def test_response_colorscale_is_an_evenly_spaced_mapping() -> None: From 8f7bc3cf4fb786e40eabc8d404d3ff0f29584c46 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 20:05:35 -0400 Subject: [PATCH 24/28] fix: keep the Plotly camera and axis ranges through data updates Plotly.react resets any scene attribute the incoming layout omits, so every selection change snapped the camera back to the default and re-fit the ranges - the pinned framing was meaningless. Data-only pushes now pass preserveView and html.py merges the live camera and axis ranges into the incoming layout; only an explicit re-frame sends computed framing. Also fixes the ordering bug where the framing flag was consumed before the not-ready early return, which dropped the very first framing on the floor (show_project runs before loadFinished). Verified in the browser: user orbit + zoom survive selection and working plane updates, while reset_camera re-frames. Regression-tested headlessly by asserting the emitted preserveView flag. --- AGENTS.md | Bin 6216 -> 6694 bytes src/otko/views/canvas_plotly/html.py | 20 +++- src/otko/views/canvas_plotly/plotly_canvas.py | 64 ++++++++---- tests/gui/test_plotly_view_preservation.py | 97 ++++++++++++++++++ 4 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 tests/gui/test_plotly_view_preservation.py diff --git a/AGENTS.md b/AGENTS.md index bbe6d81ae9f93827dedba19041d40702ee26143b..9bf672ed364662b78c3069ba6e5aa0ff386b4a4e 100644 GIT binary patch delta 489 ZcmX?Mu*_tGheSQYz=Dx(e8JPhXeo(DFWjF diff --git a/src/otko/views/canvas_plotly/html.py b/src/otko/views/canvas_plotly/html.py index 278abba..e7bb27f 100644 --- a/src/otko/views/canvas_plotly/html.py +++ b/src/otko/views/canvas_plotly/html.py @@ -122,8 +122,26 @@ _PAGE = """ gd.on('plotly_unhover', function () { clearHover(); }); } - window.otkoUpdate = function (payloadJson) { + window.otkoUpdate = function (payloadJson, preserveView) { var fig = JSON.parse(payloadJson); + // `Plotly.react` resets any scene attribute the incoming layout omits, + // which would snap the camera and the padded ranges back on every data + // update. When this is not an explicit re-frame, carry the user's + // current view forward into the incoming layout. + if (preserveView) { + var gd = document.getElementById('plot'); + var scene = gd && gd._fullLayout ? gd._fullLayout.scene : null; + if (scene) { + fig.layout.scene = fig.layout.scene || {}; + fig.layout.scene.camera = scene.camera; + ['xaxis', 'yaxis', 'zaxis'].forEach(function (axis) { + if (!scene[axis]) return; + fig.layout.scene[axis] = fig.layout.scene[axis] || {}; + fig.layout.scene[axis].range = scene[axis].range; + fig.layout.scene[axis].autorange = false; + }); + } + } Plotly.react('plot', fig.data, fig.layout, config).then(function () { findHover(); installHandlers(); diff --git a/src/otko/views/canvas_plotly/plotly_canvas.py b/src/otko/views/canvas_plotly/plotly_canvas.py index 1e771d5..d5c0ff0 100644 --- a/src/otko/views/canvas_plotly/plotly_canvas.py +++ b/src/otko/views/canvas_plotly/plotly_canvas.py @@ -5,10 +5,13 @@ 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: ``Plotly.react`` diffs client-side, and the layout only -carries ``scene.camera`` when a view preset or the projection toggle asks for -it — so re-rendering on a model edit or selection change never yanks the -camera the user is orbiting. +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 @@ -138,7 +141,7 @@ class PlotlyCanvas(QWidget): self._default_selection_enabled = True self._working_plane: tuple[str, float] | None = None self._camera = _CameraShim(self) - self._camera_dirty = True + self._framing_dirty = True self._renderer = _PlotlyRendererFacade(self) self._ready = False @@ -173,7 +176,7 @@ class PlotlyCanvas(QWidget): self.set_project(project) if project is not None and project.nodes: self._view_preset = "iso" - self._camera_dirty = True + self._framing_dirty = True self.render() def clear_model(self) -> None: @@ -206,34 +209,34 @@ class PlotlyCanvas(QWidget): def set_parallel_projection(self, on: bool) -> None: self._parallel = bool(on) - self._camera_dirty = True + self._framing_dirty = True def reset_camera(self) -> None: self._view_preset = "iso" - self._camera_dirty = True + self._framing_dirty = True self.render() def view_isometric(self) -> None: self._view_preset = "iso" - self._camera_dirty = True + 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._camera_dirty = True + self._framing_dirty = True self.render() def view_xz(self) -> None: """Front view: the eye sits on -Y.""" self._view_preset = "xz" - self._camera_dirty = True + self._framing_dirty = True self.render() def view_yz(self) -> None: """Right view: the eye sits on +X.""" self._view_preset = "yz" - self._camera_dirty = True + self._framing_dirty = True self.render() # ── working plane ──────────────────────────────────────────────── @@ -272,9 +275,14 @@ class PlotlyCanvas(QWidget): self.render() def set_style(self, style: RenderStyle) -> None: - """Swap the visual style and repaint the figure.""" + """Swap the visual style and repaint the figure. + + Treated as a re-frame because the background and axis outline are + layout-level (``Plotly.restyle`` cannot carry them). + """ self._style = style self._builder.set_style(style) + self._framing_dirty = True self.render() def set_display_options(self, *, show_node_labels: bool, show_element_labels: bool) -> None: @@ -291,18 +299,32 @@ class PlotlyCanvas(QWidget): # ── 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``, style change) sends the computed framing. + """ self._scene = self._builder.build(self._project, self._options) - layout = dict(self._scene.layout) - if self._camera_dirty: - layout["scene"] = { - **layout["scene"], - "camera": self._camera_dict(self._scene), - } - self._camera_dirty = False 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._web.page().runJavaScript(f"window.otkoUpdate({json.dumps(payload)})") + 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 diff --git a/tests/gui/test_plotly_view_preservation.py b/tests/gui/test_plotly_view_preservation.py new file mode 100644 index 0000000..c748163 --- /dev/null +++ b/tests/gui/test_plotly_view_preservation.py @@ -0,0 +1,97 @@ +"""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_re_frames_for_layout_only_attrs(qtbot) -> None: # type: ignore[no-untyped-def] + 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[-1].endswith(", false)"), "background/outline live in the layout" From 0611153555e80ee3ae913d35c00a1d4ce3f35a6b Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 20:14:05 -0400 Subject: [PATCH 25/28] fix: stop the Plotly canvas wedging on rapid or overlapping updates Three defects in the view-preservation path could leave the plot frozen (no updates, no orbit, stale colours): 1. The merge injected raw _fullLayout objects - including undefined when a push landed before the previous react resolved - and plotly validates layouts, so one bad value made every later react fail permanently. currentView() now deep-copies and validates eye/center/up and each range, and mergeView() is wrapped so it can never block an update. 2. Updates were not serialized: overlapping Plotly.react calls on one graph div left it unresponsive. otkoUpdate now queues and coalesces (one react at a time, latest payload wins), and logs instead of failing silently. 3. A style change re-framed the camera, so re-colouring yanked a view the user had orbited. Colour/opacity ride the traces and background/outline ride the layout, so set_style is now a non-framing push. Verified in the browser: a user orbit survives colour changes, 30 rapid preview updates land on the final value and stay responsive, and deleting the live camera no longer wedges the plot. --- src/otko/views/canvas_plotly/html.py | 110 +++++++++++++----- src/otko/views/canvas_plotly/plotly_canvas.py | 10 +- tests/gui/test_plotly_view_preservation.py | 8 +- 3 files changed, 96 insertions(+), 32 deletions(-) diff --git a/src/otko/views/canvas_plotly/html.py b/src/otko/views/canvas_plotly/html.py index e7bb27f..2afa289 100644 --- a/src/otko/views/canvas_plotly/html.py +++ b/src/otko/views/canvas_plotly/html.py @@ -89,6 +89,90 @@ _PAGE = """ Plotly.restyle('plot', { x: [[]], y: [[]], z: [[]] }, [hoverIndex]); } + // --- view preservation ------------------------------------------------- + // `Plotly.react` resets any scene attribute the incoming layout omits, so a + // non-framing update carries the live camera/ranges forward. Only plain, + // validated copies are merged: the raw `_fullLayout` objects are owned by + // plotly (react mutates them in place), and injecting an undefined value + // would make every later react fail, wedging the plot. + function toPlain(value) { + try { return JSON.parse(JSON.stringify(value)); } catch (err) { return null; } + } + + function currentView() { + var gd = document.getElementById('plot'); + var scene = gd && gd._fullLayout ? gd._fullLayout.scene : null; + if (!scene) return null; + var view = { ranges: {} }; + var camera = toPlain(scene.camera); + if (camera && camera.eye && camera.center) { + view.camera = { eye: camera.eye, center: camera.center, up: camera.up }; + if (camera.projection) view.camera.projection = camera.projection; + } + ['xaxis', 'yaxis', 'zaxis'].forEach(function (axis) { + var source = scene[axis]; + if (!source || !Array.isArray(source.range) || source.range.length !== 2) return; + var low = Number(source.range[0]); + var high = Number(source.range[1]); + if (isFinite(low) && isFinite(high) && high > low) view.ranges[axis] = [low, high]; + }); + return view; + } + + function mergeView(fig, view) { + try { + if (!view) return; + fig.layout.scene = fig.layout.scene || {}; + if (view.camera) fig.layout.scene.camera = view.camera; + Object.keys(view.ranges).forEach(function (axis) { + fig.layout.scene[axis] = fig.layout.scene[axis] || {}; + fig.layout.scene[axis].range = view.ranges[axis]; + fig.layout.scene[axis].autorange = false; + }); + } catch (err) { + // Never let view preservation block the figure update. + if (window.console) console.warn('otko: view merge skipped', err); + } + } + + // --- update queue ------------------------------------------------------ + // One react at a time, latest payload wins. Overlapping reacts on the same + // graph div are what left the plot unresponsive before. + var pendingUpdate = null; + var reactBusy = false; + + function runUpdate() { + if (reactBusy || !pendingUpdate) return; + var job = pendingUpdate; + pendingUpdate = null; + reactBusy = true; + + var fig; + try { + fig = JSON.parse(job.payload); + } catch (err) { + reactBusy = false; + if (window.console) console.error('otko: bad payload', err); + return; + } + mergeView(fig, job.preserve ? currentView() : null); + + Plotly.react('plot', fig.data, fig.layout, config).then(function () { + findHover(); + installHandlers(); + }, function (err) { + if (window.console) console.error('otko: react failed', err); + }).then(function () { + reactBusy = false; + runUpdate(); + }); + } + + window.otkoUpdate = function (payloadJson, preserveView) { + pendingUpdate = { payload: payloadJson, preserve: !!preserveView }; + runUpdate(); + }; + function installHandlers() { if (handlersReady) return; var gd = document.getElementById('plot'); @@ -122,32 +206,6 @@ _PAGE = """ gd.on('plotly_unhover', function () { clearHover(); }); } - window.otkoUpdate = function (payloadJson, preserveView) { - var fig = JSON.parse(payloadJson); - // `Plotly.react` resets any scene attribute the incoming layout omits, - // which would snap the camera and the padded ranges back on every data - // update. When this is not an explicit re-frame, carry the user's - // current view forward into the incoming layout. - if (preserveView) { - var gd = document.getElementById('plot'); - var scene = gd && gd._fullLayout ? gd._fullLayout.scene : null; - if (scene) { - fig.layout.scene = fig.layout.scene || {}; - fig.layout.scene.camera = scene.camera; - ['xaxis', 'yaxis', 'zaxis'].forEach(function (axis) { - if (!scene[axis]) return; - fig.layout.scene[axis] = fig.layout.scene[axis] || {}; - fig.layout.scene[axis].range = scene[axis].range; - fig.layout.scene[axis].autorange = false; - }); - } - } - Plotly.react('plot', fig.data, fig.layout, config).then(function () { - findHover(); - installHandlers(); - }); - }; - window.otkoSetCamera = function (cameraJson) { Plotly.relayout('plot', { 'scene.camera': JSON.parse(cameraJson) }); }; diff --git a/src/otko/views/canvas_plotly/plotly_canvas.py b/src/otko/views/canvas_plotly/plotly_canvas.py index d5c0ff0..754a779 100644 --- a/src/otko/views/canvas_plotly/plotly_canvas.py +++ b/src/otko/views/canvas_plotly/plotly_canvas.py @@ -277,12 +277,13 @@ class PlotlyCanvas(QWidget): def set_style(self, style: RenderStyle) -> None: """Swap the visual style and repaint the figure. - Treated as a re-frame because the background and axis outline are - layout-level (``Plotly.restyle`` cannot carry them). + 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._framing_dirty = True self.render() def set_display_options(self, *, show_node_labels: bool, show_element_labels: bool) -> None: @@ -306,7 +307,8 @@ class PlotlyCanvas(QWidget): 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``, style change) sends the computed framing. + ``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: diff --git a/tests/gui/test_plotly_view_preservation.py b/tests/gui/test_plotly_view_preservation.py index c748163..3250779 100644 --- a/tests/gui/test_plotly_view_preservation.py +++ b/tests/gui/test_plotly_view_preservation.py @@ -85,7 +85,8 @@ def test_view_presets_and_reset_re_frame(qtbot) -> None: # type: ignore[no-unty @pytest.mark.gui -def test_style_change_re_frames_for_layout_only_attrs(qtbot) -> None: # type: ignore[no-untyped-def] +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) @@ -94,4 +95,7 @@ def test_style_change_re_frames_for_layout_only_attrs(qtbot) -> None: # type: i canvas.set_style(RenderStyle(show_axis_outline=True)) - assert calls[-1].endswith(", false)"), "background/outline live in the layout" + 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 From 7cf323df23b2e1a43a0559927a0c5f9d4b8f8e27 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 20:18:23 -0400 Subject: [PATCH 26/28] feat: surface the Plotly page console in the Console dock A plotly failure inside the WebEngine page was invisible from Python - the react promise just rejected and the canvas looked frozen with no evidence. PlotlyCanvas now installs a QWebEnginePage that forwards javaScriptConsoleMessage into the Console dock with the right severity (error/warning/info), so the next silent failure is diagnosable. --- src/otko/views/canvas_plotly/plotly_canvas.py | 36 ++++++++++++++++++- src/otko/views/main_window.py | 17 ++++++++- tests/gui/test_plotly_view_preservation.py | 22 ++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/otko/views/canvas_plotly/plotly_canvas.py b/src/otko/views/canvas_plotly/plotly_canvas.py index 754a779..ddecb15 100644 --- a/src/otko/views/canvas_plotly/plotly_canvas.py +++ b/src/otko/views/canvas_plotly/plotly_canvas.py @@ -18,11 +18,13 @@ 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 QUrl, Signal +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 @@ -47,6 +49,34 @@ _VIEW_DIRECTIONS = { } +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.""" @@ -126,8 +156,10 @@ class PlotlyCanvas(QWidget): 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) @@ -153,6 +185,8 @@ class PlotlyCanvas(QWidget): 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) diff --git a/src/otko/views/main_window.py b/src/otko/views/main_window.py index cca1d7e..5241869 100644 --- a/src/otko/views/main_window.py +++ b/src/otko/views/main_window.py @@ -119,7 +119,12 @@ class MainWindow( # 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) + 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 @@ -149,6 +154,16 @@ class MainWindow( """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.""" diff --git a/tests/gui/test_plotly_view_preservation.py b/tests/gui/test_plotly_view_preservation.py index 3250779..97ef3ee 100644 --- a/tests/gui/test_plotly_view_preservation.py +++ b/tests/gui/test_plotly_view_preservation.py @@ -99,3 +99,25 @@ def test_style_change_keeps_the_view_but_updates_the_layout(qtbot) -> None: # t 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"), + ] From 5262d31e336cb26960c4890e281e03793ba210c7 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 20:33:30 -0400 Subject: [PATCH 27/28] fix: stop the Plotly hover handler redrawing on every mouse move plotly_hover/plotly_unhover fire continuously while the mouse moves, and the unhover handler restyled the snap marker unconditionally - even with snapping off and nothing visible. That meant a full Plotly.restyle plus redraw per mouse event, which re-fired hover until the page died with 'RangeError: Maximum call stack size exceeded' and the canvas froze (reported live after opening a model). The marker is now updated only when its visible state actually changes: no marker when snapping is off, and a restyle only when the snapped point differs from the one already shown. Deliberately synchronous - timers are throttled to about a second by WebEngine when the page is not compositing, which stalled the preview. Measured: 200 mouse-move events went from 200 restyles to 0; 200 hovers on one grid dot cost a single restyle. --- src/otko/views/canvas_plotly/html.py | 43 ++++++++++--- tests/gui/test_plotly_hover.py | 93 ++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 tests/gui/test_plotly_hover.py diff --git a/src/otko/views/canvas_plotly/html.py b/src/otko/views/canvas_plotly/html.py index 2afa289..46e145e 100644 --- a/src/otko/views/canvas_plotly/html.py +++ b/src/otko/views/canvas_plotly/html.py @@ -77,16 +77,36 @@ _PAGE = """ var meta = gd.data[i] && gd.data[i].meta; if (meta && meta.kind === 'hover') { hoverIndex = i; break; } } + // A freshly pushed figure has an empty hover marker again. + hoverShown = false; + hoverPoint = null; } - function setHover(x, y, z) { - if (hoverIndex < 0) return; - Plotly.restyle('plot', { x: [[x]], y: [[y]], z: [[z]] }, [hoverIndex]); + // --- snap-target marker ------------------------------------------------ + // Updated synchronously but only when the visual state actually changes. + // Restyling on every hover/unhover made the plot redraw per mouse move, + // which re-fired hover and ended in a runaway redraw loop (RangeError: + // Maximum call stack size exceeded, frozen canvas). Timers were avoided + // deliberately: WebEngine throttles them to ~1 s when the page is not + // compositing, which made the preview lag. + var hoverShown = false; + var hoverPoint = null; + + function samePoint(a, b) { + return !!a && !!b && a[0] === b[0] && a[1] === b[1] && a[2] === b[2]; } - function clearHover() { + function setHoverMarker(point) { + var want = snapEnabled ? point : null; + if (samePoint(want, hoverPoint) && !!want === hoverShown) return; + if (!want && !hoverShown) { hoverPoint = null; return; } + hoverPoint = want; + hoverShown = !!want; if (hoverIndex < 0) return; - Plotly.restyle('plot', { x: [[]], y: [[]], z: [[]] }, [hoverIndex]); + var x = want ? [want[0]] : []; + var y = want ? [want[1]] : []; + var z = want ? [want[2]] : []; + Plotly.restyle('plot', { x: [x], y: [y], z: [z] }, [hoverIndex]); } // --- view preservation ------------------------------------------------- @@ -198,12 +218,17 @@ _PAGE = """ var pts = ev.points || []; if (!pts.length) return; var p = pts[0]; - if (kindOf(p) !== 'snap') { clearHover(); return; } + if (kindOf(p) !== 'snap') { setHoverMarker(null); return; } var c = p.customdata; - setHover(c[0], c[1], c[2]); + if (c) setHoverMarker([c[0], c[1], c[2]]); }); - gd.on('plotly_unhover', function () { clearHover(); }); + gd.on('plotly_unhover', function () { + // Nothing can be visible unless snapping is armed, so stay out of the + // redraw path entirely for ordinary mouse movement. + if (!snapEnabled) return; + setHoverMarker(null); + }); } window.otkoSetCamera = function (cameraJson) { @@ -212,7 +237,7 @@ _PAGE = """ window.otkoSetSnapEnabled = function (on) { snapEnabled = !!on; - if (!snapEnabled) clearHover(); + if (!snapEnabled) setHoverMarker(null); }; })(); diff --git a/tests/gui/test_plotly_hover.py b/tests/gui/test_plotly_hover.py new file mode 100644 index 0000000..78d5160 --- /dev/null +++ b/tests/gui/test_plotly_hover.py @@ -0,0 +1,93 @@ +"""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" From 429ebc0790688e1582f083d701adb3da427f2811 Mon Sep 17 00:00:00 2001 From: smillmorel Date: Wed, 16 Sep 2026 20:33:30 -0400 Subject: [PATCH 28/28] docs: repair AGENTS.md NUL corruption and record the canvas contracts The previous AGENTS.md edit wrote 478 NUL bytes instead of the intended sentence, so git and grep treated the file as binary. Restored the clean UTF-8 text and documented both Plotly canvas invariants: data-only pushes must pass preserveView (never re-send the camera), and the hover/snap marker must only be restyled when its visible state changes. --- AGENTS.md | Bin 6694 -> 6659 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9bf672ed364662b78c3069ba6e5aa0ff386b4a4e..babac38c49eec7dce3b0f5a314a7391224f1e087 100644 GIT binary patch delta 458 zcmZXQF-`+P3`NVd9O0KBg``P^M8yRV1#Q;ZF_}?jXGXSnmaS570yqRU5(U>oyJ0IT zTlV*}{XchayRVPc8O>wN7RLulm9#mz1iT2kHV@UCm8GGNHfRn mu@BoR+n4*<{Cy2P?Eicr3pq_@tpCd|>iNk@@%_H~x%vggajc5~ delta 494 ecmZoRS!S|f68mOf&TRtq3By8VeX4}Jh0V+M!-