visualdynamics 0.1.0a67__tar.gz → 0.1.0a69__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 (226) hide show
  1. {visualdynamics-0.1.0a67/src/visualdynamics.egg-info → visualdynamics-0.1.0a69}/PKG-INFO +2 -1
  2. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/pyproject.toml +4 -0
  3. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/__init__.py +1 -1
  4. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/compatibility.py +24 -3
  5. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/channel_table.py +39 -1
  6. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/geometry.py +34 -0
  7. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/instrumentation.py +104 -10
  8. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/tables.py +31 -0
  9. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/__init__.py +37 -0
  10. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/icons.py +99 -25
  11. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/instrumentation_panel.py +30 -12
  12. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/main_window.py +376 -95
  13. visualdynamics-0.1.0a67/src/visualdynamics/gui/rigid_panel.py → visualdynamics-0.1.0a69/src/visualdynamics/gui/modes_panel.py +184 -22
  14. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/object_tables.py +36 -0
  15. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/panes.py +38 -34
  16. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/excel.py +7 -5
  17. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/project.py +89 -10
  18. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/geometry.py +72 -9
  19. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69/src/visualdynamics.egg-info}/PKG-INFO +2 -1
  20. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics.egg-info/SOURCES.txt +1 -1
  21. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics.egg-info/requires.txt +1 -0
  22. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/LICENSE +0 -0
  23. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/README.md +0 -0
  24. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/setup.cfg +0 -0
  25. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/__main__.py +0 -0
  26. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/__init__.py +0 -0
  27. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/author.py +0 -0
  28. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/averaging.py +0 -0
  29. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/classical_pulse.py +0 -0
  30. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/compliance.py +0 -0
  31. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/correlate.py +0 -0
  32. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/data.py +0 -0
  33. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/detect.py +0 -0
  34. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/dwell.py +0 -0
  35. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/entities.py +0 -0
  36. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/fem.py +0 -0
  37. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/filters.py +0 -0
  38. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/half_power.py +0 -0
  39. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/hexmesh.py +0 -0
  40. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/kurtosis.py +0 -0
  41. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/matches.py +0 -0
  42. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/mates.py +0 -0
  43. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/merge.py +0 -0
  44. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/mesh.py +0 -0
  45. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/modal_fit.py +0 -0
  46. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/octave.py +0 -0
  47. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/photos.py +0 -0
  48. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/progress.py +0 -0
  49. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/replication.py +0 -0
  50. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/replication_fit.py +0 -0
  51. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/replication_segments.py +0 -0
  52. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/report.py +0 -0
  53. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/rigid.py +0 -0
  54. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/shaker.py +0 -0
  55. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/shapes.py +0 -0
  56. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/shock_check.py +0 -0
  57. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/shocks.py +0 -0
  58. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/signature.py +0 -0
  59. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/sine.py +0 -0
  60. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/sine_tracking.py +0 -0
  61. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/snr.py +0 -0
  62. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/solids.py +0 -0
  63. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/sparse_solve.py +0 -0
  64. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/srs.py +0 -0
  65. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/stroke.py +0 -0
  66. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/sweep_rate.py +0 -0
  67. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/tetmesh.py +0 -0
  68. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/time_statistics.py +0 -0
  69. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/tone_readout.py +0 -0
  70. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/transform.py +0 -0
  71. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/truncate.py +0 -0
  72. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/unit_choices.py +0 -0
  73. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/validate.py +0 -0
  74. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/core/wavelet.py +0 -0
  75. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/decimate.py +0 -0
  76. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/deform.py +0 -0
  77. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/demo/__init__.py +0 -0
  78. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/demo/barc.py +0 -0
  79. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/demo/drone.py +0 -0
  80. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/demo/frame_wing.py +0 -0
  81. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/demo/plate.py +0 -0
  82. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/demo/shapes.py +0 -0
  83. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/app_icon.py +0 -0
  84. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/ask.py +0 -0
  85. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/author_panel.py +0 -0
  86. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/averaging_panel.py +0 -0
  87. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/console.py +0 -0
  88. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/disclaimer.py +0 -0
  89. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/drawer_tab.py +0 -0
  90. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/dwell_panel.py +0 -0
  91. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/editors.py +0 -0
  92. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/filter_panel.py +0 -0
  93. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/glyphs.py +0 -0
  94. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
  95. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
  96. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/mate_panel.py +0 -0
  97. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/merge_panel.py +0 -0
  98. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/mesh_panel.py +0 -0
  99. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/mesh_solids_panel.py +0 -0
  100. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/message_bar.py +0 -0
  101. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/movie.py +0 -0
  102. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/octave_panel.py +0 -0
  103. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/opengl.py +0 -0
  104. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/pattern_panel.py +0 -0
  105. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/preferences.py +0 -0
  106. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/project_tree.py +0 -0
  107. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/pulse_panel.py +0 -0
  108. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/record_grid.py +0 -0
  109. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/replication_fit_panel.py +0 -0
  110. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/report_editor.py +0 -0
  111. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/run_reports.py +0 -0
  112. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/segments_panel.py +0 -0
  113. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/settings_panel.py +0 -0
  114. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/shock_panel.py +0 -0
  115. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/signature_panel.py +0 -0
  116. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/sine_panel.py +0 -0
  117. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/srs_tolerance_panel.py +0 -0
  118. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/stage_drag.py +0 -0
  119. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/statistics_panel.py +0 -0
  120. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/stream_window.py +0 -0
  121. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/stroke_panel.py +0 -0
  122. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/sweep_rate_panel.py +0 -0
  123. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/tables.py +0 -0
  124. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/tone_panel.py +0 -0
  125. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/toolbars.py +0 -0
  126. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/tooltips.py +0 -0
  127. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/truncate_panel.py +0 -0
  128. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/updater.py +0 -0
  129. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/wavelet_panel.py +0 -0
  130. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/gui/windows.py +0 -0
  131. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/__init__.py +0 -0
  132. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/adf.py +0 -0
  133. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf.py +0 -0
  134. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_objects.py +0 -0
  135. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/LICENSE +0 -0
  136. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/activity_result.txt +0 -0
  137. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/channel_table.txt +0 -0
  138. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/data.txt +0 -0
  139. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/data_physics_random_control_parameters.txt +0 -0
  140. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/data_physics_shock_response_synthesis_control_parameters.txt +0 -0
  141. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/data_physics_sine_control_parameters.txt +0 -0
  142. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/geometry.txt +0 -0
  143. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/global_analysis_attributes.txt +0 -0
  144. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/global_test_attributes.txt +0 -0
  145. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/matrix.txt +0 -0
  146. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/mechanical_control_parameters.txt +0 -0
  147. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/modal_test_parameters.txt +0 -0
  148. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/mode.txt +0 -0
  149. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/parameter_set.txt +0 -0
  150. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/point_cloud.txt +0 -0
  151. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/rattlesnake_random_control_parameters.txt +0 -0
  152. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/rattlesnake_sysid_parameters.txt +0 -0
  153. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/rattlesnake_transient_control_parameters.txt +0 -0
  154. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/response_spectrum.txt +0 -0
  155. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/scalar.txt +0 -0
  156. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/unknown.txt +0 -0
  157. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/vector.txt +0 -0
  158. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/escdf_specifications/vibration_channel_table.txt +0 -0
  159. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/exodus.py +0 -0
  160. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/exporters.py +0 -0
  161. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/femap.py +0 -0
  162. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/matlab.py +0 -0
  163. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/nastran.py +0 -0
  164. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/native.py +0 -0
  165. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/notes.py +0 -0
  166. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/photo_files.py +0 -0
  167. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/punch.py +0 -0
  168. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/rattlesnake.py +0 -0
  169. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/rattlesnake_sine.py +0 -0
  170. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
  171. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/report_template.py +0 -0
  172. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/sdynpy_data.py +0 -0
  173. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/sdynpy_npz.py +0 -0
  174. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
  175. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/sep005.py +0 -0
  176. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/sniffing.py +0 -0
  177. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/step.py +0 -0
  178. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/stl.py +0 -0
  179. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/threemf.py +0 -0
  180. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/io/unv.py +0 -0
  181. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/names.py +0 -0
  182. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/__init__.py +0 -0
  183. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/averaging.py +0 -0
  184. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/bands.py +0 -0
  185. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/bars.py +0 -0
  186. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/classical_pulse.py +0 -0
  187. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/dwell.py +0 -0
  188. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/filtering.py +0 -0
  189. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/half_power.py +0 -0
  190. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/replication_fit.py +0 -0
  191. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/replication_segments.py +0 -0
  192. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/scalogram.py +0 -0
  193. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/shock_check.py +0 -0
  194. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/shocks.py +0 -0
  195. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/signature.py +0 -0
  196. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/sine_tracking.py +0 -0
  197. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/srs_tolerance.py +0 -0
  198. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/stroke.py +0 -0
  199. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/sweep_rate.py +0 -0
  200. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/time_statistics.py +0 -0
  201. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/tone_readout.py +0 -0
  202. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/tracking_filter.py +0 -0
  203. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/plot/truncation.py +0 -0
  204. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/py.typed +0 -0
  205. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/report/__init__.py +0 -0
  206. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/report/markdown.py +0 -0
  207. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/report/page.py +0 -0
  208. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/rotate.py +0 -0
  209. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/theme.py +0 -0
  210. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/units.py +0 -0
  211. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/update.py +0 -0
  212. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/__init__.py +0 -0
  213. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/_quiet.py +0 -0
  214. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/animate.py +0 -0
  215. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/banded.py +0 -0
  216. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/mac_bars.py +0 -0
  217. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/marks.py +0 -0
  218. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/paired.py +0 -0
  219. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/pick.py +0 -0
  220. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/rigid.py +0 -0
  221. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/scalogram.py +0 -0
  222. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/sinespec.py +0 -0
  223. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics/viz/waterfall.py +0 -0
  224. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
  225. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/src/visualdynamics.egg-info/entry_points.txt +0 -0
  226. {visualdynamics-0.1.0a67 → visualdynamics-0.1.0a69}/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.0a67
