visualdynamics 0.1.0a72__tar.gz → 0.1.0a73__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 (259) hide show
  1. {visualdynamics-0.1.0a72/src/visualdynamics.egg-info → visualdynamics-0.1.0a73}/PKG-INFO +1 -1
  2. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/__init__.py +1 -1
  3. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/compatibility.py +60 -27
  4. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/data.py +4 -2
  5. visualdynamics-0.1.0a73/src/visualdynamics/core/modal_response.py +298 -0
  6. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/signals.py +131 -34
  7. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/transform.py +76 -6
  8. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/deform.py +126 -0
  9. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/icons.py +16 -0
  10. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/main_window.py +326 -26
  11. visualdynamics-0.1.0a73/src/visualdynamics/gui/modal_response_panel.py +89 -0
  12. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/movie.py +76 -0
  13. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/panes.py +13 -6
  14. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/signal_panel.py +27 -19
  15. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/__init__.py +5 -0
  16. visualdynamics-0.1.0a73/src/visualdynamics/plot/log_axis.py +157 -0
  17. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/project.py +191 -24
  18. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/animate.py +77 -9
  19. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73/src/visualdynamics.egg-info}/PKG-INFO +1 -1
  20. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics.egg-info/SOURCES.txt +3 -0
  21. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/LICENSE +0 -0
  22. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/README.md +0 -0
  23. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/pyproject.toml +0 -0
  24. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/setup.cfg +0 -0
  25. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/__main__.py +0 -0
  26. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/__init__.py +0 -0
  27. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/amplitude_spectrum.py +0 -0
  28. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/author.py +0 -0
  29. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/averaging.py +0 -0
  30. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/breakpoints.py +0 -0
  31. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/chance.py +0 -0
  32. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/channel_table.py +0 -0
  33. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/classical_pulse.py +0 -0
  34. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/compliance.py +0 -0
  35. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/correlate.py +0 -0
  36. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/detect.py +0 -0
  37. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/dwell.py +0 -0
  38. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/entities.py +0 -0
  39. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/fem.py +0 -0
  40. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/filters.py +0 -0
  41. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/geometry.py +0 -0
  42. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/half_power.py +0 -0
  43. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/hexmesh.py +0 -0
  44. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/instrumentation.py +0 -0
  45. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/kurtosis.py +0 -0
  46. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/matches.py +0 -0
  47. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/mates.py +0 -0
  48. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/merge.py +0 -0
  49. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/mesh.py +0 -0
  50. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/modal_fit.py +0 -0
  51. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/octave.py +0 -0
  52. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/photos.py +0 -0
  53. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/progress.py +0 -0
  54. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/replication.py +0 -0
  55. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/replication_fit.py +0 -0
  56. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/replication_segments.py +0 -0
  57. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/report.py +0 -0
  58. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/resample.py +0 -0
  59. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/rigid.py +0 -0
  60. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/rms_check.py +0 -0
  61. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/shaker.py +0 -0
  62. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/shapes.py +0 -0
  63. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/shock_check.py +0 -0
  64. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/shocks.py +0 -0
  65. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/signature.py +0 -0
  66. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/sine.py +0 -0
  67. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/sine_distortion.py +0 -0
  68. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/sine_frequency.py +0 -0
  69. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/sine_profile.py +0 -0
  70. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/sine_tracking.py +0 -0
  71. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/snr.py +0 -0
  72. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/solids.py +0 -0
  73. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/sparse_solve.py +0 -0
  74. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/srs.py +0 -0
  75. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/stroke.py +0 -0
  76. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/sweep_rate.py +0 -0
  77. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/tables.py +0 -0
  78. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/tetmesh.py +0 -0
  79. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/time_statistics.py +0 -0
  80. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/tone_readout.py +0 -0
  81. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/transmissibility.py +0 -0
  82. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/truncate.py +0 -0
  83. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/unit_choices.py +0 -0
  84. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/validate.py +0 -0
  85. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/core/wavelet.py +0 -0
  86. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/decimate.py +0 -0
  87. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/__init__.py +0 -0
  88. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/barc.py +0 -0
  89. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/drone.py +0 -0
  90. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/frame_wing.py +0 -0
  91. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/plate.py +0 -0
  92. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/shapes.py +0 -0
  93. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/__init__.py +0 -0
  94. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/amplitude_panel.py +0 -0
  95. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/app_icon.py +0 -0
  96. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/ask.py +0 -0
  97. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/author_panel.py +0 -0
  98. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/averaging_panel.py +0 -0
  99. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/chance_panel.py +0 -0
  100. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/confidence_panel.py +0 -0
  101. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/console.py +0 -0
  102. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/disclaimer.py +0 -0
  103. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/distortion_panel.py +0 -0
  104. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/drawer_tab.py +0 -0
  105. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/dwell_panel.py +0 -0
  106. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/editors.py +0 -0
  107. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/filter_panel.py +0 -0
  108. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/frequency_panel.py +0 -0
  109. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/glyphs.py +0 -0
  110. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
  111. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
  112. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/instrumentation_panel.py +0 -0
  113. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/levels_readout.py +0 -0
  114. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/mate_panel.py +0 -0
  115. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/merge_panel.py +0 -0
  116. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/mesh_panel.py +0 -0
  117. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/mesh_solids_panel.py +0 -0
  118. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/message_bar.py +0 -0
  119. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/modes_panel.py +0 -0
  120. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/object_tables.py +0 -0
  121. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/octave_panel.py +0 -0
  122. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/opengl.py +0 -0
  123. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/pattern_panel.py +0 -0
  124. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/preferences.py +0 -0
  125. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/profile_panel.py +0 -0
  126. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/project_tree.py +0 -0
  127. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/pulse_panel.py +0 -0
  128. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/record_grid.py +0 -0
  129. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/replication_fit_panel.py +0 -0
  130. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/report_editor.py +0 -0
  131. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/resample_panel.py +0 -0
  132. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/rms_check_panel.py +0 -0
  133. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/run_reports.py +0 -0
  134. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/segments_panel.py +0 -0
  135. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/settings_panel.py +0 -0
  136. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/shock_panel.py +0 -0
  137. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/signature_panel.py +0 -0
  138. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/sine_panel.py +0 -0
  139. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/srs_tolerance_panel.py +0 -0
  140. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/stage_drag.py +0 -0
  141. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/statistics_panel.py +0 -0
  142. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/stream_window.py +0 -0
  143. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/stroke_panel.py +0 -0
  144. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/sweep_rate_panel.py +0 -0
  145. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/tables.py +0 -0
  146. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/tone_panel.py +0 -0
  147. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/toolbars.py +0 -0
  148. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/tooltips.py +0 -0
  149. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/transmissibility_panel.py +0 -0
  150. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/truncate_panel.py +0 -0
  151. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/updater.py +0 -0
  152. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/wavelet_panel.py +0 -0
  153. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/windows.py +0 -0
  154. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/__init__.py +0 -0
  155. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/adf.py +0 -0
  156. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf.py +0 -0
  157. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_objects.py +0 -0
  158. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/LICENSE +0 -0
  159. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/activity_result.txt +0 -0
  160. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/channel_table.txt +0 -0
  161. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/data.txt +0 -0
  162. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/data_physics_random_control_parameters.txt +0 -0
  163. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/data_physics_shock_response_synthesis_control_parameters.txt +0 -0
  164. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/data_physics_sine_control_parameters.txt +0 -0
  165. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/geometry.txt +0 -0
  166. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/global_analysis_attributes.txt +0 -0
  167. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/global_test_attributes.txt +0 -0
  168. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/matrix.txt +0 -0
  169. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/mechanical_control_parameters.txt +0 -0
  170. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/modal_test_parameters.txt +0 -0
  171. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/mode.txt +0 -0
  172. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/parameter_set.txt +0 -0
  173. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/point_cloud.txt +0 -0
  174. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/rattlesnake_random_control_parameters.txt +0 -0
  175. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/rattlesnake_sysid_parameters.txt +0 -0
  176. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/rattlesnake_transient_control_parameters.txt +0 -0
  177. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/response_spectrum.txt +0 -0
  178. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/scalar.txt +0 -0
  179. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/unknown.txt +0 -0
  180. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/vector.txt +0 -0
  181. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/vibration_channel_table.txt +0 -0
  182. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/excel.py +0 -0
  183. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/exodus.py +0 -0
  184. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/exporters.py +0 -0
  185. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/femap.py +0 -0
  186. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/matlab.py +0 -0
  187. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/nastran.py +0 -0
  188. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/native.py +0 -0
  189. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/notes.py +0 -0
  190. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/photo_files.py +0 -0
  191. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/punch.py +0 -0
  192. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/rattlesnake.py +0 -0
  193. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/rattlesnake_sine.py +0 -0
  194. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
  195. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/report_template.py +0 -0
  196. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/sdynpy_data.py +0 -0
  197. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/sdynpy_npz.py +0 -0
  198. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
  199. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/sep005.py +0 -0
  200. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/sniffing.py +0 -0
  201. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/step.py +0 -0
  202. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/stl.py +0 -0
  203. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/threemf.py +0 -0
  204. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/io/unv.py +0 -0
  205. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/names.py +0 -0
  206. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/amplitude_spectrum.py +0 -0
  207. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/averaging.py +0 -0
  208. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/bands.py +0 -0
  209. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/bars.py +0 -0
  210. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/breakpoints.py +0 -0
  211. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/chance.py +0 -0
  212. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/classical_pulse.py +0 -0
  213. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/dwell.py +0 -0
  214. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/filtering.py +0 -0
  215. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/half_power.py +0 -0
  216. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/replication_fit.py +0 -0
  217. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/replication_segments.py +0 -0
  218. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/resample.py +0 -0
  219. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/rms_check.py +0 -0
  220. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/scalogram.py +0 -0
  221. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/shock_check.py +0 -0
  222. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/shocks.py +0 -0
  223. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/signature.py +0 -0
  224. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/sine_distortion.py +0 -0
  225. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/sine_frequency.py +0 -0
  226. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/sine_profile.py +0 -0
  227. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/sine_tracking.py +0 -0
  228. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/srs_tolerance.py +0 -0
  229. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/stroke.py +0 -0
  230. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/sweep_rate.py +0 -0
  231. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/time_statistics.py +0 -0
  232. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/tone_readout.py +0 -0
  233. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/tracking_filter.py +0 -0
  234. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/transmissibility.py +0 -0
  235. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/truncation.py +0 -0
  236. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/py.typed +0 -0
  237. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/report/__init__.py +0 -0
  238. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/report/markdown.py +0 -0
  239. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/report/page.py +0 -0
  240. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/rotate.py +0 -0
  241. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/theme.py +0 -0
  242. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/units.py +0 -0
  243. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/update.py +0 -0
  244. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/__init__.py +0 -0
  245. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/_quiet.py +0 -0
  246. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/banded.py +0 -0
  247. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/geometry.py +0 -0
  248. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/mac_bars.py +0 -0
  249. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/marks.py +0 -0
  250. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/paired.py +0 -0
  251. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/pick.py +0 -0
  252. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/rigid.py +0 -0
  253. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/scalogram.py +0 -0
  254. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/sinespec.py +0 -0
  255. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/waterfall.py +0 -0
  256. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
  257. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics.egg-info/entry_points.txt +0 -0
  258. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/src/visualdynamics.egg-info/requires.txt +0 -0
  259. {visualdynamics-0.1.0a72 → visualdynamics-0.1.0a73}/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.0a72
