visualdynamics 0.1.0a44__tar.gz → 0.1.0a45__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. {visualdynamics-0.1.0a44/src/visualdynamics.egg-info → visualdynamics-0.1.0a45}/PKG-INFO +1 -1
  2. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/__init__.py +1 -1
  3. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/data.py +41 -32
  4. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/filters.py +1 -6
  5. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/geometry.py +258 -26
  6. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/mesh.py +46 -11
  7. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/modal_fit.py +48 -15
  8. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/report.py +0 -19
  9. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/sine.py +5 -26
  10. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/sine_tracking.py +5 -3
  11. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/truncate.py +2 -8
  12. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/demo/barc.py +152 -84
  13. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/author_panel.py +0 -1
  14. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/averaging_panel.py +6 -13
  15. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/filter_panel.py +15 -43
  16. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/icons.py +37 -2
  17. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/main_window.py +548 -74
  18. visualdynamics-0.1.0a45/src/visualdynamics/gui/merge_panel.py +105 -0
  19. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/mesh_panel.py +26 -6
  20. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/octave_panel.py +3 -8
  21. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/panes.py +5 -2
  22. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/report_editor.py +2 -3
  23. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/rigid_panel.py +2 -6
  24. visualdynamics-0.1.0a45/src/visualdynamics/gui/settings_panel.py +144 -0
  25. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/sine_panel.py +10 -48
  26. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/tables.py +65 -5
  27. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/truncate_panel.py +4 -10
  28. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/wavelet_panel.py +4 -10
  29. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/adf.py +15 -6
  30. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf.py +0 -15
  31. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/__init__.py +4 -3
  32. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/bands.py +0 -1
  33. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/tracking_filter.py +0 -1
  34. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/project.py +36 -9
  35. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/report/__init__.py +12 -25
  36. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/theme.py +2 -0
  37. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/units.py +3 -2
  38. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/banded.py +2 -15
  39. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/geometry.py +70 -30
  40. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/sinespec.py +30 -16
  41. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45/src/visualdynamics.egg-info}/PKG-INFO +1 -1
  42. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics.egg-info/SOURCES.txt +1 -0
  43. visualdynamics-0.1.0a44/src/visualdynamics/gui/settings_panel.py +0 -52
  44. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/LICENSE +0 -0
  45. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/README.md +0 -0
  46. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/pyproject.toml +0 -0
  47. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/setup.cfg +0 -0
  48. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/__main__.py +0 -0
  49. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/compatibility.py +0 -0
  50. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/__init__.py +0 -0
  51. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/author.py +0 -0
  52. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/averaging.py +0 -0
  53. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/channel_table.py +0 -0
  54. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/compliance.py +0 -0
  55. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/correlate.py +0 -0
  56. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/detect.py +0 -0
  57. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/entities.py +0 -0
  58. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/fem.py +0 -0
  59. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/kurtosis.py +0 -0
  60. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/matches.py +0 -0
  61. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/merge.py +0 -0
  62. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/octave.py +0 -0
  63. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/photos.py +0 -0
  64. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/progress.py +0 -0
  65. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/replication.py +0 -0
  66. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/rigid.py +0 -0
  67. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/shapes.py +0 -0
  68. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/shocks.py +0 -0
  69. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/snr.py +0 -0
  70. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/srs.py +0 -0
  71. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/tables.py +0 -0
  72. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/transform.py +0 -0
  73. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/unit_choices.py +0 -0
  74. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/validate.py +0 -0
  75. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/wavelet.py +0 -0
  76. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/decimate.py +0 -0
  77. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/deform.py +0 -0
  78. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/demo/__init__.py +0 -0
  79. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/demo/drone.py +0 -0
  80. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/demo/frame.py +0 -0
  81. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/demo/plate.py +0 -0
  82. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/__init__.py +0 -0
  83. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/app_icon.py +0 -0
  84. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/ask.py +0 -0
  85. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/console.py +0 -0
  86. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/disclaimer.py +0 -0
  87. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/drawer_tab.py +0 -0
  88. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/editors.py +0 -0
  89. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/glyphs.py +0 -0
  90. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
  91. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
  92. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/movie.py +0 -0
  93. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/object_tables.py +0 -0
  94. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/preferences.py +0 -0
  95. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/project_tree.py +0 -0
  96. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/record_grid.py +0 -0
  97. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/run_reports.py +0 -0
  98. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/shock_panel.py +0 -0
  99. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/stage_drag.py +0 -0
  100. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/stream_window.py +0 -0
  101. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/toolbars.py +0 -0
  102. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/updater.py +0 -0
  103. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/windows.py +0 -0
  104. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/__init__.py +0 -0
  105. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_objects.py +0 -0
  106. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/LICENSE +0 -0
  107. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/activity_result.txt +0 -0
  108. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/channel_table.txt +0 -0
  109. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/data.txt +0 -0
  110. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/data_physics_random_control_parameters.txt +0 -0
  111. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/data_physics_shock_response_synthesis_control_parameters.txt +0 -0
  112. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/data_physics_sine_control_parameters.txt +0 -0
  113. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/geometry.txt +0 -0
  114. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/global_analysis_attributes.txt +0 -0
  115. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/global_test_attributes.txt +0 -0
  116. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/matrix.txt +0 -0
  117. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/mechanical_control_parameters.txt +0 -0
  118. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/modal_test_parameters.txt +0 -0
  119. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/mode.txt +0 -0
  120. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/parameter_set.txt +0 -0
  121. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/point_cloud.txt +0 -0
  122. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/rattlesnake_random_control_parameters.txt +0 -0
  123. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/rattlesnake_sysid_parameters.txt +0 -0
  124. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/rattlesnake_transient_control_parameters.txt +0 -0
  125. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/response_spectrum.txt +0 -0
  126. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/scalar.txt +0 -0
  127. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/unknown.txt +0 -0
  128. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/vector.txt +0 -0
  129. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/vibration_channel_table.txt +0 -0
  130. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/excel.py +0 -0
  131. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/exodus.py +0 -0
  132. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/exporters.py +0 -0
  133. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/femap.py +0 -0
  134. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/matlab.py +0 -0
  135. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/nastran.py +0 -0
  136. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/native.py +0 -0
  137. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/notes.py +0 -0
  138. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/photo_files.py +0 -0
  139. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/punch.py +0 -0
  140. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/rattlesnake.py +0 -0
  141. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/rattlesnake_sine.py +0 -0
  142. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
  143. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/report_template.py +0 -0
  144. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/sdynpy_data.py +0 -0
  145. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/sdynpy_npz.py +0 -0
  146. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
  147. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/sep005.py +0 -0
  148. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/sniffing.py +0 -0
  149. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/step.py +0 -0
  150. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/stl.py +0 -0
  151. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/threemf.py +0 -0
  152. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/unv.py +0 -0
  153. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/names.py +0 -0
  154. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/averaging.py +0 -0
  155. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/bars.py +0 -0
  156. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/filtering.py +0 -0
  157. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/scalogram.py +0 -0
  158. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/shocks.py +0 -0
  159. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/sine_tracking.py +0 -0
  160. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/truncation.py +0 -0
  161. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/py.typed +0 -0
  162. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/report/markdown.py +0 -0
  163. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/report/page.py +0 -0
  164. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/rotate.py +0 -0
  165. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/update.py +0 -0
  166. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/__init__.py +0 -0
  167. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/_quiet.py +0 -0
  168. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/animate.py +0 -0
  169. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/mac_bars.py +0 -0
  170. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/marks.py +0 -0
  171. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/paired.py +0 -0
  172. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/pick.py +0 -0
  173. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/rigid.py +0 -0
  174. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/scalogram.py +0 -0
  175. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/waterfall.py +0 -0
  176. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
  177. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics.egg-info/entry_points.txt +0 -0
  178. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics.egg-info/requires.txt +0 -0
  179. {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visualdynamics
3
- Version: 0.1.0a44
3
+ Version: 0.1.0a45
4
4
  Summary: Structural Dynamics Visualization: a units-aware structural dynamics analysis toolset
5
5
  Author-email: Brandon Zwink <bzwink@gmail.com>
6
6
  License-Expression: MIT
@@ -90,7 +90,7 @@ from .units import (
90
90
  #: alpha, said in the version itself so the dialog, the About text, the
91
91
  #: installers and latest.json all say the same thing, and the first
92
92
  #: non-alpha release is offered as an upgrade (Brandon, 2026-09-11)
93
- __version__ = '0.1.0a44'
93
+ __version__ = '0.1.0a45'
94
94
 
95
95
 
96
96
  def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
@@ -122,6 +122,12 @@ REAL_FLOOR = 1e-9
122
122
  def _even_steps(abscissa, what: str) -> float:
123
123
  """The sample step, or a refusal that says which way it is wrong.
124
124
 
125
+ `what` names the act that needs the step, as the subject of the
126
+ refusal: 'Computing spectra', 'A sample rate'. Every reader of a
127
+ time history's step comes here, the sine extraction included —
128
+ it kept a copy of its own that checked the spacing but not the
129
+ order (2026-10-10).
130
+
125
131
  An abscissa is stored as it arrived — uneven spacing and out-of-order
126
132
  samples are both real, and the storage refuses neither (see
127
133
  `DataArray.abscissa`). What cannot be done on such a record is
@@ -131,15 +137,17 @@ def _even_steps(abscissa, what: str) -> float:
131
137
  record was simply in the wrong order.
132
138
  """
133
139
  steps = np.diff(abscissa)
140
+ if not len(steps):
141
+ raise ValueError(f'{what} needs at least two time samples')
134
142
  if (steps <= 0).any():
135
143
  first = int(np.flatnonzero(steps <= 0)[0])
136
144
  raise ValueError(
137
- f'{what} need time samples in order; sample {first + 1} is at '
145
+ f'{what} needs time samples in order; sample {first + 1} is at '
138
146
  f'{float(abscissa[first + 1]):g} s, after {float(abscissa[first]):g}')
139
147
  if not np.allclose(steps, steps[0], rtol=1e-6, atol=0):
140
148
  widest = float(np.abs(steps - steps[0]).max())
141
149
  raise ValueError(
142
- f'{what} need evenly spaced time samples; the step varies by '
150
+ f'{what} needs evenly spaced time samples; the step varies by '
143
151
  f'{widest:g} s across the record')
144
152
  return float(steps[0])
145
153
 
@@ -440,6 +448,28 @@ class DataArray:
440
448
  raise ValueError(f'{name} has {len(out)} entries, expected {n}')
441
449
  return out
442
450
 
451
+ def record_fields(self) -> dict[str, Any]:
452
+ """Each record's description — DOFs, quantity, units, hint,
453
+ comment and block — as the keyword arguments a derived object of
454
+ the same records is made with, every list a copy.
455
+
456
+ A filter, a truncation and an octave banding keep their source's
457
+ records and change only the numbers; this is the one statement
458
+ of what "the same records" carries, where it was written out at
459
+ each (2026-10-10). Override `comment` where the derivation says
460
+ what it did.
461
+ """
462
+ return {
463
+ 'response_dof': list(self.response_dof),
464
+ 'reference_dof': (None if self.reference_dof is None
465
+ else list(self.reference_dof)),
466
+ 'ordinate_dim': list(self.ordinate_dim),
467
+ 'ordinate_unit': list(self.ordinate_unit),
468
+ 'reference_unit': list(self.reference_unit),
469
+ 'dimension_hint': list(self.dimension_hint),
470
+ 'comment': list(self.comment),
471
+ 'block': None if self.block is None else list(self.block)}
472
+
443
473
  def rename_dof(self, old: str, new: str,
444
474
  quantity: str | None = None) -> int:
445
475
  """Give a channel's coordinate a new name, in place.
@@ -1087,9 +1117,7 @@ class TimeHistory(DataArray):
1087
1117
  Spectrum with one record per channel, carrying the channel's
1088
1118
  own quantity and units.
1089
1119
  """
1090
- if len(self.abscissa) < 2:
1091
- raise ValueError('Spectra need at least two time samples')
1092
- step = _even_steps(self.abscissa, 'Spectra')
1120
+ step = _even_steps(self.abscissa, 'Computing spectra')
1093
1121
  groups = {}
1094
1122
  for i, dof in enumerate(self.response_dof):
1095
1123
  key = (dof, self.ordinate_dim[i], self.ordinate_unit[i],
@@ -1113,9 +1141,7 @@ class TimeHistory(DataArray):
1113
1141
  @property
1114
1142
  def sample_rate(self) -> float:
1115
1143
  """Samples per second, from the abscissa — which must be even."""
1116
- if len(self.abscissa) < 2:
1117
- raise ValueError('a sample rate needs at least two time samples')
1118
- return 1.0 / _even_steps(self.abscissa, 'a sample rate')
1144
+ return 1.0 / _even_steps(self.abscissa, 'A sample rate')
1119
1145
 
1120
1146
  def channel_key(self, i: int) -> tuple[str, str, str | None]:
1121
1147
  """What makes record `i` the same channel as another.
@@ -1280,9 +1306,7 @@ class TimeHistory(DataArray):
1280
1306
 
1281
1307
  if averaging is None:
1282
1308
  averaging = self.averaging
1283
- if len(self.abscissa) < 2:
1284
- raise ValueError('PSDs need at least two time samples')
1285
- step = _even_steps(self.abscissa, 'PSDs')
1309
+ step = _even_steps(self.abscissa, 'Computing PSDs')
1286
1310
  fs = 1.0 / step
1287
1311
  samples = len(self.abscissa)
1288
1312
  if averaging is None:
@@ -3079,16 +3103,9 @@ class Psd(_Bands, DataArray):
3079
3103
  banded = resample(frequencies, self.ordinate, bounds, widths,
3080
3104
  source=self.bin_bounds())
3081
3105
  out = Psd(centers, banded,
3082
- response_dof=list(self.response_dof),
3083
- reference_dof=(None if self.reference_dof is None
3084
- else list(self.reference_dof)),
3085
- ordinate_dim=list(self.ordinate_dim),
3086
- ordinate_unit=list(self.ordinate_unit),
3087
- reference_unit=list(self.reference_unit),
3088
- dimension_hint=list(self.dimension_hint),
3089
- block=None if self.block is None else list(self.block),
3090
- bandwidth=widths,
3091
- comment=f'1/{per_octave} octave bands')
3106
+ **{**self.record_fields(),
3107
+ 'comment': f'1/{per_octave} octave bands'},
3108
+ bandwidth=widths)
3092
3109
  # banding conserves the area and changes nothing about what the
3093
3110
  # object is — a held comparison scale included, so the report's
3094
3111
  # own banding compares what the narrowband comparison compared
@@ -3615,17 +3632,9 @@ class Specification(Bounded, Psd):
3615
3632
  limits[name] = out
3616
3633
  out = Specification(
3617
3634
  centers, banded,
3618
- response_dof=list(self.response_dof),
3619
- reference_dof=(None if self.reference_dof is None
3620
- else list(self.reference_dof)),
3621
- ordinate_dim=list(self.ordinate_dim),
3622
- ordinate_unit=list(self.ordinate_unit),
3623
- reference_unit=list(self.reference_unit),
3624
- dimension_hint=list(self.dimension_hint),
3625
- block=None if self.block is None else list(self.block),
3626
- bandwidth=widths,
3627
- comment=f'1/{per_octave} octave bands',
3628
- **limits)
3635
+ **{**self.record_fields(),
3636
+ 'comment': f'1/{per_octave} octave bands'},
3637
+ bandwidth=widths, **limits)
3629
3638
  out.interpolation = 'bin'
3630
3639
  out.scale_db = self.scale_db
3631
3640
  constraints = getattr(self, 'band_constraints', None)
@@ -255,12 +255,7 @@ def filtered(data: TimeHistory, filtering: Filtering) -> TimeHistory:
255
255
  sos = design(filtering, rate)
256
256
  result = TimeHistory(
257
257
  data.abscissa, threaded_sosfiltfilt(sos, np.real(data.ordinate)),
258
- response_dof=list(data.response_dof),
259
- ordinate_dim=list(data.ordinate_dim),
260
- ordinate_unit=list(data.ordinate_unit),
261
- dimension_hint=list(data.dimension_hint),
262
- comment=list(data.comment),
263
- block=None if data.block is None else list(data.block))
258
+ **data.record_fields())
264
259
  return carrying_marks(data, result)
265
260
 
266
261
 
@@ -1203,6 +1203,32 @@ class Geometry:
1203
1203
  return [int(self.elem_id[i])
1204
1204
  for i in np.flatnonzero(self.elem_group == int(group))]
1205
1205
 
1206
+ def nodes_in(self, groups: Ids) -> list[int]:
1207
+ """The ids of the nodes the elements of these element groups
1208
+ use, ascending — what a merge scoped to them may touch.
1209
+
1210
+ Parameters
1211
+ ----------
1212
+ groups : sequence of int
1213
+ Element groups, by identifier.
1214
+
1215
+ Returns
1216
+ -------
1217
+ list of int
1218
+ The node identifiers.
1219
+
1220
+ Raises
1221
+ ------
1222
+ ValueError
1223
+ When a group is not in the geometry.
1224
+ """
1225
+ groups = [int(g) for g in groups]
1226
+ missing = [g for g in groups if g not in self.group_id.tolist()]
1227
+ if missing:
1228
+ raise ValueError(f'no element group {missing[0]}')
1229
+ return sorted({int(n) for k in np.flatnonzero(
1230
+ np.isin(self.elem_group, groups)) for n in self.elem_conn[k]})
1231
+
1206
1232
  def add_group(self, name: str = '', group_id: int | None = None) -> int:
1207
1233
  """Declare an element group. Returns its id.
1208
1234
 
@@ -1466,7 +1492,8 @@ class Geometry:
1466
1492
  self.validate()
1467
1493
  return {'nodes': removed_nodes, 'elements': len(orphan_elements)}
1468
1494
 
1469
- def coincident_nodes(self, tolerance: float) -> dict[int, int]:
1495
+ def coincident_nodes(self, tolerance: float,
1496
+ nodes: Ids | None = None) -> dict[int, int]:
1470
1497
  """{node: the node it coincides with}: every node within
1471
1498
  `tolerance` of another, mapped to the lowest id among those it is
1472
1499
  joined to — the one a merge keeps. Chains join: a within tolerance
@@ -1477,6 +1504,10 @@ class Geometry:
1477
1504
  tolerance : float
1478
1505
  How close two nodes must be to be one point, as the
1479
1506
  coordinates are held: meters once units are defined.
1507
+ nodes : sequence of int, optional
1508
+ Only these nodes are candidates, by id: a node outside them
1509
+ is not merged, nor merged into, however close. Every node
1510
+ when omitted.
1480
1511
 
1481
1512
  Returns
1482
1513
  -------
@@ -1485,10 +1516,13 @@ class Geometry:
1485
1516
  """
1486
1517
  from scipy.spatial import cKDTree
1487
1518
 
1488
- if len(self.node_id) < 2:
1519
+ rows = (np.arange(len(self.node_id)) if nodes is None
1520
+ else np.flatnonzero(np.isin(self.node_id,
1521
+ [int(n) for n in nodes])))
1522
+ if len(rows) < 2:
1489
1523
  return {}
1490
- pairs = cKDTree(self.node_xyz).query_pairs(float(tolerance))
1491
- parent = list(range(len(self.node_id)))
1524
+ pairs = cKDTree(self.node_xyz[rows]).query_pairs(float(tolerance))
1525
+ parent = list(range(len(rows)))
1492
1526
 
1493
1527
  def root(i: int) -> int:
1494
1528
  while parent[i] != i:
@@ -1501,18 +1535,18 @@ class Geometry:
1501
1535
  if a != b:
1502
1536
  parent[max(a, b)] = min(a, b)
1503
1537
  groups: dict[int, list[int]] = {}
1504
- for i in range(len(self.node_id)):
1538
+ for i in range(len(rows)):
1505
1539
  groups.setdefault(root(i), []).append(i)
1506
1540
  out = {}
1507
1541
  for members in groups.values():
1508
1542
  if len(members) < 2:
1509
1543
  continue
1510
- ids = sorted(int(self.node_id[i]) for i in members)
1544
+ ids = sorted(int(self.node_id[rows[i]]) for i in members)
1511
1545
  for other in ids[1:]:
1512
1546
  out[other] = ids[0]
1513
1547
  return out
1514
1548
 
1515
- def duplicate_elements(self) -> dict[int, int]:
1549
+ def duplicate_elements(self, groups: Ids | None = None) -> dict[int, int]:
1516
1550
  """{element id: the element it is} for every solid element on
1517
1551
  exactly the nodes of an earlier one, of the same type — two
1518
1552
  bricks filling one cell. The lowest id is the one that stays.
@@ -1526,12 +1560,27 @@ class Geometry:
1526
1560
  overlap: the region counted twice, its stiffness and mass
1527
1561
  doubled, and every face of each pair drawn as interior, so the
1528
1562
  middle of the X vanished from the view (Brandon, 2026-10-02).
1563
+
1564
+ Parameters
1565
+ ----------
1566
+ groups : sequence of int, optional
1567
+ Keeps the search to these element groups' elements, by id;
1568
+ every element when omitted.
1569
+
1570
+ Returns
1571
+ -------
1572
+ dict of int to int
1573
+ Each element that fills a cell another already fills, to the
1574
+ element it is.
1529
1575
  """
1576
+ wanted = None if groups is None else {int(g) for g in groups}
1530
1577
  first: dict[tuple, int] = {}
1531
1578
  out: dict[int, int] = {}
1532
1579
  for k, conn in enumerate(self.elem_conn):
1533
1580
  if element_family(int(self.elem_type[k])) not in SOLID_FAMILIES:
1534
1581
  continue
1582
+ if wanted is not None and int(self.elem_group[k]) not in wanted:
1583
+ continue
1535
1584
  key = (int(self.elem_type[k]), tuple(sorted(int(n) for n in conn)))
1536
1585
  eid = int(self.elem_id[k])
1537
1586
  if key in first:
@@ -1540,16 +1589,180 @@ class Geometry:
1540
1589
  first[key] = eid
1541
1590
  return out
1542
1591
 
1543
- def merge_duplicate_elements(self) -> int:
1592
+ def merge_duplicate_elements(self, groups: Ids | None = None) -> int:
1544
1593
  """Make elements that are one cell one element (`duplicate_elements`):
1545
1594
  the later of each pair is removed, the earlier — and its element group —
1546
- stays. Returns how many were removed."""
1547
- going = self.duplicate_elements()
1595
+ stays.
1596
+
1597
+ Parameters
1598
+ ----------
1599
+ groups : sequence of int, optional
1600
+ Keeps it to these element groups' elements, by id; every
1601
+ element when omitted.
1602
+
1603
+ Returns
1604
+ -------
1605
+ int
1606
+ How many elements were removed.
1607
+ """
1608
+ going = self.duplicate_elements(groups)
1548
1609
  if going:
1549
1610
  self.delete_elements(list(going))
1550
1611
  return len(going)
1551
1612
 
1552
- def merge_coincident_nodes(self, tolerance: float) -> dict[str, int]:
1613
+ def _merge_candidates(self, groups: Ids | None,
1614
+ exclude: Ids | None) -> list[int] | None:
1615
+ """The node ids a merge may touch: those of `groups`' elements
1616
+ (every node when None), less `exclude`; None for every node."""
1617
+ nodes = None if groups is None else self.nodes_in(groups)
1618
+ if not exclude:
1619
+ return nodes
1620
+ left_out = {int(n) for n in exclude}
1621
+ base = self.node_id.tolist() if nodes is None else nodes
1622
+ return [int(n) for n in base if int(n) not in left_out]
1623
+
1624
+ def node_spacing(self, groups: Ids | None = None) -> float | None:
1625
+ """The mesh's finest spacing: the shortest distance between two
1626
+ nodes of one element, zero-length elements passed over — a
1627
+ brick's shortest edge, a quad8's half edge. What Merge Coincident
1628
+ Nodes sizes its default tolerance from (a quarter of it, Brandon,
1629
+ 2026-10-11): parts meshed apart sit well inside it, and no two
1630
+ nodes of one element do.
1631
+
1632
+ Parameters
1633
+ ----------
1634
+ groups : sequence of int, optional
1635
+ Only these element groups' elements, by id; every element
1636
+ when omitted.
1637
+
1638
+ Returns
1639
+ -------
1640
+ float or None
1641
+ The spacing, as the coordinates are held; None when there is
1642
+ no element with two distinct nodes.
1643
+ """
1644
+ rows = (range(len(self.elem_conn)) if groups is None
1645
+ else np.flatnonzero(np.isin(self.elem_group,
1646
+ [int(g) for g in groups])))
1647
+ by_count: dict[int, list[np.ndarray]] = {}
1648
+ for k in rows:
1649
+ conn = self.elem_conn[k]
1650
+ if len(conn) > 1:
1651
+ by_count.setdefault(len(conn), []).append(conn)
1652
+ finest = np.inf
1653
+ for count, conns in by_count.items():
1654
+ xyz = self.node_xyz[self.node_index(np.concatenate(conns))]
1655
+ xyz = xyz.reshape(len(conns), count, 3)
1656
+ first, second = np.triu_indices(count, 1)
1657
+ gaps = np.linalg.norm(xyz[:, first] - xyz[:, second], axis=-1)
1658
+ gaps = gaps[gaps > 0.0]
1659
+ if gaps.size:
1660
+ finest = min(finest, float(gaps.min()))
1661
+ return None if not np.isfinite(finest) else finest
1662
+
1663
+ def coincident_spots(self, tolerance: float, groups: Ids | None = None,
1664
+ exclude: Ids | None = None) -> list[list[int]]:
1665
+ """The places a merge would make one node: each a list of the
1666
+ node ids that coincide there, ascending, so the first is the one
1667
+ that stays. What Merge Coincident Nodes marks in the view before
1668
+ anything is merged, a place at a time, for a person to leave some
1669
+ out (Brandon, 2026-10-11).
1670
+
1671
+ Parameters
1672
+ ----------
1673
+ tolerance : float
1674
+ How close two nodes must be to be one, as the coordinates are
1675
+ held: meters once units are defined.
1676
+ groups : sequence of int, optional
1677
+ Only the nodes these element groups' elements use, by id;
1678
+ every node when omitted.
1679
+ exclude : sequence of int, optional
1680
+ Nodes left out, by id: neither merged nor merged into.
1681
+
1682
+ Returns
1683
+ -------
1684
+ list of list of int
1685
+ One list per place, in the order of the node each keeps.
1686
+ """
1687
+ mapping = self.coincident_nodes(
1688
+ tolerance, self._merge_candidates(groups, exclude))
1689
+ spots: dict[int, list[int]] = {}
1690
+ for going, kept in mapping.items():
1691
+ spots.setdefault(int(kept), [int(kept)]).append(int(going))
1692
+ return [sorted(nodes) for _kept, nodes in sorted(spots.items())]
1693
+
1694
+ def merge_folds(self, tolerance: float, groups: Ids | None = None,
1695
+ exclude: Ids | None = None) -> str | None:
1696
+ """Why this merge would be refused, or None: the sentence
1697
+ `merge_coincident_nodes` raises when the tolerance would fold an
1698
+ element onto itself — asked ahead, so a pane can say it before
1699
+ its button is pressed.
1700
+
1701
+ Parameters
1702
+ ----------
1703
+ tolerance : float
1704
+ As `merge_coincident_nodes`.
1705
+ groups : sequence of int, optional
1706
+ As `merge_coincident_nodes`.
1707
+ exclude : sequence of int, optional
1708
+ As `merge_coincident_nodes`.
1709
+
1710
+ Returns
1711
+ -------
1712
+ str or None
1713
+ """
1714
+ return self._folding(self.coincident_nodes(
1715
+ tolerance, self._merge_candidates(groups, exclude)), tolerance)
1716
+
1717
+ def merge_collapses(self, tolerance: float, groups: Ids | None = None,
1718
+ exclude: Ids | None = None) -> list[int]:
1719
+ """The links this merge would remove, by element id: the line
1720
+ elements — a rigid link, a spring, a beam — whose nodes all become
1721
+ one node, which then join nothing. A joint built with rigid
1722
+ links and then merged takes them out, where the merge used to be
1723
+ refused for folding them (Brandon, 2026-10-11).
1724
+
1725
+ Parameters
1726
+ ----------
1727
+ tolerance : float
1728
+ As `merge_coincident_nodes`.
1729
+ groups : sequence of int, optional
1730
+ As `merge_coincident_nodes`.
1731
+ exclude : sequence of int, optional
1732
+ As `merge_coincident_nodes`.
1733
+
1734
+ Returns
1735
+ -------
1736
+ list of int
1737
+ """
1738
+ return self._collapsed(self.coincident_nodes(
1739
+ tolerance, self._merge_candidates(groups, exclude)))
1740
+
1741
+ def _collapsed(self, mapping: dict[int, int]) -> list[int]:
1742
+ if not mapping:
1743
+ return []
1744
+ out = []
1745
+ for i, conn in enumerate(self.elem_conn):
1746
+ if (len(conn) > 1 and ELEMENT_TYPES[int(self.elem_type[i])][2] == 'line'
1747
+ and len({mapping.get(int(n), int(n)) for n in conn}) == 1):
1748
+ out.append(int(self.elem_id[i]))
1749
+ return out
1750
+
1751
+ def _folding(self, mapping: dict[int, int], tolerance: float) -> str | None:
1752
+ gone = set(self._collapsed(mapping))
1753
+ for i, conn in enumerate(self.elem_conn):
1754
+ if int(self.elem_id[i]) in gone:
1755
+ continue
1756
+ renamed = [mapping.get(int(n), int(n)) for n in conn]
1757
+ if len(set(renamed)) < len(renamed):
1758
+ return (f'a tolerance of {tolerance:g} folds element '
1759
+ f'{int(self.elem_id[i])} onto itself — two of its own '
1760
+ 'nodes are within it; use a smaller tolerance')
1761
+ return None
1762
+
1763
+ def merge_coincident_nodes(self, tolerance: float,
1764
+ groups: Ids | None = None,
1765
+ exclude: Ids | None = None) -> dict[str, int]:
1553
1766
  """Make nodes that are one point one node: every element and
1554
1767
  element naming a node within `tolerance` of another is renamed
1555
1768
  to the lowest id among them, and the rest are removed. Plates
@@ -1558,34 +1771,53 @@ class Geometry:
1558
1771
 
1559
1772
  Refused, with the element named, when the tolerance would fold an
1560
1773
  element onto itself — two of its own corners within it — since
1561
- that is a tolerance larger than the mesh, not a coincidence.
1774
+ that is a tolerance larger than the mesh, not a coincidence. A
1775
+ line element whose nodes all become one — a rigid link across
1776
+ the gap the merge closes — is removed instead
1777
+ (`merge_collapses`).
1562
1778
  Elements the merge leaves on exactly the same nodes are one cell
1563
1779
  filled twice, and are made one (`merge_duplicate_elements`).
1564
1780
 
1781
+ With `groups`, only what those element groups hold: the nodes
1782
+ their elements use are the candidates, so two groups picked
1783
+ together are tied to each other and to nothing else, and only
1784
+ their elements are made one where they fill a cell twice. A merge
1785
+ over a whole geometry ties every part to every part it touches,
1786
+ which is further than a person usually means (Brandon,
1787
+ 2026-10-11: the app offers the merge on element groups only).
1788
+
1565
1789
  Parameters
1566
1790
  ----------
1567
1791
  tolerance : float
1568
1792
  How close two nodes must be to be one, as the coordinates are
1569
1793
  held: meters once units are defined.
1794
+ groups : sequence of int, optional
1795
+ The element groups whose nodes are merged, by id. Every node
1796
+ of the geometry when omitted.
1797
+ exclude : sequence of int, optional
1798
+ Nodes left out of the merge, by id: neither merged nor merged
1799
+ into — the places a person unpicked in the app
1800
+ (`coincident_spots`).
1570
1801
 
1571
1802
  Returns
1572
1803
  -------
1573
1804
  dict of str to int
1574
1805
  'merged', the nodes removed, 'into', the nodes they became,
1575
- and 'duplicates', the elements removed for filling a cell
1576
- another already filled.
1806
+ 'duplicates', the elements removed for filling a cell another
1807
+ already filled, and 'links', the line elements removed for
1808
+ joining nodes that became one.
1577
1809
  """
1578
- mapping = self.coincident_nodes(tolerance)
1810
+ mapping = self.coincident_nodes(
1811
+ tolerance, self._merge_candidates(groups, exclude))
1579
1812
  if not mapping:
1580
- duplicates = self.merge_duplicate_elements()
1581
- return {'merged': 0, 'into': 0, 'duplicates': duplicates}
1582
- for i, conn in enumerate(self.elem_conn):
1583
- renamed = [mapping.get(int(n), int(n)) for n in conn]
1584
- if len(set(renamed)) < len(renamed):
1585
- raise ValueError(
1586
- f'a tolerance of {tolerance:g} folds element '
1587
- f'{int(self.elem_id[i])} onto itself — two of its own '
1588
- 'nodes are within it; use a smaller tolerance')
1813
+ duplicates = self.merge_duplicate_elements(groups)
1814
+ return {'merged': 0, 'into': 0, 'duplicates': duplicates,
1815
+ 'links': 0}
1816
+ refusal = self._folding(mapping, tolerance)
1817
+ if refusal is not None:
1818
+ raise ValueError(refusal)
1819
+ links = self._collapsed(mapping)
1820
+ self.delete_elements(links)
1589
1821
  self.elem_conn = [np.asarray([mapping.get(int(n), int(n))
1590
1822
  for n in conn], dtype=np.int64)
1591
1823
  for conn in self.elem_conn]
@@ -1593,10 +1825,10 @@ class Geometry:
1593
1825
  for name in ('node_id', 'node_def_cs', 'node_disp_cs', 'node_color'):
1594
1826
  setattr(self, name, getattr(self, name)[keep])
1595
1827
  self.node_xyz = self.node_xyz[keep]
1596
- duplicates = self.merge_duplicate_elements()
1828
+ duplicates = self.merge_duplicate_elements(groups)
1597
1829
  self.validate()
1598
1830
  return {'merged': len(mapping), 'into': len(set(mapping.values())),
1599
- 'duplicates': duplicates}
1831
+ 'duplicates': duplicates, 'links': len(links)}
1600
1832
 
1601
1833
  def delete_coordinate_systems(self, cs_ids: Ids) -> dict[str, int]:
1602
1834
  """Remove coordinate systems, reassigning any node that used them.