visualdynamics 0.1.0a42__tar.gz → 0.1.0a43__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 (177) hide show
  1. {visualdynamics-0.1.0a42/src/visualdynamics.egg-info → visualdynamics-0.1.0a43}/PKG-INFO +1 -1
  2. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/__init__.py +1 -1
  3. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/data.py +3 -0
  4. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/shapes.py +6 -2
  5. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/main_window.py +54 -61
  6. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/movie.py +45 -38
  7. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/object_tables.py +5 -5
  8. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/plot/__init__.py +22 -37
  9. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/project.py +23 -8
  10. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/report/__init__.py +2 -2
  11. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/theme.py +6 -0
  12. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/animate.py +164 -247
  13. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/marks.py +54 -40
  14. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/paired.py +2 -11
  15. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/waterfall.py +1 -3
  16. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43/src/visualdynamics.egg-info}/PKG-INFO +1 -1
  17. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/LICENSE +0 -0
  18. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/README.md +0 -0
  19. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/pyproject.toml +0 -0
  20. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/setup.cfg +0 -0
  21. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/__main__.py +0 -0
  22. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/compatibility.py +0 -0
  23. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/__init__.py +0 -0
  24. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/author.py +0 -0
  25. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/averaging.py +0 -0
  26. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/channel_table.py +0 -0
  27. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/compliance.py +0 -0
  28. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/correlate.py +0 -0
  29. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/detect.py +0 -0
  30. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/entities.py +0 -0
  31. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/fem.py +0 -0
  32. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/filters.py +0 -0
  33. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/geometry.py +0 -0
  34. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/kurtosis.py +0 -0
  35. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/matches.py +0 -0
  36. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/merge.py +0 -0
  37. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/mesh.py +0 -0
  38. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/modal_fit.py +0 -0
  39. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/octave.py +0 -0
  40. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/photos.py +0 -0
  41. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/progress.py +0 -0
  42. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/replication.py +0 -0
  43. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/report.py +0 -0
  44. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/rigid.py +0 -0
  45. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/shocks.py +0 -0
  46. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/sine.py +0 -0
  47. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/sine_tracking.py +0 -0
  48. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/snr.py +0 -0
  49. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/srs.py +0 -0
  50. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/tables.py +0 -0
  51. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/transform.py +0 -0
  52. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/truncate.py +0 -0
  53. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/unit_choices.py +0 -0
  54. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/validate.py +0 -0
  55. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/core/wavelet.py +0 -0
  56. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/decimate.py +0 -0
  57. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/deform.py +0 -0
  58. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/demo/__init__.py +0 -0
  59. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/demo/barc.py +0 -0
  60. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/demo/drone.py +0 -0
  61. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/demo/frame.py +0 -0
  62. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/demo/plate.py +0 -0
  63. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/__init__.py +0 -0
  64. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/app_icon.py +0 -0
  65. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/ask.py +0 -0
  66. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/author_panel.py +0 -0
  67. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/averaging_panel.py +0 -0
  68. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/console.py +0 -0
  69. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/disclaimer.py +0 -0
  70. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/drawer_tab.py +0 -0
  71. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/editors.py +0 -0
  72. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/filter_panel.py +0 -0
  73. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/glyphs.py +0 -0
  74. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
  75. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
  76. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/icons.py +0 -0
  77. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/mesh_panel.py +0 -0
  78. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/octave_panel.py +0 -0
  79. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/panes.py +0 -0
  80. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/preferences.py +0 -0
  81. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/project_tree.py +0 -0
  82. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/record_grid.py +0 -0
  83. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/report_editor.py +0 -0
  84. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/rigid_panel.py +0 -0
  85. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/run_reports.py +0 -0
  86. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/settings_panel.py +0 -0
  87. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/shock_panel.py +0 -0
  88. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/sine_panel.py +0 -0
  89. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/stage_drag.py +0 -0
  90. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/stream_window.py +0 -0
  91. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/tables.py +0 -0
  92. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/toolbars.py +0 -0
  93. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/truncate_panel.py +0 -0
  94. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/updater.py +0 -0
  95. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/wavelet_panel.py +0 -0
  96. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/gui/windows.py +0 -0
  97. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/__init__.py +0 -0
  98. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/adf.py +0 -0
  99. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf.py +0 -0
  100. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_objects.py +0 -0
  101. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/LICENSE +0 -0
  102. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/activity_result.txt +0 -0
  103. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/channel_table.txt +0 -0
  104. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/data.txt +0 -0
  105. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/data_physics_random_control_parameters.txt +0 -0
  106. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/data_physics_shock_response_synthesis_control_parameters.txt +0 -0
  107. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/data_physics_sine_control_parameters.txt +0 -0
  108. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/geometry.txt +0 -0
  109. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/global_analysis_attributes.txt +0 -0
  110. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/global_test_attributes.txt +0 -0
  111. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/matrix.txt +0 -0
  112. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/mechanical_control_parameters.txt +0 -0
  113. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/modal_test_parameters.txt +0 -0
  114. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/mode.txt +0 -0
  115. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/parameter_set.txt +0 -0
  116. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/point_cloud.txt +0 -0
  117. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/rattlesnake_random_control_parameters.txt +0 -0
  118. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/rattlesnake_sysid_parameters.txt +0 -0
  119. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/rattlesnake_transient_control_parameters.txt +0 -0
  120. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/response_spectrum.txt +0 -0
  121. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/scalar.txt +0 -0
  122. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/unknown.txt +0 -0
  123. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/vector.txt +0 -0
  124. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/escdf_specifications/vibration_channel_table.txt +0 -0
  125. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/excel.py +0 -0
  126. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/exodus.py +0 -0
  127. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/exporters.py +0 -0
  128. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/femap.py +0 -0
  129. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/matlab.py +0 -0
  130. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/nastran.py +0 -0
  131. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/native.py +0 -0
  132. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/notes.py +0 -0
  133. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/photo_files.py +0 -0
  134. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/punch.py +0 -0
  135. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/rattlesnake.py +0 -0
  136. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/rattlesnake_sine.py +0 -0
  137. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
  138. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/report_template.py +0 -0
  139. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/sdynpy_data.py +0 -0
  140. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/sdynpy_npz.py +0 -0
  141. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
  142. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/sep005.py +0 -0
  143. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/sniffing.py +0 -0
  144. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/step.py +0 -0
  145. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/stl.py +0 -0
  146. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/threemf.py +0 -0
  147. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/io/unv.py +0 -0
  148. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/names.py +0 -0
  149. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/plot/averaging.py +0 -0
  150. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/plot/bands.py +0 -0
  151. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/plot/bars.py +0 -0
  152. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/plot/filtering.py +0 -0
  153. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/plot/scalogram.py +0 -0
  154. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/plot/shocks.py +0 -0
  155. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/plot/sine_tracking.py +0 -0
  156. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/plot/tracking_filter.py +0 -0
  157. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/plot/truncation.py +0 -0
  158. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/py.typed +0 -0
  159. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/report/markdown.py +0 -0
  160. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/report/page.py +0 -0
  161. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/rotate.py +0 -0
  162. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/units.py +0 -0
  163. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/update.py +0 -0
  164. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/__init__.py +0 -0
  165. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/_quiet.py +0 -0
  166. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/banded.py +0 -0
  167. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/geometry.py +0 -0
  168. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/mac_bars.py +0 -0
  169. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/pick.py +0 -0
  170. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/rigid.py +0 -0
  171. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/scalogram.py +0 -0
  172. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics/viz/sinespec.py +0 -0
  173. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics.egg-info/SOURCES.txt +0 -0
  174. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
  175. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics.egg-info/entry_points.txt +0 -0
  176. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/src/visualdynamics.egg-info/requires.txt +0 -0
  177. {visualdynamics-0.1.0a42 → visualdynamics-0.1.0a43}/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.0a42