3
+ Version: 0.1.0a73
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
@@ -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.0a72'
100
+ __version__ = '0.1.0a73'
101
101
 
102
102
 
103
103
  def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
@@ -19,9 +19,12 @@ importing data before its geometry does not mark everything red.
19
19
 
20
20
  A data object whose DOFs are all modal coordinates (`M1` ... `Mn`, see
21
21
  `validate.modal_coordinate`) names no node. It answers to a shape set,
22
- the one it was transformed through, and fits wherever there is a set
23
- with at least as many modes as it names: a set in its object group when
24
- it is in one, any set in the project otherwise.
22
+ the one it was transformed or computed through, and fits wherever there
23
+ is a set with at least as many modes as it names: a set in its object
24
+ group when it is in one, any set in the project otherwise. A time
25
+ history can hold both kinds of record, forces with the modal responses
26
+ computed from them appended: its physical records answer to the
27
+ geometry and its modal records to the shape sets, each by its own rule.
25
28
 
26
29
  Not checked here:
27
30
 
@@ -51,9 +54,11 @@ MAX_LISTED = 6 # DOFs named in a message before it summarizes
51
54
  class Issue:
52
55
  """Why one object does not fit the geometry or shape set it answers to.
53
56
 
54
- `kind` is 'dofs-not-in-geometry' or 'modes-not-in-set'. `missing_dofs`
55
- lists the DOFs at fault, and `sub_items` the indices of the records,
56
- shapes or channels that name them.
57
+ `kind` is 'dofs-not-in-geometry', 'modes-not-in-set' (an object of
58
+ modal coordinates alone), or 'modes-beyond-set' (modal records beside
59
+ physical ones, the physical ones fitting). `missing_dofs` lists the
60
+ DOFs at fault, and `sub_items` the indices of the records, shapes or
61
+ channels that name them.
57
62
  """
