From f361fee969fed57a1913f5ca9b39d01498198e4c Mon Sep 17 00:00:00 2001 From: smill Date: Fri, 11 Sep 2026 13:19:59 -0400 Subject: [PATCH] feat: named case-result load combinations with full GUI support Snapshots the current development tree, headlined by proper load combinations (user request): a reusable LoadCombination entity of weighted completed static-case results (e.g. 1.2xDead + 1.6xLive). - core: LoadCombination/LoadCombinationItem entities, Project integration (lookup, unique ids, reference validation) - services: combinations.py (linear superposition + envelope), exported via services __init__ - commands: undoable Add/Delete/Update for combinations - GUI: Load Combinations manager dialog, Run-dialog evaluation, envelope display in Results panel, Combinations tab in Table dock - tests: unit coverage (validation, math, error paths) + integration superposition check vs a single factored run --- .gitattributes | 2 + .github/ISSUE_TEMPLATE/bug_report.yml | 56 + .github/ISSUE_TEMPLATE/feature_request.yml | 31 + .github/pull_request_template.md | 26 + .github/workflows/ci.yml | 86 + .github/workflows/ci.yml.disabled | 48 + .gitignore | 63 + .pre-commit-config.yaml | 25 + AGENTS.md | 58 + CONTRIBUTING.md | 97 + LICENSE | 661 + NOTICE | 70 + README.md | 195 + docs/QUICK_GUIDE.md | 132 + docs/README.md | 26 + .../adr/ADR-0001-gidopensees-schema-import.md | 331 + docs/adr/ADR-0002-headless-gui-dep-split.md | 91 + docs/architecture.md | 80 + docs/gap-analysis-gidopensees.md | 221 + docs/logo.svg | 46 + docs/roadmap.md | 197 + docs/screenshots/main_window.png | Bin 0 -> 516835 bytes examples/.gitkeep | 0 examples/README.md | 227 + examples/_ex4_portal2d_common.py | 460 + examples/basic_truss.osmodel | 298 + examples/basic_truss.py | 163 + examples/beam_quad_2d.osmodel | 3483 ++ examples/beam_quad_2d.py | 200 + examples/cantilever.osmodel | 379 + examples/cantilever.py | 121 + examples/concrete04_cantilever.osmodel | 275 + examples/concrete04_cantilever.py | 215 + examples/data/A10000.txt | 1598 + examples/data/BM68elc.acc | 800 + .../data/EigenAnal_twoStoreyFrame1.tcl.txt | 144 + .../EigenAnal_twoStoreyShearFrame7.tcl.txt | 167 + examples/data/Ex1a.Canti2D.EQ.modif.tcl.txt | 95 + examples/data/Ex1a.Canti2D.EQ.tcl.txt | 86 + examples/data/Ex1a.Canti2D.Push.tcl.txt | 80 + examples/data/Ex1b.Portal2D.EQ.tcl.txt | 95 + examples/data/Ex1b.Portal2D.Push.tcl.txt | 94 + .../Ex2a.Canti2D.ElasticElement.EQ.tcl.txt | 231 + .../Ex2a.Canti2D.ElasticElement.Push.tcl.txt | 207 + .../Ex2b.Canti2D.InelasticSection.EQ.tcl.txt | 256 + ...Ex2b.Canti2D.InelasticSection.Push.tcl.txt | 233 + ...Canti2D.analyze.Dynamic.EQ.Uniform.tcl.txt | 147 + .../Ex3.Canti2D.analyze.Static.Push.tcl.txt | 119 + .../Ex3.Canti2D.build.ElasticElement.tcl.txt | 116 + ...anti2D.build.InelasticFiberSection.tcl.txt | 179 + ...Ex3.Canti2D.build.InelasticSection.tcl.txt | 139 + ...tal2D.analyze.Dynamic.sine.Uniform.tcl.txt | 78 + .../Ex4.Portal2D.analyze.Static.Push.tcl.txt | 69 + .../Ex4.Portal2D.build.ElasticElement.tcl.txt | 108 + ...rtal2D.build.InelasticFiberSection.tcl.txt | 178 + ...x4.Portal2D.build.InelasticSection.tcl.txt | 139 + .../eigen_two_storey_one_bay_frame.osmodel | 302 + examples/eigen_two_storey_one_bay_frame.py | 106 + examples/eigen_two_storey_shear_frame.osmodel | 330 + examples/eigen_two_storey_shear_frame.py | 117 + examples/elastic_frame.osmodel | 993 + examples/elastic_frame.py | 313 + examples/ex1a_canti2d.osmodel | 4268 +++ examples/ex1a_canti2d.py | 241 + examples/ex1a_canti2d_eq.osmodel | 8207 +++++ examples/ex1a_canti2d_eq.py | 212 + examples/ex1b_portal2d.osmodel | 4360 +++ examples/ex1b_portal2d.py | 228 + examples/ex2a_canti2d_elastic_element.osmodel | 4268 +++ examples/ex2a_canti2d_elastic_element.py | 251 + .../ex2b_canti2d_inelastic_section.osmodel | 4296 +++ examples/ex2b_canti2d_inelastic_section.py | 276 + ...2c_canti2d_inelastic_fiber_section.osmodel | 4324 +++ .../ex2c_canti2d_inelastic_fiber_section.py | 324 + examples/ex3_canti2d_elastic_element.osmodel | 4268 +++ examples/ex3_canti2d_elastic_element.py | 138 + ...x3_canti2d_inelastic_fiber_section.osmodel | 4324 +++ .../ex3_canti2d_inelastic_fiber_section.py | 169 + .../ex3_canti2d_inelastic_section.osmodel | 4296 +++ examples/ex3_canti2d_inelastic_section.py | 142 + examples/ex4_portal2d_elastic_element.osmodel | 1573 + examples/ex4_portal2d_elastic_element.py | 56 + ...4_portal2d_inelastic_fiber_section.osmodel | 1633 + .../ex4_portal2d_inelastic_fiber_section.py | 56 + .../ex4_portal2d_inelastic_section.osmodel | 1605 + examples/ex4_portal2d_inelastic_section.py | 56 + examples/moment_curvature.osmodel | 335 + examples/moment_curvature.py | 249 + examples/portal_frame.osmodel | 516 + examples/portal_frame.py | 138 + examples/portal_pushover.osmodel | 353 + examples/portal_pushover.py | 189 + examples/rc_frame_earthquake.osmodel | 850 + examples/rc_frame_earthquake.py | 161 + examples/rc_frame_gravity.osmodel | 422 + examples/rc_frame_gravity.py | 232 + examples/rc_frame_pushover.osmodel | 484 + examples/rc_frame_pushover.py | 139 + examples/sdof_pushover.osmodel | 202 + examples/sdof_pushover.py | 131 + examples/space_frame_3d.osmodel | 1136 + examples/space_frame_3d.py | 196 + .../recorders/elem_1.out | 400 + .../recorders/elem_10.out | 400 + .../recorders/elem_11.out | 400 + .../recorders/elem_12.out | 400 + .../recorders/elem_13.out | 400 + .../recorders/elem_14.out | 400 + .../recorders/elem_15.out | 400 + .../recorders/elem_16.out | 400 + .../recorders/elem_2.out | 400 + .../recorders/elem_3.out | 400 + .../recorders/elem_4.out | 400 + .../recorders/elem_5.out | 400 + .../recorders/elem_6.out | 400 + .../recorders/elem_7.out | 400 + .../recorders/elem_8.out | 400 + .../recorders/elem_9.out | 400 + .../recorders/node_1.out | 400 + .../recorders/node_10.out | 400 + .../recorders/node_10_accel.out | 400 + .../recorders/node_10_disp.out | 400 + .../recorders/node_10_vel.out | 400 + .../recorders/node_11.out | 400 + .../recorders/node_11_accel.out | 400 + .../recorders/node_11_disp.out | 400 + .../recorders/node_11_vel.out | 400 + .../recorders/node_12.out | 400 + .../recorders/node_12_accel.out | 400 + .../recorders/node_12_disp.out | 400 + .../recorders/node_12_vel.out | 400 + .../recorders/node_1_accel.out | 400 + .../recorders/node_1_disp.out | 400 + .../recorders/node_1_vel.out | 400 + .../recorders/node_2.out | 400 + .../recorders/node_2_accel.out | 400 + .../recorders/node_2_disp.out | 400 + .../recorders/node_2_vel.out | 400 + .../recorders/node_3.out | 400 + .../recorders/node_3_accel.out | 400 + .../recorders/node_3_disp.out | 400 + .../recorders/node_3_vel.out | 400 + .../recorders/node_4.out | 400 + .../recorders/node_4_accel.out | 400 + .../recorders/node_4_disp.out | 400 + .../recorders/node_4_vel.out | 400 + .../recorders/node_5.out | 400 + .../recorders/node_5_accel.out | 400 + .../recorders/node_5_disp.out | 400 + .../recorders/node_5_vel.out | 400 + .../recorders/node_6.out | 400 + .../recorders/node_6_accel.out | 400 + .../recorders/node_6_disp.out | 400 + .../recorders/node_6_vel.out | 400 + .../recorders/node_7.out | 400 + .../recorders/node_7_accel.out | 400 + .../recorders/node_7_disp.out | 400 + .../recorders/node_7_vel.out | 400 + .../recorders/node_8.out | 400 + .../recorders/node_8_accel.out | 400 + .../recorders/node_8_disp.out | 400 + .../recorders/node_8_vel.out | 400 + .../recorders/node_9.out | 400 + .../recorders/node_9_accel.out | 400 + .../recorders/node_9_disp.out | 400 + .../recorders/node_9_vel.out | 400 + pyproject.toml | 129 + snip1.png | Bin 0 -> 154626 bytes snip2.png | Bin 0 -> 214678 bytes snip3.png | Bin 0 -> 313192 bytes specifications/15-rebuild-adoption-plan.md | 383 + specifications/README.md | 37 + src/otko/__init__.py | 5 + src/otko/__main__.py | 16 + src/otko/_const.py | 13 + src/otko/app.py | 69 + src/otko/commands/__init__.py | 121 + src/otko/commands/analysis.py | 80 + src/otko/commands/base.py | 47 + src/otko/commands/combinations.py | 82 + src/otko/commands/constraints.py | 91 + src/otko/commands/elements.py | 281 + src/otko/commands/grid.py | 74 + src/otko/commands/loads.py | 384 + src/otko/commands/materials.py | 86 + src/otko/commands/nodes.py | 192 + src/otko/commands/project.py | 36 + src/otko/commands/sections.py | 82 + src/otko/commands/series_and_patterns.py | 163 + src/otko/commands/transforms.py | 214 + src/otko/core/__init__.py | 182 + src/otko/core/_base.py | 28 + src/otko/core/analysis/__init__.py | 274 + src/otko/core/catalog/README.md | 59 + src/otko/core/catalog/__init__.py | 202 + src/otko/core/catalog/curated/__init__.py | 0 src/otko/core/catalog/generated/__init__.py | 217 + .../generated/auto_equal_constraint.py | 537 + .../catalog/generated/auto_zero_length.py | 698 + src/otko/core/catalog/generated/bondsp01.py | 126 + .../concrete01_zero_tensile_strength.py | 237 + .../concrete02_linear_tension_softening.py | 237 + .../generated/concrete04_popovics_concrete.py | 236 + src/otko/core/catalog/generated/concrete06.py | 239 + src/otko/core/catalog/generated/concretecm.py | 264 + .../catalog/generated/conditions/__init__.py | 187 + .../line_beam_contact_master_end_nodes.py | 23 + .../line_body_constraint_slave_nodes.py | 112 + .../conditions/line_displacements.py | 28 + .../line_equal_constraint_slave_nodes.py | 33 + .../generated/conditions/line_forces.py | 28 + .../catalog/generated/conditions/line_mass.py | 31 + .../line_rayleigh_damping_elements.py | 27 + .../conditions/line_rayleigh_damping_nodes.py | 27 + .../generated/conditions/line_restraints.py | 322 + .../line_rigid_diaphragm_slave_nodes.py | 26 + .../conditions/line_rigid_link_slave_nodes.py | 28 + .../conditions/line_uniform_forces.py | 25 + .../point_beam_contact_slave_nodes.py | 30 + .../point_body_constraint_master_node.py | 25 + .../point_body_constraint_slave_nodes.py | 112 + .../conditions/point_displacements.py | 28 + .../point_equal_constraint_master_node.py | 25 + .../point_equal_constraint_slave_nodes.py | 33 + .../generated/conditions/point_forces.py | 28 + .../point_ground_motion_from_record.py | 26 + .../generated/conditions/point_mass.py | 31 + .../point_rayleigh_damping_nodes.py | 27 + .../generated/conditions/point_restraints.py | 322 + .../point_rigid_diaphragm_master_node.py | 23 + .../point_rigid_diaphragm_slave_nodes.py | 26 + .../point_rigid_link_master_node.py | 23 + .../point_rigid_link_slave_nodes.py | 28 + .../conditions/point_sine_ground_motion.py | 69 + .../generated/conditions/point_zerolength.py | 195 + .../conditions/surface_displacements.py | 28 + .../generated/conditions/surface_forces.py | 28 + .../generated/conditions/surface_mass.py | 31 + .../surface_rayleigh_damping_elements.py | 27 + .../surface_rayleigh_damping_nodes.py | 27 + .../conditions/surface_restraints.py | 322 + .../generated/conditions/volume_mass.py | 31 + .../volume_rayleigh_damping_elements.py | 27 + .../volume_rayleigh_damping_nodes.py | 27 + .../generated/conditions/zerolength.py | 195 + src/otko/core/catalog/generated/contact.py | 29 + .../catalog/generated/corotational_truss.py | 237 + src/otko/core/catalog/generated/damage2p.py | 38 + .../displacement_based_beam_column.py | 35 + src/otko/core/catalog/generated/elastic.py | 244 + .../catalog/generated/elastic_beam_column.py | 329 + .../catalog/generated/elastic_isotropic.py | 83 + .../generated/elastic_membrane_plate.py | 29 + .../catalog/generated/elastic_orthotropic.py | 35 + .../generated/elastic_perfectly_plastic.py | 407 + .../elastic_perfectly_plastic_with_gap.py | 377 + .../core/catalog/generated/elastic_section.py | 376 + .../elastic_timoshenko_beam_column.py | 358 + src/otko/core/catalog/generated/fiber.py | 2086 ++ .../core/catalog/generated/fiber_custom.py | 66 + src/otko/core/catalog/generated/fiberint.py | 48 + ...eraction_displacement_based_beam_column.py | 36 + .../generated/force_based_beam_column.py | 81 + .../catalog/generated/ground_motion_record.py | 117 + .../core/catalog/generated/hyperbolic_gap.py | 66 + src/otko/core/catalog/generated/hysteretic.py | 664 + .../core/catalog/generated/initial_strain.py | 27 + .../core/catalog/generated/initial_stress.py | 27 + .../core/catalog/generated/j2plasticity.py | 31 + .../core/catalog/generated/layeredshell.py | 56 + src/otko/core/catalog/generated/minmax.py | 193 + src/otko/core/catalog/generated/parallel.py | 167 + .../core/catalog/generated/plate_fiber.py | 30 + .../generated/pressuredependmultiyield.py | 111 + .../generated/pressuredependmultiyield02.py | 108 + .../generated/pressureindependmultiyield.py | 92 + src/otko/core/catalog/generated/pysimple1.py | 76 + src/otko/core/catalog/generated/quad.py | 41 + src/otko/core/catalog/generated/quadup.py | 77 + src/otko/core/catalog/generated/qzsimple1.py | 77 + .../catalog/generated/ramberg_osgood_steel.py | 68 + .../catalog/generated/reinforcing_steel.py | 70 + .../catalog/generated/section_aggregator.py | 225 + src/otko/core/catalog/generated/series.py | 167 + src/otko/core/catalog/generated/shell.py | 33 + src/otko/core/catalog/generated/shelldkgq.py | 31 + .../generated/standard_brick_element.py | 34 + src/otko/core/catalog/generated/steel01.py | 208 + src/otko/core/catalog/generated/steel02.py | 260 + src/otko/core/catalog/generated/tri31.py | 36 + src/otko/core/catalog/generated/truss.py | 237 + src/otko/core/catalog/generated/tzsimple1.py | 72 + .../core/catalog/generated/user_material.py | 27 + src/otko/core/catalog/generated/viscous.py | 65 + .../core/catalog/generated/viscous_damper.py | 94 + src/otko/core/constraints.py | 19 + src/otko/core/defaults.py | 166 + src/otko/core/geometry/__init__.py | 43 + src/otko/core/geometry/elements.py | 213 + src/otko/core/geometry/grid.py | 211 + src/otko/core/geometry/node.py | 44 + src/otko/core/loads/__init__.py | 255 + src/otko/core/materials/__init__.py | 246 + src/otko/core/project.py | 433 + src/otko/core/sections/__init__.py | 230 + src/otko/core/units.py | 91 + src/otko/resources/icons-dark/assign-load.svg | 8 + .../resources/icons-dark/assign-support.svg | 9 + src/otko/resources/icons-dark/cases.svg | 7 + .../resources/icons-dark/clear-display.svg | 7 + src/otko/resources/icons-dark/define-grid.svg | 11 + src/otko/resources/icons-dark/draw-frame.svg | 7 + src/otko/resources/icons-dark/draw-node.svg | 6 + .../resources/icons-dark/material-library.svg | 7 + src/otko/resources/icons-dark/quick-guide.svg | 7 + src/otko/resources/icons-dark/run.svg | 5 + .../resources/icons-dark/section-library.svg | 8 + .../resources/icons-dark/show-deformed.svg | 8 + .../resources/icons-dark/show-extruded.svg | 7 + .../icons-dark/show-force-diagram.svg | 9 + .../resources/icons-dark/show-local-axes.svg | 10 + .../resources/icons-dark/show-undeformed.svg | 7 + .../resources/icons-dark/toggle-parallel.svg | 6 + src/otko/resources/icons-dark/tool-select.svg | 6 + src/otko/resources/icons-dark/view-front.svg | 9 + src/otko/resources/icons-dark/view-iso.svg | 7 + src/otko/resources/icons-dark/view-right.svg | 9 + src/otko/resources/icons-dark/view-top.svg | 9 + .../resources/icons-dark/zoom-extents.svg | 9 + src/otko/resources/icons/.gitkeep | 0 src/otko/resources/icons/app.icns | Bin 0 -> 111829 bytes src/otko/resources/icons/app.ico | Bin 0 -> 29933 bytes src/otko/resources/icons/app.png | Bin 0 -> 47006 bytes src/otko/resources/icons/app.svg | 24 + src/otko/resources/icons/assign-load.svg | 8 + src/otko/resources/icons/assign-support.svg | 9 + src/otko/resources/icons/cases.svg | 7 + src/otko/resources/icons/clear-display.svg | 7 + src/otko/resources/icons/define-grid.svg | 11 + src/otko/resources/icons/draw-frame.svg | 7 + src/otko/resources/icons/draw-node.svg | 6 + src/otko/resources/icons/material-library.svg | 7 + src/otko/resources/icons/quick-guide.svg | 7 + src/otko/resources/icons/run.svg | 5 + src/otko/resources/icons/section-library.svg | 8 + src/otko/resources/icons/show-deformed.svg | 8 + src/otko/resources/icons/show-extruded.svg | 7 + .../resources/icons/show-force-diagram.svg | 9 + src/otko/resources/icons/show-local-axes.svg | 10 + src/otko/resources/icons/show-undeformed.svg | 7 + src/otko/resources/icons/toggle-parallel.svg | 6 + src/otko/resources/icons/tool-select.svg | 6 + src/otko/resources/icons/view-front.svg | 9 + src/otko/resources/icons/view-iso.svg | 7 + src/otko/resources/icons/view-right.svg | 9 + src/otko/resources/icons/view-top.svg | 9 + src/otko/resources/icons/zoom-extents.svg | 9 + src/otko/resources/themes/.gitkeep | 0 src/otko/resources/themes/app.qss | 40 + src/otko/services/__init__.py | 52 + src/otko/services/_emit.py | 680 + src/otko/services/_run.py | 727 + src/otko/services/animation_export.py | 96 + src/otko/services/combinations.py | 235 + src/otko/services/deformation.py | 173 + src/otko/services/element_forces.py | 191 + src/otko/services/export.py | 393 + src/otko/services/material_tester.py | 350 + src/otko/services/opensees_runner.py | 76 + src/otko/services/peer_record.py | 92 + src/otko/services/persistence.py | 60 + src/otko/services/qt_workers.py | 65 + src/otko/services/results.py | 165 + src/otko/services/section_bbox.py | 186 + src/otko/services/section_properties.py | 118 + src/otko/services/spectrum.py | 193 + src/otko/viewmodels/__init__.py | 6 + src/otko/viewmodels/analysis_runner.py | 79 + src/otko/viewmodels/project_vm.py | 118 + src/otko/views/__init__.py | 0 src/otko/views/action_handlers.py | 1103 + src/otko/views/canvas3d/__init__.py | 8 + src/otko/views/canvas3d/diagram_renderer.py | 289 + src/otko/views/canvas3d/model_canvas.py | 449 + src/otko/views/canvas3d/model_renderer.py | 1244 + src/otko/views/canvas3d/selection.py | 110 + src/otko/views/canvas3d/style.py | 51 + src/otko/views/dialogs/__init__.py | 78 + src/otko/views/dialogs/add_node.py | 116 + src/otko/views/dialogs/assign_equal_dof.py | 117 + src/otko/views/dialogs/assign_hinge.py | 235 + src/otko/views/dialogs/assign_load.py | 159 + src/otko/views/dialogs/assign_masses.py | 120 + src/otko/views/dialogs/assign_property.py | 169 + src/otko/views/dialogs/assign_support.py | 145 + src/otko/views/dialogs/assign_zls.py | 66 + src/otko/views/dialogs/case_forms.py | 637 + src/otko/views/dialogs/case_manager.py | 280 + src/otko/views/dialogs/combination_dialog.py | 258 + src/otko/views/dialogs/coord_grid_systems.py | 440 + src/otko/views/dialogs/define_grid_data.py | 476 + src/otko/views/dialogs/display_options.py | 45 + src/otko/views/dialogs/distributed_load.py | 167 + src/otko/views/dialogs/grid_system.py | 186 + src/otko/views/dialogs/linear_time_series.py | 56 + src/otko/views/dialogs/locate_origin.py | 106 + src/otko/views/dialogs/material_forms.py | 398 + src/otko/views/dialogs/material_library.py | 194 + src/otko/views/dialogs/material_tester.py | 292 + src/otko/views/dialogs/mirror.py | 62 + src/otko/views/dialogs/move.py | 53 + src/otko/views/dialogs/path_time_series.py | 187 + src/otko/views/dialogs/pattern_loads.py | 360 + src/otko/views/dialogs/plain_pattern.py | 64 + src/otko/views/dialogs/quick_grid_lines.py | 104 + src/otko/views/dialogs/quick_guide.py | 71 + src/otko/views/dialogs/replicate.py | 87 + src/otko/views/dialogs/run_analysis.py | 263 + src/otko/views/dialogs/section_editor.py | 412 + src/otko/views/dialogs/section_forms.py | 292 + src/otko/views/dialogs/section_library.py | 213 + src/otko/views/dialogs/uniform_excitation.py | 111 + src/otko/views/dock_manager.py | 694 + src/otko/views/docks/__init__.py | 27 + src/otko/views/docks/_labels.py | 63 + src/otko/views/docks/console.py | 108 + src/otko/views/docks/deformed_shape.py | 98 + src/otko/views/docks/force_diagram.py | 151 + src/otko/views/docks/hysteresis.py | 344 + src/otko/views/docks/mode_shape_animator.py | 208 + src/otko/views/docks/property_editor.py | 317 + src/otko/views/docks/pushover_curve.py | 196 + src/otko/views/docks/response_spectrum.py | 169 + src/otko/views/docks/results_panel.py | 203 + src/otko/views/docks/table_dock.py | 1385 + src/otko/views/docks/time_history.py | 216 + src/otko/views/formatting.py | 97 + src/otko/views/main_window.py | 246 + src/otko/views/menu_builder.py | 689 + src/otko/views/render_controls.py | 500 + src/otko/views/ribbon/__init__.py | 0 src/otko/views/tools/__init__.py | 15 + src/otko/views/tools/_node_reuse.py | 150 + src/otko/views/tools/base.py | 133 + src/otko/views/tools/draw_frame.py | 91 + src/otko/views/tools/draw_node.py | 164 + src/otko/views/tools/draw_truss.py | 87 + tests/__init__.py | 0 tests/conftest.py | 30 + tests/gui/__init__.py | 0 tests/gui/test_analysis_commands.py | 151 + tests/gui/test_analysis_runner.py | 115 + tests/gui/test_assign_load_pattern_name.py | 147 + tests/gui/test_assign_masses.py | 54 + tests/gui/test_assign_zls.py | 62 + tests/gui/test_canvas_grid_snap.py | 259 + tests/gui/test_case_forms.py | 84 + tests/gui/test_commands.py | 181 + .../test_constraint_and_display_dialogs.py | 40 + tests/gui/test_convert_element_type.py | 185 + tests/gui/test_define_dialogs.py | 45 + tests/gui/test_draw_frame_tool.py | 228 + tests/gui/test_draw_node_tool.py | 149 + tests/gui/test_draw_truss_tool.py | 121 + tests/gui/test_grid_dialog.py | 188 + tests/gui/test_main_window.py | 41 + tests/gui/test_material_dialog_concrete04.py | 121 + tests/gui/test_material_form_ranges.py | 155 + tests/gui/test_material_tester_dialog.py | 99 + tests/gui/test_path_ts_dialogs.py | 161 + tests/gui/test_pattern_load_audit.py | 212 + tests/gui/test_property_commands.py | 211 + tests/gui/test_property_forms.py | 130 + tests/gui/test_pushover_curve_units.py | 107 + tests/gui/test_run_analysis.py | 76 + tests/gui/test_section_extrusion.py | 86 + tests/gui/test_section_forms.py | 100 + tests/gui/test_selection.py | 70 + tests/gui/test_transforms.py | 152 + tests/gui/test_tree_multi_select.py | 61 + tests/gui/test_units_status_bar.py | 57 + tests/gui/test_update_element_fields.py | 99 + tests/gui/test_viewport_axes.py | 107 + tests/gui/test_working_plane.py | 207 + tests/integration/__init__.py | 0 tests/integration/test_basic_truss.py | 54 + tests/integration/test_beam_quad_2d.py | 113 + tests/integration/test_combinations.py | 90 + tests/integration/test_concrete04_runner.py | 172 + tests/integration/test_dof_coverage.py | 76 + .../test_eigen_two_storey_one_bay_frame.py | 56 + .../test_eigen_two_storey_shear_frame.py | 59 + tests/integration/test_elastic_frame.py | 124 + tests/integration/test_ex1a_canti2d.py | 69 + tests/integration/test_ex1a_canti2d_eq.py | 53 + tests/integration/test_ex1b_portal2d.py | 69 + .../test_ex2a_canti2d_elastic_element.py | 74 + .../test_ex2b_canti2d_inelastic_section.py | 75 + ...st_ex2c_canti2d_inelastic_fiber_section.py | 74 + .../integration/test_ex3_canti2d_variants.py | 91 + .../integration/test_ex4_portal2d_variants.py | 99 + tests/integration/test_material_tester.py | 332 + tests/integration/test_moment_curvature.py | 338 + .../test_moment_curvature_example.py | 56 + tests/integration/test_pattern_factors.py | 65 + tests/integration/test_rc_frame_earthquake.py | 56 + tests/integration/test_rc_frame_gravity.py | 63 + tests/integration/test_rc_frame_pushover.py | 72 + .../integration/test_runner_imposed_motion.py | 138 + tests/integration/test_runner_modal.py | 90 + tests/integration/test_runner_static.py | 70 + tests/integration/test_runner_transient.py | 144 + tests/services/__init__.py | 1 + tests/services/test_element_forces.py | 197 + tests/services/test_export.py | 240 + tests/services/test_imposed_support_motion.py | 193 + tests/services/test_peer_record.py | 69 + tests/services/test_persistence.py | 107 + tests/services/test_runner_translation.py | 300 + tests/services/test_section_bbox.py | 128 + tests/services/test_transient_and_export.py | 108 + tests/tools/__init__.py | 0 tests/tools/fixtures/minimal.mat | 35 + tests/tools/fixtures/minimal_expected.json | 178 + tests/tools/test_parse_schemas.py | 390 + tests/unit/__init__.py | 0 tests/unit/catalog/__init__.py | 0 tests/unit/catalog/test_generated.py | 153 + tests/unit/core/__init__.py | 0 tests/unit/core/materials/__init__.py | 0 tests/unit/core/materials/test_concrete04.py | 147 + tests/unit/test_architecture.py | 197 + tests/unit/test_ci_config.py | 39 + tests/unit/test_combinations.py | 180 + tests/unit/test_diagram_renderer.py | 135 + tests/unit/test_docs.py | 51 + tests/unit/test_elements.py | 63 + tests/unit/test_formatting.py | 51 + tests/unit/test_grid_system.py | 275 + tests/unit/test_materials.py | 126 + tests/unit/test_meta_gates.py | 29 + tests/unit/test_node.py | 53 + tests/unit/test_packaging.py | 72 + tests/unit/test_pattern_factors.py | 236 + tests/unit/test_phase8a.py | 312 + tests/unit/test_phase8b.py | 231 + tests/unit/test_phase8c.py | 203 + tests/unit/test_phase9.py | 215 + tests/unit/test_project.py | 289 + tests/unit/test_renderer.py | 188 + tests/unit/test_section_shape.py | 98 + tests/unit/test_smoke.py | 17 + tests/unit/test_unit_labels.py | 49 + tests/unit/test_zerolength_section.py | 64 + tools/__init__.py | 0 tools/gidopensees_import/__init__.py | 0 tools/gidopensees_import/codegen.py | 481 + tools/gidopensees_import/parse_schemas.py | 471 + tools/gidopensees_import/schema_model.py | 74 + tools/gidopensees_import/schemas.json | 28064 ++++++++++++++++ 560 files changed, 178701 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/ci.yml.disabled create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 AGENTS.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 docs/QUICK_GUIDE.md create mode 100644 docs/README.md create mode 100644 docs/adr/ADR-0001-gidopensees-schema-import.md create mode 100644 docs/adr/ADR-0002-headless-gui-dep-split.md create mode 100644 docs/architecture.md create mode 100644 docs/gap-analysis-gidopensees.md create mode 100644 docs/logo.svg create mode 100644 docs/roadmap.md create mode 100644 docs/screenshots/main_window.png create mode 100644 examples/.gitkeep create mode 100644 examples/README.md create mode 100644 examples/_ex4_portal2d_common.py create mode 100644 examples/basic_truss.osmodel create mode 100644 examples/basic_truss.py create mode 100644 examples/beam_quad_2d.osmodel create mode 100644 examples/beam_quad_2d.py create mode 100644 examples/cantilever.osmodel create mode 100644 examples/cantilever.py create mode 100644 examples/concrete04_cantilever.osmodel create mode 100644 examples/concrete04_cantilever.py create mode 100644 examples/data/A10000.txt create mode 100644 examples/data/BM68elc.acc create mode 100644 examples/data/EigenAnal_twoStoreyFrame1.tcl.txt create mode 100644 examples/data/EigenAnal_twoStoreyShearFrame7.tcl.txt create mode 100644 examples/data/Ex1a.Canti2D.EQ.modif.tcl.txt create mode 100644 examples/data/Ex1a.Canti2D.EQ.tcl.txt create mode 100644 examples/data/Ex1a.Canti2D.Push.tcl.txt create mode 100644 examples/data/Ex1b.Portal2D.EQ.tcl.txt create mode 100644 examples/data/Ex1b.Portal2D.Push.tcl.txt create mode 100644 examples/data/Ex2a.Canti2D.ElasticElement.EQ.tcl.txt create mode 100644 examples/data/Ex2a.Canti2D.ElasticElement.Push.tcl.txt create mode 100644 examples/data/Ex2b.Canti2D.InelasticSection.EQ.tcl.txt create mode 100644 examples/data/Ex2b.Canti2D.InelasticSection.Push.tcl.txt create mode 100644 examples/data/Ex3.Canti2D.analyze.Dynamic.EQ.Uniform.tcl.txt create mode 100644 examples/data/Ex3.Canti2D.analyze.Static.Push.tcl.txt create mode 100644 examples/data/Ex3.Canti2D.build.ElasticElement.tcl.txt create mode 100644 examples/data/Ex3.Canti2D.build.InelasticFiberSection.tcl.txt create mode 100644 examples/data/Ex3.Canti2D.build.InelasticSection.tcl.txt create mode 100644 examples/data/Ex4.Portal2D.analyze.Dynamic.sine.Uniform.tcl.txt create mode 100644 examples/data/Ex4.Portal2D.analyze.Static.Push.tcl.txt create mode 100644 examples/data/Ex4.Portal2D.build.ElasticElement.tcl.txt create mode 100644 examples/data/Ex4.Portal2D.build.InelasticFiberSection.tcl.txt create mode 100644 examples/data/Ex4.Portal2D.build.InelasticSection.tcl.txt create mode 100644 examples/eigen_two_storey_one_bay_frame.osmodel create mode 100644 examples/eigen_two_storey_one_bay_frame.py create mode 100644 examples/eigen_two_storey_shear_frame.osmodel create mode 100644 examples/eigen_two_storey_shear_frame.py create mode 100644 examples/elastic_frame.osmodel create mode 100644 examples/elastic_frame.py create mode 100644 examples/ex1a_canti2d.osmodel create mode 100644 examples/ex1a_canti2d.py create mode 100644 examples/ex1a_canti2d_eq.osmodel create mode 100644 examples/ex1a_canti2d_eq.py create mode 100644 examples/ex1b_portal2d.osmodel create mode 100644 examples/ex1b_portal2d.py create mode 100644 examples/ex2a_canti2d_elastic_element.osmodel create mode 100644 examples/ex2a_canti2d_elastic_element.py create mode 100644 examples/ex2b_canti2d_inelastic_section.osmodel create mode 100644 examples/ex2b_canti2d_inelastic_section.py create mode 100644 examples/ex2c_canti2d_inelastic_fiber_section.osmodel create mode 100644 examples/ex2c_canti2d_inelastic_fiber_section.py create mode 100644 examples/ex3_canti2d_elastic_element.osmodel create mode 100644 examples/ex3_canti2d_elastic_element.py create mode 100644 examples/ex3_canti2d_inelastic_fiber_section.osmodel create mode 100644 examples/ex3_canti2d_inelastic_fiber_section.py create mode 100644 examples/ex3_canti2d_inelastic_section.osmodel create mode 100644 examples/ex3_canti2d_inelastic_section.py create mode 100644 examples/ex4_portal2d_elastic_element.osmodel create mode 100644 examples/ex4_portal2d_elastic_element.py create mode 100644 examples/ex4_portal2d_inelastic_fiber_section.osmodel create mode 100644 examples/ex4_portal2d_inelastic_fiber_section.py create mode 100644 examples/ex4_portal2d_inelastic_section.osmodel create mode 100644 examples/ex4_portal2d_inelastic_section.py create mode 100644 examples/moment_curvature.osmodel create mode 100644 examples/moment_curvature.py create mode 100644 examples/portal_frame.osmodel create mode 100644 examples/portal_frame.py create mode 100644 examples/portal_pushover.osmodel create mode 100644 examples/portal_pushover.py create mode 100644 examples/rc_frame_earthquake.osmodel create mode 100644 examples/rc_frame_earthquake.py create mode 100644 examples/rc_frame_gravity.osmodel create mode 100644 examples/rc_frame_gravity.py create mode 100644 examples/rc_frame_pushover.osmodel create mode 100644 examples/rc_frame_pushover.py create mode 100644 examples/sdof_pushover.osmodel create mode 100644 examples/sdof_pushover.py create mode 100644 examples/space_frame_3d.osmodel create mode 100644 examples/space_frame_3d.py create mode 100644 examples/space_frame_3d_results/recorders/elem_1.out create mode 100644 examples/space_frame_3d_results/recorders/elem_10.out create mode 100644 examples/space_frame_3d_results/recorders/elem_11.out create mode 100644 examples/space_frame_3d_results/recorders/elem_12.out create mode 100644 examples/space_frame_3d_results/recorders/elem_13.out create mode 100644 examples/space_frame_3d_results/recorders/elem_14.out create mode 100644 examples/space_frame_3d_results/recorders/elem_15.out create mode 100644 examples/space_frame_3d_results/recorders/elem_16.out create mode 100644 examples/space_frame_3d_results/recorders/elem_2.out create mode 100644 examples/space_frame_3d_results/recorders/elem_3.out create mode 100644 examples/space_frame_3d_results/recorders/elem_4.out create mode 100644 examples/space_frame_3d_results/recorders/elem_5.out create mode 100644 examples/space_frame_3d_results/recorders/elem_6.out create mode 100644 examples/space_frame_3d_results/recorders/elem_7.out create mode 100644 examples/space_frame_3d_results/recorders/elem_8.out create mode 100644 examples/space_frame_3d_results/recorders/elem_9.out create mode 100644 examples/space_frame_3d_results/recorders/node_1.out create mode 100644 examples/space_frame_3d_results/recorders/node_10.out create mode 100644 examples/space_frame_3d_results/recorders/node_10_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_10_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_10_vel.out create mode 100644 examples/space_frame_3d_results/recorders/node_11.out create mode 100644 examples/space_frame_3d_results/recorders/node_11_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_11_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_11_vel.out create mode 100644 examples/space_frame_3d_results/recorders/node_12.out create mode 100644 examples/space_frame_3d_results/recorders/node_12_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_12_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_12_vel.out create mode 100644 examples/space_frame_3d_results/recorders/node_1_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_1_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_1_vel.out create mode 100644 examples/space_frame_3d_results/recorders/node_2.out create mode 100644 examples/space_frame_3d_results/recorders/node_2_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_2_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_2_vel.out create mode 100644 examples/space_frame_3d_results/recorders/node_3.out create mode 100644 examples/space_frame_3d_results/recorders/node_3_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_3_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_3_vel.out create mode 100644 examples/space_frame_3d_results/recorders/node_4.out create mode 100644 examples/space_frame_3d_results/recorders/node_4_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_4_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_4_vel.out create mode 100644 examples/space_frame_3d_results/recorders/node_5.out create mode 100644 examples/space_frame_3d_results/recorders/node_5_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_5_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_5_vel.out create mode 100644 examples/space_frame_3d_results/recorders/node_6.out create mode 100644 examples/space_frame_3d_results/recorders/node_6_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_6_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_6_vel.out create mode 100644 examples/space_frame_3d_results/recorders/node_7.out create mode 100644 examples/space_frame_3d_results/recorders/node_7_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_7_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_7_vel.out create mode 100644 examples/space_frame_3d_results/recorders/node_8.out create mode 100644 examples/space_frame_3d_results/recorders/node_8_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_8_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_8_vel.out create mode 100644 examples/space_frame_3d_results/recorders/node_9.out create mode 100644 examples/space_frame_3d_results/recorders/node_9_accel.out create mode 100644 examples/space_frame_3d_results/recorders/node_9_disp.out create mode 100644 examples/space_frame_3d_results/recorders/node_9_vel.out create mode 100644 pyproject.toml create mode 100644 snip1.png create mode 100644 snip2.png create mode 100644 snip3.png create mode 100644 specifications/15-rebuild-adoption-plan.md create mode 100644 specifications/README.md create mode 100644 src/otko/__init__.py create mode 100644 src/otko/__main__.py create mode 100644 src/otko/_const.py create mode 100644 src/otko/app.py create mode 100644 src/otko/commands/__init__.py create mode 100644 src/otko/commands/analysis.py create mode 100644 src/otko/commands/base.py create mode 100644 src/otko/commands/combinations.py create mode 100644 src/otko/commands/constraints.py create mode 100644 src/otko/commands/elements.py create mode 100644 src/otko/commands/grid.py create mode 100644 src/otko/commands/loads.py create mode 100644 src/otko/commands/materials.py create mode 100644 src/otko/commands/nodes.py create mode 100644 src/otko/commands/project.py create mode 100644 src/otko/commands/sections.py create mode 100644 src/otko/commands/series_and_patterns.py create mode 100644 src/otko/commands/transforms.py create mode 100644 src/otko/core/__init__.py create mode 100644 src/otko/core/_base.py create mode 100644 src/otko/core/analysis/__init__.py create mode 100644 src/otko/core/catalog/README.md create mode 100644 src/otko/core/catalog/__init__.py create mode 100644 src/otko/core/catalog/curated/__init__.py create mode 100644 src/otko/core/catalog/generated/__init__.py create mode 100644 src/otko/core/catalog/generated/auto_equal_constraint.py create mode 100644 src/otko/core/catalog/generated/auto_zero_length.py create mode 100644 src/otko/core/catalog/generated/bondsp01.py create mode 100644 src/otko/core/catalog/generated/concrete01_zero_tensile_strength.py create mode 100644 src/otko/core/catalog/generated/concrete02_linear_tension_softening.py create mode 100644 src/otko/core/catalog/generated/concrete04_popovics_concrete.py create mode 100644 src/otko/core/catalog/generated/concrete06.py create mode 100644 src/otko/core/catalog/generated/concretecm.py create mode 100644 src/otko/core/catalog/generated/conditions/__init__.py create mode 100644 src/otko/core/catalog/generated/conditions/line_beam_contact_master_end_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/line_body_constraint_slave_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/line_displacements.py create mode 100644 src/otko/core/catalog/generated/conditions/line_equal_constraint_slave_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/line_forces.py create mode 100644 src/otko/core/catalog/generated/conditions/line_mass.py create mode 100644 src/otko/core/catalog/generated/conditions/line_rayleigh_damping_elements.py create mode 100644 src/otko/core/catalog/generated/conditions/line_rayleigh_damping_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/line_restraints.py create mode 100644 src/otko/core/catalog/generated/conditions/line_rigid_diaphragm_slave_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/line_rigid_link_slave_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/line_uniform_forces.py create mode 100644 src/otko/core/catalog/generated/conditions/point_beam_contact_slave_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/point_body_constraint_master_node.py create mode 100644 src/otko/core/catalog/generated/conditions/point_body_constraint_slave_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/point_displacements.py create mode 100644 src/otko/core/catalog/generated/conditions/point_equal_constraint_master_node.py create mode 100644 src/otko/core/catalog/generated/conditions/point_equal_constraint_slave_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/point_forces.py create mode 100644 src/otko/core/catalog/generated/conditions/point_ground_motion_from_record.py create mode 100644 src/otko/core/catalog/generated/conditions/point_mass.py create mode 100644 src/otko/core/catalog/generated/conditions/point_rayleigh_damping_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/point_restraints.py create mode 100644 src/otko/core/catalog/generated/conditions/point_rigid_diaphragm_master_node.py create mode 100644 src/otko/core/catalog/generated/conditions/point_rigid_diaphragm_slave_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/point_rigid_link_master_node.py create mode 100644 src/otko/core/catalog/generated/conditions/point_rigid_link_slave_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/point_sine_ground_motion.py create mode 100644 src/otko/core/catalog/generated/conditions/point_zerolength.py create mode 100644 src/otko/core/catalog/generated/conditions/surface_displacements.py create mode 100644 src/otko/core/catalog/generated/conditions/surface_forces.py create mode 100644 src/otko/core/catalog/generated/conditions/surface_mass.py create mode 100644 src/otko/core/catalog/generated/conditions/surface_rayleigh_damping_elements.py create mode 100644 src/otko/core/catalog/generated/conditions/surface_rayleigh_damping_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/surface_restraints.py create mode 100644 src/otko/core/catalog/generated/conditions/volume_mass.py create mode 100644 src/otko/core/catalog/generated/conditions/volume_rayleigh_damping_elements.py create mode 100644 src/otko/core/catalog/generated/conditions/volume_rayleigh_damping_nodes.py create mode 100644 src/otko/core/catalog/generated/conditions/zerolength.py create mode 100644 src/otko/core/catalog/generated/contact.py create mode 100644 src/otko/core/catalog/generated/corotational_truss.py create mode 100644 src/otko/core/catalog/generated/damage2p.py create mode 100644 src/otko/core/catalog/generated/displacement_based_beam_column.py create mode 100644 src/otko/core/catalog/generated/elastic.py create mode 100644 src/otko/core/catalog/generated/elastic_beam_column.py create mode 100644 src/otko/core/catalog/generated/elastic_isotropic.py create mode 100644 src/otko/core/catalog/generated/elastic_membrane_plate.py create mode 100644 src/otko/core/catalog/generated/elastic_orthotropic.py create mode 100644 src/otko/core/catalog/generated/elastic_perfectly_plastic.py create mode 100644 src/otko/core/catalog/generated/elastic_perfectly_plastic_with_gap.py create mode 100644 src/otko/core/catalog/generated/elastic_section.py create mode 100644 src/otko/core/catalog/generated/elastic_timoshenko_beam_column.py create mode 100644 src/otko/core/catalog/generated/fiber.py create mode 100644 src/otko/core/catalog/generated/fiber_custom.py create mode 100644 src/otko/core/catalog/generated/fiberint.py create mode 100644 src/otko/core/catalog/generated/flexure_shear_interaction_displacement_based_beam_column.py create mode 100644 src/otko/core/catalog/generated/force_based_beam_column.py create mode 100644 src/otko/core/catalog/generated/ground_motion_record.py create mode 100644 src/otko/core/catalog/generated/hyperbolic_gap.py create mode 100644 src/otko/core/catalog/generated/hysteretic.py create mode 100644 src/otko/core/catalog/generated/initial_strain.py create mode 100644 src/otko/core/catalog/generated/initial_stress.py create mode 100644 src/otko/core/catalog/generated/j2plasticity.py create mode 100644 src/otko/core/catalog/generated/layeredshell.py create mode 100644 src/otko/core/catalog/generated/minmax.py create mode 100644 src/otko/core/catalog/generated/parallel.py create mode 100644 src/otko/core/catalog/generated/plate_fiber.py create mode 100644 src/otko/core/catalog/generated/pressuredependmultiyield.py create mode 100644 src/otko/core/catalog/generated/pressuredependmultiyield02.py create mode 100644 src/otko/core/catalog/generated/pressureindependmultiyield.py create mode 100644 src/otko/core/catalog/generated/pysimple1.py create mode 100644 src/otko/core/catalog/generated/quad.py create mode 100644 src/otko/core/catalog/generated/quadup.py create mode 100644 src/otko/core/catalog/generated/qzsimple1.py create mode 100644 src/otko/core/catalog/generated/ramberg_osgood_steel.py create mode 100644 src/otko/core/catalog/generated/reinforcing_steel.py create mode 100644 src/otko/core/catalog/generated/section_aggregator.py create mode 100644 src/otko/core/catalog/generated/series.py create mode 100644 src/otko/core/catalog/generated/shell.py create mode 100644 src/otko/core/catalog/generated/shelldkgq.py create mode 100644 src/otko/core/catalog/generated/standard_brick_element.py create mode 100644 src/otko/core/catalog/generated/steel01.py create mode 100644 src/otko/core/catalog/generated/steel02.py create mode 100644 src/otko/core/catalog/generated/tri31.py create mode 100644 src/otko/core/catalog/generated/truss.py create mode 100644 src/otko/core/catalog/generated/tzsimple1.py create mode 100644 src/otko/core/catalog/generated/user_material.py create mode 100644 src/otko/core/catalog/generated/viscous.py create mode 100644 src/otko/core/catalog/generated/viscous_damper.py create mode 100644 src/otko/core/constraints.py create mode 100644 src/otko/core/defaults.py create mode 100644 src/otko/core/geometry/__init__.py create mode 100644 src/otko/core/geometry/elements.py create mode 100644 src/otko/core/geometry/grid.py create mode 100644 src/otko/core/geometry/node.py create mode 100644 src/otko/core/loads/__init__.py create mode 100644 src/otko/core/materials/__init__.py create mode 100644 src/otko/core/project.py create mode 100644 src/otko/core/sections/__init__.py create mode 100644 src/otko/core/units.py create mode 100644 src/otko/resources/icons-dark/assign-load.svg create mode 100644 src/otko/resources/icons-dark/assign-support.svg create mode 100644 src/otko/resources/icons-dark/cases.svg create mode 100644 src/otko/resources/icons-dark/clear-display.svg create mode 100644 src/otko/resources/icons-dark/define-grid.svg create mode 100644 src/otko/resources/icons-dark/draw-frame.svg create mode 100644 src/otko/resources/icons-dark/draw-node.svg create mode 100644 src/otko/resources/icons-dark/material-library.svg create mode 100644 src/otko/resources/icons-dark/quick-guide.svg create mode 100644 src/otko/resources/icons-dark/run.svg create mode 100644 src/otko/resources/icons-dark/section-library.svg create mode 100644 src/otko/resources/icons-dark/show-deformed.svg create mode 100644 src/otko/resources/icons-dark/show-extruded.svg create mode 100644 src/otko/resources/icons-dark/show-force-diagram.svg create mode 100644 src/otko/resources/icons-dark/show-local-axes.svg create mode 100644 src/otko/resources/icons-dark/show-undeformed.svg create mode 100644 src/otko/resources/icons-dark/toggle-parallel.svg create mode 100644 src/otko/resources/icons-dark/tool-select.svg create mode 100644 src/otko/resources/icons-dark/view-front.svg create mode 100644 src/otko/resources/icons-dark/view-iso.svg create mode 100644 src/otko/resources/icons-dark/view-right.svg create mode 100644 src/otko/resources/icons-dark/view-top.svg create mode 100644 src/otko/resources/icons-dark/zoom-extents.svg create mode 100644 src/otko/resources/icons/.gitkeep create mode 100644 src/otko/resources/icons/app.icns create mode 100644 src/otko/resources/icons/app.ico create mode 100644 src/otko/resources/icons/app.png create mode 100644 src/otko/resources/icons/app.svg create mode 100644 src/otko/resources/icons/assign-load.svg create mode 100644 src/otko/resources/icons/assign-support.svg create mode 100644 src/otko/resources/icons/cases.svg create mode 100644 src/otko/resources/icons/clear-display.svg create mode 100644 src/otko/resources/icons/define-grid.svg create mode 100644 src/otko/resources/icons/draw-frame.svg create mode 100644 src/otko/resources/icons/draw-node.svg create mode 100644 src/otko/resources/icons/material-library.svg create mode 100644 src/otko/resources/icons/quick-guide.svg create mode 100644 src/otko/resources/icons/run.svg create mode 100644 src/otko/resources/icons/section-library.svg create mode 100644 src/otko/resources/icons/show-deformed.svg create mode 100644 src/otko/resources/icons/show-extruded.svg create mode 100644 src/otko/resources/icons/show-force-diagram.svg create mode 100644 src/otko/resources/icons/show-local-axes.svg create mode 100644 src/otko/resources/icons/show-undeformed.svg create mode 100644 src/otko/resources/icons/toggle-parallel.svg create mode 100644 src/otko/resources/icons/tool-select.svg create mode 100644 src/otko/resources/icons/view-front.svg create mode 100644 src/otko/resources/icons/view-iso.svg create mode 100644 src/otko/resources/icons/view-right.svg create mode 100644 src/otko/resources/icons/view-top.svg create mode 100644 src/otko/resources/icons/zoom-extents.svg create mode 100644 src/otko/resources/themes/.gitkeep create mode 100644 src/otko/resources/themes/app.qss create mode 100644 src/otko/services/__init__.py create mode 100644 src/otko/services/_emit.py create mode 100644 src/otko/services/_run.py create mode 100644 src/otko/services/animation_export.py create mode 100644 src/otko/services/combinations.py create mode 100644 src/otko/services/deformation.py create mode 100644 src/otko/services/element_forces.py create mode 100644 src/otko/services/export.py create mode 100644 src/otko/services/material_tester.py create mode 100644 src/otko/services/opensees_runner.py create mode 100644 src/otko/services/peer_record.py create mode 100644 src/otko/services/persistence.py create mode 100644 src/otko/services/qt_workers.py create mode 100644 src/otko/services/results.py create mode 100644 src/otko/services/section_bbox.py create mode 100644 src/otko/services/section_properties.py create mode 100644 src/otko/services/spectrum.py create mode 100644 src/otko/viewmodels/__init__.py create mode 100644 src/otko/viewmodels/analysis_runner.py create mode 100644 src/otko/viewmodels/project_vm.py create mode 100644 src/otko/views/__init__.py create mode 100644 src/otko/views/action_handlers.py create mode 100644 src/otko/views/canvas3d/__init__.py create mode 100644 src/otko/views/canvas3d/diagram_renderer.py create mode 100644 src/otko/views/canvas3d/model_canvas.py create mode 100644 src/otko/views/canvas3d/model_renderer.py create mode 100644 src/otko/views/canvas3d/selection.py create mode 100644 src/otko/views/canvas3d/style.py create mode 100644 src/otko/views/dialogs/__init__.py create mode 100644 src/otko/views/dialogs/add_node.py create mode 100644 src/otko/views/dialogs/assign_equal_dof.py create mode 100644 src/otko/views/dialogs/assign_hinge.py create mode 100644 src/otko/views/dialogs/assign_load.py create mode 100644 src/otko/views/dialogs/assign_masses.py create mode 100644 src/otko/views/dialogs/assign_property.py create mode 100644 src/otko/views/dialogs/assign_support.py create mode 100644 src/otko/views/dialogs/assign_zls.py create mode 100644 src/otko/views/dialogs/case_forms.py create mode 100644 src/otko/views/dialogs/case_manager.py create mode 100644 src/otko/views/dialogs/combination_dialog.py create mode 100644 src/otko/views/dialogs/coord_grid_systems.py create mode 100644 src/otko/views/dialogs/define_grid_data.py create mode 100644 src/otko/views/dialogs/display_options.py create mode 100644 src/otko/views/dialogs/distributed_load.py create mode 100644 src/otko/views/dialogs/grid_system.py create mode 100644 src/otko/views/dialogs/linear_time_series.py create mode 100644 src/otko/views/dialogs/locate_origin.py create mode 100644 src/otko/views/dialogs/material_forms.py create mode 100644 src/otko/views/dialogs/material_library.py create mode 100644 src/otko/views/dialogs/material_tester.py create mode 100644 src/otko/views/dialogs/mirror.py create mode 100644 src/otko/views/dialogs/move.py create mode 100644 src/otko/views/dialogs/path_time_series.py create mode 100644 src/otko/views/dialogs/pattern_loads.py create mode 100644 src/otko/views/dialogs/plain_pattern.py create mode 100644 src/otko/views/dialogs/quick_grid_lines.py create mode 100644 src/otko/views/dialogs/quick_guide.py create mode 100644 src/otko/views/dialogs/replicate.py create mode 100644 src/otko/views/dialogs/run_analysis.py create mode 100644 src/otko/views/dialogs/section_editor.py create mode 100644 src/otko/views/dialogs/section_forms.py create mode 100644 src/otko/views/dialogs/section_library.py create mode 100644 src/otko/views/dialogs/uniform_excitation.py create mode 100644 src/otko/views/dock_manager.py create mode 100644 src/otko/views/docks/__init__.py create mode 100644 src/otko/views/docks/_labels.py create mode 100644 src/otko/views/docks/console.py create mode 100644 src/otko/views/docks/deformed_shape.py create mode 100644 src/otko/views/docks/force_diagram.py create mode 100644 src/otko/views/docks/hysteresis.py create mode 100644 src/otko/views/docks/mode_shape_animator.py create mode 100644 src/otko/views/docks/property_editor.py create mode 100644 src/otko/views/docks/pushover_curve.py create mode 100644 src/otko/views/docks/response_spectrum.py create mode 100644 src/otko/views/docks/results_panel.py create mode 100644 src/otko/views/docks/table_dock.py create mode 100644 src/otko/views/docks/time_history.py create mode 100644 src/otko/views/formatting.py create mode 100644 src/otko/views/main_window.py create mode 100644 src/otko/views/menu_builder.py create mode 100644 src/otko/views/render_controls.py create mode 100644 src/otko/views/ribbon/__init__.py create mode 100644 src/otko/views/tools/__init__.py create mode 100644 src/otko/views/tools/_node_reuse.py create mode 100644 src/otko/views/tools/base.py create mode 100644 src/otko/views/tools/draw_frame.py create mode 100644 src/otko/views/tools/draw_node.py create mode 100644 src/otko/views/tools/draw_truss.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/gui/__init__.py create mode 100644 tests/gui/test_analysis_commands.py create mode 100644 tests/gui/test_analysis_runner.py create mode 100644 tests/gui/test_assign_load_pattern_name.py create mode 100644 tests/gui/test_assign_masses.py create mode 100644 tests/gui/test_assign_zls.py create mode 100644 tests/gui/test_canvas_grid_snap.py create mode 100644 tests/gui/test_case_forms.py create mode 100644 tests/gui/test_commands.py create mode 100644 tests/gui/test_constraint_and_display_dialogs.py create mode 100644 tests/gui/test_convert_element_type.py create mode 100644 tests/gui/test_define_dialogs.py create mode 100644 tests/gui/test_draw_frame_tool.py create mode 100644 tests/gui/test_draw_node_tool.py create mode 100644 tests/gui/test_draw_truss_tool.py create mode 100644 tests/gui/test_grid_dialog.py create mode 100644 tests/gui/test_main_window.py create mode 100644 tests/gui/test_material_dialog_concrete04.py create mode 100644 tests/gui/test_material_form_ranges.py create mode 100644 tests/gui/test_material_tester_dialog.py create mode 100644 tests/gui/test_path_ts_dialogs.py create mode 100644 tests/gui/test_pattern_load_audit.py create mode 100644 tests/gui/test_property_commands.py create mode 100644 tests/gui/test_property_forms.py create mode 100644 tests/gui/test_pushover_curve_units.py create mode 100644 tests/gui/test_run_analysis.py create mode 100644 tests/gui/test_section_extrusion.py create mode 100644 tests/gui/test_section_forms.py create mode 100644 tests/gui/test_selection.py create mode 100644 tests/gui/test_transforms.py create mode 100644 tests/gui/test_tree_multi_select.py create mode 100644 tests/gui/test_units_status_bar.py create mode 100644 tests/gui/test_update_element_fields.py create mode 100644 tests/gui/test_viewport_axes.py create mode 100644 tests/gui/test_working_plane.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/test_basic_truss.py create mode 100644 tests/integration/test_beam_quad_2d.py create mode 100644 tests/integration/test_combinations.py create mode 100644 tests/integration/test_concrete04_runner.py create mode 100644 tests/integration/test_dof_coverage.py create mode 100644 tests/integration/test_eigen_two_storey_one_bay_frame.py create mode 100644 tests/integration/test_eigen_two_storey_shear_frame.py create mode 100644 tests/integration/test_elastic_frame.py create mode 100644 tests/integration/test_ex1a_canti2d.py create mode 100644 tests/integration/test_ex1a_canti2d_eq.py create mode 100644 tests/integration/test_ex1b_portal2d.py create mode 100644 tests/integration/test_ex2a_canti2d_elastic_element.py create mode 100644 tests/integration/test_ex2b_canti2d_inelastic_section.py create mode 100644 tests/integration/test_ex2c_canti2d_inelastic_fiber_section.py create mode 100644 tests/integration/test_ex3_canti2d_variants.py create mode 100644 tests/integration/test_ex4_portal2d_variants.py create mode 100644 tests/integration/test_material_tester.py create mode 100644 tests/integration/test_moment_curvature.py create mode 100644 tests/integration/test_moment_curvature_example.py create mode 100644 tests/integration/test_pattern_factors.py create mode 100644 tests/integration/test_rc_frame_earthquake.py create mode 100644 tests/integration/test_rc_frame_gravity.py create mode 100644 tests/integration/test_rc_frame_pushover.py create mode 100644 tests/integration/test_runner_imposed_motion.py create mode 100644 tests/integration/test_runner_modal.py create mode 100644 tests/integration/test_runner_static.py create mode 100644 tests/integration/test_runner_transient.py create mode 100644 tests/services/__init__.py create mode 100644 tests/services/test_element_forces.py create mode 100644 tests/services/test_export.py create mode 100644 tests/services/test_imposed_support_motion.py create mode 100644 tests/services/test_peer_record.py create mode 100644 tests/services/test_persistence.py create mode 100644 tests/services/test_runner_translation.py create mode 100644 tests/services/test_section_bbox.py create mode 100644 tests/services/test_transient_and_export.py create mode 100644 tests/tools/__init__.py create mode 100644 tests/tools/fixtures/minimal.mat create mode 100644 tests/tools/fixtures/minimal_expected.json create mode 100644 tests/tools/test_parse_schemas.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/catalog/__init__.py create mode 100644 tests/unit/catalog/test_generated.py create mode 100644 tests/unit/core/__init__.py create mode 100644 tests/unit/core/materials/__init__.py create mode 100644 tests/unit/core/materials/test_concrete04.py create mode 100644 tests/unit/test_architecture.py create mode 100644 tests/unit/test_ci_config.py create mode 100644 tests/unit/test_combinations.py create mode 100644 tests/unit/test_diagram_renderer.py create mode 100644 tests/unit/test_docs.py create mode 100644 tests/unit/test_elements.py create mode 100644 tests/unit/test_formatting.py create mode 100644 tests/unit/test_grid_system.py create mode 100644 tests/unit/test_materials.py create mode 100644 tests/unit/test_meta_gates.py create mode 100644 tests/unit/test_node.py create mode 100644 tests/unit/test_packaging.py create mode 100644 tests/unit/test_pattern_factors.py create mode 100644 tests/unit/test_phase8a.py create mode 100644 tests/unit/test_phase8b.py create mode 100644 tests/unit/test_phase8c.py create mode 100644 tests/unit/test_phase9.py create mode 100644 tests/unit/test_project.py create mode 100644 tests/unit/test_renderer.py create mode 100644 tests/unit/test_section_shape.py create mode 100644 tests/unit/test_smoke.py create mode 100644 tests/unit/test_unit_labels.py create mode 100644 tests/unit/test_zerolength_section.py create mode 100644 tools/__init__.py create mode 100644 tools/gidopensees_import/__init__.py create mode 100644 tools/gidopensees_import/codegen.py create mode 100644 tools/gidopensees_import/parse_schemas.py create mode 100644 tools/gidopensees_import/schema_model.py create mode 100644 tools/gidopensees_import/schemas.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..a422b15 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,56 @@ +name: Bug report +description: Report a defect in OTKO +labels: ["bug"] +body: + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear and concise description of the bug. + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + placeholder: | + 1. Open a new project + 2. Click 'Draw Frame' + 3. ... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: input + id: version + attributes: + label: OTKO version + placeholder: 0.0.1 + validations: + required: true + - type: input + id: python + attributes: + label: Python version + placeholder: 3.11.7 + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating System + options: + - Windows + - macOS + - Linux + validations: + required: true + - type: textarea + id: logs + attributes: + label: Console output / traceback + render: shell diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..c4a8ec2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,31 @@ +name: Feature request +description: Propose a new feature or improvement +labels: ["enhancement"] +body: + - type: textarea + id: motivation + attributes: + label: Motivation + description: What engineering problem does this solve? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + validations: + required: true + - type: dropdown + id: phase + attributes: + label: Roadmap phase this fits into + options: + - "Phase 1 — Core Model" + - "Phase 2 — OpenSees Service" + - "Phase 3 — 3D Viewport" + - "Phase 4 — Modeling Tools" + - "Phase 5 — Properties" + - "Phase 6 — Analysis Pipeline" + - "Phase 7 — Post-processing" + - "Phase 8 — Earthquake Engineering" + - "Out of scope / new" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..5e81062 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,26 @@ +## Summary + + + +## Linked issue + +Closes # + +## Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor / cleanup +- [ ] Documentation +- [ ] CI / tooling + +## Architectural checklist + +- [ ] No `from PySide6` in `core/` or `services/` +- [ ] No `import openseespy` in `core/` or `views/` +- [ ] Public functions have type hints + docstrings +- [ ] Heavy work runs off the GUI thread + +## Testing + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..56068ce --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install ruff + - run: ruff check src tests + - run: ruff format --check src tests + + type: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install -e ".[gui,dev]" + - run: mypy src/otko/core src/otko/services src/otko/viewmodels + + test-headless: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10", "3.11", "3.12"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - 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: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10", "3.11", "3.12"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install Linux Qt deps + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libegl1 libxkbcommon-x11-0 libxcb-icccm4 \ + libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \ + libxcb-shape0 libxcb-sync1 libxcb-xfixes0 libxcb-xinerama0 \ + libxcb-cursor0 libdbus-1-3 libgl1 libxkbcommon0 xvfb + - run: pip install -e ".[gui,dev]" + - name: Run GUI tests (Linux, headless) + if: runner.os == 'Linux' + run: xvfb-run -a pytest tests/gui + - name: Run GUI tests (Win/Mac) + if: runner.os != 'Linux' + run: pytest tests/gui + + test-integration: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10", "3.11", "3.12"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: pip install -e ".[gui,dev]" + - run: pytest tests/integration -m slow diff --git a/.github/workflows/ci.yml.disabled b/.github/workflows/ci.yml.disabled new file mode 100644 index 0000000..ebc3520 --- /dev/null +++ b/.github/workflows/ci.yml.disabled @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - run: pip install ruff mypy + - run: ruff check src tests + - run: ruff format --check src tests + + test: + needs: lint + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10", "3.11", "3.12"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install Linux Qt deps + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libegl1 libxkbcommon-x11-0 libxcb-icccm4 \ + libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \ + libxcb-shape0 libxcb-sync1 libxcb-xfixes0 libxcb-xinerama0 \ + libxcb-cursor0 libdbus-1-3 libgl1 libxkbcommon0 xvfb + - run: pip install -e ".[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" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c82640e --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# Byte-compiled / optimized +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Distribution / packaging +.Python +build/ +dist/ +*.egg-info/ +*.egg +pip-wheel-metadata/ + +# Virtual envs +.venv/ +venv/ +env/ + +# Test / coverage +.pytest_cache/ +.coverage +.coverage.* +htmlcov/ +.cache +.mypy_cache/ +.ruff_cache/ + +# Qt +*.qm +*.pyc +.qt_for_python/ + +# Compiled .qrc → _rc.py (regenerable) +*_rc.py + +# IDEs +.idea/ +.vscode/ +*.swp +*~ + +# OS +.DS_Store +Thumbs.db + +# Project artifacts +*.log +results/ +*.h5 +*.hdf5 +scratch/ + +# Local AI agent state (Claude Code, Cursor, etc.) +.claude/ +.cursor/ + +# Local example WIP / verification outputs (not for the public repo) +examples/data/Ex*_tmp/ +examples/ozan*.osmodel +examples/ozan_results/ +/Ex*.csv +/Ex*.png diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..20d6830 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + args: ["--maxkb=500"] + - id: check-merge-conflict + - id: mixed-line-ending + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.4 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.10.0 + hooks: + - id: mypy + additional_dependencies: [pydantic, numpy] + files: ^src/otko/(core|services|viewmodels)/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bdc5f2e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,58 @@ +# AGENTS.md — otko + +Pre-alpha SAP2000-style desktop GUI for OpenSeesPy. Python 3.10+; **Windows requires 3.12+** (`openseespywin==3.8.0.0` has no 3.11 wheel). Solver pinned: `openseespy==3.8.0.0`. + +## Install + +```bash +pip install -e ".[gui,dev]" # desktop: Qt + PyVista + dev tools +pip install -e . # headless: core + services only, no Qt (scripts, notebooks, web backends) +python -m otko # launch GUI (src/otko/app.py:run) +``` + +`scipy`/`pandas` in `[gui]` extras are phantom deps (not imported as of 2026-06) — do not add imports expecting them. + +## Architecture (enforced in review — see PR template) + +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. +- `viewmodels/` bridges core↔Qt (signals, `QUndoStack`); `commands/` holds `QUndoCommand` subclasses. +- Rules: public functions need type hints + docstring; new domain entities go through Pydantic validation; ops >50 ms run off the GUI thread (`AnalysisWorker` in QThread, cancel via `isInterruptionRequested()`, results cross threads as lightweight `ResultsHandle` to HDF5). + +Runner emits OpenSeesPy commands in fixed order (`docs/architecture.md`): `wipe → model → node → fix → material → section → geomTransf → element → timeSeries → pattern/load → recorder → system/numberer/... → analyze`. Never reorder. + +## Verify (in this order) + +```bash +ruff check src tests +ruff format src tests # line-length 100, E501 ignored +mypy src/otko/core src/otko/services +pytest -m "not slow" # CI gate: lint → this, on 3.10/3.11/3.12 × ubuntu/windows/macos +``` + +Focused runs: `pytest tests/unit` (pure logic, ms), `pytest tests/gui -k ` (pytest-qt, needs display), `pytest tests/integration -k ` (real OpenSeesPy runs). Single test: `pytest tests/unit/test_project.py::test_name -q`. Markers: `gui`, `slow`. Linux GUI tests need `xvfb-run -a pytest ...` plus system Qt libs (see `ci.yml` apt list). + +Notes: `tests/conftest.py` auto-`ops.wipe()`s the OpenSees domain between tests (lazy import so core tests stay Qt/OpenSees-free). Coverage omits `views/`. Commit style: Conventional Commits (`feat:`, `fix:`, ...). `pre-commit install` runs ruff + mypy (mypy hook scoped to `core|services|viewmodels`). + +## Git & Forgejo + +Remote is a self-hosted Forgejo, SSH-only: + +```bash +git remote add origin ssh://git@smill-home.ddns.net/smill/otko.git +``` + +- Web UI / HTTPS clone: `https://smill-home.ddns.net/forgejo/smill/otko.git` (note the `/forgejo` subpath — SSH URLs don't have it). +- Forgejo SSH runs through the system sshd on **port 22** (`SSH_PORT = 22` in the server's `app.ini`). If the site ever advertises a `:2222` SSH URL again, that config regressed — push/pull with port 22 anyway and flag it. +- Auth is by SSH key (Forgejo → Settings → SSH Keys). Test: `ssh -T git@smill-home.ddns.net` should answer `Hi there, ! ... Forgejo does not provide shell access.` +- Default branch `main`. Work on short-lived branches (`fix/`, `docs/`), open a PR against `main`. Never commit directly to `main` from an agent session. +- Repo root is `otko/` itself. (On the dev laptop there is also an empty, commit-less parent repo at `Sync/coding/` — ignore it; all git work happens inside `otko/`.) + +## Examples & persistence + +- `examples/*.py` are source of truth; `examples/*.osmodel` are generated artifacts (checked in). Never hand-edit `.osmodel` — change the script and regen: `python examples/cantilever.py` (each script saves, reloads, asserts clean round-trip). +- Projects persist as single Pydantic-validated JSON `.osmodel` (diffable); analysis output goes to `*.osresults.h5` (HDF5, one group per case). +- Quick smoke: open `examples/cantilever.osmodel` → run `Tip-Load` → M3 peaks 50 kN·m at fixed end. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ab9023e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,97 @@ +# Contributing + +Thanks for helping with OTKO — a SAP2000-style desktop GUI for +OpenSeesPy. Early-stage project: the bar is architecture cleanliness, not +feature count. If your change breaks a layering rule below, it won't +merge — no matter how useful the feature. + +## Dev setup + +```bash +python -m venv .venv +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 # line-length 100, E501 ignored +mypy src/otko/core src/otko/services +pytest -m "not slow" # CI gate: lint → this, on 3.10/3.11/3.12 × ubuntu/windows/macos +``` + +Focused runs are faster while iterating: `pytest tests/unit` (pure logic, +milliseconds), `pytest tests/gui -k ` (pytest-qt, needs a display; +Linux GUI tests want `xvfb-run -a pytest ...`), and +`pytest tests/integration -k ` (real OpenSeesPy runs). Single test: +`pytest tests/unit/test_project.py::test_name -q`. Markers: `gui`, `slow`. + +## Architectural rules (enforced in review) + +OTKO uses a strict one-way **MVVM + service** architecture. Dependencies +flow outward-in only: + +``` +views → viewmodels → services → core +``` + +`commands` sits alongside the bridge and owns every model mutation. + +1. `core/` is pure Python — stdlib + numpy + pydantic. It may **not** + import Qt or `openseespy`. Period. +2. `services/` may use `core` + `h5py` + `openseespy`, but may **not** + import Qt. +3. `views/` (PySide6/pyvistaqt) may **not** import `openseespy` directly — + go through a service. No business logic in `views`. +4. `viewmodels/` bridges `core` ↔ Qt (signals, `QUndoStack`). +5. `commands/` holds the `QUndoCommand` subclasses; all model mutations + go through `commands`, not ad-hoc edits in `views`. +6. Public functions and methods need type hints and a docstring. +7. New domain entities go through Pydantic validation. +8. Long-running operations (>50 ms) run off the GUI thread (`AnalysisWorker` + in a `QThread`, cancelled via `isInterruptionRequested()`; results cross + threads as a lightweight `ResultsHandle` written to HDF5). + +The full package map and the fixed OpenSeesPy command order the runner +emits live in [`docs/architecture.md`](docs/architecture.md). Never +reorder the runner's commands. + +## Branch model + +`main` is the default and integration branch. Work on short-lived topic +branches cut from `main` — `feat/`, `fix/`, or +`docs/` — and open a pull request against `main`. Do not commit +directly to `main` from an agent session. There is no `develop` branch. + +## Commit style + +Conventional Commits — `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, +`chore:`, `ci:`. Keep each commit focused; a `feat:` commit should add a +feature, not mix one in with unrelated refactors. + +## Documentation + +If your change is user-visible, update +[`docs/QUICK_GUIDE.md`](docs/QUICK_GUIDE.md) and the relevant roadmap or +ADR entry. Project model files are `.osmodel` (Pydantic-validated JSON); +regenerate the checked-in `examples/*.osmodel` from their scripts with +`python examples/.py` rather than hand-editing them. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. 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/README.md b/README.md new file mode 100644 index 0000000..c291d67 --- /dev/null +++ b/README.md @@ -0,0 +1,195 @@ +