3
+ Version: 0.1.0a69
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
@@ -32,6 +32,7 @@ Requires-Dist: pyvista
32
32
  Requires-Dist: pyvistaqt
33
33
  Requires-Dist: vtk
34
34
  Requires-Dist: PySide6
35
+ Requires-Dist: shiboken6
35
36
  Requires-Dist: pyqtgraph
36
37
  Requires-Dist: openpyxl
37
38
  Requires-Dist: pandas
@@ -79,6 +79,10 @@ dependencies = [
79
79
  # pyvista's own cap is what decides which VTK is installed.
80
80
  "vtk",
81
81
  "PySide6",
82
+ # PySide6's own binding layer (LGPL-3.0, the same terms), installed
83
+ # with it: imported by name for the exit's window release
84
+ # (gui.release_widgets), so declared rather than relied on arriving
85
+ "shiboken6",
82
86
  "pyqtgraph",
83
87
  "openpyxl",
84
88
  # A channel table is a spreadsheet, and a DataFrame is what carries a
@@ -97,7 +97,7 @@ from .units import (
97
97
  #: alpha, said in the version itself so the dialog, the About text, the
98
98
  #: installers and latest.json all say the same thing, and the first
99
99
  #: non-alpha release is offered as an upgrade (Brandon, 2026-09-11)
100
- __version__ = '0.1.0a67'
100
+ __version__ = '0.1.0a69'
101
101
 
102
102
 
103
103
  def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
@@ -137,6 +137,24 @@ def _listed(dofs: Sequence[str]) -> str:
137
137
  return text
138
138
 
139
139
 
140
+ def _channel_named(table: Any, row: int) -> str:
141
+ """A channel as the issue lists it: its DOF, or, before it has one,
142
+ its number and what it still lacks ('channel 3 (no node yet)'). A
143
+ channel with a node and no direction is listed only when the node is
144
+ not on the geometry, so its node is named: 'channel 5 at node 9 (no
145
+ direction yet)'."""
146
+ # A channel added blank listed as '', so the message ended at its
147
+ # colon with nothing named (Brandon, 2026-10-22).
148
+ node, direction = str(table['node'][row]), str(table['direction'][row])
149
+ number = int(table['channel'][row])
150
+ if node and direction:
151
+ return table.dof_strings()[row]
152
+ if node:
153
+ return f'channel {number} at node {node} (no direction yet)'
154
+ lacking = 'node' if direction else 'node or direction'
155
+ return f'channel {number} (no {lacking} yet)'
156
+
157
+
140
158
  def modal_object(obj: Any) -> int | None:
141
159
  """The highest mode a data object's coordinates name, when every
142
160
  one of its DOFs is a modal coordinate, else None."""
@@ -283,12 +301,15 @@ def check_object(name: str, obj: Any, geometry: Geometry | None,
283
301
  if geometry.missing_dofs([dof])]
284
302
  if not flagged:
285
303
  return None
286
- missing = [dofs[i] for i in flagged]
304
+ named = [_channel_named(obj, i) for i in flagged]
287
305
  return Issue(
288
306
  name=name, kind='dofs-not-in-geometry',
289
307
  message=(f'{len(flagged)} of {len(dofs)} channels are not in '
290
- f'geometry {geometry_name!r}: {_listed(missing)}'),
291
- missing_dofs=missing, sub_items=flagged)
308
+ f'geometry {geometry_name!r}: {_listed(named)}'),
309
+ # only what is a DOF: a channel not yet placed names none
310
+ missing_dofs=[dofs[i] for i in flagged
311
+ if str(obj['node'][i]) and str(obj['direction'][i])],
312
+ sub_items=flagged)
292
313
 