3
+ Version: 0.1.0a43
4
4
  Summary: Structural Dynamics Visualization: a units-aware structural dynamics analysis toolset
5
5
  Author-email: Brandon Zwink <bzwink@gmail.com>
6
6
  License-Expression: MIT
@@ -90,7 +90,7 @@ from .units import (
90
90
  #: alpha, said in the version itself so the dialog, the About text, the
91
91
  #: installers and latest.json all say the same thing, and the first
92
92
  #: non-alpha release is offered as an upgrade (Brandon, 2026-09-11)
93
- __version__ = '0.1.0a42'
93
+ __version__ = '0.1.0a43'
94
94
 
95
95
 
96
96
  def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
@@ -3032,6 +3032,9 @@ class Psd(_Bands, DataArray):
3032
3032
  """
3033
3033
  spec = _recast(self, Specification)
3034
3034
  spec.limits = {}
3035
+ # a requirement is not scaled to be compared: the test level
3036
+ # belongs to the measurement
3037
+ spec.__dict__.pop('scale_db', None)
3035
3038
  spec.interpolation = ('bin' if self.bandwidth is not None
3036
3039
  else Specification.reading_of(self.abscissa))
3037
3040
  return spec
@@ -893,6 +893,7 @@ class ShapeSet:
893
893
  other: ShapeSet, other_mode: int,
894
894
  other_geometry: Geometry | None = None, *,
895
895
  on_basis: bool = False,
896
+ projected: ShapeSet | None = None,
896
897
  names: tuple[str, str] = ('first', 'second'),
897
898
  **kwargs: Any) -> Any:
898
899
  """This mode and another set's overlaid, swinging together, as
@@ -920,6 +921,10 @@ class ShapeSet:
920
921
  The other set's geometry; this one's when omitted.
921
922
  on_basis : bool, default False
922
923
  Draw the other set's projection on this geometry instead.
924
+ projected : ShapeSet, optional
925
+ The other set already carried onto this one's DOFs
926
+ (`correlate.project_shapes`), for a script animating many
927
+ pairs of the same two sets: made here when omitted.
923
928
  names : tuple of str
924
929
  What the caption calls the two sets.
925
930
  **kwargs
@@ -934,8 +939,7 @@ class ShapeSet:
934
939
  from ..viz.animate import animate_pair, pair_deflections
935
940
 
936
941
  other_geometry = other_geometry or geometry
937
- projected = None
938
- if other_geometry is not geometry:
942
+ if other_geometry is not geometry and projected is None:
939
943
  from .correlate import project_shapes
940
944
  projected, _report = project_shapes(other, other_geometry,
941
945
  self, geometry)
@@ -151,9 +151,14 @@ from ..rotate import (
151
151
  snapped,
152
152
  wrapped,
153
153
  )
154
- from ..theme import OVERLAY_ALPHA
154
+ from ..theme import PAIR_COLORS
155
155
  from ..theme import theme as resolve_theme
156
156
  from ..units import DEFAULT_SYSTEM, SYSTEMS, UnitSystem
157
+
158
+ # the playback clock is the viz layer's, so a saved movie and a script's
159
+ # `movie=` play at the window's own rate by construction
160
+ from ..viz.animate import MOVIE_FPS as FRAMES_PER_SECOND
161
+ from ..viz.animate import SECONDS_PER_CYCLE, SECONDS_PER_RECORD
157
162
  from ..viz.geometry import (
158
163
  AXIS_COLORS,
159
164
  ENTITY_LABEL_LIMIT,
@@ -222,10 +227,7 @@ COMBO_CELL_PADDING = 44
222
227
  #: a dragged turn snaps to whole degrees of this; a typed angle does not
223
228
  ANGLE_STEP = 1.0
224
229
 
225
- FRAMES_PER_SECOND = 30
226
230
  PHASE_STEPS = 120 # frames in one mode-shape cycle
227
- SECONDS_PER_CYCLE = 2.0 # every mode animates at this rate, whatever its Hz
228
- SECONDS_PER_RECORD = 10.0 # a time record plays in about this long
229
231
 
230
232
  # What Faster and Slower step through. Doublings, because that is how a
231
233
  # speed reads — half as fast, twice as fast — and a ladder rather than a
@@ -1138,7 +1140,6 @@ class MainWindow(QMainWindow):
1138
1140
  self._fit_parabola = None
1139
1141
  self._fit_damping_label = None
1140
1142
  self._fit_coherence_name = None
1141
- self._fit_records = None
1142
1143
  #: what the plot's top and bottom edges mean as damping, kept
1143
1144
  #: across fits — an article's plausible range rarely changes
1144
1145
  #: between two of its own FRF sets
@@ -3182,8 +3183,12 @@ class MainWindow(QMainWindow):
3182
3183
  rewrites per line is in every frame; then playback is put back
3183
3184
  where it was. `path` skips the dialog (scripts, tests).
3184
3185
  """
3185
- from ..viz.animate import cycle_parameters, sweep_parameters
3186
- from .movie import MOVIE_FPS, write_movie
3186
+ from ..viz.animate import (
3187
+ cycle_parameters,
3188
+ movie_frames,
3189
+ sweep_parameters,
3190
+ )
3191
+ from .movie import write_movie
3187
3192
 
3188
3193
  if self.animator is None or self.scene.plotter is None:
3189
3194
  return None
@@ -3201,28 +3206,24 @@ class MainWindow(QMainWindow):
3201
3206
  phase = self.phase_slider.value()
3202
3207
  held = self._cursor.value() if self._cursor is not None else None
3203
3208
  if self._shape_mode:
3204
- parameters = cycle_parameters(SECONDS_PER_CYCLE / self.speed,
3205
- MOVIE_FPS)
3209
+ parameters = cycle_parameters(SECONDS_PER_CYCLE / self.speed)
3210
+ step = self.animator.set_parameter
3206
3211
  else:
3207
3212
  parameters = sweep_parameters(len(self._cursor_abscissa),
3208
- SECONDS_PER_RECORD / self.speed,
3209
- MOVIE_FPS)
3213
+ SECONDS_PER_RECORD / self.speed)
3210
3214
 
3211
- def frames():
3212
- for parameter in parameters:
3213
- if self._shape_mode:
3214
- self._draw_frame(parameter)
3215
- else:
3216
- self._cursor.setValue(
3217
- float(self._cursor_abscissa[parameter]))
3218
- yield self.scene.plotter.screenshot(return_img=True)
3215
+ def step(index):
3216
+ # the cursor, not the animator: it is where an envelope
3217
+ # rewrites its caption, so every frame carries its line
3218
+ self._cursor.setValue(float(self._cursor_abscissa[index]))
3219
3219
 
3220
3220
  # seconds on a large model or a Retina screen, with input held
3221
3221
  # off: say so, rather than look hung
3222
3222
  self._show_status(f'Saving {os.path.basename(path)}…')
3223
3223
  QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
3224
3224
  try:
3225
- count = write_movie(path, frames(), fps=MOVIE_FPS)
3225
+ count = write_movie(path, movie_frames(self.scene.plotter, step,
3226
+ parameters))
3226
3227
  except RuntimeError as error: # MovieUnavailable among them
3227
3228
  self._show_status(str(error))
3228
3229
  return None
@@ -3234,7 +3235,7 @@ class MainWindow(QMainWindow):
3234
3235
  self._draw_frame(phase / PHASE_STEPS * 2 * np.pi)
3235
3236
  self.set_playing(playing)
3236
3237
  self._show_status(
3237
- f'Saved {os.path.basename(path)} — {count / MOVIE_FPS:.3g} s, '
3238
+ f'Saved {os.path.basename(path)} — {count / FRAMES_PER_SECOND:.3g} s, '
3238
3239
  f'{os.path.getsize(path) / 1e6:.2g} MB')
3239
3240
  return path
3240
3241
 
@@ -3774,7 +3775,7 @@ class MainWindow(QMainWindow):
3774
3775
 
3775
3776
  # the Basis keeps the comparison overlay's baseline blue; other
3776
3777
  # groups cycle the curve palette
3777
- LINK_ROLE_COLORS: ClassVar[dict] = {'Basis': '#4c92d9'}
3778
+ LINK_ROLE_COLORS: ClassVar[dict] = {'Basis': PAIR_COLORS[0]}
3778
3779
 
3779
3780
  def _role_holder(self, role):
3780
3781
  """The real object group holding the type's `role`.
@@ -5375,14 +5376,8 @@ class MainWindow(QMainWindow):
5375
5376
  said['origin'] = tuple(float(v) for v in matrix[3])
5376
5377
  cs_id = int(geometry.cs_id[row])
5377
5378
  geometry.place_coordinate_system(cs_id, **said)
5378
- journal = self.project.journal
5379
- before = len(journal)
5380
5379
  self.project.record_call(geometry, 'place_coordinate_system', cs_id,
5381
- **said)
5382
- if len(journal) > before > 0:
5383
- stem = journal[-1].split('=', 1)[0]
5384
- if journal[-2].split('=', 1)[0] == stem:
5385
- journal[-2:] = journal[-1:]
5380
+ settle=True, **said)
5386
5381
 
5387
5382
  def _cursor_on_ring(self, matrix, axis, position):
5388
5383
  """Where the cursor's ray meets the plane the ring lies in."""
@@ -8336,7 +8331,7 @@ class MainWindow(QMainWindow):
8336
8331
  color_a = self._link_color(a_name)
8337
8332
  color_b = self._link_color(b_name)
8338
8333
  if color_b is None or color_b == color_a:
8339
- color_b = next(c for c in ('#ff8c2b', '#3fb950')
8334
+ color_b = next(c for c in (PAIR_COLORS[1], '#3fb950')
8340
8335
  if c != color_a)
8341
8336
  self._compare['colors'] = (color_a, color_b)
8342
8337
  alphas = overlay_alphas(self.link_role(a_name),
@@ -8358,9 +8353,7 @@ class MainWindow(QMainWindow):
8358
8353
  return None
8359
8354
 
8360
8355
  def _build_comparison_animator(self, first_pair, second_pair,
8361
- caption, showing,
8362
- colors=(None, '#ff8c2b'),
8363
- alphas=(1.0, OVERLAY_ALPHA)):
8356
+ caption, showing, colors, alphas):
8364
8357
  """Like _build_animator, twice: two moving copies in one
8365
8358
  scene (`viz.animate.build_pair`), each (geometry, deflection)
8366
8359
  pair on its own mesh, in its own color and at its own opacity."""
@@ -9809,20 +9802,21 @@ class MainWindow(QMainWindow):
9809
9802
  plot another (Brandon, 2026-08-30).
9810
9803
  """
9811
9804
  from ..plot import octave_preview
9805
+ from ..viz.marks import octave_steps
9812
9806
 
9813
9807
  self._clear_octave()
9814
9808
  panel = self.data_pane.octave_panel
9815
9809
  try:
9816
- self.octave_previews = octave_preview(
9817
- self.data_pane.graphics.ci.items, psd, panel.per_octave(),
9818
- self.unit_system, resolve_theme(self.theme_name), records)
9819
- banded = psd.to_octave(panel.per_octave())
9810
+ steps = octave_steps(psd, panel.per_octave(), self.unit_system)
9820
9811
  except ValueError as refusal:
9821
9812
  panel.show_bands(None)
9822
9813
  panel.show()
9823
9814
  self._show_status(f'No octave preview: {refusal}')
9824
9815
  return
9825
- panel.show_bands(len(banded.abscissa))
9816
+ self.octave_previews = octave_preview(
9817
+ self.data_pane.graphics.ci.items, steps,
9818
+ resolve_theme(self.theme_name), records)
9819
+ panel.show_bands(len(steps[0].abscissa))
9826
9820
  panel.show()
9827
9821
  self._journal_octave_view(psd, panel.per_octave())
9828
9822
 
@@ -10320,7 +10314,7 @@ class MainWindow(QMainWindow):
10320
10314
  return self._render_banded(
10321
10315
  spec_name, spec, spec_records, m_data, m_name,
10322
10316
  m_records,
10323
- scale_db=self._family_scale_db(spec, m_name))
10317
+ scale_db=self.project.comparison_scale(spec, m_name))
10324
10318
  # a specification bounds autospectra; drawn beside a full CPSD its 30
10325
10319
  # cross terms have no limit near them and bury the six that do —
10326
10320
  # and the window resolves each measured entry's comparison scale
@@ -11813,7 +11807,8 @@ class MainWindow(QMainWindow):
11813
11807
  def read_as_specification_act(self) -> None:
11814
11808
  """The bar's Read as Specification: the selected PSD becomes the
11815
11809
  requirement it is, in place (`Project.read_as_specification`)."""
11816
- self._read_as(Psd, 'Select a PSD to read as a specification',
11810
+ self._read_as('read_as_specification',
11811
+ 'Select a PSD to read as a specification',
11817
11812
  self.project.read_as_specification,
11818
11813
  'read as a specification — compare a measurement '
11819
11814
  'against it by selecting both')
@@ -11821,23 +11816,26 @@ class MainWindow(QMainWindow):
11821
11816
  def read_as_psd_act(self) -> None:
11822
11817
  """The bar's Read as PSD: the selected specification is plain
11823
11818
  data again, in place (`Project.read_as_psd`)."""
11824
- self._read_as(Specification, 'Select a specification to read as a PSD',
11819
+ self._read_as('read_as_psd',
11820
+ 'Select a specification without limits to read as a PSD',
11825
11821
  self.project.read_as_psd, 'read as a PSD')
11826
11822
 
11827
- def _read_as(self, kind, refusal, verb, said) -> None:
11823
+ def _read_as(self, verb_name, refusal, verb, said) -> None:
11828
11824
  """Swap the selected object for its other reading, under the
11829
11825
  same name: the tree row, its grid and the view follow, the way a
11830
- recompute's in-place swap does."""
11831
- acted = self._act_on(kind, refusal, verb)
11826
+ recompute's in-place swap does. Whether it applies is the
11827
+ project's own answer (`Project.verbs`), the one the bar asked."""
11828
+ def applies(obj):
11829
+ return obj is not None and verb_name in dict(
11830
+ self.project.verbs(self.project.name_of(obj)))
11831
+
11832
+ acted = self._act_on(applies, refusal, verb)
11832
11833
  if acted is None:
11833
11834
  return
11834
11835
  name = acted[0]
11835
- obj = self.objects[name]
11836
11836
  item = self._item_for_object(name)
11837
11837
  if item is not None:
11838
- self._refresh_item(item, obj)
11839
- item.setToolTip(0, self._object_tooltip(name, obj))
11840
- self._build_children(item, obj, name)
11838
+ self._refresh_item(item, self.objects[name])
11841
11839
  self._report_content_changed()
11842
11840
  self.render_current()
11843
11841
  self._show_status(f'{name} {said}')
@@ -12330,8 +12328,6 @@ class MainWindow(QMainWindow):
12330
12328
  # every mode's residues. Chosen by the project's rule, and
12331
12329
  # journaled with the records, so the fit_modes line replays
12332
12330
  # this fit and not another (it did not, until 2026-10-09)
12333
- self._fit_records = None if records is None else [
12334
- int(i) for i in records]
12335
12331
  coherence = self.project.fit_coherence(name, records)
12336
12332
  self.fit = ModalFitSession(
12337
12333
  obj, records,
@@ -13253,8 +13249,10 @@ class MainWindow(QMainWindow):
13253
13249
  f'at={picks!r}'
13254
13250
  + (f', refine={self.fit.refined}' if self.fit.refined
13255
13251
  else '')
13256
- + (f', records={self._fit_records!r}'
13257
- if self._fit_records is not None else '')
13252
+ # the session's own rows: every record is the default
13253
+ + (f', records={self.fit.rows!r}'
13254
+ if self.fit.rows != list(range(self.fit.frf.num_records))
13255
+ else '')
13258
13256
  + (f', coherence={self._fit_coherence_name!r}'
13259
13257
  if self._fit_coherence_name else '')
13260
13258
  + f', name={self.fit_object_name!r})')
@@ -13291,17 +13289,12 @@ class MainWindow(QMainWindow):
13291
13289
  if isinstance(data, Specification)]
13292
13290
  if len(specs) != 1:
13293
13291
  return None
13294
- return {name: self._family_scale_db(specs[0], name)
13292
+ return {name: self.project.comparison_scale(specs[0], name)
13295
13293
  for name, data, _r in series
13296
13294
  if isinstance(data, Psd)
13297
13295
  and not isinstance(data, Specification)
13298
13296
  and name in self.objects}
13299
13297
 
13300
- def _family_scale_db(self, spec, name):
13301
- """The comparison scale for `name`, by the project's rule
13302
- (`Project.comparison_scale`), which the report reads too."""
13303
- return self.project.comparison_scale(spec, name)
13304
-
13305
13298
  def _show_comparison_scaling(self, series):
13306
13299
  """Put the comparison's scaling on the bar, or take it away.
13307
13300
 
@@ -13325,7 +13318,7 @@ class MainWindow(QMainWindow):
13325
13318
  self.data_pane.show_scaling(None)
13326
13319
  return
13327
13320
  self._scaling_pair = (specs[0][0], measured[0][0])
13328
- db = self._family_scale_db(specs[0][1], measured[0][0])
13321
+ db = self.project.comparison_scale(specs[0][1], measured[0][0])
13329
13322
  self.data_pane.show_scaling(f'{db:+d} dB' if db else '0 dB')
13330
13323
 
13331
13324
  def _comparison_scale_edited(self, text):
@@ -13370,7 +13363,7 @@ class MainWindow(QMainWindow):
13370
13363
  else f'{measured_name} and {len(family) - 1} linked '
13371
13364
  f'PSD{"s" * (len(family) > 2)}')
13372
13365
  if value is None:
13373
- detected = self._family_scale_db(spec, measured_name)
13366
+ detected = self.project.comparison_scale(spec, measured_name)
13374
13367
  self._show_status('Scaling returned to automatic — detected '
13375
13368
  f'{detected:+d} dB from the data')
13376
13369
  else:
@@ -13399,7 +13392,7 @@ class MainWindow(QMainWindow):
13399
13392
  return None
13400
13393
  name, data, records = measured[0]
13401
13394
  rows = compare_all(specs[0][0], data, specs[0][1], records,
13402
- scale_db=self._family_scale_db(specs[0][0], name))
13395
+ scale_db=self.project.comparison_scale(specs[0][0], name))
13403
13396
  if not rows:
13404
13397
  return None
13405
13398
  # a pair that is not compared says why, on the status line the
@@ -24,11 +24,12 @@ import struct
24
24
  from collections.abc import Iterable
25
25
  from functools import cache
26
26
  from pathlib import Path
27
+ from typing import Any
27
28
 
28
29
  import numpy as np
29
30
 
30
- #: frames a second in a saved animation — the window's own rate
31
- MOVIE_FPS = 30
31
+ from ..plot import _application
32
+ from ..viz.animate import MOVIE_FPS
32
33
 
33
34
  #: frames sent ahead of what the file holds. The encoder answers after
34
35
  #: two or three (measured, VideoToolbox, Qt 6.12), so four never waits
@@ -44,19 +45,6 @@ class MovieUnavailable(RuntimeError):
44
45
  """This machine cannot encode H.264, so no movie is written."""
45
46
 
46
47
 
47
- def _application():
48
- """A Qt application, made if a script has none — the recorder
49
- needs one, and a headless script has no window to have made it.
50
-
51
- A widgets application, not a QGuiApplication: Qt keeps the first
52
- one made for the life of the process, and a script that later
53
- opens a window would abort on its first widget.
54
- """
55
- from PySide6.QtWidgets import QApplication
56
-
57
- return QApplication.instance() or QApplication([])
58
-
59
-
60
48
  @cache
61
49
  def _mp4_codecs() -> list:
62
50
  """The video codecs this machine can encode into an MP4 — asked
@@ -99,12 +87,21 @@ def unavailable_reason() -> str | None:
99
87
  'so an animation cannot be saved as a video here')
100
88
 
101
89
 
102
- def _rgba(frame: np.ndarray) -> np.ndarray:
103
- """A screenshot as the RGBA the frame input is declared to take."""
104
- frame = np.asarray(frame, dtype=np.uint8)
105
- if frame.shape[2] == 3:
106
- frame = np.dstack([frame, np.full(frame.shape[:2], 255, np.uint8)])
107
- return np.ascontiguousarray(frame)
90
+ def _image(frame: np.ndarray) -> Any:
91
+ """A screenshot as an RGBA QImage that owns its pixels — the encoder
92
+ reads a frame on its own thread, after the array may be gone. An
93
+ RGB screenshot is converted by Qt rather than padded in numpy: 0.07
94
+ against 3.5 ms a frame at 1024 × 768 (measured 2026-10-10)."""
95
+ from PySide6.QtGui import QImage
96
+
97
+ frame = np.ascontiguousarray(frame, dtype=np.uint8)
98
+ height, width, depth = frame.shape
99
+ if depth == 3:
100
+ return QImage(frame.data, width, height, 3 * width,
101
+ QImage.Format.Format_RGB888).convertToFormat(
102
+ QImage.Format.Format_RGBA8888)
103
+ return QImage(frame.data, width, height, 4 * width,
104
+ QImage.Format.Format_RGBA8888).copy()
108
105
 
109
106
 
110
107
  def write_movie(path: str | Path, frames: Iterable[np.ndarray],
@@ -133,7 +130,6 @@ def write_movie(path: str | Path, frames: Iterable[np.ndarray],
133
130
  if reason is not None:
134
131
  raise MovieUnavailable(reason)
135
132
  from PySide6.QtCore import QEventLoop, QSize, QTimer, QUrl
136
- from PySide6.QtGui import QImage
137
133
  from PySide6.QtMultimedia import (
138
134
  QMediaCaptureSession,
139
135
  QMediaFormat,
@@ -147,8 +143,8 @@ def write_movie(path: str | Path, frames: Iterable[np.ndarray],
147
143
  first = next(source_frames, None)
148
144
  if first is None:
149
145
  raise ValueError('an animation needs at least one frame')
150
- first = _rgba(first)
151
- height, width = first.shape[:2]
146
+ first = _image(first)
147
+ width, height = first.width(), first.height()
152
148
 
153
149
  path = Path(path).resolve()
154
150
 
@@ -191,8 +187,8 @@ def write_movie(path: str | Path, frames: Iterable[np.ndarray],
191
187
  # still starting and keeps ten (2026-10-09). The next
192
188
  # durationChanged sends on.
193
189
  return
194
- frame = state['pending']
195
- if frame is None:
190
+ image = state['pending']
191
+ if image is None:
196
192
  frame = next(state['frames'], None)
197
193
  if frame is None:
198
194
  # the end marker queues like a frame: refused when
@@ -200,22 +196,18 @@ def write_movie(path: str | Path, frames: Iterable[np.ndarray],
200
196
  # stops — sent again on the next ready signal
201
197
  state['ended'] = source.sendVideoFrame(QVideoFrame())
202
198
  return
203
- frame = _rgba(frame)
204
- if frame.shape[:2] != (height, width):
199
+ image = _image(frame)
200
+ if (image.width(), image.height()) != (width, height):
205
201
  # raised here it would vanish into Qt's slot call
206
202
  state['error'] = 'the frames changed size mid-movie'
207
203
  state['frames'] = iter(())
208
204
  continue
209
- # copied: the encoder reads the frame on its own thread,
210
- # after this array may be gone
211
- image = QImage(frame.data, width, height, 4 * width,
212
- QImage.Format.Format_RGBA8888).copy()
213
205
  video = QVideoFrame(image)
214
206
  video.setStartTime(state['sent'] * 1_000_000 // fps)
215
207
  video.setEndTime((state['sent'] + 1) * 1_000_000 // fps)
216
208
  if not source.sendVideoFrame(video):
217
209
  # the encoder is full: keep this one for when it asks
218
- state['pending'] = frame
210
+ state['pending'] = image
219
211
  return
220
212
  state['pending'] = None
221
213
  state['sent'] += 1
@@ -240,18 +232,33 @@ def write_movie(path: str | Path, frames: Iterable[np.ndarray],
240
232
  state['frames'], state['pending'] = iter(()), None
241
233
  recorder.stop()
242
234
 
243
- watchdog.timeout.connect(stalled)
244
- source.readyToSendVideoFrame.connect(send)
245
- recorder.durationChanged.connect(lambda _duration: send())
246
- recorder.recorderStateChanged.connect(changed)
247
- recorder.errorOccurred.connect(failed)
235
+ connected = ((watchdog.timeout, stalled),
236
+ (source.readyToSendVideoFrame, send),
237
+ (recorder.durationChanged, lambda _duration: send()),
238
+ (recorder.recorderStateChanged, changed),
239
+ (recorder.errorOccurred, failed))
240
+ for signal, slot in connected:
241
+ signal.connect(slot)
248
242
  recorder.record()
249
243
  if not state['finished']:
250
244
  loop.exec(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
251
245
  watchdog.stop()
246
+ # the slots hold the recorder, the input and the timer they are
247
+ # connected to — a cycle Python cannot see through Qt — so a save
248
+ # kept all of it alive for the life of the process until let go
249
+ for signal, slot in connected:
250
+ signal.disconnect(slot)
252
251
  if state['error']:
253
252
  raise RuntimeError(f'The movie could not be written: '
254
253
  f'{state["error"]}')
254
+ # read back, not trusted: the pacing above leans on how one Qt
255
+ # buffers frames, and a Qt that buffers otherwise would write a
256
+ # short movie without a word (Principle 12 takes each Qt as it comes)
257
+ codec, kept = mp4_reading(path)
258
+ if codec != b'avc1' or kept != state['sent']:
259
+ raise RuntimeError(
260
+ f'The movie came out wrong: {kept} of {state["sent"]} frames '
261
+ f'as {(codec or b"nothing").decode(errors="replace")}')
255
262
  return state['sent']
256
263
 
257
264
 
@@ -9,6 +9,7 @@ beside them is not.
9
9
  from __future__ import annotations
10
10
 
11
11
  from collections.abc import Callable, Mapping, Sequence
12
+ from functools import partial
12
13
  from typing import TYPE_CHECKING, Any
13
14
 
14
15
  import numpy as np
@@ -78,11 +79,10 @@ def matched_modes_model(matched: MatchedModes, objects: Mapping[str, Any],
78
79
  from ..core.tables import MATCHED_DECIMALS, cell_text, matched_rows
79
80
 
80
81
  headers, rows = matched_rows(matched, objects)
81
- columns = [
82
- Column(title, lambda s, r, i=i: s[r][i],
83
- **({} if decimals is None else
84
- {'format': lambda v, d=decimals: cell_text(v, d)}))
85
- for i, (title, decimals) in enumerate(zip(headers, MATCHED_DECIMALS))]
82
+ columns = [Column(title, lambda s, r, i=i: s[r][i],
83
+ format=partial(cell_text, decimals=decimals))
84
+ for i, (title, decimals)
85
+ in enumerate(zip(headers, MATCHED_DECIMALS))]
86
86
  return TableModel(rows, columns, len, parent)
87
87
 
88
88
 
@@ -1977,17 +1977,19 @@ def _beyond(values, up):
1977
1977
 
1978
1978
 
1979
1979
  def _application():
1980
+ """A Qt application, made if a script has none — a plot rendered to a
1981
+ file, and a movie being encoded, both need one.
1982
+
1983
+ A widgets application, not a QGuiApplication: Qt keeps the first
1984
+ one made for the life of the process, and a script that later opens
1985
+ a window would abort on its first widget.
1986
+ """
1980
1987
  from PySide6.QtWidgets import QApplication
1981
1988
 
1982
1989
  return QApplication.instance() or QApplication([])
1983
1990
 
1984
1991
 
1985
- #: the readings `plot_data(marks=...)` puts over a trace
1986
- FLAT_READINGS = ('averaging', 'shocks', 'truncation', 'filter', 'octave')
1987
-
1988
-
1989
- def octave_preview(items: Iterable[Any], psd: Any,
1990
- per_octave: int | None, unit_system: Any,
1992
+ def octave_preview(items: Iterable[Any], steps: tuple[Any, Any, Any],
1991
1993
  colors: Mapping[str, str],
1992
1994
  records: Sequence[int] | None = None
1993
1995
  ) -> list[tuple[Any, Any]]:
@@ -1998,16 +2000,12 @@ def octave_preview(items: Iterable[Any], psd: Any,
1998
2000
  window's octave view and `plot_data(marks='octave')`; returns the
1999
2001
  (plot, curve) pairs drawn.
2000
2002
 
2001
- Raises
2002
- ------
2003
- ValueError
2004
- When the spectrum cannot be banded.
2003
+ `steps` is `viz.marks.octave_steps`' answer, banded once by the
2004
+ caller, which reads the band count off the same banding.
2005
2005
  """
2006
2006
  import pyqtgraph as pg
2007
2007
 
2008
- from ..viz.marks import octave_steps
2009
-
2010
- banded, x, rows = octave_steps(psd, per_octave, unit_system)
2008
+ banded, x, rows = steps
2011
2009
  wanted = (range(rows.shape[0]) if records is None
2012
2010
  else [int(i) for i in records])
2013
2011
  pen = pg.mkPen(colors['filter_preview'], width=2)
@@ -2032,7 +2030,8 @@ def octave_preview(items: Iterable[Any], psd: Any,
2032
2030
  def _mark(layout, data, marks, theme=None, per_octave=None,
2033
2031
  unit_system=None):
2034
2032
  """A reading over every plot in `layout` — the frames, the shocks,
2035
- the cut, the filter's preview or the octave steps.
2033
+ the cut, the filter's preview or the octave steps — as the window's
2034
+ toggles draw it (`viz.marks.held_reading`).
2036
2035
 
2037
2036
  Locked, because a mark you can drag is a mark that reports the drag
2038
2037
  to somebody, and in a standalone plot there is nobody. Returns the
@@ -2040,51 +2039,37 @@ def _mark(layout, data, marks, theme=None, per_octave=None,
2040
2039
  """
2041
2040
  import pyqtgraph as pg
2042
2041
 
2043
- from ..core.averaging import Averaging
2044
- from ..core.data import Psd, TimeHistory
2042
+ from ..viz.marks import held_reading, octave_steps
2045
2043
 
2046
2044
  if marks is None:
2047
2045
  return []
2048
- if marks not in FLAT_READINGS:
2049
- raise ValueError(f'{marks!r} is not a reading of a plot: '
2050
- + ', '.join(repr(r) for r in FLAT_READINGS))
2046
+ held = held_reading(data, marks)
2051
2047
  colors = resolve_theme(theme)
2052
2048
  plots = [item for item in layout.ci.items if isinstance(item, pg.PlotItem)]
2053
2049
  if marks == 'octave':
2054
- if not isinstance(data, Psd):
2055
- raise TypeError('octave bands read a PSD, not '
2056
- f'{type(data).__name__}')
2057
- return octave_preview(plots, data, per_octave, unit_system, colors)
2058
- if not isinstance(data, TimeHistory):
2059
- raise TypeError(f'{marks} marks belong on a time history, not on '
2060
- f'{type(data).__name__}')
2050
+ return octave_preview(
2051
+ plots, octave_steps(data, per_octave, unit_system), colors)
2061
2052
  if marks == 'shocks':
2062
- from ..core.shocks import suggest
2063
2053
  from .shocks import ShockOverlay
2064
2054
 
2065
- found = tuple(data.shocks or suggest(data))
2066
- return [ShockOverlay(plot, found, colors, locked=True)
2055
+ return [ShockOverlay(plot, held, colors, locked=True)
2067
2056
  for plot in plots]
2068
2057
  if marks == 'filter':
2069
2058
  from .filtering import FilterOverlay
2070
2059
 
2071
- filtering = data.filtering or data.suggest_filtering()
2072
- return [FilterOverlay(plot, filtering, data.sample_rate, colors)
2060
+ return [FilterOverlay(plot, held, data.sample_rate, colors)
2073
2061
  for plot in plots]
2074
2062
  if marks == 'truncation':
2075
2063
  from .truncation import TruncationOverlay
2076
2064
 
2077
2065
  abscissa = np.asarray(data.abscissa, dtype=float)
2078
- truncation = data.truncation or data.suggest_truncation()
2079
- return [TruncationOverlay(plot, truncation, float(abscissa[0]),
2066
+ return [TruncationOverlay(plot, held, float(abscissa[0]),
2080
2067
  float(abscissa[-1]), colors)
2081
2068
  for plot in plots]
2082
2069
  from .averaging import AveragingOverlay
2083
2070
 
2084
- samples = len(data.abscissa)
2085
- averaging = data.averaging or Averaging.for_records(samples)
2086
- return [AveragingOverlay(plot, averaging, data.sample_rate, samples,
2087
- colors, locked=True)
2071
+ return [AveragingOverlay(plot, held, data.sample_rate,
2072
+ len(data.abscissa), colors, locked=True)
2088
2073
  for plot in plots]
2089
2074
 
2090
2075