+ OTKO +

+ +

+ A SAP2000-style desktop GUI for + OpenSeesPy. + Draw the model, click run, look at the diagrams. +

+ +

+ Pre-alpha. Under active development. APIs and file formats will change. +

+ +--- + +![OTKO main window](docs/screenshots/main_window.png) + +## Why + +OpenSees does nonlinear FEM well. Its user interface is a script. +OTKO puts a visual front-end on it: + +- Draw nodes, frames, supports, and loads on a snapped grid. +- Assign materials, sections, and load patterns through dialogs. +- Run static, modal, pushover, and time-history analyses with progress + and cancel. +- Look at the results — deformed shape, mode shapes, force + diagrams, pushover curves, time-history plots, hysteresis loops. +- Save the model as one `.osmodel` JSON file. Diffs cleanly in Git, + builds cleanly from Python. + +Underneath, the `core` Pydantic model works fine from a script or +notebook. The GUI is a front-end, not the whole product. + +## What works today + +- **Modeling** — grids, nodes, frames (elastic + force-based), trusses, + quads, zero-length sections, restraints, equalDOF constraints, + distributed loads, ground motions (`PathTimeSeries` / + `UniformExcitation`). +- **Materials and sections** — `Steel01`, `Steel02`, `Concrete01`, + `Concrete02`, `ElasticPP`, `Hysteretic`, fiber sections (rectangular / + circular patches + rebar layers), `SectionAggregator`, + `BeamWithHinges`. +- **Analyses** — static (load- or displacement-controlled), modal, + displacement-controlled pushover, transient time-history with + mode-1 Rayleigh damping. Chained workflows: gravity preload → + `loadConst -time 0.0` → pushover or transient. +- **Post-processing** — deformed shape (with scale slider), animated + mode shapes, axial / shear / moment diagrams, pushover curves + (in display units), time-history plots, hysteresis loops, + response-spectrum SRSS / CQC, snapshot + video export. +- **Persistence** — one JSON `.osmodel` per project, Pydantic-validated, + round-trips clean. +- **Examples** — 20+ verified examples, including OpenSees + Wiki Examples 1–4 and a fiber-section RC frame pushover. + See [`examples/README.md`](examples/README.md). + +## Tech stack + +| Layer | Library | +| ------------ | ------------------------------------ | +| GUI | PySide6 (Qt 6) | +| 3D viewport | PyVista + pyvistaqt (VTK) | +| 2D plots | pyqtgraph | +| Solver | OpenSeesPy 3.8.0.0 | +| Numerics | NumPy | +| Storage | Pydantic v2 (model), h5py (results) | +| Tests | pytest, pytest-qt | +| Lint / type | ruff, mypy | + +## Architecture + +Strict MVVM + service layer. `core` is pure Python — no Qt, +no OpenSeesPy imports — and unit-tests in isolation. + +``` +views (Qt) → viewmodels → services (OpenSeesRunner, Persistence) → core (model) +``` + +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): + +```bash +git clone ssh://git@smill-home.ddns.net/smill/otko.git +cd otko + +python -m venv .venv +.venv\Scripts\activate # Windows +source .venv/bin/activate # Linux / macOS + +pip install -e ".[gui,dev]" +``` + +**Headless** (core + services only, no Qt): + +```bash +pip install -e . +``` + +That pulls pydantic, numpy, h5py, openseespy and nothing else. +Use it for scripts, notebooks, and web backends that reuse +`otko.core` or `otko.services` without the GUI. + +Python 3.10+. On Windows use **3.12+** — `openseespywin==3.8.0.0` +has no 3.11 wheel (`Requires-Python >=3.12`). Both pins already +live in `pyproject.toml`. + +## Quick start + +```bash +python -m otko +``` + +Then: + +1. **File → Open** → `examples/cantilever.osmodel`. +2. **Analyze → Cases** → run `Tip-Load`. +3. **Display → Show Force Diagram** → **M3**: linear moment, + 50 kN·m at the fixed end. **V2**: constant -10 kN. +4. **Display → Show Deformed Shape** → cantilever curve, as advertised. + +Nonlinear version: open `examples/portal_pushover.osmodel`, +run `Push-X`, **Display → Show Pushover Curve**. Elastic ramp, +then a yield plateau as the base hinges form. + +## Run the test suite + +```bash +pytest tests/unit # pure logic, milliseconds +pytest tests/gui # Qt event-loop tests (pytest-qt) +pytest tests/integration # real OpenSeesPy runs on bundled examples +``` + +CI runs lint + the non-`slow` subset on Linux / macOS / Windows +× Python 3.10 / 3.11 / 3.12. + +## Roadmap + +[`docs/roadmap.md`](docs/roadmap.md) has the phase-by-phase plan. +Phases 0–7 (modeling, analysis, post-processing) are mostly done. +Phase 8 (isolators, ground-motion library, IDA, fiber-section +editor polish) is where the open work is. + +## Collaborators wanted + +Most useful to people who already work with OpenSees and want a +shorter path from idea to model — and would rather build it together +than alone. Open an issue or say hi if you are: + +- A **structural / earthquake engineer** who knows OpenSees Tcl + or OpenSeesPy and can tell us when a feature is almost right + but not quite. +- A **researcher** running pushover, IDA, or response-spectrum studies + who can check the GUI against hand-built scripts. +- A **Python / Qt developer** into scientific desktop apps, + VTK rendering, or Pydantic schema design. +- A **student** learning FEM and GUI architecture at the same time — + the examples and tests are meant to read as documentation. +- A **UX / icon designer** willing to argue about dialogs, toolbar + icons, and visual language. + +Bug reports and reproducible test cases count as contributions. +See [`CONTRIBUTING.md`](CONTRIBUTING.md) for setup and the rules +enforced in review. + +## License + +OTKO is **GNU Affero General Public License v3.0** +([`LICENSE`](LICENSE)). Read the license itself, not just this: + +- Research, education, personal projects: fine, keep the copyright + notice. +- Fork and modify: fine. +- Distribute it (modified or not): release your full source under + AGPL-3.0. +- Run a modified version as a network service: release your + modifications under AGPL-3.0. + +Commercial forks stay open. If you need a different arrangement +(e.g. closed-source commercial license), open an issue. + +Copyright © 2026 Ozan and contributors. 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/docs/adr/ADR-0001-gidopensees-schema-import.md b/docs/adr/ADR-0001-gidopensees-schema-import.md new file mode 100644 index 0000000..248f824 --- /dev/null +++ b/docs/adr/ADR-0001-gidopensees-schema-import.md @@ -0,0 +1,331 @@ +# ADR-0001 — Import gidopensees Schemas into OTKO + +| Field | Value | +|---|---| +| **Status** | Proposed | +| **Date** | 2026-05-22 | +| **Author** | ogunc | +| **Deciders** | Core maintainers | +| **Source project** | [gidopensees](https://github.com/rclab-auth/gidopensees) — AUTh Lab of R/C and Masonry Structures | + +--- + +## 1. Context + +### Why gidopensees? + +gidopensees is the most complete published schema inventory for the OpenSees +material/element/condition set. It covers 60+ material types, 30+ element +types, and 31 boundary-condition and load types — all expressed as GiD +preprocessor `.mat` / `.cnd` BOOK definitions, with DEPENDENCIES (field +visibility rules), `#UNITS#` annotations, and TKWIDGET hooks for +auto-fill presets and Wiki links. + +OTKO currently supports ~34 of these objects (see +`docs/gap-analysis-gidopensees.md`). Importing gidopensees schemas would +close 23 P1 gaps (Phase 8 targets) and 21 P2 gaps without requiring us to +reverse-engineer OpenSeesPy docs for each type. + +### What we are NOT doing + +This ADR covers **schema definitions only** — Pydantic model fields, field +metadata, and default values. It does not cover: + +- The `bas/` Python code-generation templates (separate ADR). +- The `tcl/` TKWIDGET Tcl implementations (separate ADR). +- Any runtime OpenSeesPy command emission — that lives in + `services/opensees_runner.py` and follows naturally once a schema + is accepted. + +--- + +## 2. Decision + +### 2.1 New package: `core/catalog/` + +A new package `src/otko/core/catalog/` will hold +gidopensees-derived schema definitions alongside curated additions. + +``` +src/otko/core/catalog/ +├── __init__.py # re-exports curated + generated public symbols +├── generated/ # output of the codegen tool — DO NOT edit by hand +│ ├── _header.py # shared attribution header (inserted by codegen) +│ ├── steel.py +│ ├── concrete.py +│ ├── other_uniaxial.py +│ ├── nd_materials.py +│ ├── sections.py +│ └── elements.py +└── curated/ # human-reviewed, hand-edited overrides and additions + ├── README.md # explains the curated/ contract + └── … +``` + +**Existing modules are untouched.** The objects in `core/materials/`, +`core/sections/`, `core/geometry/elements.py` remain authoritative for +every type already supported. Catalog objects enter the UI only after +equivalence is established (see §2.7). + +### 2.2 Codegen tool: `tools/gidopensees_import/` + +A build-time parser + code-generator lives in +`tools/gidopensees_import/`, outside `src/`: + +``` +tools/gidopensees_import/ +├── README.md +├── parse_mat.py # parses OpenSees.mat into an intermediate IR +├── parse_cnd.py # parses OpenSees.cnd into an intermediate IR +├── codegen.py # renders IR → Pydantic v2 model source files +├── ir.py # intermediate representation dataclasses +└── tests/ # unit tests for the parser and codegen +``` + +The tool runs once per gidopensees update. Its output (`generated/`) is +committed so CI never requires gidopensees to be present. The tool is +invoked manually by a maintainer: + +```bash +python tools/gidopensees_import/codegen.py \ + --mat path/to/OpenSees.mat \ + --cnd path/to/OpenSees.cnd \ + --out src/otko/core/catalog/generated/ +``` + +**Rationale:** keeping the parser outside `src/` prevents it from being +imported at runtime, avoids adding GiD file parsing as a dependency of +the installable package, and makes the "run once, commit output" contract +explicit to contributors. + +### 2.3 Attribution + +Every file under `core/catalog/generated/` carries this header comment +(inserted by `codegen.py`): + +```python +# This file is derived from gidopensees. +# Source: https://github.com/rclab-auth/gidopensees +# Authors: AUTh Lab of R/C and Masonry Structures +# (https://rclab.civil.auth.gr/) +# Modifications: generated by tools/gidopensees_import/codegen.py +``` + +Files under `curated/` carry a similar header when they derive from +gidopensees content. + +### 2.4 `.osmodel` format backward-compatibility + +The `.osmodel` JSON format uses Pydantic discriminated unions keyed on a +`"type"` field. New material types from `core/catalog/` appear as new +discriminator values. The union in `core/materials/__init__.py` is +extended only when a catalog type is promoted to stable (see §2.7). + +Old project files that do not contain the new discriminator values load +cleanly: Pydantic ignores unknown items in lists when +`model_config = ConfigDict(extra="ignore")`, and the `Project` validator +will log (not raise) on unknown type strings if we add a graceful +fallback. + +**Migration path:** +1. Catalog type is added with a temporary discriminator value + (e.g. `"catalog.Concrete04"`). +2. After verification (§2.7) it is promoted to a stable value + (e.g. `"Concrete04"`) and the temporary value is kept as an alias for + one minor version. +3. A `migrate_osmodel.py` script in `tools/` handles the rename if needed. + +No existing `.osmodel` file ever breaks on open. + +### 2.5 Field metadata for TKWIDGET hooks + +gidopensees BOOK definitions contain TKWIDGET directives that fire +auto-fill presets (e.g. `SteelUniaxMaterial::GenerateValues`), Wiki links +(`TK_MaterialWikiInfo`), and the Material Tester dialog +(`TK_MaterialTester`). These are deferred from this ADR. + +Each field or model where a TKWIDGET hook is relevant carries a +`json_schema_extra` annotation recording the hook name: + +```python +class Concrete04(CatalogEntity): + ... + class model_config(ConfigDict): + json_schema_extra = { + "tkwidget_hooks": [ + "ConcreteUniaxMaterial::GenerateValues", + "TK_MaterialWikiInfo", + "TK_MaterialTester", + ] + } +``` + +This metadata is visible to future UI layers without coupling `core/` to +Qt. Implementation of the actual preset dialogs and Wiki-link buttons +comes in a later UI phase. + +### 2.6 DEPENDENCIES → `dependent_schemas` metadata (not validators) + +gidopensees BOOK DEPENDENCIES express field-visibility rules: + +``` +(1, RESTORE, Gap_length, #CURRENT#), (0, HIDE, Gap_length, #CURRENT#) +``` + +These are **viewmodel / UI concerns**, not data-validity rules, and +therefore must not become Pydantic validators in `core/`. + +Each Pydantic model that has visibility dependencies stores them as +`json_schema_extra["dependencies"]` — a list of dicts describing the +trigger field, trigger value, and affected fields. The viewmodel layer +reads these at dialog-construction time to wire up the show/hide logic. + +**Example:** + +```python +class ViscousDamper(CatalogEntity): + activate_gap: Literal[0, 1] = 0 + gap_length: float | None = None + + model_config = ConfigDict( + json_schema_extra={ + "dependencies": [ + {"trigger": "activate_gap", "value": 1, "restore": ["gap_length"]}, + {"trigger": "activate_gap", "value": 0, "hide": ["gap_length"]}, + ] + } + ) +``` + +### 2.7 Unit-annotated fields (`#UNITS#`) + +gidopensees marks numeric fields with `#UNITS#` where the value's +interpretation is unit-system–dependent (forces in kN, lengths in m, etc.). + +OTKO already has `core/units.py` with `UnitSystem` and +`UnitLabels`. It does **not** currently attach unit metadata to individual +model fields — the unit system is a project-level property and all numeric +values are stored in the project's native unit system, with `UnitLabels` +used only for display. + +**Intended design:** Generated catalog models will mark unit-annotated fields +using a `Field` `metadata` entry (Pydantic v2 `Annotated` style): + +```python +from typing import Annotated +from pydantic import Field + +class UnitTag: + """Marker for fields whose display label depends on UnitSystem.""" + def __init__(self, quantity: str): + self.quantity = quantity # e.g. "force", "length", "stress" + +ForceMagnitude = Annotated[float, UnitTag("force")] +LengthValue = Annotated[float, UnitTag("length")] +StressValue = Annotated[float, UnitTag("stress")] +``` + +These type aliases would live in `core/catalog/_units.py`. No conversion logic +is added to `core/`; the viewmodel layer reads `UnitTag.quantity` to +select the right `UnitLabels` field for axis labels and input hints. + +**Implementation status (2026-05-22) — DEFERRED:** The codegen tool does not +yet emit `UnitTag` annotations. Fields corresponding to `#UNITS#` entries in +the gidopensees source are currently emitted as `str` with a +`# TODO: unit-aware type` comment preserving the gidopensees default string +(e.g. `yield_stress_fy: str = '500 MPa' # TODO: unit-aware type`). This is +a conscious deferral: the `str` placeholder keeps the field present and +round-trippable without binding the codebase to a unit-system convention that +is not yet finalised. A dedicated unit-system layer — covering `UnitTag`, +`_units.py`, and viewmodel wiring — is tracked as future work and will be +addressed in a follow-up ADR before any `#UNITS#` field is promoted to stable. + +**If this convention is inadequate** (e.g. if we need per-field unit +conversion in the future), a follow-up ADR should address it before +the convention is applied beyond `catalog/`. + +### 2.8 Verification plan + +A catalog type is promoted from generated → stable only when **all three** +of the following are satisfied: + +1. **Schema equivalence test** (`tests/unit/catalog/test__schema.py`): + Constructs a model instance with the same arguments as the + corresponding Tcl example from the OpenSees Wiki and asserts that + `model.model_dump()` produces the expected dict. No OpenSeesPy import. + +2. **Round-trip test** (`tests/unit/catalog/test__roundtrip.py`): + Serialises the model to JSON (`.osmodel` fragment), deserialises it + back, and asserts equality. Confirms the discriminator and all field + aliases survive the round-trip. + +3. **Integration smoke test** (`tests/integration/catalog/test_.py`): + Builds a minimal project using the new type, runs it through + `OpenSeesRunner`, and checks that the runner does not raise and that at + least one result quantity (reaction, displacement, or force) is finite. + Tagged `@pytest.mark.slow` and skipped if `openseespy` is not installed. + +Manual review checklist (for the PR that promotes a type): + +- [ ] Attribution header present in the generated file. +- [ ] `json_schema_extra["tkwidget_hooks"]` populated where applicable. +- [ ] `json_schema_extra["dependencies"]` populated for every DEPENDENCY + in the source BOOK. +- [ ] `#UNITS#` fields use the correct `UnitTag` quantity string. +- [ ] The type discriminator value does not collide with any existing type + in `core/materials/__init__.py`, `core/sections/__init__.py`, or + `core/geometry/elements.py`. +- [ ] The integration smoke test result has been spot-checked against the + gidopensees wiki reference or an independent OpenSees Tcl run. + +--- + +## 3. Alternatives considered + +### 3A: Extend existing `core/materials/__init__.py` directly + +Rejected. The existing module is small and well-tested; adding 60+ +unverified types creates noise and makes equivalence tracking harder. +A separate `catalog/` namespace keeps the boundary clear. + +### 3B: Use gidopensees at runtime (import `.mat` on startup) + +Rejected. The GiD BOOK format is proprietary and requires the GiD parser. +Depending on gidopensees at runtime adds a third-party dependency to the +installed package and makes offline / air-gapped installs harder. The +codegen + committed-output approach keeps the package dependency-clean. + +### 3C: Hand-write every new type without the codegen tool + +Would work but loses the systematic relationship between gidopensees +DEPENDENCIES / TKWIDGET metadata and the Pydantic model. The codegen +pipeline preserves that metadata structurally so UI implementors can +reference it rather than re-reading `.mat` files. + +--- + +## 4. Risks + +| Risk | Likelihood | Impact | Mitigation | +|---|---|---|---| +| Namespace collision between `catalog/` and existing `core/` types | Medium | Medium | Verify discriminator values before promotion; CI check added to the merge checklist | +| gidopensees schema drift (upstream changes `.mat`) | Low | Medium | `generated/` is committed; only re-run codegen intentionally; diff the output and add a CHANGELOG entry | +| Testing surface explosion (60+ new types × 3 test tiers) | High | Low | Only promoted types get full test coverage; generated-but-not-yet-promoted types have schema + round-trip tests only | +| Attribution omission | Low | High | Codegen always inserts the header; a pre-commit hook (`grep -r "rclab-auth/gidopensees" core/catalog/generated/` must pass) enforces it | +| `#UNITS#` convention inadequacy | Medium | Medium | Convention is isolated to `core/catalog/_units.py`; a follow-up ADR can replace it without touching existing `core/` code | + +--- + +## 5. Out of scope for this ADR + +- `bas/` Python template parsing and code generation (separate ADR). +- `tcl/` TKWIDGET Tcl implementations (separate ADR). +- Seismic isolator element schemas (`elastomericBearing*`, + `frictionPendulumBearing`, etc.) — those are new elements, not directly + in the gidopensees BOOK format; they get their own ADR. +- IDA batch runner (analysis feature, not schema). +- Fiber-section visual editor UI polish (UI feature, not schema). +- The OpenSeesPy command emission side — `services/opensees_runner.py` + will need updates for each promoted type, but those changes follow + naturally from the schema and are reviewed in the same PR as the + integration smoke test. diff --git a/docs/adr/ADR-0002-headless-gui-dep-split.md b/docs/adr/ADR-0002-headless-gui-dep-split.md new file mode 100644 index 0000000..6f9e653 --- /dev/null +++ b/docs/adr/ADR-0002-headless-gui-dep-split.md @@ -0,0 +1,91 @@ +# ADR-0002 — Split pyproject dependencies into headless base and `gui` optional extra + +| Field | Value | +|---|---| +| **Status** | Accepted | +| **Date** | 2026-06-15 | +| **Author** | ogunc | + +--- + +## 1. Context + +`otko.core` is pure Pydantic v2 (no Qt, no OpenSeesPy imports — stated +explicitly in `core/__init__.py`). `otko.services` adds NumPy, h5py, and +OpenSeesPy for headless computation. Together, these two packages can be used from +scripts, Jupyter notebooks, and web backends **without any Qt or 3D-rendering stack**. + +Before this ADR, every `pip install otko` pulled in PySide6, pyvista, +pyvistaqt, vtk, pyqtgraph, and imageio — roughly 800 MB of GUI/visualization +packages — even when only the headless computation layer was needed. Web backends and +CI machines without a display had to work around this with `--no-deps`, which is +fragile and skips genuine compute-layer deps (numpy, h5py) too. + +## 2. Decision + +Split `[project.dependencies]` into two tiers in `pyproject.toml`: + +### Headless base (`pip install -e .`) + +Packages imported by `core/` and the non-Qt parts of `services/`: + +| Package | Where used | +|---------|-----------| +| `pydantic>=2.5` | All `core/` modules, `material_tester.py` | +| `numpy>=1.26` | `services/` computation modules (7 files) | +| `h5py>=3.10` | `opensees_runner._run_transient()`, `TransientResults` accessors | +| `openseespy==3.8.0.0` | Lazy import in `OpenSeesRunner.__init__` | +| `openseespywin==3.8.0.0 ; sys_platform=='win32'` | Windows DLL companion | + +### GUI extra (`pip install -e ".[gui]"`) + +Packages only needed by `views/`, `viewmodels/`, `commands/`, `qt_workers.py`, +and `animation_export.py` (which drives a live PyVista plotter): + +`PySide6`, `pyvista`, `pyvistaqt`, `vtk`, `pyqtgraph`, `imageio[ffmpeg]`, +`scipy` (forward-compat, currently a phantom dep), `pandas` (same). + +## 3. Consequences + +- **Desktop developers** install with `pip install -e ".[gui,dev]"`. No change to + what gets installed; only the install command changes from `.[dev]` → `.[gui,dev]`. +- **Web backends / scripts / notebooks** install with `pip install -e .` (or + `pip install otko`) and get a lean ~50 MB environment. +- **otko-web** can drop the `--no-deps` workaround and install the + package normally. The web backend's `requirements.txt` no longer needs to list + pydantic/numpy/h5py separately — they come from the base install. +- **scipy and pandas** are listed under `[gui]` as phantom deps (currently never + imported anywhere in the codebase). They are kept to avoid surprise breakage if a + future feature adds them; audited and flagged on 2026-06-15. + +## 4. Verification + +After installing only the base set: + +```python +import sys +from otko.core import Project +from otko.services.opensees_runner import OpenSeesRunner +from otko.services.material_tester import test_uniaxial_material + +# Run a modal analysis on the bundled two-storey shear frame example +import json +from pathlib import Path +from otko.core import ModalCase + +data = json.loads(Path("examples/eigen_two_storey_shear_frame.osmodel").read_text()) +project = Project.model_validate(data) +modal_case = next(c for c in project.analyses if isinstance(c, ModalCase)) +runner = OpenSeesRunner(project) +runner.build() +results = runner._run_modal(modal_case) + +assert len(results.eigenvalues) == 2 +assert all(ev > 0 for ev in results.eigenvalues) + +gui_packages = {"PySide6", "pyvista", "pyvistaqt", "vtk", "pyqtgraph"} +assert not gui_packages.intersection(sys.modules), \ + f"GUI package imported: {gui_packages & sys.modules.keys()}" + +print("PASS — headless modal analysis complete, no GUI packages imported") +``` diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..0c56d01 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,80 @@ +# Architecture + +## Layering + +OTKO uses a strict **MVVM + service layer** architecture. Dependencies +flow in **one direction only**: outer layers may depend on inner layers, never +the reverse. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ views/ Qt widgets, dialogs, 3D canvas — PySide6 only │ +│ ▲ │ +│ │ signals/slots, viewmodel binding │ +│ viewmodels/ Qt-aware adapters, QUndoStack, selection state │ +│ ▲ │ +│ │ pure Python calls │ +│ services/ OpenSeesRunner, PersistenceService, Results │ +│ ▲ │ +│ │ │ +│ core/ Project, Node, Element, Material — pure Python │ +│ NO Qt imports. NO openseespy imports. │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Why this matters + +- `core` tests without a display server, without OpenSees, without Qt. + CI runs `pytest tests/unit/` in milliseconds. +- Swapping solvers (e.g. `xara`, a future fork) touches + `services/opensees_runner.py` and nothing else. +- A future CLI or notebook front-end reuses `core` and `services` as-is. + +## Package map + +| Package | Responsibility | Allowed imports | +|---|---|---| +| `core` | Domain entities and invariants | stdlib, numpy, pydantic | +| `services` | I/O, solver invocation, persistence | core + stdlib + h5py + openseespy | +| `viewmodels` | Bridge core ↔ Qt; expose Qt signals; manage undo/redo | core, services, PySide6 | +| `views` | Pure UI; no business logic | PySide6, pyvistaqt, viewmodels | +| `commands` | `QUndoCommand` subclasses; mutate model via services | services, viewmodels | + +## Threading + +The Qt main thread owns all widgets. Heavy computation happens elsewhere: + +- **OpenSees analysis** runs in a `QThread` worker (`services.opensees_runner.AnalysisWorker`). +- The worker emits `progress(int)`, `log(str)`, `finished(ResultsHandle)` signals. +- The worker checks `QThread.currentThread().isInterruptionRequested()` between + analysis steps so the user can cancel. +- Results are written to HDF5; only a lightweight `ResultsHandle` (file path + + metadata) crosses the thread boundary. + +## Persistence + +- Project files: `*.osmodel` — a JSON document validated by Pydantic models. + Human-readable, diff-able, version-controllable. +- Results files: `*.osresults.h5` — HDF5; one group per analysis case; datasets + for displacements, reactions, element forces, stresses. + +## OpenSeesPy command sequencing + +`OpenSeesRunner` always emits commands in this order; the model layer enforces +that all required pieces exist before a run can be requested: + +1. `wipe()` and `model('basic', '-ndm', ndm, '-ndf', ndf)` +2. `node(...)` for every node +3. `fix(...)` for every restrained DOF +4. `uniaxialMaterial(...)` / `nDMaterial(...)` +5. `section(...)` (if used) +6. `geomTransf(...)` for frame elements +7. `element(...)` for every element +8. `timeSeries(...)` +9. `pattern(...)` with nested `load(...)` +10. `recorder(...)` +11. `system / numberer / constraints / integrator / algorithm / analysis` +12. `analyze(...)` + +Any deviation from this order is a runtime error in OpenSees. The runner +asserts the order at the service boundary; the UI never has to think about it. diff --git a/docs/gap-analysis-gidopensees.md b/docs/gap-analysis-gidopensees.md new file mode 100644 index 0000000..012eda6 --- /dev/null +++ b/docs/gap-analysis-gidopensees.md @@ -0,0 +1,221 @@ +# Gap Analysis — OTKO vs gidopensees + +**Source:** `D:\GitHub\gidopensees` (AUTh Lab of R/C and Masonry Structures) +**Scope:** Material / section / element / constraint / load / damping schema coverage. +**Date:** 2026-05-22 + +## How to read this table + +| Column | Meaning | +|---|---| +| **Category** | Schema group (material family, element type, etc.) | +| **Object** | Name as it appears in gidopensees BOOK/CONDITION | +| **OTKO name** | Corresponding class in `core/` (if any) | +| **In OTKO?** | ✅ fully supported · 🟡 partial · ❌ missing | +| **In gidopensees?** | ✅ · ❌ | +| **Priority** | P0 = already done · P1 = Phase 8 target · P2 = later | + +Priority rationale: +- **P0** — already shipped; included for completeness. +- **P1** — high-value for earthquake-engineering practice; aligns with Phase 8 + roadmap items (isolators, Rayleigh per-region, confined concrete models, + shell elements, floor diaphragm constraints). +- **P2** — valid but lower-frequency in typical EQ-engineering workflows + (soil p-y/t-z/q-z springs, 3-D solid elements, multi-yield plasticity, + contact elements). + +--- + +## 1. Uniaxial Materials + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Uniaxial / linear | Elastic | `ElasticUniaxial` | ✅ | ✅ | P0 | +| Uniaxial / elastic-plastic | Elastic_Perfectly_Plastic | `ElasticPP` | ✅ | ✅ | P0 | +| Uniaxial / elastic-plastic | Elastic_Perfectly_Plastic_with_Gap | — | ❌ | ✅ | P1 | +| Uniaxial / damper | Viscous | — | ❌ | ✅ | P1 | +| Uniaxial / damper | Viscous_Damper (Maxwell) | — | ❌ | ✅ | P1 | +| Uniaxial / gap | Hyperbolic_Gap | — | ❌ | ✅ | P2 | +| Uniaxial / soil | PySimple1 | — | ❌ | ✅ | P2 | +| Uniaxial / soil | TzSimple1 | — | ❌ | ✅ | P2 | +| Uniaxial / soil | QzSimple1 | — | ❌ | ✅ | P2 | +| Uniaxial / bond-slip | BondSP01 | — | ❌ | ✅ | P2 | + +## 2. Steel Uniaxial Materials + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Steel | Steel01 | `Steel01` | ✅ | ✅ | P0 | +| Steel | Steel02 | `Steel02` | ✅ | ✅ | P0 | +| Steel | Hysteretic | `HystereticMaterial` | ✅ | ✅ | P0 | +| Steel | Reinforcing_steel (DoDD-Restrepo) | — | ❌ | ✅ | P1 | +| Steel | Ramberg-Osgood_steel | — | ❌ | ✅ | P2 | + +## 3. Concrete Uniaxial Materials + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Concrete | Concrete01_(Zero_tensile_strength) | `Concrete01` | ✅ | ✅ | P0 | +| Concrete | Concrete02_(Linear_tension_softening) | `Concrete02` | ✅ | ✅ | P0 | +| Concrete | Concrete04_(Popovics) | — | ❌ | ✅ | P1 | +| Concrete | Concrete06 | — | ❌ | ✅ | P2 | +| Concrete | ConcreteCM (Chang-Mander) | — | ❌ | ✅ | P1 | + +## 4. Combined Materials + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Combination | Series | — | ❌ | ✅ | P1 | +| Combination | Parallel | — | ❌ | ✅ | P1 | +| Combination | Section_Aggregator (in .mat) | `SectionAggregator` | ✅ | ✅ | P0 | + +## 5. nD (Multi-dimensional) Materials + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| nD | Elastic_Isotropic | `ElasticIsotropic` | ✅ | ✅ | P0 | +| nD | Elastic_Orthotropic | — | ❌ | ✅ | P2 | +| nD | J2Plasticity | — | ❌ | ✅ | P2 | +| nD | Damage2p | — | ❌ | ✅ | P2 | +| nD | PressureIndependMultiYield | — | ❌ | ✅ | P2 | +| nD | PressureDependMultiYield | — | ❌ | ✅ | P2 | +| nD | PressureDependMultiYield02 | — | ❌ | ✅ | P2 | +| nD | Contact | — | ❌ | ✅ | P2 | + +## 6. Sections + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Section | Elastic_Section | `ElasticSection` | ✅ | ✅ | P0 | +| Section | Fiber | `FiberSection` | ✅ | ✅ | P0 | +| Section | Fiber_Custom | `FiberSection` (manual fibres) | 🟡 | ✅ | P0 | +| Section | FiberInt (interaction P-M) | — | ❌ | ✅ | P1 | +| Section | Plate_Fiber | — | ❌ | ✅ | P2 | +| Section | Elastic_Membrane_Plate | — | ❌ | ✅ | P2 | +| Section | LayeredShell | — | ❌ | ✅ | P2 | +| Section | Section_Aggregator | `SectionAggregator` | ✅ | ✅ | P0 | + +## 7. Beam-Column Elements + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Frame | Elastic_Beam-Column | `ElasticBeamColumn` | ✅ | ✅ | P0 | +| Frame | Elastic_Timoshenko_Beam-Column | — | ❌ | ✅ | P1 | +| Frame | Force-Based_Beam-Column | `ForceBeamColumn` | ✅ | ✅ | P0 | +| Frame | Displacement-Based_Beam-Column | `DispBeamColumn` | ✅ | ✅ | P0 | +| Frame | Flexure-Shear_Interaction_DispBeamColumn | — | ❌ | ✅ | P2 | +| Frame | BeamWithHinges | `BeamWithHingesElement` | ✅ | ❌ | P0 | + +## 8. Truss Elements + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Truss | Truss | `TrussElement` | ✅ | ✅ | P0 | +| Truss | Corotational_Truss | `CorotTrussElement` | ✅ | ✅ | P0 | + +## 9. Surface / Plate Elements + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Surface | Quad | `QuadElement` | ✅ | ✅ | P0 | +| Surface | Shell (ShellMITC4 / MITC4) | — | ❌ | ✅ | P1 | +| Surface | ShellDKGQ | — | ❌ | ✅ | P1 | +| Surface | Tri31 | — | ❌ | ✅ | P2 | +| Surface | QuadUP (u-p pore pressure) | — | ❌ | ✅ | P2 | + +## 10. Solid Elements + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Solid | Standard_Brick_Element | — | ❌ | ✅ | P2 | + +## 11. Zero-Length / Special Elements + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Special | Auto_Zero_Length (per-DOF uniaxial) | `ZeroLengthElement` | ✅ | ✅ | P0 | +| Special | Auto_equal_constraint (auto equalDOF) | `EqualDOFConstraint` | ✅ | ✅ | P0 | +| Special | ZeroLengthSection | `ZeroLengthSectionElement` | ✅ | ❌ | P0 | +| Special | BeamContact (master/slave) | — | ❌ | ✅ | P2 | + +## 12. Restraints (Boundary Conditions) + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Restraint | Point_Restraints | Node.restraint (6-tuple) | ✅ | ✅ | P0 | +| Restraint | Line_Restraints (auto-apply to nodes on line) | — | ❌ | ✅ | P2 | +| Restraint | Surface_Restraints (auto-apply to nodes on surface) | — | ❌ | ✅ | P2 | + +## 13. Nodal Loads & Displacements + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Load | Point_Forces | `NodalLoad` | ✅ | ✅ | P0 | +| Load | Line_Forces (nodal, along a line) | — | ❌ | ✅ | P2 | +| Load | Surface_Forces (nodal, on a surface) | — | ❌ | ✅ | P2 | +| Load | Line_Uniform_Forces | `UniformElementLoad` | ✅ | ✅ | P0 | +| Load | Point_Displacements (imposed) | — | ❌ | ✅ | P1 | +| Load | Line_Displacements (imposed, on nodes along line) | — | ❌ | ✅ | P2 | +| Load | Surface_Displacements (imposed, on nodes on surface) | — | ❌ | ✅ | P2 | + +## 14. Ground Motions + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Ground motion | Point_Ground_Motion_from_Record | `PathTimeSeries` + `UniformExcitationPattern` | ✅ | ✅ | P0 | +| Ground motion | Point_Sine_Ground_Motion | — (no `TrigTimeSeries`) | ❌ | ✅ | P1 | +| Ground motion | Records (BOOK 8 — ground motion file library) | `PathTimeSeries.file_path` (single file, no library) | 🟡 | ✅ | P1 | + +## 15. Constraints + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Constraint | Point_Equal_constraint (master + slave) | `EqualDOFConstraint` | ✅ | ✅ | P0 | +| Constraint | Line_Equal_constraint (slave nodes on line) | — | ❌ | ✅ | P1 | +| Constraint | Point_Rigid_link (Bar / Beam) | — | ❌ | ✅ | P1 | +| Constraint | Line_Rigid_link (slave nodes on line) | — | ❌ | ✅ | P1 | +| Constraint | Point_Rigid_diaphragm (master + slave, XY/YZ/ZX plane) | — | ❌ | ✅ | P1 | +| Constraint | Line_Rigid_diaphragm (slave nodes on line) | — | ❌ | ✅ | P1 | + +## 16. Mass + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Mass | Point_Mass | node mass (Properties dock + SetMassCommand) | ✅ | ✅ | P0 | +| Mass | Line_Mass (auto-lump to nodes) | — | ❌ | ✅ | P1 | +| Mass | Surface_Mass | — | ❌ | ✅ | P2 | +| Mass | Volume_Mass | — | ❌ | ✅ | P2 | + +## 17. Rayleigh Damping + +| Category | Object (gidopensees) | OTKO name | In OTKO? | In gidopensees? | Priority | +|---|---|---|---|---|---| +| Damping | Global αM + βK (TransientCase fields) | `TransientCase.rayleigh_alpha_m/beta_k` | ✅ | 🟡 | P0 | +| Damping | Mode-1 stiffness-proportional βK auto-compute | `TransientCase.rayleigh_mode1_damping` | ✅ | ❌ | P0 | +| Damping | Per-region Rayleigh (Line/Surface/Volume/Point) | — | ❌ | ✅ | P1 | + +--- + +## Summary + +| Status | Count | +|---|---| +| ✅ Fully in OTKO | 34 | +| 🟡 Partial | 3 | +| ❌ P1 targets (Phase 8 additions) | 23 | +| ❌ P2 deferred | 21 | + +**Top P1 targets** (highest EQ-engineering impact, not in OTKO yet): + +1. `ElasticPP_with_Gap` — bearing pad / isolation gap nonlinearity +2. `Viscous` / `Viscous_Damper` — supplemental damping devices +3. `ReinforcingSteel` — DoDD-Restrepo model for well-detailed rebar +4. `Concrete04` (Popovics) / `ConcreteCM` (Chang-Mander) — better confined concrete +5. `Series` / `Parallel` — material combination building blocks for isolation systems +6. `RigidDiaphragm` — floor slab constraint, essential for 3D building models +7. `RigidLink` — column/beam offset rigid connections +8. `Shell` (MITC4 / ShellDKGQ) — wall / slab elements +9. `FiberInt` — P-M interaction section for axial-flexure coupling +10. `PointDisplacement` imposed load — displacement-based loading at nodes +11. Per-region Rayleigh damping — finer damping control for mixed models +12. Sine ground motion / ground motion record library — GM workflow completion diff --git a/docs/logo.svg b/docs/logo.svg new file mode 100644 index 0000000..3b6f06f --- /dev/null +++ b/docs/logo.svg @@ -0,0 +1,46 @@ + + + OTKO + Wordmark logo for OTKO — a SAP2000-style desktop GUI for OpenSeesPy. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OTKO + + + A SAP2000-STYLE GUI FOR OPENSEESPY + + + diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..00d0d55 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,197 @@ +# Roadmap + +Eight phases. 0–7 are the core GUI plus post-processing. Phase 8 is the +earthquake-engineering primitives — the part that makes it a research +tool instead of a model viewer. + +Status legend: ✅ done · 🟡 partial · ⬜ planned · ✂️ deferred / out-of-scope. + +## Phase 0 — Scaffolding ✅ +- ✅ Repo, `.gitignore`, `pyproject.toml` +- ✅ Pre-commit + ruff + mypy +- ✅ GitHub Actions CI (Linux/Mac/Win × Py 3.10–3.12) +- ✅ `python -m otko` opens a `MainWindow` with PyVista 3D + viewport, model-tree dock, property dock, console dock, working-plane + toolbar, and a full menu bar (File / Edit / Define / Assign / + Analyze / Display / View / Options / Help) +- 🟡 Application icon and About dialog — wordmark logo done; native + OS icon (`.ico` / `.icns`) still pending + +## Phase 1 — Core Data Model ✅ +- ✅ `core.geometry`: `Node`, `Element`, `TrussElement`, `CorotTrussElement`, + `ElasticBeamColumn`, `ForceBeamColumn`, `DispBeamColumn`, + `ZeroLengthElement`, `ZeroLengthSectionElement`, `BeamWithHingesElement`, + `QuadElement`, plus `GridSystem` / `CoordinateSystem` +- ✅ `core.materials`: `ElasticIsotropic`, `ElasticUniaxial`, `ElasticPP`, + `Steel01`, `Steel02`, `Concrete01`, `Concrete02`, `HystereticMaterial` +- ✅ `core.sections`: `ElasticSection`, `FiberSection` with rectangular + / circular patches and straight rebar layers, `SectionAggregator` +- ✅ `core.loads`: `NodalLoad`, `UniformElementLoad`, `LinearTimeSeries`, + `ConstantTimeSeries`, `PathTimeSeries`, `PlainLoadPattern`, + `UniformExcitationPattern`, `ResponseSpectrum` +- ✅ `core.analysis`: `StaticCase`, `ModalCase`, `TransientCase`, + `PushoverCase`, `ResponseSpectrumCase` — including chained preload + via `preload_case_ids` and pattern removal for free-vibration runs +- ✅ `core.constraints`: `EqualDOFConstraint` (multi-point constraints) +- ✅ `core.project.Project` aggregator with id allocation, validation, + `validate_references()` +- ✅ `services.persistence`: `.osmodel` (Pydantic JSON) load/save with + round-trip-clean assertion in every example script + +## Phase 2 — OpenSees Service ✅ +- ✅ `services.opensees_runner.OpenSeesRunner` emits commands in the + canonical order documented in [`architecture.md`](architecture.md) +- ✅ Verified examples (matched analytically or against the OpenSees + Wiki Tcl reference): cantilever (point + UDL), portal frame, basic + truss, SDOF pushover, RC frame gravity / pushover / earthquake, + Examples 1–4 family, two-storey shear / one-bay frames, simply + supported beam with quad elements +- ✅ `AnalysisWorker(QObject)` runnable inside a `QThread` with + `progress(int)` / `log(str)` / `finished(ResultsHandle)` signals + +## Phase 3 — 3D Viewport ✅ +- ✅ `views.canvas3d.ModelCanvas` (subclass of `QtInteractor` from pyvistaqt) +- ✅ Grid plane, world axes triad, view-cube-style preset buttons + (Isometric / Top XY / Front XZ / Right YZ), parallel projection toggle +- ✅ Node rendering as glyphs; element rendering as tubes (frames / + trusses) and shells (quads); supports rendered as gizmos +- ✅ Mouse picking → `nodePicked` / `elementPicked` signals; pixel-space + grid snap (rejects clicks more than 15 px from an intersection) +- ✅ Selection highlighting with in-place colour updates + +## Phase 4 — Modeling Tools ✅ +- ✅ Grid system dialog (X / Y / Z spacing, generates nodes); SAP2000-style + table editor; off-grid clicks rejected +- ✅ Working-plane filter — grid + snap restricted to the active level +- ✅ Draw Node / Draw Frame / Draw Truss tools with hover snap highlight +- ✅ Inline element editing from the Properties dock (truss area, + any scalar field) +- ✅ Assign Support tool (Free / Pin / Roller / Fix + custom 6-DOF dialog) +- ✅ Assign Load: nodal loads, distributed beam loads, ground motions +- ✅ Assign EqualDOF (multi-point constraints) from the UI +- ✅ Show Extruded Sections toolbar shortcut +- ✅ Undo / Redo via `QUndoStack` for every model mutation +- 🟡 Replicate / Mirror / Move / Extrude — basic copy works; story-extrude + and mirror still pending + +## Phase 5 — Properties ✅ +- ✅ Material library dialog (CRUD) +- ✅ Section library dialog (incl. `FiberSection` rows that previously + crashed are now handled) +- ✅ Property editor dock — context-aware, multi-selection assignment, + inline mass editor +- 🟡 Fiber-section visual editor — exists; some UI polish still needed + +## Phase 6 — Analysis Pipeline ✅ +- ✅ Analysis case manager dialog with case-type factories +- ✅ Run dialog with progress + log + cancel +- ✅ Per-run Rayleigh damping override (no project mutation) +- ✅ Results stored to `.osresults.h5` +- 🟡 Convergence diagnostics view (residuals per step) — partial info + in run log; dedicated diagnostics dock pending + +## Phase 7 — Post-processing ✅ +- ✅ Deformed shape with scale-factor slider +- ✅ Mode-shape animator (1-indexed; play / scrub / scale) +- ✅ Element force diagrams (axial, shear, moment) with auto-pick of + the largest-magnitude component on dock open, numerical labels at + global min/max ends +- ✅ Time-history plotter (pyqtgraph) with displacement / velocity / + acceleration switching +- ✅ Hysteresis plotter — node DOF orbits and element local-force loops +- ✅ Pushover curve view in display units +- ✅ Response-spectrum view (Sa-T curve with modal-period markers and a + mass-participation table) +- ✅ Snapshot / video export (mode shapes + time histories) via + `imageio[ffmpeg]` +- ⬜ **Render performance pass** — collapse per-entity actors into glyphed + PolyData (single draw call), in-place colour updates for selection, + AA, lower-tessellation spheres. Target: 10k nodes / 20k frames @ 30 fps + +## Phase 8 — Earthquake Engineering 🟡 +- ✅ `HystereticMaterial`, `BeamWithHinges`, `FiberSection` → all + flowing into the runner, end-to-end pushover example +- ✅ Response spectrum generator + SRSS / CQC modal combination +- ✅ Ground-motion import via `PathTimeSeries` + `UniformExcitationPattern`, + with an example wired up against the OpenSees A10000 record +- ✅ `ZeroLengthSectionElement` for moment-curvature workflows; closed-form + verification example shipped +- ✅ `Concrete04` (Popovics) end-to-end: model → runner → UI form → tests → + fiber-section cantilever example +- ✅ **Material Tester service** (`services/material_tester.py`) — headless, + Qt-free; runs any uniaxial material through a monotonic or cyclic strain + protocol in an isolated single-element model and returns the full + stress–strain history. Verified: Elastic linearity, ElasticPP plateau, + Steel01 hysteresis energy (EPP formula, <1%), Concrete04 Popovics C1 + continuity; state-cleanup and interleave proofs. +- ⬜ **Material Tester dialog** — Qt front-end for the service above; live + stress–strain plot with strain-amplitude and step controls +- ⬜ Seismic isolators: `elastomericBearing*`, `frictionPendulumBearing`, + `singleFPBearing`, `TripleFrictionPendulum` +- ⬜ Ground-motion library (PEER-style record set + scaling tools) +- ⬜ IDA (Incremental Dynamic Analysis) batch runner +- 🟡 Fiber-section editor — exists for rectangular / circular sections; + confined / unconfined visual presets pending + +## Out-of-scope (for now) +- ✂️ Code-checking (TBDY-2018, ASCE 41, Eurocode 8) +- ✂️ Soil-structure interaction GUI +- ✂️ Cloud / collaborative editing +- ✂️ Native shell-element rendering / pre-processing (quads exist as a + primitive, but a proper shell workflow is its own phase) + +## Next sessions — backlog (Sept 2026 cooldown session) + +Session handoff first: ~60 files of uncommitted work in the tree +(Table dock, extrusion shapes, exporter, pattern_factors, all audit +fixes). Commit per Conventional Commits on `develop` before new work +(`feat:`/`fix:` split per lane), then `ruff check src tests && +ruff format src tests`, `mypy`, `pytest -m "not slow"`. + +Requested (user-ordered): + +1. ⬜ Toolbar button icons — `resources/icons/` exists; wire `QIcon`s + in `menu_builder.py` toolbar builders (`@designer` lane: layout, + hierarchy, affordances). Include OS icon (`.ico`/`.icns`, Phase 0 🟡). +2. ⬜ Shell objects — analysis first: `QuadElement` today is continuum + (plane stress/strain). Scope OpenSees `ShellMITC4`/`ShellDKGQ` + + shell sections/materials, then core element + `_emit` + renderer + quad→shell + section dialog. Keep solver-source untouched + (emit `-factor`-style floats only). (`@oracle` for the scope call.) +3. ⬜ Input-dialog layout/usability pass — continue the Lane C pattern: + `QFormLayout` consistency, prefill from selection (done for assign + dialogs), inline validation messages instead of silent reverts, + units-aware labels. (`@designer` for layout, orchestrator for copy.) +4. ⬜ Hover tooltips — two halves: (a) canvas entity hover (node/element + id + key values via existing picking signals); (b) widget tooltip + audit (every toolbar button/dialog field documents itself). +5. ⬜ Load visibility filter — show only loads of the selected pattern / + case; hide the rest. Renderer load-overlay filter + selector combo + (builds on the Table Loads tabs' pattern filter). Natural home: + View toolbar next to Show Local Axes. + +Identified this session (audit + build leftovers): + +6. ⬜ Named load combinations (deferred phase 3) — `pattern_factors` + covers per-case factoring; add a reusable named-combo entity only + if one combo must be shared across many cases. +7. ⬜ Per-row Run + status in Table Analyses tab — run control lives + only in the Run dialog today; add per-case Run button + last-run + status (converged/failed/when). No overlap: the tab lists cases, + this operates them. +8. ⬜ Case-manager edit preservation — `modelMutated` while the manager + is open rebuilds the form and discards in-progress edits (audit C2). +9. ⬜ Table dock phase-4 polish — CSV copy/paste, column visibility + (deferred from the Table plan). +10. ⬜ Display-settings persistence — extruded-sections / local-axes / + parallel-projection toggles reset per project; persist viewport + prefs in `QSettings` (no `DisplaySettings` module exists yet). +11. ⬜ GUI test coverage under xvfb — `views/*` omitted from coverage; + lanes verified via offscreen smoke only. Add pytest-qt tests for: + dock toggles, Level refresh, post-state teardown, Table edits, + factor spins, local-axes overlay. +12. ⬜ Pin ruff version — local ruff (0.15.x) flags pre-existing drift + (UP037/RUF001/RUF003/E702) that repo CI doesn't; pin in + `pyproject.toml` or baseline-allowlist so `ruff check` is green. +13. ⬜ Exported-Tcl round trip — `.py` export is solver-verified; + add an equivalent exec-and-compare test for the `.tcl` renderer. diff --git a/docs/screenshots/main_window.png b/docs/screenshots/main_window.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f38cb62dabe756e06f890aba2b761c88a1f98d GIT binary patch literal 516835 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�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