293
314
  return None
294
315
 
@@ -359,7 +359,10 @@ class ChannelTable:
359
359
  dtype='int64')
360
360
  else:
361
361
  frame[name] = pd.Series(values, dtype='string').fillna('')
362
- frame['channel_type'] = cls._typed_by_unit(frame)
362
+ # a Series of its own type, as the other text columns: a bare list
363
+ # with no channels in it read as an empty float column
364
+ frame['channel_type'] = pd.Series(cls._typed_by_unit(frame),
365
+ dtype='string')
363
366
  return frame
364
367
 
365
368
  @classmethod
@@ -559,6 +562,41 @@ class ChannelTable:
559
562
  declared = str(self.frame.loc[row, 'channel_type'])
560
563
  return list(ORDINATE_UNITS.get(declared, ALL_ORDINATE_UNITS))
561
564
 
565
+ def add_channels(self, count: int = 1) -> list[int]:
566
+ """Add blank channels at the end of the table, numbered on from
567
+ the highest channel in it (from 1 in an empty table).
568
+
569
+ Every other cell takes its column's default, so the node,
570
+ direction and unit are blank until they are typed in.
571
+
572
+ Parameters
573
+ ----------
574
+ count : int, default 1
575
+ How many channels to add.
576
+
577
+ Returns
578
+ -------
579
+ list of int
580
+ The new channels' numbers.
581
+
582
+ Raises
583
+ ------
584
+ ValueError
585
+ If `count` is less than 1.
586
+ """
587
+ import pandas as pd
588
+
589
+ count = int(count)
590
+ if count < 1:
591
+ raise ValueError(f'cannot add {count} channels; add at least one')
592
+ start = int(self.frame['channel'].max()) + 1 if self.num_channels else 1
593
+ numbers = list(range(start, start + count))
594
+ added = self._typed(pd.DataFrame(
595
+ {name: numbers if name == 'channel' else [spec.default] * count
596
+ for name, spec in self.COLUMNS.items()}))
597
+ self.frame = pd.concat([self.frame, added], ignore_index=True)
598
+ return numbers
599
+
562
600
  def delete_channels(self, indices: Sequence[int]) -> None:
563
601
  """Remove the given rows in place. At least one row must remain.
564
602
 
@@ -2700,3 +2700,37 @@ class Geometry:
2700
2700
  from ..viz.geometry import plot_dofs
2701
2701
  return plot_dofs(self, source, quantity, unit_system=unit_system,
2702
2702
  **kwargs)
2703
+
2704
+ def plot_channels(self, table: Any, role: str = 'all',
2705
+ unit_system: UnitSystem | None = None,
2706
+ **kwargs: Any) -> Any:
2707
+ """Draw this geometry with a labeled arrow at each channel's DOF
2708
+ in a channel table, as the app does when the two are selected
2709
+ together.
2710
+
2711
+ A channel with no node or direction yet, or at a node this
2712
+ geometry does not have, has no arrow.
2713
+
2714
+ Parameters
2715
+ ----------
2716
+ table : ChannelTable
2717
+ The channels to draw.
2718
+ role : str, default 'all'
2719
+ 'reference', 'response' or 'monitor' to draw only the
2720
+ channels in that role, 'control' for the channels with
2721
+ Control checked, or 'all' for every channel.
2722
+ unit_system : UnitSystem, optional
2723
+ Units to draw in.
2724
+ **kwargs
2725
+ Passed through to the scene.
2726
+
2727
+ Returns
2728
+ -------
2729
+ object
2730
+ The plotter the scene is in.
2731
+ """
2732
+ # Brandon, 2026-10-22: a channel table and a geometry selected
2733
+ # together draw the table's DOFs, picked by role.
2734
+ from ..viz.geometry import plot_channels
2735
+ return plot_channels(self, table, role, unit_system=unit_system,
2736
+ **kwargs)
@@ -23,7 +23,10 @@ surface allows. A node where surfaces meet at an edge or a corner is a
23
23
  candidate on each of them, so that a sensor sits flat on one; within a
24
24
  surface the faces' normals at the node are averaged. A plate's normal
25
25
  points away from the middle of the model. Beams contribute every node,
26
- measured in the global system.
26
+ measured in the global system. A place nearer than `edge_distance` to
27
+ its surface's edge is not a candidate, since a sensor's base needs flat
28
+ ground around the spot it is glued to; an edge here is wherever the
29
+ surface ends, at a sharp edge, a free edge, a hole or where parts touch.
27
30
 
28
31
  `local_shapes` turns a shape set's translations into each candidate's
29
32
  own directions, and `effective_independence` chooses the sensors:
@@ -52,7 +55,16 @@ import numpy as np
52
55
  #: neighboring faces whose outward normals differ by more than this, in
53
56
  #: degrees, are on separate surfaces: a block's edge is 90, a
54
57
  #: cylinder's facets a few degrees each
55
- EDGE_ANGLE = 40.0
58
+ # Brandon, 2026-10-22: 20 degrees, from 40
59
+ EDGE_ANGLE = 20.0
60
+
61
+ #: how far a sensor stays from its surface's edge, in meters: a quarter
62
+ #: of an inch (Brandon, 2026-10-22)
63
+ EDGE_DISTANCE = 0.25 * 0.0254
64
+
65
+ #: a surface of less than this share of the whole outside's area is
66
+ #: left out unless picked (`default_surfaces`)
67
+ SMALL_SURFACE = 0.01
56
68
 
57
69
  #: removal in batches stops once this many times the wanted count
58
70
  #: remain, and the rest is removed one at a time
@@ -231,8 +243,35 @@ def skin(geometry: Any, *, edge_angle: float = EDGE_ANGLE) -> list[Surface]:
231
243
  return found
232
244
 
233
245
 
246
+ def default_surfaces(found: list[Surface]) -> list[int]:
247
+ """The surfaces sensors go on unless told otherwise: every one of at
248
+ least `SMALL_SURFACE` of the whole outside's area. Sensors go on
249
+ the significant surfaces of a test article; a small one is most
250
+ often a sliver along an edge, a fillet or a hole's bore. When no
251
+ surface is that large, all of them.
252
+
253
+ Parameters
254
+ ----------
255
+ found : list of Surface
256
+ From `skin`.
257
+
258
+ Returns
259
+ -------
260
+ list of int
261
+ Their ids, in order.
262
+ """
263
+ # Brandon, 2026-10-22: at 20 degrees the cone split into 193
264
+ # surfaces, most of them a single element; "any surfaces less than
265
+ # 1% of the total surface area should be de-selected by default ...
266
+ # we just mount to significant surfaces usually"
267
+ total = sum(s.area for s in found)
268
+ kept = [s.id for s in found if s.area >= SMALL_SURFACE * total]
269
+ return kept or [s.id for s in found]
270
+
271
+
234
272
  def candidates(geometry: Any, groups: Any = None, surfaces: Any = None, *,
235
- edge_angle: float = EDGE_ANGLE, found: Any = None) -> Candidates:
273
+ edge_angle: float = EDGE_ANGLE, found: Any = None,
274
+ edge_distance: float = 0.0) -> Candidates:
236
275
  """Every place on `geometry` a sensor can be mounted, as an
237
276
  experimental geometry with a coordinate system per place.
238
277
 
