visualdynamics 0.1.0a71__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.0a71/src/visualdynamics.egg-info → visualdynamics-0.1.0a73}/PKG-INFO +1 -1
  2. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/__init__.py +1 -1
  3. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/compatibility.py +60 -27
  4. visualdynamics-0.1.0a73/src/visualdynamics/core/amplitude_spectrum.py +455 -0
  5. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/author.py +231 -6
  6. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/averaging.py +135 -0
  7. visualdynamics-0.1.0a73/src/visualdynamics/core/breakpoints.py +316 -0
  8. visualdynamics-0.1.0a73/src/visualdynamics/core/chance.py +516 -0
  9. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/channel_table.py +38 -11
  10. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/compliance.py +11 -1
  11. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/data.py +453 -38
  12. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/filters.py +13 -4
  13. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/merge.py +6 -0
  14. visualdynamics-0.1.0a73/src/visualdynamics/core/modal_response.py +298 -0
  15. visualdynamics-0.1.0a73/src/visualdynamics/core/resample.py +692 -0
  16. visualdynamics-0.1.0a73/src/visualdynamics/core/rms_check.py +661 -0
  17. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/shocks.py +2 -1
  18. visualdynamics-0.1.0a73/src/visualdynamics/core/signals.py +1022 -0
  19. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/sine.py +27 -11
  20. visualdynamics-0.1.0a73/src/visualdynamics/core/sine_distortion.py +765 -0
  21. visualdynamics-0.1.0a73/src/visualdynamics/core/sine_frequency.py +411 -0
  22. visualdynamics-0.1.0a73/src/visualdynamics/core/sine_profile.py +964 -0
  23. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/stroke.py +17 -14
  24. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/transform.py +76 -6
  25. visualdynamics-0.1.0a73/src/visualdynamics/core/transmissibility.py +441 -0
  26. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/decimate.py +6 -0
  27. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/deform.py +126 -0
  28. visualdynamics-0.1.0a73/src/visualdynamics/gui/amplitude_panel.py +205 -0
  29. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/author_panel.py +255 -28
  30. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/averaging_panel.py +12 -3
  31. visualdynamics-0.1.0a73/src/visualdynamics/gui/chance_panel.py +132 -0
  32. visualdynamics-0.1.0a73/src/visualdynamics/gui/confidence_panel.py +162 -0
  33. visualdynamics-0.1.0a73/src/visualdynamics/gui/distortion_panel.py +112 -0
  34. visualdynamics-0.1.0a73/src/visualdynamics/gui/frequency_panel.py +125 -0
  35. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/icons.py +209 -1
  36. visualdynamics-0.1.0a73/src/visualdynamics/gui/levels_readout.py +255 -0
  37. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/main_window.py +1405 -31
  38. visualdynamics-0.1.0a73/src/visualdynamics/gui/modal_response_panel.py +89 -0
  39. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/movie.py +76 -0
  40. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/panes.py +257 -29
  41. visualdynamics-0.1.0a73/src/visualdynamics/gui/profile_panel.py +145 -0
  42. visualdynamics-0.1.0a73/src/visualdynamics/gui/resample_panel.py +211 -0
  43. visualdynamics-0.1.0a73/src/visualdynamics/gui/rms_check_panel.py +159 -0
  44. visualdynamics-0.1.0a73/src/visualdynamics/gui/signal_panel.py +364 -0
  45. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/sine_panel.py +23 -2
  46. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/statistics_panel.py +9 -3
  47. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/stroke_panel.py +2 -1
  48. visualdynamics-0.1.0a73/src/visualdynamics/gui/transmissibility_panel.py +107 -0
  49. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/native.py +7 -0
  50. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/rattlesnake.py +23 -4
  51. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/__init__.py +1143 -63
  52. visualdynamics-0.1.0a73/src/visualdynamics/plot/amplitude_spectrum.py +123 -0
  53. visualdynamics-0.1.0a73/src/visualdynamics/plot/breakpoints.py +284 -0
  54. visualdynamics-0.1.0a73/src/visualdynamics/plot/chance.py +223 -0
  55. visualdynamics-0.1.0a73/src/visualdynamics/plot/log_axis.py +157 -0
  56. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/replication_fit.py +4 -2
  57. visualdynamics-0.1.0a73/src/visualdynamics/plot/resample.py +218 -0
  58. visualdynamics-0.1.0a73/src/visualdynamics/plot/rms_check.py +161 -0
  59. visualdynamics-0.1.0a73/src/visualdynamics/plot/sine_distortion.py +198 -0
  60. visualdynamics-0.1.0a73/src/visualdynamics/plot/sine_frequency.py +203 -0
  61. visualdynamics-0.1.0a73/src/visualdynamics/plot/sine_profile.py +208 -0
  62. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/stroke.py +1 -1
  63. visualdynamics-0.1.0a73/src/visualdynamics/plot/transmissibility.py +297 -0
  64. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/project.py +454 -25
  65. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/report/__init__.py +51 -3
  66. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/animate.py +77 -9
  67. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73/src/visualdynamics.egg-info}/PKG-INFO +1 -1
  68. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics.egg-info/SOURCES.txt +33 -0
  69. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/LICENSE +0 -0
  70. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/README.md +0 -0
  71. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/pyproject.toml +0 -0
  72. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/setup.cfg +0 -0
  73. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/__main__.py +0 -0
  74. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/__init__.py +0 -0
  75. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/classical_pulse.py +0 -0
  76. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/correlate.py +0 -0
  77. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/detect.py +0 -0
  78. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/dwell.py +0 -0
  79. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/entities.py +0 -0
  80. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/fem.py +0 -0
  81. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/geometry.py +0 -0
  82. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/half_power.py +0 -0
  83. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/hexmesh.py +0 -0
  84. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/instrumentation.py +0 -0
  85. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/kurtosis.py +0 -0
  86. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/matches.py +0 -0
  87. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/mates.py +0 -0
  88. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/mesh.py +0 -0
  89. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/modal_fit.py +0 -0
  90. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/octave.py +0 -0
  91. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/photos.py +0 -0
  92. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/progress.py +0 -0
  93. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/replication.py +0 -0
  94. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/replication_fit.py +0 -0
  95. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/replication_segments.py +0 -0
  96. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/report.py +0 -0
  97. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/rigid.py +0 -0
  98. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/shaker.py +0 -0
  99. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/shapes.py +0 -0
  100. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/shock_check.py +0 -0
  101. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/signature.py +0 -0
  102. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/sine_tracking.py +0 -0
  103. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/snr.py +0 -0
  104. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/solids.py +0 -0
  105. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/sparse_solve.py +0 -0
  106. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/srs.py +0 -0
  107. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/sweep_rate.py +0 -0
  108. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/tables.py +0 -0
  109. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/tetmesh.py +0 -0
  110. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/time_statistics.py +0 -0
  111. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/tone_readout.py +0 -0
  112. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/truncate.py +0 -0
  113. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/unit_choices.py +0 -0
  114. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/validate.py +0 -0
  115. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/core/wavelet.py +0 -0
  116. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/__init__.py +0 -0
  117. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/barc.py +0 -0
  118. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/drone.py +0 -0
  119. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/frame_wing.py +0 -0
  120. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/plate.py +0 -0
  121. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/demo/shapes.py +0 -0
  122. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/__init__.py +0 -0
  123. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/app_icon.py +0 -0
  124. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/ask.py +0 -0
  125. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/console.py +0 -0
  126. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/disclaimer.py +0 -0
  127. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/drawer_tab.py +0 -0
  128. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/dwell_panel.py +0 -0
  129. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/editors.py +0 -0
  130. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/filter_panel.py +0 -0
  131. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/glyphs.py +0 -0
  132. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
  133. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
  134. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/instrumentation_panel.py +0 -0
  135. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/mate_panel.py +0 -0
  136. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/merge_panel.py +0 -0
  137. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/mesh_panel.py +0 -0
  138. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/mesh_solids_panel.py +0 -0
  139. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/message_bar.py +0 -0
  140. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/modes_panel.py +0 -0
  141. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/object_tables.py +0 -0
  142. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/octave_panel.py +0 -0
  143. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/opengl.py +0 -0
  144. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/pattern_panel.py +0 -0
  145. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/preferences.py +0 -0
  146. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/project_tree.py +0 -0
  147. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/pulse_panel.py +0 -0
  148. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/record_grid.py +0 -0
  149. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/replication_fit_panel.py +0 -0
  150. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/report_editor.py +0 -0
  151. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/run_reports.py +0 -0
  152. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/segments_panel.py +0 -0
  153. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/settings_panel.py +0 -0
  154. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/shock_panel.py +0 -0
  155. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/signature_panel.py +0 -0
  156. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/srs_tolerance_panel.py +0 -0
  157. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/stage_drag.py +0 -0
  158. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/stream_window.py +0 -0
  159. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/sweep_rate_panel.py +0 -0
  160. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/tables.py +0 -0
  161. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/tone_panel.py +0 -0
  162. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/toolbars.py +0 -0
  163. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/tooltips.py +0 -0
  164. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/truncate_panel.py +0 -0
  165. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/updater.py +0 -0
  166. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/wavelet_panel.py +0 -0
  167. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/gui/windows.py +0 -0
  168. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/__init__.py +0 -0
  169. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/adf.py +0 -0
  170. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf.py +0 -0
  171. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_objects.py +0 -0
  172. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/LICENSE +0 -0
  173. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/activity_result.txt +0 -0
  174. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/channel_table.txt +0 -0
  175. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/data.txt +0 -0
  176. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/data_physics_random_control_parameters.txt +0 -0
  177. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/data_physics_shock_response_synthesis_control_parameters.txt +0 -0
  178. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/data_physics_sine_control_parameters.txt +0 -0
  179. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/geometry.txt +0 -0
  180. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/global_analysis_attributes.txt +0 -0
  181. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/global_test_attributes.txt +0 -0
  182. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/matrix.txt +0 -0
  183. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/mechanical_control_parameters.txt +0 -0
  184. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/modal_test_parameters.txt +0 -0
  185. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/mode.txt +0 -0
  186. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/parameter_set.txt +0 -0
  187. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/point_cloud.txt +0 -0
  188. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/rattlesnake_random_control_parameters.txt +0 -0
  189. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/rattlesnake_sysid_parameters.txt +0 -0
  190. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/rattlesnake_transient_control_parameters.txt +0 -0
  191. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/response_spectrum.txt +0 -0
  192. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/scalar.txt +0 -0
  193. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/unknown.txt +0 -0
  194. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/vector.txt +0 -0
  195. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/escdf_specifications/vibration_channel_table.txt +0 -0
  196. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/excel.py +0 -0
  197. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/exodus.py +0 -0
  198. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/exporters.py +0 -0
  199. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/femap.py +0 -0
  200. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/matlab.py +0 -0
  201. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/nastran.py +0 -0
  202. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/notes.py +0 -0
  203. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/photo_files.py +0 -0
  204. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/punch.py +0 -0
  205. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/rattlesnake_sine.py +0 -0
  206. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
  207. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/report_template.py +0 -0
  208. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/sdynpy_data.py +0 -0
  209. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/sdynpy_npz.py +0 -0
  210. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
  211. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/sep005.py +0 -0
  212. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/sniffing.py +0 -0
  213. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/step.py +0 -0
  214. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/stl.py +0 -0
  215. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/threemf.py +0 -0
  216. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/io/unv.py +0 -0
  217. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/names.py +0 -0
  218. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/averaging.py +0 -0
  219. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/bands.py +0 -0
  220. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/bars.py +0 -0
  221. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/classical_pulse.py +0 -0
  222. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/dwell.py +0 -0
  223. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/filtering.py +0 -0
  224. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/half_power.py +0 -0
  225. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/replication_segments.py +0 -0
  226. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/scalogram.py +0 -0
  227. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/shock_check.py +0 -0
  228. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/shocks.py +0 -0
  229. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/signature.py +0 -0
  230. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/sine_tracking.py +0 -0
  231. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/srs_tolerance.py +0 -0
  232. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/sweep_rate.py +0 -0
  233. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/time_statistics.py +0 -0
  234. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/tone_readout.py +0 -0
  235. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/tracking_filter.py +0 -0
  236. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/plot/truncation.py +0 -0
  237. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/py.typed +0 -0
  238. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/report/markdown.py +0 -0
  239. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/report/page.py +0 -0
  240. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/rotate.py +0 -0
  241. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/theme.py +0 -0
  242. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/units.py +0 -0
  243. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/update.py +0 -0
  244. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/__init__.py +0 -0
  245. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/_quiet.py +0 -0
  246. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/banded.py +0 -0
  247. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/geometry.py +0 -0
  248. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/mac_bars.py +0 -0
  249. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/marks.py +0 -0
  250. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/paired.py +0 -0
  251. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/pick.py +0 -0
  252. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/rigid.py +0 -0
  253. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/scalogram.py +0 -0
  254. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/sinespec.py +0 -0
  255. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics/viz/waterfall.py +0 -0
  256. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
  257. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics.egg-info/entry_points.txt +0 -0
  258. {visualdynamics-0.1.0a71 → visualdynamics-0.1.0a73}/src/visualdynamics.egg-info/requires.txt +0 -0
  259. {visualdynamics-0.1.0a71 → 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.0a71
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.0a71'
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
@@ -0,0 +1,455 @@
1
+ """A record's amplitude spectrum: each line scaled so a sine reads its
2
+ amplitude, in peak units, under a window chosen for it.
3
+
4
+ `TimeHistory.compute_spectra` is the raw transform: a rectangular
5
+ window, no scaling (a sine of amplitude A on a line reads A·N/2) and a
6
+ complex mean across the averages, so content whose phase moves from one
7
+ frame to the next averages toward zero. Other code depends on it as it
8
+ is. This module is the scaled spectrum beside it, the one a tone is read
9
+ from in g peak.
10
+
11
+ The scaling, for a frame of N samples x_n windowed by w_n:
12
+
13
+ A(f_k) = 2·|X(k)| / Σw, X(k) = Σ w_n x_n e^(-i2πkn/N)
14
+
15
+ with the 2 left off at the DC line and, for an even frame, the Nyquist
16
+ line, which have no mirror twin at negative frequency to fold in. A
17
+ sine of amplitude A that sits on line k transforms to A·Σw/2 there, so
18
+ the line reads A whatever the window. Σw is the window's amplitude
19
+ correction: Σw/N, the window's mean, is its coherent gain (0.5 for
20
+ Hann), and dividing by Σw rather than N undoes it (a correction of
21
+ ×2.000 for Hann). It is not the correction a density takes. A density is
22
+ mean square per hertz and is corrected by the sum of the window's
23
+ squares (`TimeHistory.compute_psds`); the two differ by the window's
24
+ noise bandwidth, and applying the amplitude correction to a density
25
+ reads every ASD 1.5 times too high under Hann.
26
+
27
+ The frames' magnitudes are averaged, not their complex values. A tone
28
+ reads its amplitude in every frame whatever its phase there, so the
29
+ average reads it too: a tone that is not locked to the frames, or a
30
+ record saved as captures with a fresh phase each, keeps its amplitude
31
+ where a complex mean would shrink it. A broadband floor reads its mean
32
+ magnitude per line, which is not a level: it moves with the line spacing
33
+ and the window, and a random record's level is read from its ASD. An
34
+ amplitude spectrum is for tones. The same averaging makes a tone that is
35
+ not well clear of the floor read high: the floor's magnitude adds to the
36
+ tone's in every frame and does not average away.
37
+
38
+ The window is chosen here, not taken from the record's averaging. The
39
+ frames are the averaging's (its frame length, overlap, start, frame
40
+ count and detrend), because those are what was set for the record; but
41
+ the window right for a density of random data and the window right for a
42
+ tone's amplitude are not the same window. Hann is the default. It reads
43
+ a tone on a line exactly and one midway between lines up to 1.42 dB low,
44
+ because the tallest line then sits half a line off the tone's frequency
45
+ (scalloping). The flat top's crown is flat across a line, so a tone
46
+ reads within about 0.01 dB of its amplitude wherever it falls, at the
47
+ cost of a main lobe several lines wide; it is the window for amplitude
48
+ accuracy. Its correction depends on the variant a program implements and
49
+ how it is normalized; the one here is scipy's `flattop`, periodic, whose
50
+ coherent gain is 0.2156 (a correction of ×4.639). Every reading states
51
+ its window and its correction, so a number read here is never carried to
52
+ another program without them.
53
+
54
+ References
55
+ ----------
56
+ The amplitude correction is the window's coherent gain and the loss of
57
+ a tone between lines its scalloping loss, both Harris's; the one-sided
58
+ scaling and the doubling left off at DC and Nyquist are how the
59
+ autospectrum beside it is scaled.
60
+
61
+ 1. Harris, F. J. (1978). "On the Use of Windows for Harmonic Analysis
62
+ with the Discrete Fourier Transform." *Proceedings of the IEEE*,
63
+ 66(1), 51-83.
64
+ [doi:10.1109/PROC.1978.10837](https://doi.org/10.1109/PROC.1978.10837)
65
+ Coherent gain, equivalent noise bandwidth and scalloping loss,
66
+ window by window, and the flat-top window.
67
+ 2. Bendat, J. S., & Piersol, A. G. (2010). *Random Data: Analysis and
68
+ Measurement Procedures*, 4th ed. Wiley. The finite Fourier transform
69
+ of a record and its one-sided spectra.
70
+ """
71
+
72
+ from __future__ import annotations
73
+
74
+ from dataclasses import dataclass, field, replace
75
+ from typing import Any
76
+
77
+ import numpy as np
78
+
79
+ #: a channel whose lines above DC, its frames' means taken out, are no
80
+ #: more than this share of its largest line, DC included, is constant:
81
+ #: what is left is rounding, not a tone
82
+ CONSTANT_SHARE = 1e-9
83
+
84
+ #: the frame the window's arithmetic is worked out on when no frame
85
+ #: length is given. A periodic cosine-sum window's mean is its first
86
+ #: coefficient at any length that holds it, so the coherent gain does not
87
+ #: depend on this; it only has to be long enough for every window offered
88
+ GAIN_LENGTH = 4096
89
+
90
+
91
+ def coherent_gain(window: Any = 'hann', length: int | None = None,
92
+ parameter: float | None = None) -> float:
93
+ """A window's coherent gain: its mean, Σw/N.
94
+
95
+ The share of a sine's amplitude a line on the sine keeps through the
96
+ window: 1 for a rectangle (scipy's 'boxcar'), 0.5 for Hann, 0.2156
97
+ for scipy's flat top. Its reciprocal is the amplitude correction
98
+ (`amplitude_correction`).
99
+
100
+ Parameters
101
+ ----------
102
+ window : str or Averaging, default 'hann'
103
+ A window by any name `core.averaging.normalize_window` takes, or
104
+ an `Averaging`, whose window, parameter and frame length are
105
+ used.
106
+ length : int, optional
107
+ The frame length in samples; `GAIN_LENGTH` when omitted. The
108
+ window is periodic, as a spectral analysis uses it.
109
+ parameter : float, optional
110
+ The Tukey window's alpha or the Kaiser window's beta.
111
+
112
+ Returns
113
+ -------
114
+ float
115
+ """
116
+ from .averaging import Averaging, window_shape
117
+
118
+ if isinstance(window, Averaging):
119
+ parameter = window.window_parameter
120
+ length = window.frame_length if length is None else length
121
+ window = window.window
122
+ n = GAIN_LENGTH if length is None else int(length)
123
+ shape = np.asarray(window_shape(window, n, parameter), dtype=np.float64)
124
+ return float(np.mean(shape))
125
+
126
+
127
+ def amplitude_correction(window: Any = 'hann', length: int | None = None,
128
+ parameter: float | None = None) -> float:
129
+ """The factor a windowed transform's lines are multiplied by so a
130
+ sine reads its amplitude: N/Σw, one over the coherent gain.
131
+
132
+ 2.000 for Hann, 4.639 for scipy's flat top, 1 for a rectangle. The
133
+ arguments are as for `coherent_gain`.
134
+ """
135
+ return 1.0 / coherent_gain(window, length, parameter)
136
+
137
+
138
+ def scalloping(window: Any = 'hann', length: int | None = None,
139
+ parameter: float | None = None) -> float:
140
+ """The share of its amplitude a sine midway between two lines reads
141
+ at the taller of them: 0.849 for Hann (1.42 dB low), 0.637 for a
142
+ rectangle (3.92 dB low), within a part in a thousand of 1 for the
143
+ flat top.
144
+
145
+ Harris's scalloping loss, worked out from the window's own shape
146
+ (`core.tone_readout.leakage`), so it holds for every window offered.
147
+ The arguments are as for `coherent_gain`.
148
+ """
149
+ from .tone_readout import leakage
150
+
151
+ # the leakage kernel is in power, the share of a tone's mean square
152
+ # a line receives; its root is the share of the amplitude
153
+ return float(np.sqrt(leakage(window, length, parameter).scallop(0.5)))
154
+
155
+
156
+ def amplitude_lines(frames: Any, shape: Any) -> np.ndarray:
157
+ """The amplitude spectrum of frames already windowed, their
158
+ magnitudes averaged.
159
+
160
+ Parameters
161
+ ----------
162
+ frames : array_like
163
+ One windowed frame per row, every row the same length N.
164
+ shape : array_like
165
+ The window they were multiplied by, N samples: its sum is the
166
+ amplitude correction.
167
+
168
+ Returns
169
+ -------
170
+ numpy.ndarray
171
+ N//2 + 1 lines, from DC up: 2·mean|X|/Σw, and mean|X|/Σw at DC
172
+ and at the Nyquist line of an even frame.
173
+ """
174
+ import scipy.fft as scipy_fft
175
+
176
+ frames = np.atleast_2d(np.asarray(frames, dtype=np.float64))
177
+ total = float(np.sum(np.asarray(shape, dtype=np.float64)))
178
+ if not total > 0.0:
179
+ raise ValueError('the window sums to nothing, so no line can be '
180
+ 'scaled to an amplitude')
181
+ n = frames.shape[1]
182
+ lines = np.mean(np.abs(scipy_fft.rfft(frames, axis=1, workers=-1)),
183
+ axis=0)
184
+ scale = np.full(lines.shape, 2.0 / total)
185
+ # no mirror twin at DC, nor at the Nyquist line of an even frame:
186
+ # doubled, a record's mean would read twice itself
187
+ scale[0] = 1.0 / total
188
+ if n % 2 == 0:
189
+ scale[-1] = 1.0 / total
190
+ return lines * scale
191
+
192
+
193
+ @dataclass(frozen=True, eq=False)
194
+ class AmplitudeSpectrum:
195
+ """What `amplitude_spectrum` returns: a record's amplitude spectrum,
196
+ one row per channel, with the window and its correction stated.
197
+
198
+ `amplitude` is in peak units, SI as the record is stored (m/s² for an
199
+ acceleration). `frames` is how many frames each channel's
200
+ magnitudes were averaged over. `coherent_gain` is the window's mean
201
+ and `correction` its reciprocal, the factor its lines were
202
+ multiplied by; `noise_bandwidth` is in lines and `scalloping` is the
203
+ share of its amplitude a sine midway between lines reads.
204
+ `leveled` is the same spectrum of the same frames with each frame's
205
+ mean taken out before its window, which `tallest` reads: a record's
206
+ mean leaks through the window's main lobe into the lines just above
207
+ DC (under Hann a 5 g mean reads 5 g on line 1 as well as at DC), and
208
+ a tone on those lines is read without it. It is `amplitude` itself
209
+ when the averaging levels its frames already.
210
+ `from_averaging` says whether the frames are the record's averaging
211
+ (False: each record whole as one frame, the record having none).
212
+ `lowest_line` is the lowest line a tone is read on exactly under this
213
+ window: below it the tone's mirror at negative frequency is inside
214
+ the main lobe (1 for Hann, 3 for the flat top).
215
+ """
216
+
217
+ frequencies: np.ndarray
218
+ amplitude: np.ndarray
219
+ response_dof: list[str]
220
+ ordinate_dim: list[str]
221
+ dimension_hint: list[str | None]
222
+ window: str
223
+ window_parameter: float | None
224
+ frame_length: int
225
+ sample_rate: float
226
+ frames: tuple[int, ...]
227
+ coherent_gain: float
228
+ noise_bandwidth: float
229
+ scalloping: float
230
+ leveled: np.ndarray | None = None
231
+ from_averaging: bool = True
232
+ labels: list[str] = field(default_factory=list)
233
+ lowest_line: int = 1
234
+
235
+ @property
236
+ def correction(self) -> float:
237
+ """The amplitude correction applied: N/Σw (2.000 for Hann)."""
238
+ return 1.0 / self.coherent_gain
239
+
240
+ @property
241
+ def spacing(self) -> float:
242
+ """The line spacing in Hz: the sample rate over the frame
243
+ length."""
244
+ return self.sample_rate / self.frame_length
245
+
246
+ @property
247
+ def scalloping_db(self) -> float:
248
+ """How far low, in dB, a sine midway between lines reads: 1.42
249
+ for Hann, as a positive number."""
250
+ return float(-20.0 * np.log10(self.scalloping))
251
+
252
+ def label(self, record: int) -> str:
253
+ """The channel's name, as the record labels it."""
254
+ if record < len(self.labels):
255
+ return self.labels[record]
256
+ return str(self.response_dof[record])
257
+
258
+ def tallest(self, record: int = 0,
259
+ band: tuple[float, float] | None = None
260
+ ) -> tuple[float, float]:
261
+ """(frequency, amplitude) of the tallest line of channel
262
+ `record` above DC, read off the frames with their means taken
263
+ out (`leveled`). A record's mean is not a tone, and it leaks:
264
+ under Hann a mean of 5 g reads 5 g at DC and 5 g again on the
265
+ line above, and under the flat top on four lines above. Taken
266
+ out before the window, it leaks into none, so a tone on line 1
267
+ reads its own line and amplitude under Hann. A tone's own
268
+ mirror at negative frequency lies 2k lines from it, and while
269
+ that is inside the main lobe the two add: the flat top's lobe
270
+ reaches five lines, so its lowest line read exactly is line 3
271
+ (`lowest_line`, and `mirror_note` says so).
272
+
273
+ `band` is (low, high) in Hz to look within; the whole spectrum
274
+ above DC when omitted. A line that is not a number is passed
275
+ over.
276
+
277
+ Raises
278
+ ------
279
+ ValueError
280
+ When no line of the channel is a number (it holds samples
281
+ that are not finite), when every line is zero (the channel
282
+ is zero throughout, or in the band), when the channel is
283
+ constant (its lines above DC are rounding beside its mean),
284
+ or when the band holds no line above DC.
285
+ """
286
+ f = self.frequencies
287
+ read = self.amplitude if self.leveled is None else self.leveled
288
+ values = np.asarray(read[record], dtype=np.float64)
289
+ name = self.label(record)
290
+ finite = np.isfinite(values)
291
+ if not finite.any():
292
+ raise ValueError(f'{name} holds samples that are not finite, so '
293
+ 'no line of its spectrum is a number')
294
+ above = (np.arange(len(f)) >= 1) & finite
295
+ drawn = np.asarray(self.amplitude[record], dtype=np.float64)
296
+ largest = float(np.max(np.abs(drawn[np.isfinite(drawn)]), initial=0.0))
297
+ # a constant channel's mean, taken out, leaves lines of 1e-17 of
298
+ # it, which would be read as the tallest line
299
+ if (largest > 0.0 and above.any()
300
+ and np.max(values[above]) <= CONSTANT_SHARE * largest):
301
+ raise ValueError(f'{name} is constant throughout: there is no '
302
+ 'tone to read')
303
+ keep = above
304
+ if band is not None:
305
+ low, high = band
306
+ keep &= (f >= low) & (f <= high)
307
+ if not keep.any():
308
+ raise ValueError('no line to read in that band')
309
+ if not np.max(values[keep]) > 0.0:
310
+ where = ('zero throughout' if band is None
311
+ else f'zero from {band[0]:g} to {band[1]:g} Hz')
312
+ raise ValueError(f'{name} is {where}: there is no tone to read')
313
+ at = int(np.flatnonzero(keep)[np.argmax(values[keep])])
314
+ return float(f[at]), float(values[at])
315
+
316
+ def mirror_note(self, frequency: float) -> str | None:
317
+ """What to say of a tallest line read at `frequency` Hz when it
318
+ is below `lowest_line`, where the window mixes a tone with its
319
+ own mirror and neither the line nor the amplitude is the tone's;
320
+ None at or above it."""
321
+ below = self.lowest_line * self.spacing
322
+ if frequency >= below:
323
+ return None
324
+ return (f'below {below:g} Hz the {self.window} window mixes a tone '
325
+ 'with its mirror at negative frequency; read it under hann')
326
+
327
+ def correction_text(self) -> str:
328
+ """The window and its correction, in words, as the plot and the
329
+ pane state them: 'hann window, amplitude correction ×2.000'."""
330
+ name = self.window
331
+ if self.window_parameter is not None:
332
+ name = f'{name} ({self.window_parameter:g})'
333
+ return f'{name} window, amplitude correction ×{self.correction:.3f}'
334
+
335
+ def describe(self, record: int = 0, unit: str = '',
336
+ scale: float = 1.0) -> str:
337
+ """One line for a status bar: the tallest line of channel
338
+ `record` in peak units, then the window and its correction.
339
+ `scale` converts the amplitude into `unit`."""
340
+ frequency, amplitude = self.tallest(record)
341
+ value = f'{amplitude * scale:.4g} {unit} peak'.replace(' ', ' ')
342
+ note = self.mirror_note(frequency)
343
+ return (f'{self.label(record)}: tallest line {value} at '
344
+ f'{frequency:.4g} Hz; {self.correction_text()}, '
345
+ f'{_frames_text(self.frames[record])} averaged as '
346
+ 'magnitudes' + ('' if note is None else f'; {note}'))
347
+
348
+
349
+ def _frames_text(count: int) -> str:
350
+ return '1 frame' if count == 1 else f'{count} frames'
351
+
352
+
353
+ def channel_row(history: Any, record: int) -> int:
354
+ """The row of an amplitude spectrum of `history` that record
355
+ `record` is averaged into.
356
+
357
+ A row is a channel (`TimeHistory.channel_key`), in the order its
358
+ first record comes, so a history with one record per channel has
359
+ its rows in record order, and every capture of a channel saved as
360
+ captures maps to that channel's one row.
361
+ """
362
+ keys: list = []
363
+ for i in range(len(history.response_dof)):
364
+ key = history.channel_key(i)
365
+ if key not in keys:
366
+ keys.append(key)
367
+ return keys.index(history.channel_key(record))
368
+
369
+
370
+ def amplitude_spectrum(history: Any, averaging: Any = None, *,
371
+ window: Any = 'hann',
372
+ parameter: float | None = None) -> AmplitudeSpectrum:
373
+ """The amplitude spectrum of every channel of a time history, each
374
+ line scaled so a sine reads its amplitude, the frames' magnitudes
375
+ averaged.
376
+
377
+ The frames are the averaging's: `averaging` when given, else the
378
+ record's own, else each record whole as one frame, as
379
+ `TimeHistory.compute_psds` cuts them. Only the window is replaced:
380
+ `window` and `parameter` are this spectrum's own (the module says
381
+ why). Channels are grouped as a PSD groups them
382
+ (`TimeHistory.channel_key`), so a record saved as captures averages
383
+ every capture's frames together.
384
+
385
+ Parameters
386
+ ----------
387
+ history : TimeHistory
388
+ The record, evenly sampled.
389
+ averaging : Averaging, optional
390
+ How to cut it into frames.
391
+ window : str, default 'hann'
392
+ Any window `core.averaging.WINDOWS` names; 'flattop' for a
393
+ tone's amplitude wherever it falls between lines.
394
+ parameter : float, optional
395
+ The Tukey window's alpha or the Kaiser window's beta.
396
+
397
+ Returns
398
+ -------
399
+ AmplitudeSpectrum
400
+
401
+ Raises
402
+ ------
403
+ ValueError
404
+ If the samples are not evenly spaced, the frames do not fit in
405
+ the record, or the window is not one offered.
406
+ """
407
+ from .averaging import normalize_window
408
+ from .tone_readout import leakage, noise_bandwidth
409
+
410
+ key = normalize_window(window)
411
+ given = averaging if averaging is not None else history.averaging
412
+ used = history._resolved_averaging(averaging)
413
+ used = replace(used, window=key, window_parameter=(
414
+ None if parameter is None else float(parameter)))
415
+ # the framing every spectrum of a record shares, so these frames are
416
+ # the PSD's frames exactly; its density scale is not this one
417
+ frequencies, _density, groups = history._spectral_frame(used)
418
+ # and again with each frame's mean taken out before its window, the
419
+ # lines a tone is read from (`AmplitudeSpectrum.tallest`); frames the
420
+ # averaging levels already are those
421
+ leveled = (groups if used.detrend != 'none' else
422
+ history._spectral_frame(replace(used, detrend='mean'))[2])
423
+ shape = used.shape()
424
+ rows, flat, dofs, dims, hints, counts, labels = [], [], [], [], [], [], []
425
+ first = {}
426
+ for i in range(len(history.response_dof)):
427
+ first.setdefault(history.channel_key(i), i)
428
+ for (dof, dim, hint), windowed in groups.items():
429
+ rows.append(amplitude_lines(windowed, shape))
430
+ flat.append(rows[-1] if leveled is groups else
431
+ amplitude_lines(leveled[(dof, dim, hint)], shape))
432
+ dofs.append(dof)
433
+ dims.append(dim)
434
+ hints.append(hint)
435
+ counts.append(len(windowed))
436
+ labels.append(history.record_label(first[(dof, dim, hint)]))
437
+ amplitude = np.asarray(rows, dtype=np.float64)
438
+ return AmplitudeSpectrum(
439
+ frequencies=np.asarray(frequencies, dtype=np.float64),
440
+ amplitude=amplitude,
441
+ response_dof=dofs, ordinate_dim=dims, dimension_hint=hints,
442
+ window=key, window_parameter=used.window_parameter,
443
+ frame_length=int(used.frame_length),
444
+ sample_rate=float(history.sample_rate),
445
+ frames=tuple(counts),
446
+ coherent_gain=coherent_gain(used),
447
+ noise_bandwidth=float(noise_bandwidth(used)),
448
+ scalloping=scalloping(used),
449
+ leveled=(amplitude if leveled is groups
450
+ else np.asarray(flat, dtype=np.float64)),
451
+ from_averaging=given is not None,
452
+ labels=labels,
453
+ # a tone on line k has its mirror 2k lines off, and the two part
454
+ # once that reaches the main lobe's edge, its half-width
455
+ lowest_line=int(np.ceil(leakage(used).half_width / 2)))