58
63
 
59
64
  name: str
@@ -169,28 +174,39 @@ def modal_object(obj: Any) -> int | None:
169
174
  return max(indices)
170
175
 
171
176
 
177
+ def modal_records(obj: Any) -> list[int]:
178
+ """The records of a data object at modal coordinates; empty for
179
+ anything else."""
180
+ from .core.data import DataArray
181
+
182
+ if not isinstance(obj, DataArray):
183
+ return []
184
+ return [i for i, dof in enumerate(obj.response_dof) if _index(dof)]
185
+
186
+
172
187
  def check_modal(name: str, obj: Any, companions: Mapping[str, Any]
173
188
  ) -> Issue | None:
174
189
  """The Issue for a modal object against the shape sets among its
175
190
  companions, or None when one of them has every mode it names.
176
191
 
177
192
  The Issue's kind is 'modes-not-in-set', and its `sub_items` are the
178
- records that name a mode beyond the largest set.
193
+ records that name a mode beyond the largest set. A mixed object's
194
+ physical records name no mode and are never among them.
179
195
  """
180
196
  from .core.shapes import ShapeSet
181
197
 
182
- highest = modal_object(obj)
198
+ records = range(obj.num_records)
199
+ highest = max(_index(obj.response_dof[i]) for i in records)
183
200
  sets = {other: value for other, value in companions.items()
184
201
  if isinstance(value, ShapeSet) and other != name}