@@ -244,13 +283,19 @@ def candidates(geometry: Any, groups: Any = None, surfaces: Any = None, *,
244
283
  The element groups sensors may go on. All of them when omitted.
245
284
  A face shared with a group that is not picked is still inside.
246
285
  surfaces : sequence of int, optional
247
- The surfaces sensors may go on, by `Surface.id`. All of those in
248
- `groups` when omitted.
286
+ The surfaces sensors may go on, by `Surface.id`. Those of
287
+ `groups` that `default_surfaces` keeps when omitted: every one
288
+ of at least `SMALL_SURFACE` of the outside's area.
249
289
  edge_angle : float, default EDGE_ANGLE
250
290
  Degrees between two neighboring faces' normals beyond which they
251
291
  are on separate surfaces.
252
292
  found : list of Surface, optional
253
293
  `skin(geometry, edge_angle=edge_angle)`, when already found.
294
+ edge_distance : float, default 0
295
+ How near a place may be to its surface's edge, in the
296
+ geometry's coordinates (meters once its units are defined),
297
+ measured in a straight line. Nothing is left out at 0. A
298
+ surface with no edge, such as a whole sphere, keeps every place.
254
299
 
255
300
  Returns
256
301
  -------
@@ -260,11 +305,12 @@ def candidates(geometry: Any, groups: Any = None, surfaces: Any = None, *,
260
305
 
261
306
  picked = ({int(g) for g in geometry.group_id} if groups is None
262
307
  else {int(g) for g in groups})
263
- wanted = None if surfaces is None else {int(s) for s in surfaces}
264
308
  xyz = geometry.node_xyz
265
309
  row_of = {int(n): k for k, n in enumerate(geometry.node_id)}
310
+ by_id = np.argsort(geometry.node_id)
266
311
  if found is None:
267
312
  found = skin(geometry, edge_angle=edge_angle)
313
+ wanted = set(default_surfaces(found) if surfaces is None else (int(s) for s in surfaces))
268
314
 
269
315
  # a place per node per surface, the surface's faces at the node
270
316
  # averaged by area; the largest first, so a node's own id goes to it
@@ -278,6 +324,11 @@ def candidates(geometry: Any, groups: Any = None, surfaces: Any = None, *,
278
324
  at, where = np.unique(corner_nodes, return_inverse=True)
279
325
  summed = np.zeros((len(at), 3))
280
326
  np.add.at(summed, where, np.repeat(weight, per_face, axis=0))
327
+ if edge_distance > 0:
328
+ rows = by_id[np.searchsorted(geometry.node_id, at, sorter=by_id)]
329
+ away = _away_from_edge(surface.faces, at, xyz[rows], xyz, by_id,
330
+ geometry.node_id, edge_distance)
331
+ at, summed = at[away], summed[away]
281
332
  nodes.append(at)
282
333
  sums.append(summed)
283
334
  kinds.extend([surface.kind] * len(at))
@@ -623,7 +674,8 @@ class Plan:
623
674
 
624
675
  def plan(shapes: Any, geometry: Any, triaxial: int, uniaxial: int = 0, *,
625
676
  modes: Any = None, groups: Any = None, surfaces: Any = None,
626
- edge_angle: float | None = None, axes: Any = (2,)) -> Plan:
677
+ edge_angle: float | None = None, axes: Any = (2,),
678
+ edge_distance: float | None = None) -> Plan:
627
679
  """Plan a test's accelerometers from a finite element model.
628
680
 
629
681
  Parameters
@@ -642,14 +694,20 @@ def plan(shapes: Any, geometry: Any, triaxial: int, uniaxial: int = 0, *,
642
694
  groups : sequence of int, optional
643
695
  The element groups sensors may go on. All when omitted.
644
696
  surfaces : sequence of int, optional
645
- The surfaces sensors may go on, by `Surface.id`. All of those in
646
- `groups` when omitted.
697
+ The surfaces sensors may go on, by `Surface.id`. Those of
698
+ `groups` that `default_surfaces` keeps when omitted.
647
699
  edge_angle : float, optional
648
700
  Degrees between neighboring faces' normals beyond which they are
649
701
  on separate surfaces; `EDGE_ANGLE` when omitted.
650
702
  axes : sequence of int, default (2,)
651
703
  The directions a uni-axial accelerometer may measure: 0, 1, 2
652
704
  for a place's X, Y, Z. Z, the surface normal, by default.
705
+ edge_distance : float, optional
706
+ How near a sensor may be to its surface's edge, in the
707
+ geometry's coordinates. `EDGE_DISTANCE`, a quarter of an inch,
708
+ when omitted and the geometry's units are defined; 0 when they
709
+ are not, since a distance means nothing against coordinates
710
+ with no unit.
653
711
 
654
712
  Returns
655
713
  -------
@@ -665,8 +723,11 @@ def plan(shapes: Any, geometry: Any, triaxial: int, uniaxial: int = 0, *,
665
723
  modes = [int(m) for m in modes]
666
724
  if not modes:
667
725
  raise ValueError('no modes to measure: every mode is a rigid-body mode at 0 Hz')
726
+ if edge_distance is None:
727
+ edge_distance = EDGE_DISTANCE if geometry.units_defined else 0.0
668
728
  places = candidates(geometry, groups, surfaces,
669
- edge_angle=EDGE_ANGLE if edge_angle is None else float(edge_angle))
729
+ edge_angle=EDGE_ANGLE if edge_angle is None else float(edge_angle),
730
+ edge_distance=float(edge_distance))
670
731
  blocks, measured = local_shapes(shapes, modes, places, geometry)
671
732
  placement = effective_independence(blocks, triaxial, uniaxial, axes=axes,
672
733
  allowed=measured, sources=places.source,
@@ -770,6 +831,39 @@ def assemble(shapes: Any, modes: Any, places: Candidates, blocks: Any,
770
831
  return Plan(kept_geometry, table, test_shapes, placement)
771
832
 
772
833
 
834
+ def _away_from_edge(faces: list, at: np.ndarray, points: np.ndarray, xyz: np.ndarray,
835
+ by_id: np.ndarray, node_id: np.ndarray, distance: float) -> np.ndarray:
836
+ """Which of the nodes `at`, at `points`, are at least `distance`
837
+ from the edge of the surface made of `faces`: the sides only one of
838
+ its faces has."""
839
+ from scipy.spatial import cKDTree
840
+
841
+ sides = np.array([sorted((f[k], f[(k + 1) % len(f)])) for f in faces
842
+ for k in range(len(f))], dtype=np.int64).reshape(-1, 2)
843
+ unique, count = np.unique(sides, axis=0, return_counts=True)
844
+ edge = unique[count == 1]
845
+ if not len(edge):
846
+ return np.ones(len(at), dtype=bool)
847
+ ends = xyz[by_id[np.searchsorted(node_id, edge.ravel(), sorter=by_id)]].reshape(-1, 2, 3)
848
+ start, along = ends[:, 0], ends[:, 1] - ends[:, 0]
849
+ middle = start + along / 2
850
+ half = float(np.linalg.norm(along, axis=1).max()) / 2
851
+ # every side whose middle is near enough that the side could be
852
+ # nearer than `distance`, then its exact distance
853
+ near = cKDTree(middle).query_ball_point(points, distance + half, return_sorted=False)
854
+ which = np.repeat(np.arange(len(at)), [len(n) for n in near])
855
+ side = np.fromiter((k for n in near for k in n), dtype=np.int64, count=len(which))
856
+ if not len(side):
857
+ return np.ones(len(at), dtype=bool)
858
+ span = np.einsum('ij,ij->i', along[side], along[side])
859
+ t = np.einsum('ij,ij->i', points[which] - start[side], along[side])
860
+ t = np.clip(np.divide(t, span, out=np.zeros_like(t), where=span > 0), 0.0, 1.0)
861
+ gap = np.linalg.norm(points[which] - start[side] - t[:, None] * along[side], axis=1)
862
+ away = np.ones(len(at), dtype=bool)
863
+ away[which[gap < distance]] = False
864
+ return away
865
+
866
+
773
867
  def _remove(kept: np.ndarray, loss: np.ndarray, target: int) -> np.ndarray:
774
868
  """`kept` less its weakest: a tenth of it while far more remain than
775
869
  are wanted, otherwise the single weakest."""
@@ -76,6 +76,37 @@ def cell_text(value: Any, decimals: int | None) -> str:
76
76
  return str(value) if decimals is None else f'{value:.{decimals}f}'
77
77
 
78
78
 
79
+ def mode_heading(mode: int) -> str:
80
+ """The column heading of mode `mode` (from 0) in a table of shape
81
+ values: 'M1' for the first."""
82
+ return f'M{mode + 1}'
83
+
84
+
85
+ def shape_value_text(value: Any) -> str:
86
+ """One shape value as text, to six significant figures; a complex
87
+ one as its real and imaginary parts, '0.5-0.25j'."""
88
+ import numpy as np
89
+
90
+ if np.iscomplexobj(value):
91
+ return f'{value.real:.6g}{value.imag:+.6g}j'
92
+ return f'{float(value):.6g}'
93
+
94
+
95
+ def shape_values(shapes: Any) -> tuple[list[str], list[list[str]]]:
96
+ """``(headers, rows)`` of a shape set's values: a row per DOF, the
97
+ DOF first, then a column per mode ('M1', 'M2', ...).
98
+
99
+ Rows are DOFs and columns modes, the way a mode shape matrix is
100
+ usually written, which is the transpose of `ShapeSet.shape_matrix`.
101
+ """
102
+ # Brandon, 2026-10-22, with the table of them beside the mode list.
103
+ headers = ['DOF'] + [mode_heading(m) for m in range(shapes.num_shapes)]
104
+ rows = [[str(dof)] + [shape_value_text(value)
105
+ for value in shapes.shape_matrix[:, i]]
106
+ for i, dof in enumerate(shapes.coordinate)]
107
+ return headers, rows
108
+
109
+
79
110
  def table_of(obj: Any, geometry: Any = None,
80
111
  objects: Mapping[str, Any] | None = None
81
112
  ) -> tuple[list[str], list[list[str]]] | None:
@@ -244,4 +244,41 @@ def main(argv: Sequence[str] | None = None) -> int:
244
244
  # while both still stand.
245
245
  app.clipboard().clear()
246
246
  app.processEvents()
247
+ release_widgets(app)
247
248
  return code
249
+
250
+
251
+ def release_widgets(app: Any) -> int:
252
+ """Delete the windows Python still owns, one by one, while the
253
+ application stands, and return how many there were.
254
+
255
+ Left to the interpreter's exit, PySide deletes every widget it still
256
+ owns in one sweep over its own table, in no particular order. A
257
+ window deleted there takes its children with it, and the sweep then
258
+ reaches a child it still lists and deletes it again: the process
259
+ segfaults on its way out. Deleting each window here, through its
260
+ wrapper, leaves the sweep nothing to do.
261
+ """
262
+ # a gate worker segfaulted at exit three times on 2026-10-22, in
263
+ # PySide's moduleShutdown sweep, on a QListWidget, a QComboBox and a
264
+ # QRadioButton of dialogs the tests had left open; PLAN.md, "Nothing
265
+ # of Python's left for PySide's sweep"
266
+ import gc
267
+
268
+ import shiboken6
269
+ from PySide6.QtCore import QCoreApplication, QEvent
270
+
271
+ # the cycles first: a window held by nothing but its own connections
272
+ # goes the way Python owns it, not the sweep's
273
+ gc.collect()
274
+ released = 0
275
+ for widget in app.topLevelWidgets():
276
+ # through shiboken, not deleteLater: a window Qt deletes leaves
277
+ # its children's wrappers standing, and the sweep still crashed
278
+ if (shiboken6.isValid(widget) and shiboken6.ownedByPython(widget)
279
+ and widget.parent() is None):
280
+ shiboken6.delete(widget)
281
+ released += 1
282
+ QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
283
+ app.processEvents()
284
+ return released
@@ -162,12 +162,7 @@ def _draw_geometry(painter, color):
162
162
  def _draw_new_geometry(painter, color):
163
163
  """The geometry's frame with a plus: an empty geometry to build."""
164
164
  _draw_geometry(painter, color)
165
- painter.setPen(Qt.PenStyle.NoPen)
166
- painter.setBrush(QBrush(QColor('#2ca02c')))
167
- painter.drawEllipse(QPointF(50, 50), 13, 13)
168
- painter.setBrush(QBrush(QColor('white')))
169
- painter.drawRoundedRect(QRectF(48, 42, 4, 16), 1, 1)
170
- painter.drawRoundedRect(QRectF(42, 48, 16, 4), 1, 1)
165
+ _plus(painter, 50, 50)
171
166
 
172
167
 
173
168
  #: A pulse and its ringdown. The shape a time history wears, and the
@@ -284,6 +279,12 @@ def _draw_specification(painter, color):
284
279
  _psd_body(painter, color)
285
280
 
286
281
 
282
+ def _draw_new_specification(painter, color):
283
+ """The specification's glyph with a plus: a specification to write."""
284
+ _draw_specification(painter, color)
285
+ _plus(painter, 50, 50)
286
+
287
+
287
288
  def _draw_srs(painter, color):
288
289
  """The shape every shock response spectrum has.
289
290
 
@@ -969,6 +970,65 @@ def _draw_channel_table(painter, color):
969
970
  painter.drawLine(QPointF(32, 15), QPointF(32, 49))
970
971
 
971
972
 
973
+ def _plus(painter, x, y):
974
+ """The green disc with a white plus that marks a control as making
975
+ something new, centered at `(x, y)`."""
976
+ painter.setPen(Qt.PenStyle.NoPen)
977
+ painter.setBrush(QBrush(QColor('#2ca02c')))
978
+ painter.drawEllipse(QPointF(x, y), 13, 13)
979
+ painter.setBrush(QBrush(QColor('white')))
980
+ painter.drawRoundedRect(QRectF(x - 2, y - 8, 4, 16), 1, 1)
981
+ painter.drawRoundedRect(QRectF(x - 8, y - 2, 16, 4), 1, 1)
982
+
983
+
984
+ def _draw_new_channel_table(painter, color):
985
+ """The channel table with a plus: an empty one to fill in."""
986
+ _draw_channel_table(painter, color)
987
+ _plus(painter, 50, 50)
988
+
989
+
990
+ def _draw_add_channel(painter, color):
991
+ """A header and one row, with a plus on the row: a channel added to
992
+ the table."""
993
+ header = QColor(color)
994
+ header.setAlpha(110)
995
+ painter.fillRect(QRectF(8, 16, 48, 10), QBrush(header))
996
+ painter.setPen(_pen(color, 4))
997
+ painter.setBrush(Qt.BrushStyle.NoBrush)
998
+ painter.drawRoundedRect(QRectF(8, 16, 48, 22), 3, 3)
999
+ painter.drawLine(QPointF(8, 26), QPointF(56, 26))
1000
+ painter.drawLine(QPointF(32, 16), QPointF(32, 38))
1001
+ _plus(painter, 32, 50)
1002
+
1003
+
1004
+ def _draw_mac(painter, color):
1005
+ """A three by three grid, its diagonal full and the rest faint: a
1006
+ set's MAC against itself."""
1007
+ faint = QColor(color)
1008
+ faint.setAlpha(70)
1009
+ painter.setPen(Qt.PenStyle.NoPen)
1010
+ for row in range(3):
1011
+ for column in range(3):
1012
+ painter.setBrush(QBrush(QColor(color) if row == column else faint))
1013
+ painter.drawRect(QRectF(10 + 15 * column, 10 + 15 * row, 13, 13))
1014
+
1015
+
1016
+ def _draw_shape_values(painter, color):
1017
+ """A table headed along its top and down its left side: values with
1018
+ a DOF for each row and a mode for each column."""
1019
+ header = QColor(color)
1020
+ header.setAlpha(110)
1021
+ painter.fillRect(QRectF(11, 15, 42, 10), QBrush(header))
1022
+ painter.fillRect(QRectF(11, 25, 12, 24), QBrush(header))
1023
+ painter.setPen(_pen(color, 4))
1024
+ painter.setBrush(Qt.BrushStyle.NoBrush)
1025
+ painter.drawRoundedRect(QRectF(11, 15, 42, 34), 3, 3)
1026
+ for y in (25, 37):
1027
+ painter.drawLine(QPointF(11, y), QPointF(53, y))
1028
+ for x in (23, 38):
1029
+ painter.drawLine(QPointF(x, 15), QPointF(x, 49))
1030
+
1031
+
972
1032
  def _draw_shape_set(painter, color):
973
1033
  """A deformed mode shape with nodes riding on it."""
974
1034
  points = [(10 + i, 32 - 16 * math.sin(i / 44 * 2 * math.pi))
@@ -1867,40 +1927,50 @@ def _draw_coordinate_systems(painter, color):
1867
1927
  painter.drawEllipse(origin, 5, 5)
1868
1928
 
1869
1929
 
1930
+ def _depth(painter, color, *edges):
1931
+ """The edges a solid's face is extruded along, thinner than the face
1932
+ so they read as going back.
1933
+
1934
+ `_element_shape` leaves the pen off to fill its node dots, so these
1935
+ take a pen of their own.
1936
+ """
1937
+ # drawn with the dots' empty pen they were invisible: the tet wore
1938
+ # the triangle's icon and the brick the quad's (Brandon, 2026-10-22)
1939
+ painter.setPen(_pen(color, 5))
1940
+ for start, end in edges:
1941
+ painter.drawLine(QPointF(*start), QPointF(*end))
1942
+
1943
+
1870
1944
  def _draw_tet(painter, color):
1871
- """A tetrahedron: a triangle with its apex drawn to the far corner."""
1945
+ """A tetrahedron: a triangle with its far corner inside, joined to
1946
+ the three."""
1872
1947
  _element_shape(painter, color, [(32, 10), (54, 50), (10, 50)])
1873
- painter.drawLine(QPointF(32, 10), QPointF(34, 36))
1874
- painter.drawLine(QPointF(54, 50), QPointF(34, 36))
1875
- painter.drawLine(QPointF(10, 50), QPointF(34, 36))
1948
+ _depth(painter, color, ((32, 10), (34, 36)), ((54, 50), (34, 36)),
1949
+ ((10, 50), (34, 36)))
1876
1950
 
1877
1951
 
1878
1952
  def _draw_wedge(painter, color):
1879
- """A wedge: a triangle extruded sideways."""
1880
- _element_shape(painter, color, [(12, 20), (28, 48), (12, 48)])
1881
- painter.drawLine(QPointF(12, 20), QPointF(40, 14))
1882
- painter.drawLine(QPointF(28, 48), QPointF(54, 42))
1883
- painter.drawLine(QPointF(12, 48), QPointF(40, 42))
1884
- painter.drawLine(QPointF(40, 14), QPointF(54, 42))
1885
- painter.drawLine(QPointF(40, 14), QPointF(40, 42))
1886
- painter.drawLine(QPointF(40, 42), QPointF(54, 42))
1953
+ """A wedge: an upright triangle extruded back and to the right, a
1954
+ triangular prism, the hidden edges left out as the brick leaves
1955
+ them."""
1956
+ # Brandon, 2026-10-22: the old sideways ramp did not read as a wedge
1957
+ _element_shape(painter, color, [(24, 24), (40, 54), (8, 54)])
1958
+ _depth(painter, color, ((24, 24), (38, 10)), ((40, 54), (54, 40)),
1959
+ ((38, 10), (54, 40)))
1887
1960
 
1888
1961
 
1889
1962
  def _draw_hex(painter, color):
1890
1963
  """A brick: a square with its back face offset."""
1891
1964
  _element_shape(painter, color, [(10, 22), (42, 22), (42, 54), (10, 54)])
1892
- painter.drawLine(QPointF(10, 22), QPointF(22, 10))
1893
- painter.drawLine(QPointF(42, 22), QPointF(54, 10))
1894
- painter.drawLine(QPointF(42, 54), QPointF(54, 42))
1895
- painter.drawLine(QPointF(22, 10), QPointF(54, 10))
1896
- painter.drawLine(QPointF(54, 10), QPointF(54, 42))
1965
+ _depth(painter, color, ((10, 22), (22, 10)), ((42, 22), (54, 10)),
1966
+ ((42, 54), (54, 42)), ((22, 10), (54, 10)), ((54, 10), (54, 42)))
1897
1967
 
1898
1968
 
1899
1969
  def _draw_pyramid(painter, color):
1900
1970
  """A pyramid: a quad base with an apex."""
1901
1971
  _element_shape(painter, color, [(10, 50), (42, 50), (54, 40), (22, 40)])
1902
- for corner in ((10, 50), (42, 50), (54, 40), (22, 40)):
1903
- painter.drawLine(QPointF(*corner), QPointF(32, 10))
1972
+ _depth(painter, color, *(((corner), (32, 10))
1973
+ for corner in ((10, 50), (42, 50), (54, 40), (22, 40))))
1904
1974
 
1905
1975
 
1906
1976
  def _draw_elements(painter, color):
@@ -2150,6 +2220,10 @@ def control_icon(name: str) -> QIcon:
2150
2220
  'faster': _draw_faster, 'slower': _draw_slower,
2151
2221
  'up': _draw_up, 'down': _draw_down,
2152
2222
  'add': _draw_add, 'new_geometry': _draw_new_geometry,
2223
+ 'new_channel_table': _draw_new_channel_table,
2224
+ 'mac': _draw_mac, 'shape_values': _draw_shape_values,
2225
+ 'new_specification': _draw_new_specification,
2226
+ 'add_channel': _draw_add_channel,
2153
2227
  'beam': _draw_beam, 'tri': _draw_tri, 'quad': _draw_quad,
2154
2228
  'edit': _draw_edit, 'edit_on': _draw_edit_on, 'trash': _draw_trash,
2155
2229
  'colormap': _draw_colormap,