185
202
  if any(value.num_shapes >= highest for value in sets.values()):
186
203
  return None
187
- beyond = [i for i, dof in enumerate(obj.response_dof)
188
- if _index(dof) > max((v.num_shapes for v in sets.values()),
189
- default=0)]
204
+ beyond = [i for i in records
205
+ if _index(obj.response_dof[i])
206
+ > max((v.num_shapes for v in sets.values()), default=0)]
190
207
  if not sets:
191
208
  message = (f'modal coordinates up to M{highest}, and no shape set '
192
- 'to answer to — link the set they were transformed '
193
- 'through')
209
+ 'to answer to — link the set they belong to')
194
210
  else:
195
211
  most = max(sets.items(), key=lambda item: item[1].num_shapes)
196
212
  message = (f'modal coordinates up to M{highest}, but shape set '
@@ -219,7 +235,9 @@ def blocks_a_link(issue: Issue | None) -> bool:
219
235
 
220
236
  What blocks is a modal object with no shape set that has as many
221
237
  modes as it names, since it has nothing to be drawn against in any
222
- geometry. (`Project.link` refuses two geometries in one group before
238
+ geometry. A time history holding modal records beside physical ones
239
+ does not block: its physical records are drawn, and the shortfall of
240
+ its modal ones is reported ('modes-beyond-set'). (`Project.link` refuses two geometries in one group before
223
241
  this is called.)
224
242
 
225
243
  Parameters
@@ -230,15 +248,15 @@ def blocks_a_link(issue: Issue | None) -> bool:
230
248
  Returns
231
249
  -------
232
250
  bool
233
- True for a 'modes-not-in-set' issue, False for None or a
234
- 'dofs-not-in-geometry' issue.
251
+ True for a 'modes-not-in-set' issue, False for None or any
252
+ other kind.
235
253
  """
236
254
  # Missing nodes stopped blocking after a real report failed on them
237
255
  # twice in two days (Brandon, 2026-09-20 and 2026-09-21), the second
238
256
  # time on an object whose channels were all virtual. That is why the
239
257
  # first attempt, refusing only when nothing overlapped, was not a
240
258
  # line that could be drawn either.
241
- return issue is not None and issue.kind != 'dofs-not-in-geometry'
259
+ return issue is not None and issue.kind == 'modes-not-in-set'
242
260
 
243
261
 
244
262
  def check_object(name: str, obj: Any, geometry: Geometry | None,
@@ -261,7 +279,8 @@ def check_object(name: str, obj: Any, geometry: Geometry | None,
261
279
  return None
262
280
  if modal_object(obj) is not None:
263
281
  return check_modal(name, obj, companions or {})
264
- if geometry is None:
282
+ modal = modal_records(obj)
283
+ if geometry is None and not modal:
265
284
  return None
266
285
 
267
286
  if isinstance(obj, ShapeSet):
@@ -277,8 +296,14 @@ def check_object(name: str, obj: Any, geometry: Geometry | None,
277
296
  sub_items=list(range(obj.num_shapes)))
278
297
 
279
298
  if isinstance(obj, DataArray):
299
+ # modal records beside physical ones (Brandon, 2026-10-25: forces
300
+ # with their modal responses appended went red, every M record
301
+ # read as a node the geometry lacked): each kind by its own rule
302
+ skipped = set(modal)
280
303
  missing, flagged = [], []
281
304
  for i in range(obj.num_records):
305
+ if geometry is None or i in skipped:
306
+ continue
282
307
  dofs = [obj.response_dof[i]]
283
308
  if obj.reference_dof is not None:
284
309
  dofs.append(obj.reference_dof[i])
@@ -286,14 +311,21 @@ def check_object(name: str, obj: Any, geometry: Geometry | None,
286
311
  if gone:
287
312
  flagged.append(i)
288
313
  missing.extend(d for d in gone if d not in missing)
289
- if not flagged:
314
+ short = check_modal(name, obj, companions or {}) if modal else None
315
+ if not flagged and short is None:
290
316
  return None
291
- return Issue(
292
- name=name, kind='dofs-not-in-geometry',
293
- message=(f'{len(flagged)} of {obj.num_records} records reference '
294
- f'DOFs not in geometry {geometry_name!r}: '
295
- f'{_listed(missing)}'),
296
- missing_dofs=missing, sub_items=flagged)
317
+ said, kind = [], 'modes-beyond-set'
318
+ if flagged:
319
+ kind = 'dofs-not-in-geometry'
320
+ said.append(f'{len(flagged)} of {obj.num_records} records '
321
+ f'reference DOFs not in geometry {geometry_name!r}: '
322
+ f'{_listed(missing)}')
323
+ if short is not None:
324
+ said.append(short.message)
325
+ missing += short.missing_dofs
326
+ flagged = sorted(flagged + short.sub_items)
327
+ return Issue(name=name, kind=kind, message='; '.join(said),
328
+ missing_dofs=missing, sub_items=flagged)
297
329
 
298
330
  if isinstance(obj, ChannelTable):
299
331
  dofs = obj.dof_strings()
@@ -374,11 +406,12 @@ def check_compatibility(objects: Mapping[str, Any],
374
406
  for name, obj in objects.items():
375
407
  if modal_object(obj) is not None:
376
408
  issue = check_modal(name, obj, companions(name))
377
- elif geometry_name is None:
409
+ elif geometry_name is None and not modal_records(obj):
378
410
  continue
379
411
  else:
380
412
  against = home(name)
381
- issue = check_object(name, obj, geometries.get(against), against)
413
+ issue = check_object(name, obj, geometries.get(against), against,
414
+ companions=companions(name))
382
415
  if issue is not None:
383
416
  report.issues[name] = issue
384
417
  return report
@@ -1333,8 +1333,10 @@ class TimeHistory(DataArray):
1333
1333
  **kwargs
1334
1334
  Passed to `visualdynamics.viz.animate.animate_time`:
1335
1335
  `quantity=` picks which measurement moves the geometry in a
1336
- mixed record (the most common one by default),
1337
- `screenshot=` saves a still at the sample with the largest
1336
+ mixed record (the most common one by default), `shapes=` a
1337
+ shape set plays the record's modal coordinates through its
1338
+ mode shapes, `screenshot=` saves a still at the sample with
1339
+ the largest
1338
1340
  deflection, `movie=` saves the record once through as an MP4,
1339
1341
  `seconds=` sets how long that takes.
1340
1342
 
@@ -0,0 +1,298 @@
1
+ """Modal responses: how each mode of a model answers the forces a record
2
+ holds, computed in modal coordinates and kept beside the forces.
3
+
4
+ A model's modes are a compact description of how it moves: a large
5
+ finite element model has many more physical DOFs than modes in a
6
+ bandwidth, and the response anywhere follows from the modal responses
7
+ by u = Φq. So the response to a test's forces is computed and kept as
8
+ modal responses, one record per mode, appended to the time history the
9
+ forces are in; the physical response at any DOF is a later expansion.
10
+
11
+ The forces are made modal first, pᵣ = Σₖ φₖᵣ fₖ, the projection
12
+ `transform.to_modal` makes of a force record (a record already holding
13
+ modal forces is used as it is). Each mode r then answers its own
14
+ equation,
15
+
16
+ q̈ᵣ + 2ζᵣωᵣ q̇ᵣ + ωᵣ² qᵣ = pᵣ(t) / mᵣ,
17
+
18
+ with ωᵣ, ζᵣ and mᵣ the set's frequency, damping and modal mass (one for
19
+ a mass-normalized set). It is solved exactly for a force that varies
20
+ linearly between samples: the mode's state equation discretized with a
21
+ first-order hold, which is a second-order recursive filter run over the
22
+ record. Nothing else is approximated, so the start of a record, a
23
+ transient, and a rigid-body mode (ωᵣ = 0, whose displacement under a
24
+ net force grows without bound, as it does) are all answered as they are.
25
+ The record starts at rest.
26
+
27
+ The response is a displacement, a velocity or an acceleration of each
28
+ modal coordinate, `M1` … `Mn` named as everywhere in the package (the
29
+ mode's 1-based index in its set). Through a mass-normalized set the
30
+ units carry a half power of mass, the `modal_*` dimensions, as
31
+ `transform` gives them; through any other set they are left undeclared
32
+ and hinted.
33
+
34
+ References
35
+ ----------
36
+ .. [1] G. F. Franklin, J. D. Powell and M. L. Workman, *Digital Control
37
+ of Dynamic Systems*, 3rd ed., Addison-Wesley, 1998, sec. 6.3.2
38
+ (the triangle-hold equivalent).
39
+ .. [2] R. R. Craig and A. J. Kurdila, *Fundamentals of Structural
40
+ Dynamics*, 2nd ed., Wiley, 2006, ch. 10.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import copy
46
+ from collections.abc import Sequence
47
+
48
+ import numpy as np
49
+
50
+ from ..units import UNKNOWN
51
+ from .data import TimeHistory
52
+ from .shapes import ShapeSet
53
+ from .transform import MODAL, modal_dofs, reads_as, to_modal
54
+
55
+ #: what each response is, by the name a user picks
56
+ QUANTITIES: dict[str, str] = {'displacement': 'length',
57
+ 'velocity': 'velocity',
58
+ 'acceleration': 'acceleration'}
59
+
60
+ #: the modal force dimension `transform` gives a mass-normalized set
61
+ MODAL_FORCE = MODAL['force']
62
+
63
+
64
+ def _mode_response(force: np.ndarray, frequency: float, damping: float,
65
+ mass: float, dt: float, quantity: str) -> np.ndarray:
66
+ """A mode's response to its modal force, sampled every `dt` and
67
+ linear between samples, from rest.
68
+
69
+ The state x = (q, q̇) over one step with the force linear from u[k]
70
+ to u[k+1] is exactly x[k+1] = Φ x[k] + (Γa − Γb) u[k] + Γb u[k+1],
71
+ with Φ, Γa and Γb blocks of the exponential of the step's augmented
72
+ matrix. Written in w[k] = x[k] − Γb u[k], it is a causal state
73
+ equation, and so a second-order filter; at rest x[0] = 0 means
74
+ w[0] = −Γb u[0], whose decay C Φᵏ w[0] is added to the filter's
75
+ output, which starts from w[0] = 0.
76
+ """
77
+ # scipy's cont2discrete(method='foh') is the triangle-hold
78
+ # equivalent, which is centered: a rigid mode under a constant force
79
+ # came out half a sample early (3·dt·T/2 too far after T seconds),
80
+ # and a two-DOF model 0.37 % off a direct simulation (2026-10-25)
81
+ from scipy.linalg import expm
82
+ from scipy.signal import lfilter, ss2tf
83
+
84
+ w = 2.0 * np.pi * float(frequency)
85
+ z = float(damping)
86
+ a = np.array([[0.0, 1.0], [-w * w, -2.0 * z * w]])
87
+ b = np.array([[0.0], [1.0 / mass]])
88
+ if quantity == 'displacement':
89
+ c, d = np.array([[1.0, 0.0]]), np.array([[0.0]])
90
+ elif quantity == 'velocity':
91
+ c, d = np.array([[0.0, 1.0]]), np.array([[0.0]])
92
+ else:
93
+ c, d = np.array([[-w * w, -2.0 * z * w]]), np.array([[1.0 / mass]])
94
+ augmented = np.zeros((4, 4))
95
+ augmented[:2, :2] = a * dt
96
+ augmented[:2, 2:3] = b * dt
97
+ augmented[2, 3] = 1.0
98
+ blocks = expm(augmented)
99
+ phi, gamma_a, gamma_b = blocks[:2, :2], blocks[:2, 2:3], blocks[:2, 3:4]
100
+ num, den = ss2tf(phi, phi @ gamma_b + gamma_a - gamma_b, c,
101
+ c @ gamma_b + d)
102
+ out = lfilter(np.real(num[0]), np.real(den), force)
103
+ if force.size and force[0] != 0.0:
104
+ # C Φᵏ Γb for k = 0, 1, …: the impulse response of (Φ, Γb, C),
105
+ # one sample on
106
+ num_0, den_0 = ss2tf(phi, gamma_b, c, np.zeros((1, 1)))
107
+ impulse = np.zeros(force.size + 1)
108
+ impulse[0] = 1.0
109
+ decay = lfilter(np.real(num_0[0]), np.real(den_0), impulse)[1:]
110
+ out -= force[0] * decay
111
+ return out
112
+
113
+
114
+ def modal_forces(history: TimeHistory, shapes: ShapeSet,
115
+ records: Sequence[int] | None = None) -> TimeHistory:
116
+ """The modal forces the record's force channels put on every mode of
117
+ the set, one record per mode, `M1` … `Mn`.
118
+
119
+ Force channels on DOFs the set covers are projected, pᵣ = Σ φₖᵣ fₖ;
120
+ a record whose channels are already the set's modal forces is used
121
+ as it is. Other channels (the responses) are passed over.
122
+
123
+ Raises
124
+ ------
125
+ ValueError
126
+ If no picked record is a force on a DOF the set covers, the set
127
+ is complex or has no modes.
128
+ """
129
+ if shapes.num_shapes == 0:
130
+ raise ValueError('the set has no modes')
131
+ if shapes.is_complex:
132
+ raise ValueError('the set is complex: a real force cannot drive a '
133
+ 'complex modal coordinate here')
134
+ picked = (list(range(history.num_records)) if records is None
135
+ else sorted({int(i) for i in records}))
136
+ names = set(modal_dofs(shapes))
137
+ modal = [i for i in picked if history.response_dof[i] in names
138
+ and history.ordinate_dim[i] in (MODAL_FORCE, 'force', UNKNOWN)
139
+ and (history.ordinate_dim[i] == MODAL_FORCE
140
+ or history.dimension_hint[i] == MODAL_FORCE)]
141
+ if modal and reads_as(history, shapes) == 'modal':
142
+ rows = np.zeros((shapes.num_shapes, len(history.abscissa)))
143
+ order = modal_dofs(shapes)
144
+ for i in modal:
145
+ rows[order.index(history.response_dof[i])] += history.ordinate[i]
146
+ dim = history.ordinate_dim[modal[0]]
147
+ return TimeHistory(history.abscissa, rows, response_dof=order,
148
+ ordinate_dim=dim,
149
+ dimension_hint=None if dim != UNKNOWN else MODAL_FORCE)
150
+ forces = [i for i in picked if history.ordinate_dim[i] == 'force']
151
+ if not forces:
152
+ raise ValueError('no force channel picked: the modal response is '
153
+ 'the answer to forces, and the record holds none '
154
+ 'on the DOFs picked')
155
+ projected, _report = to_modal(history, shapes, records=forces)
156
+ return projected
157
+
158
+
159
+ def modal_responses(history: TimeHistory, shapes: ShapeSet, *,
160
+ modes: Sequence[int] | None = None,
161
+ quantity: str = 'acceleration',
162
+ records: Sequence[int] | None = None,
163
+ label: str = 'shapes') -> TimeHistory:
164
+ """Each picked mode's response to the record's forces, one record per
165
+ mode, at the modal coordinates `M1` … `Mn`.
166
+
167
+ Parameters
168
+ ----------
169
+ history : TimeHistory
170
+ Evenly sampled, holding force channels on DOFs the set covers,
171
+ or the set's modal forces.
172
+ shapes : ShapeSet
173
+ Real modes with frequency, damping and modal mass.
174
+ modes : sequence of int, optional
175
+ Which modes, by index in the set from zero; every mode when
176
+ omitted.
177
+ quantity : str, default 'acceleration'
178
+ 'displacement', 'velocity' or 'acceleration' of each modal
179
+ coordinate.
180
+ records : sequence of int, optional
181
+ Which of the history's records are the forces; every force
182
+ channel when omitted.
183
+ label : str, default 'shapes'
184
+ The set's name, written into each record's comment, which is how
185
+ a later run through the same set finds the records to replace.
186
+
187
+ Returns
188
+ -------
189
+ TimeHistory
190
+ One record per mode picked, on the history's own time axis.
191
+
192
+ Raises
193
+ ------
194
+ ValueError
195
+ If the quantity is not one of `QUANTITIES`, a mode is not in the
196
+ set, the record is not evenly sampled, or as `modal_forces`.
197
+ """
198
+ if quantity not in QUANTITIES:
199
+ raise ValueError(f'{quantity!r} is not a response: one of '
200
+ f'{", ".join(QUANTITIES)}')
201
+ chosen = (list(range(shapes.num_shapes)) if modes is None
202
+ else sorted({int(k) for k in modes}))
203
+ bad = [k for k in chosen if not 0 <= k < shapes.num_shapes]
204
+ if bad:
205
+ raise ValueError(f'no mode {bad[0] + 1}: the set has '
206
+ f'{shapes.num_shapes}')
207
+ if not chosen:
208
+ raise ValueError('no mode picked')
209
+ t = np.asarray(history.abscissa, dtype=float)
210
+ steps = np.diff(t)
211
+ if t.size < 2 or not np.allclose(steps, steps[0], rtol=1e-6, atol=0.0):
212
+ raise ValueError('the record is not evenly sampled')
213
+ dt = float(steps[0])
214
+ forces = modal_forces(history, shapes, records)
215
+ names = modal_dofs(shapes)
216
+ rows = np.empty((len(chosen), t.size))
217
+ for row, k in enumerate(chosen):
218
+ rows[row] = _mode_response(np.asarray(forces.ordinate[k], dtype=float),
219
+ shapes.frequency[k], shapes.damping[k],
220
+ float(shapes.modal_mass[k]), dt, quantity)
221
+ force_dim = forces.ordinate_dim[0]
222
+ physical = QUANTITIES[quantity]
223
+ known = force_dim == MODAL_FORCE
224
+ dim = MODAL[physical] if known else UNKNOWN
225
+ hint = None if known else MODAL[physical]
226
+ comments = [f'{label}: modal {quantity}, mode {k + 1} at '
227
+ f'{float(shapes.frequency[k]):.4g} Hz' for k in chosen]
228
+ return TimeHistory(t, rows, response_dof=[names[k] for k in chosen],
229
+ ordinate_dim=dim, dimension_hint=hint,
230
+ comment=comments)
231
+
232
+
233
+ def made_through(history: TimeHistory, label: str) -> list[int]:
234
+ """The records of `history` that an earlier run through the set
235
+ named `label` appended (`modal_responses` writes the name into each
236
+ record's comment)."""
237
+ prefix = f'{label}: modal '
238
+ return [i for i, comment in enumerate(history.comment)
239
+ if comment.startswith(prefix)]
240
+
241
+
242
+ def appended(history: TimeHistory, responses: TimeHistory,
243
+ label: str) -> TimeHistory:
244
+ """`history` with `responses` appended as records after its own, the
245
+ records an earlier run through the same set made taken out first.
246
+ Its averaging, shock windows, roles and other settings are kept;
247
+ `history` itself is not changed."""
248
+ keep = [i for i in range(history.num_records)
249
+ if i not in set(made_through(history, label))]
250
+ out = copy.copy(history)
251
+ n = len(responses.response_dof)
252
+
253
+ def joined(field: str, new: list) -> list | None:
254
+ old = getattr(history, field)
255
+ if old is None and all(v is None for v in new):
256
+ return None
257
+ old = [None] * history.num_records if old is None else list(old)
258
+ return [old[i] for i in keep] + list(new)
259
+
260
+ out.ordinate = np.vstack([np.asarray(history.ordinate)[keep],
261
+ responses.ordinate])
262
+ out.response_dof = [history.response_dof[i] for i in keep] + list(
263
+ responses.response_dof)
264
+ out.ordinate_dim = [history.ordinate_dim[i] for i in keep] + list(
265
+ responses.ordinate_dim)
266
+ out.ordinate_unit = [history.ordinate_unit[i] for i in keep] + list(
267
+ responses.ordinate_unit)
268
+ out.reference_unit = [history.reference_unit[i] for i in keep] + list(
269
+ responses.reference_unit)
270
+ out.comment = [history.comment[i] for i in keep] + list(responses.comment)
271
+ out.dimension_hint = [history.dimension_hint[i] for i in keep] + list(
272
+ responses.dimension_hint)
273
+ out.reference_dof = joined('reference_dof', [None] * n)
274
+ # a history with no capture labels keeps none: blank labels would
275
+ # read as one capture named '' (2026-10-25)
276
+ out.block = (None if history.block is None
277
+ else [b or '' for b in joined('block', [''] * n)])
278
+ return out
279
+
280
+
281
+ #: the most samples a preview computes, so that it answers while the
282
+ #: pane is open; Compute runs the whole record
283
+ PREVIEW_SAMPLES = 2 ** 16
284
+
285
+
286
+ def preview_responses(history: TimeHistory, shapes: ShapeSet, *,
287
+ modes: Sequence[int] | None = None,
288
+ quantity: str = 'acceleration',
289
+ records: Sequence[int] | None = None,
290
+ label: str = 'shapes') -> TimeHistory:
291
+ """`modal_responses` over the first `PREVIEW_SAMPLES` of the record:
292
+ the same responses there, since each starts from rest at the
293
+ record's start."""
294
+ short = copy.copy(history)
295
+ short.abscissa = np.asarray(history.abscissa)[:PREVIEW_SAMPLES]
296
+ short.ordinate = np.asarray(history.ordinate)[:, :PREVIEW_SAMPLES]
297
+ return modal_responses(short, shapes, modes=modes, quantity=quantity,
298
+ records=records, label=label)