visualdynamics 0.1.0a75__tar.gz → 0.1.0a76__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 (295) hide show
  1. {visualdynamics-0.1.0a75/src/visualdynamics.egg-info → visualdynamics-0.1.0a76}/PKG-INFO +1 -1
  2. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/__init__.py +1 -1
  3. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/author.py +22 -11
  4. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/classical_pulse.py +11 -25
  5. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/compliance.py +110 -0
  6. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/data.py +5 -5
  7. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/drive_prediction.py +131 -114
  8. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/half_power.py +9 -2
  9. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/kurtosis.py +10 -2
  10. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/matrix_checks.py +55 -28
  11. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/matrix_comparison.py +9 -90
  12. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/overload.py +13 -0
  13. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/prediction.py +26 -3
  14. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/replication_segments.py +5 -5
  15. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/resample.py +2 -1
  16. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/shock_synthesis.py +3 -8
  17. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/sine_distortion.py +5 -0
  18. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/sine_tracking.py +5 -0
  19. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/srs.py +41 -22
  20. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/stroke.py +20 -5
  21. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/sweep_rate.py +1 -11
  22. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/time_statistics.py +3 -3
  23. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/main_window.py +114 -14
  24. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/panes.py +5 -5
  25. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/resample_panel.py +15 -5
  26. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/sine_panel.py +1 -8
  27. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/mixed_mode.py +3 -8
  28. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/project.py +48 -1
  29. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/report/__init__.py +62 -22
  30. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/report/features.py +1 -1
  31. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/report/plot_conventions.py +19 -4
  32. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/report/prediction.py +9 -1
  33. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/report/skeleton.py +15 -9
  34. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76/src/visualdynamics.egg-info}/PKG-INFO +1 -1
  35. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/LICENSE +0 -0
  36. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/README.md +0 -0
  37. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/pyproject.toml +0 -0
  38. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/setup.cfg +0 -0
  39. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/__main__.py +0 -0
  40. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/compatibility.py +0 -0
  41. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/__init__.py +0 -0
  42. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/amplitude_spectrum.py +0 -0
  43. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/averaging.py +0 -0
  44. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/breakpoints.py +0 -0
  45. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/calibration.py +0 -0
  46. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/chance.py +0 -0
  47. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/channel_health.py +0 -0
  48. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/channel_table.py +0 -0
  49. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/correlate.py +0 -0
  50. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/detect.py +0 -0
  51. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/dwell.py +0 -0
  52. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/entities.py +0 -0
  53. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/fem.py +0 -0
  54. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/filters.py +0 -0
  55. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/geometry.py +0 -0
  56. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/hexmesh.py +0 -0
  57. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/instrumentation.py +0 -0
  58. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/matches.py +0 -0
  59. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/mates.py +0 -0
  60. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/merge.py +0 -0
  61. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/mesh.py +0 -0
  62. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/mixed_mode.py +0 -0
  63. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/modal_fit.py +0 -0
  64. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/modal_response.py +0 -0
  65. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/octave.py +0 -0
  66. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/photos.py +0 -0
  67. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/progress.py +0 -0
  68. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/replication.py +0 -0
  69. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/replication_fit.py +0 -0
  70. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/report.py +0 -0
  71. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/report_caption.py +0 -0
  72. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/rigid.py +0 -0
  73. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/rms_check.py +0 -0
  74. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/sensor_catalog.py +0 -0
  75. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/sensors.py +0 -0
  76. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/shaker.py +0 -0
  77. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/shapes.py +0 -0
  78. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/shells.py +0 -0
  79. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/shock_check.py +0 -0
  80. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/shocks.py +0 -0
  81. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/signals.py +0 -0
  82. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/signature.py +0 -0
  83. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/sine.py +0 -0
  84. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/sine_frequency.py +0 -0
  85. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/sine_profile.py +0 -0
  86. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/snr.py +0 -0
  87. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/solids.py +0 -0
  88. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/sparse_solve.py +0 -0
  89. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/tables.py +0 -0
  90. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/tetmesh.py +0 -0
  91. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/tone_readout.py +0 -0
  92. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/transform.py +0 -0
  93. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/transmissibility.py +0 -0
  94. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/truncate.py +0 -0
  95. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/unit_choices.py +0 -0
  96. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/validate.py +0 -0
  97. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/core/wavelet.py +0 -0
  98. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/decimate.py +0 -0
  99. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/deform.py +0 -0
  100. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/demo/__init__.py +0 -0
  101. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/demo/barc.py +0 -0
  102. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/demo/drone.py +0 -0
  103. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/demo/frame_wing.py +0 -0
  104. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/demo/plate.py +0 -0
  105. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/demo/shapes.py +0 -0
  106. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/__init__.py +0 -0
  107. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/amplitude_panel.py +0 -0
  108. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/app_icon.py +0 -0
  109. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/ask.py +0 -0
  110. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/author_panel.py +0 -0
  111. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/averaging_panel.py +0 -0
  112. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/chance_panel.py +0 -0
  113. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/confidence_panel.py +0 -0
  114. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/console.py +0 -0
  115. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/disclaimer.py +0 -0
  116. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/distortion_panel.py +0 -0
  117. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/drawer_tab.py +0 -0
  118. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/drive_prediction_panel.py +0 -0
  119. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/dwell_panel.py +0 -0
  120. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/editors.py +0 -0
  121. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/filter_panel.py +0 -0
  122. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/frequency_panel.py +0 -0
  123. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/glyphs.py +0 -0
  124. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
  125. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
  126. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/icons.py +0 -0
  127. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/instrumentation_panel.py +0 -0
  128. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/levels_readout.py +0 -0
  129. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/mate_panel.py +0 -0
  130. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/matrix_check_panel.py +0 -0
  131. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/matrix_comparison_panel.py +0 -0
  132. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/merge_panel.py +0 -0
  133. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/mesh_panel.py +0 -0
  134. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/mesh_solids_panel.py +0 -0
  135. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/message_bar.py +0 -0
  136. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/mixed_panel.py +0 -0
  137. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/modal_response_panel.py +0 -0
  138. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/modes_panel.py +0 -0
  139. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/movie.py +0 -0
  140. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/object_tables.py +0 -0
  141. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/octave_panel.py +0 -0
  142. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/opengl.py +0 -0
  143. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/overload_panel.py +0 -0
  144. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/pattern_panel.py +0 -0
  145. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/preferences.py +0 -0
  146. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/profile_panel.py +0 -0
  147. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/project_tree.py +0 -0
  148. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/pulse_panel.py +0 -0
  149. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/record_grid.py +0 -0
  150. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/replication_fit_panel.py +0 -0
  151. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/report_editor.py +0 -0
  152. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/report_features_panel.py +0 -0
  153. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/rms_check_panel.py +0 -0
  154. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/run_reports.py +0 -0
  155. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/segments_panel.py +0 -0
  156. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/settings_panel.py +0 -0
  157. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/shock_panel.py +0 -0
  158. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/signal_panel.py +0 -0
  159. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/signature_panel.py +0 -0
  160. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/srs_tolerance_panel.py +0 -0
  161. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/stage_drag.py +0 -0
  162. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/statistics_panel.py +0 -0
  163. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/stream_window.py +0 -0
  164. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/stroke_panel.py +0 -0
  165. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/sweep_rate_panel.py +0 -0
  166. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/tables.py +0 -0
  167. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/text_import.py +0 -0
  168. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/tone_panel.py +0 -0
  169. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/toolbars.py +0 -0
  170. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/tooltips.py +0 -0
  171. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/transmissibility_panel.py +0 -0
  172. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/truncate_panel.py +0 -0
  173. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/updater.py +0 -0
  174. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/wavelet_panel.py +0 -0
  175. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/gui/windows.py +0 -0
  176. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/__init__.py +0 -0
  177. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/adf.py +0 -0
  178. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/delimited.py +0 -0
  179. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf.py +0 -0
  180. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_objects.py +0 -0
  181. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/LICENSE +0 -0
  182. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/activity_result.txt +0 -0
  183. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/channel_table.txt +0 -0
  184. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/data.txt +0 -0
  185. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/data_physics_random_control_parameters.txt +0 -0
  186. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/data_physics_shock_response_synthesis_control_parameters.txt +0 -0
  187. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/data_physics_sine_control_parameters.txt +0 -0
  188. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/geometry.txt +0 -0
  189. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/global_analysis_attributes.txt +0 -0
  190. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/global_test_attributes.txt +0 -0
  191. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/matrix.txt +0 -0
  192. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/mechanical_control_parameters.txt +0 -0
  193. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/modal_test_parameters.txt +0 -0
  194. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/mode.txt +0 -0
  195. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/parameter_set.txt +0 -0
  196. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/point_cloud.txt +0 -0
  197. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/rattlesnake_random_control_parameters.txt +0 -0
  198. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/rattlesnake_sysid_parameters.txt +0 -0
  199. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/rattlesnake_transient_control_parameters.txt +0 -0
  200. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/response_spectrum.txt +0 -0
  201. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/scalar.txt +0 -0
  202. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/unknown.txt +0 -0
  203. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/vector.txt +0 -0
  204. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/escdf_specifications/vibration_channel_table.txt +0 -0
  205. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/excel.py +0 -0
  206. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/exodus.py +0 -0
  207. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/exporters.py +0 -0
  208. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/femap.py +0 -0
  209. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/matlab.py +0 -0
  210. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/nastran.py +0 -0
  211. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/native.py +0 -0
  212. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/notes.py +0 -0
  213. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/photo_files.py +0 -0
  214. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/punch.py +0 -0
  215. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/rattlesnake.py +0 -0
  216. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/rattlesnake_sine.py +0 -0
  217. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
  218. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/report_template.py +0 -0
  219. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/sdynpy_data.py +0 -0
  220. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/sdynpy_npz.py +0 -0
  221. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
  222. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/sep005.py +0 -0
  223. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/sniffing.py +0 -0
  224. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/stated_units.py +0 -0
  225. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/step.py +0 -0
  226. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/stl.py +0 -0
  227. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/threemf.py +0 -0
  228. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/io/unv.py +0 -0
  229. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/names.py +0 -0
  230. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/__init__.py +0 -0
  231. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/amplitude_spectrum.py +0 -0
  232. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/averaging.py +0 -0
  233. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/bands.py +0 -0
  234. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/bars.py +0 -0
  235. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/breakpoints.py +0 -0
  236. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/chance.py +0 -0
  237. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/classical_pulse.py +0 -0
  238. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/drive_prediction.py +0 -0
  239. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/dwell.py +0 -0
  240. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/filtering.py +0 -0
  241. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/half_power.py +0 -0
  242. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/log_axis.py +0 -0
  243. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/matrix_checks.py +0 -0
  244. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/matrix_comparison.py +0 -0
  245. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/overload.py +0 -0
  246. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/replication_fit.py +0 -0
  247. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/replication_segments.py +0 -0
  248. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/report_caption.py +0 -0
  249. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/resample.py +0 -0
  250. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/rms_check.py +0 -0
  251. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/scalogram.py +0 -0
  252. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/shock_check.py +0 -0
  253. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/shock_synthesis.py +0 -0
  254. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/shocks.py +0 -0
  255. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/signature.py +0 -0
  256. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/sine_distortion.py +0 -0
  257. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/sine_frequency.py +0 -0
  258. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/sine_profile.py +0 -0
  259. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/sine_tracking.py +0 -0
  260. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/srs_tolerance.py +0 -0
  261. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/stroke.py +0 -0
  262. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/sweep_rate.py +0 -0
  263. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/text_import.py +0 -0
  264. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/time_statistics.py +0 -0
  265. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/tone_readout.py +0 -0
  266. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/tracking_filter.py +0 -0
  267. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/transmissibility.py +0 -0
  268. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/plot/truncation.py +0 -0
  269. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/py.typed +0 -0
  270. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/report/calibration.py +0 -0
  271. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/report/markdown.py +0 -0
  272. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/report/page.py +0 -0
  273. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/rotate.py +0 -0
  274. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/theme.py +0 -0
  275. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/units.py +0 -0
  276. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/update.py +0 -0
  277. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/__init__.py +0 -0
  278. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/_quiet.py +0 -0
  279. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/animate.py +0 -0
  280. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/banded.py +0 -0
  281. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/geometry.py +0 -0
  282. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/mac_bars.py +0 -0
  283. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/marks.py +0 -0
  284. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/paired.py +0 -0
  285. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/pick.py +0 -0
  286. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/rigid.py +0 -0
  287. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/scalogram.py +0 -0
  288. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/sensors.py +0 -0
  289. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/sinespec.py +0 -0
  290. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics/viz/waterfall.py +0 -0
  291. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics.egg-info/SOURCES.txt +0 -0
  292. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
  293. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics.egg-info/entry_points.txt +0 -0
  294. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/src/visualdynamics.egg-info/requires.txt +0 -0
  295. {visualdynamics-0.1.0a75 → visualdynamics-0.1.0a76}/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.0a75
3
+ Version: 0.1.0a76
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.0a75'
100
+ __version__ = '0.1.0a76'
101
101
 
102
102
 
103
103
  def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
@@ -1907,11 +1907,19 @@ def read_at_lines(old_f: np.ndarray, values: np.ndarray,
1907
1907
  """A specification record read at new lines.
1908
1908
 
1909
1909
  At its own lines the record keeps its own values. Between them it
1910
- is interpolated the way a specification is read, the magnitude
1911
- log-log and the phase linearly, so a line the sheet added does not
1912
- change what a channel the sheet does not hold means. Outside its
1913
- range it takes its end values.
1910
+ is interpolated the way a specification is read, so a line the
1911
+ sheet added does not change what a channel the sheet does not hold
1912
+ means: a cross term by `compliance.cross_term_at` (the magnitude
1913
+ log-log between points above zero, straight through a zero, the
1914
+ phase linearly), a real record by its magnitude log-log over the
1915
+ points above zero. Outside its range it takes its end values.
1914
1916
  """
1917
+ # A cross term was read here by its own rule, bridging a zero at a
1918
+ # breakpoint as a gap, and the drive prediction read the sheet's
1919
+ # rule while the matrix comparison read the specification's: the
1920
+ # same cross term gave two coherences (2026-10-28). One reader now.
1921
+ from .compliance import cross_term_at
1922
+
1915
1923
  values = np.asarray(values)
1916
1924
  exact = np.isin(lines, old_f)
1917
1925
  if exact.all():
@@ -1923,16 +1931,19 @@ def read_at_lines(old_f: np.ndarray, values: np.ndarray,
1923
1931
  out = np.full(len(lines), np.nan, dtype=values.dtype)
1924
1932
  out[exact] = values[np.searchsorted(old_f, lines[exact])]
1925
1933
  new = ~exact
1934
+ if np.iscomplexobj(values):
1935
+ written = np.isfinite(values)
1936
+ read = cross_term_at(old_f[written], values[written], lines[new])
1937
+ # the end values outside the range, as the real branch holds them
1938
+ below, above = lines[new] < old_f[0], lines[new] > old_f[-1]
1939
+ read[below], read[above] = values[0], values[-1]
1940
+ out[new] = read
1941
+ return out
1926
1942
  if positive.sum() >= 2:
1927
1943
  size = np.exp(np.interp(log_new[new], log_old[positive],
1928
1944
  np.log(magnitude[positive])))
1929
1945
  else:
1930
1946
  size = np.interp(lines[new], old_f, np.nan_to_num(magnitude))
1931
- if np.iscomplexobj(values):
1932
- phase = np.interp(lines[new], old_f,
1933
- np.unwrap(np.angle(values)))
1934
- out[new] = size * np.exp(1j * phase)
1935
- else:
1936
- out[new] = size * np.sign(np.interp(lines[new], old_f,
1937
- np.nan_to_num(values)))
1947
+ out[new] = size * np.sign(np.interp(lines[new], old_f,
1948
+ np.nan_to_num(values)))
1938
1949
  return out
@@ -906,28 +906,14 @@ def check_pulse(history: TimeHistory, pulse: ClassicalPulse,
906
906
  def pulse_channel(history: TimeHistory,
907
907
  channel: int | str | None = None) -> int:
908
908
  """The row of `history` a pulse is read from: `channel` by index or
909
- DOF, or the first acceleration channel; refused when it is not an
910
- acceleration, since a pulse's velocity change is the area under an
911
- acceleration and nothing else."""
912
- rows = range(history.num_records)
913
- if channel is None:
914
- row = next((i for i in rows
915
- if history.known_dim(i) == 'acceleration'), None)
916
- if row is None:
917
- raise ValueError('a pulse is read from an acceleration, and '
918
- 'this record holds none')
919
- return row
920
- if isinstance(channel, str):
921
- dofs = [str(dof) for dof in history.response_dof]
922
- if channel not in dofs:
923
- raise ValueError(f'the record has no channel {channel}')
924
- row = dofs.index(channel)
925
- else:
926
- row = int(channel)
927
- if not 0 <= row < history.num_records:
928
- raise ValueError(f'the record has {history.num_records} '
929
- f'channels; there is no channel {row}')
930
- if history.known_dim(row) != 'acceleration':
931
- raise ValueError(f'{history.response_dof[row]} is not an '
932
- 'acceleration, and a pulse is read from one')
933
- return row
909
+ DOF, or the first acceleration channel whose unit is declared
910
+ (`core.shaker.acceleration_channel`, the one rule for the stroke
911
+ and the replication fit too); refused otherwise, since a pulse's
912
+ velocity change is the area under an acceleration in a known scale
913
+ and nothing else."""
914
+ # It read a channel by `known_dim` (2026-10-28), so a record whose
915
+ # unit was only hinted was judged raw against an SI nominal: a 50 g
916
+ # half-sine stored in g passed as 5.1 g with every point inside.
917
+ from .shaker import acceleration_channel
918
+
919
+ return acceleration_channel(history, channel)
@@ -88,6 +88,116 @@ def log_interpolate(frequencies: ArrayLike, spec_frequencies: ArrayLike,
88
88
  return out
89
89
 
90
90
 
91
+ def cross_term_at(frequencies: ArrayLike, values: ArrayLike,
92
+ lines: ArrayLike) -> np.ndarray:
93
+ """A cross term written at breakpoints, read at `lines` the way a
94
+ specification is drawn: the magnitude log-log between two points
95
+ that are both above zero and straight between two that are not (a
96
+ coherence of zero, two channels stated to be independent, is a
97
+ value and not a gap), the phase linearly. Not a number outside the
98
+ points, and at lines at or below zero hertz.
99
+
100
+ Parameters
101
+ ----------
102
+ frequencies : array_like
103
+ The breakpoints' frequencies, in Hz, in any order.
104
+ values : array_like
105
+ The complex cross term at each breakpoint.
106
+ lines : array_like
107
+ Where to read it.
108
+
109
+ Returns
110
+ -------
111
+ numpy.ndarray
112
+ Complex, one value per line.
113
+ """
114
+ # Two readers of this existed, one in the matrix comparison and one
115
+ # in the drive prediction through the authoring sheet, and they
116
+ # disagreed on a zero at a breakpoint: the sheet's bridged it as a
117
+ # gap, so one specification read 0.81 coherence in the prediction
118
+ # and 0.32 in the comparison at the same line (2026-10-28). This is
119
+ # the one reader, and the zero is a statement.
120
+ frequencies = np.asarray(frequencies, dtype=float)
121
+ values = np.asarray(values)
122
+ lines = np.asarray(lines, dtype=float)
123
+ usable = np.isfinite(frequencies) & (frequencies > 0.0)
124
+ order = np.argsort(frequencies[usable])
125
+ f = frequencies[usable][order]
126
+ v = values[usable][order].astype(complex)
127
+ out = np.full(lines.shape, np.nan, dtype=complex)
128
+ if f.size < 2:
129
+ return out
130
+ inside = (lines >= f[0]) & (lines <= f[-1])
131
+ at = lines[inside]
132
+ k = np.clip(np.searchsorted(f, at, side='right') - 1, 0, f.size - 2)
133
+ m1, m2 = np.abs(v[k]), np.abs(v[k + 1])
134
+ part = (at - f[k]) / (f[k + 1] - f[k])
135
+ with np.errstate(divide='ignore', invalid='ignore'):
136
+ logs = np.exp(np.log(m1) + (np.log(m2) - np.log(m1))
137
+ * np.log(at / f[k]) / np.log(f[k + 1] / f[k]))
138
+ magnitude = np.where((m1 > 0.0) & (m2 > 0.0), logs,
139
+ m1 + (m2 - m1) * part)
140
+ phase = np.interp(at, f, np.unwrap(np.angle(v)))
141
+ out[inside] = magnitude * np.exp(1j * phase)
142
+ return out
143
+
144
+
145
+ def record_at(data: Any, record: int, lines: ArrayLike) -> np.ndarray:
146
+ """One record of a specification read at `lines`, the way it is
147
+ drawn, and not a number where it is not written.
148
+
149
+ An autospectrum is written where it is above zero, and between
150
+ breakpoints ('log_log') is read log-log with its gaps
151
+ (`log_interpolate`). A cross term is written wherever it is a
152
+ number, zero included, and is read between breakpoints by
153
+ `cross_term_at`; whether its coherence is defined is for its two
154
+ autospectra to say. On lines or bands ('bin') a record takes the
155
+ value of the bin the line falls in. The matrix comparison and the
156
+ drive prediction both read a specification here.
157
+
158
+ Parameters
159
+ ----------
160
+ data : Psd or Specification
161
+ The specification.
162
+ record : int
163
+ Which record.
164
+ lines : array_like
165
+ Where to read it, in Hz.
166
+
167
+ Returns
168
+ -------
169
+ numpy.ndarray
170
+ Real for an autospectrum, complex for a cross term.
171
+ """
172
+ from .data import autospectrum
173
+
174
+ lines = np.asarray(lines, dtype=float)
175
+ frequencies = np.asarray(data.abscissa, dtype=float)
176
+ values = np.asarray(data.ordinate)[record]
177
+ auto = autospectrum(data, record)
178
+ written = np.isfinite(frequencies) & (
179
+ data.written(record) if auto else np.isfinite(values))
180
+ if getattr(data, 'interpolation', 'bin') == 'log_log':
181
+ if auto:
182
+ return log_interpolate(lines, frequencies, np.real(values))
183
+ return cross_term_at(frequencies[written], values[written], lines)
184
+ left, right = data.bin_bounds()
185
+ order = np.argsort(left)
186
+ left, right = left[order], right[order]
187
+ out = np.full(lines.shape, np.nan, dtype=float if auto else complex)
188
+ if not left.size:
189
+ return out
190
+ # the last bin starting at or below each line, if that bin reaches it
191
+ k = np.searchsorted(left, lines, side='right') - 1
192
+ found = k >= 0
193
+ k = np.maximum(k, 0)
194
+ found &= (lines >= left[k]) & (lines < right[k])
195
+ source = order[k]
196
+ found &= written[source]
197
+ out[found] = (np.real(values) if auto else values)[source[found]]
198
+ return out
199
+
200
+
91
201
  def log_log_areas(frequencies: ArrayLike, values: ArrayLike,
92
202
  lows: ArrayLike, highs: ArrayLike) -> np.ndarray:
93
203
  """The exact area under the log-log curve through these points, over
@@ -2201,6 +2201,7 @@ class TimeHistory(DataArray):
2201
2201
  # statement.
2202
2202
  from .srs import DEFAULT_Q, KINDS, PER_OCTAVE, octave_frequencies
2203
2203
  from .srs import peaks as srs_peaks
2204
+ from .srs import spectra as srs_spectra
2204
2205
 
2205
2206
  rate = self.sample_rate
2206
2207
  windows = self.srs_windows(shocks)
@@ -2217,11 +2218,10 @@ class TimeHistory(DataArray):
2217
2218
  for i, record in enumerate(self.ordinate):
2218
2219
  for k, window in enumerate(windows):
2219
2220
  first, last = window.bounds(rate)
2220
- highest, lowest = srs_peaks(record.real[first:last],
2221
- frequencies, rate, q=q)
2222
- rows['positive'].append(highest)
2223
- rows['negative'].append(-lowest)
2224
- rows['maximax'].append(np.maximum(highest, -lowest))
2221
+ read = srs_spectra(*srs_peaks(record.real[first:last],
2222
+ frequencies, rate, q=q))
2223
+ for kind in KINDS:
2224
+ rows[kind].append(read[kind])
2225
2225
  dofs.append(self.response_dof[i])
2226
2226
  dims.append(self.ordinate_dim[i])
2227
2227
  hints.append(self.dimension_hint[i])
@@ -116,6 +116,7 @@ from typing import TYPE_CHECKING, Any
116
116
 
117
117
  import numpy as np
118
118
 
119
+ from .matrix_checks import channel_labels, runs
119
120
  from .progress import Ticker
120
121
  from .shaker import ShakerRating
121
122
 
@@ -203,12 +204,25 @@ def regularized_inverse(plant: Any, *, method: str = 'truncated',
203
204
  If `method` is not one of `METHODS`, or the tolerance or the
204
205
  weight is out of its range.
205
206
  """
206
- method = str(method)
207
- if method not in METHODS:
207
+ _check_inverse(method, tolerance, weight)
208
+ plant = np.asarray(plant, dtype=np.complex128)
209
+ lines, rows, columns = plant.shape
210
+ inverse = np.full((lines, columns, rows), np.nan, dtype=np.complex128)
211
+ kept = np.zeros(lines, dtype=int)
212
+ finite = np.isfinite(plant).all(axis=(1, 2))
213
+ if not finite.any():
214
+ return inverse, kept
215
+ inverse[finite], kept[finite] = inverse_from_svd(
216
+ *np.linalg.svd(plant[finite], full_matrices=False), method=method,
217
+ tolerance=tolerance, weight=weight, scale=scale)
218
+ return inverse, kept
219
+
220
+
221
+ def _check_inverse(method: str, tolerance: float, weight: float) -> None:
222
+ if str(method) not in METHODS:
208
223
  raise ValueError(f'the inverse is {" or ".join(map(repr, METHODS))}, '
209
224
  f'not {method!r}')
210
- tolerance = float(tolerance)
211
- weight = float(weight)
225
+ tolerance, weight = float(tolerance), float(weight)
212
226
  if method == 'truncated' and not 0.0 < tolerance < 1.0:
213
227
  raise ValueError('the truncation tolerance is a share of the '
214
228
  'largest singular value, above 0 and under 1, '
@@ -216,14 +230,39 @@ def regularized_inverse(plant: Any, *, method: str = 'truncated',
216
230
  if method == 'tikhonov' and not (np.isfinite(weight) and weight > 0.0):
217
231
  raise ValueError('the Tikhonov weight has to be above zero and '
218
232
  f'finite, not {weight:g}')
219
- plant = np.asarray(plant, dtype=np.complex128)
220
- lines, rows, columns = plant.shape
221
- inverse = np.full((lines, columns, rows), np.nan, dtype=np.complex128)
222
- kept = np.zeros(lines, dtype=int)
223
- finite = np.isfinite(plant).all(axis=(1, 2))
224
- if not finite.any():
225
- return inverse, kept
226
- u, sigma, vh = np.linalg.svd(plant[finite], full_matrices=False)
233
+
234
+
235
+ def inverse_from_svd(u: Any, sigma: Any, vh: Any, *,
236
+ method: str = 'truncated', tolerance: float = TRUNCATION,
237
+ weight: float = TIKHONOV, scale: float | None = None
238
+ ) -> tuple[np.ndarray, np.ndarray]:
239
+ """`regularized_inverse` from the plant's singular value
240
+ decomposition, (lines, rows, k), (lines, k) and (lines, k, columns)
241
+ as `numpy.linalg.svd` returns them with `full_matrices=False`:
242
+ the prediction decomposes the plant once and reads its condition
243
+ number off the same singular values.
244
+
245
+ Parameters
246
+ ----------
247
+ u, sigma, vh : array_like
248
+ The decomposition, every line finite.
249
+ method, tolerance, weight, scale
250
+ As `regularized_inverse` takes them.
251
+
252
+ Returns
253
+ -------
254
+ tuple of numpy.ndarray
255
+ The inverse, (lines, columns, rows), and the directions kept
256
+ at each line.
257
+ """
258
+ # The prediction decomposed the plant twice, once for the condition
259
+ # number and once here: two thirds of a 20 x 20 prediction's time
260
+ # (2026-10-28).
261
+ _check_inverse(method, tolerance, weight)
262
+ method, tolerance, weight = str(method), float(tolerance), float(weight)
263
+ u = np.asarray(u, dtype=np.complex128)
264
+ sigma = np.asarray(sigma, dtype=float)
265
+ vh = np.asarray(vh, dtype=np.complex128)
227
266
  largest = sigma[:, :1]
228
267
  if method == 'truncated':
229
268
  keep = (sigma > 0.0) & (sigma >= tolerance * largest)
@@ -238,9 +277,8 @@ def regularized_inverse(plant: Any, *, method: str = 'truncated',
238
277
  with np.errstate(divide='ignore', invalid='ignore'):
239
278
  gain = np.where(denominator > 0.0, sigma / denominator, 0.0)
240
279
  # V diag(gain) U^H, line by line
241
- inverse[finite] = np.einsum('lji,lj,lkj->lik', vh.conj(), gain, u.conj())
242
- kept[finite] = keep.sum(axis=1)
243
- return inverse, kept
280
+ inverse = np.einsum('lji,lj,lkj->lik', vh.conj(), gain, u.conj())
281
+ return inverse, keep.sum(axis=1)
244
282
 
245
283
 
246
284
  def through(plant: Any, density: Any) -> np.ndarray:
@@ -268,25 +306,18 @@ def _channel(dof: Any, dimension: str, side: int) -> tuple[str, str]:
268
306
  return str(dof), channel_quantities(dimension)[side]
269
307
 
270
308
 
271
- def _labels(keys: list[tuple[str, str]]) -> list[str]:
272
- """A label per (DOF, quantity) channel: the DOF, with the quantity
273
- added only where one DOF carries two."""
274
- dofs = [dof for dof, _quantity in keys]
275
- return [dof if dofs.count(dof) == 1 else f'{dof} {quantity}'
276
- for dof, quantity in keys]
277
-
278
-
279
309
  def _plant(frf: Frf) -> tuple[list[tuple[str, str]], list[tuple[str, str]],
280
310
  np.ndarray, np.ndarray]:
281
- """The FRF's records as (rows, columns, matrix, filled): a row per
282
- response channel and a column per drive, (lines, rows, columns),
283
- with `filled` saying which entries a record supplied. Refuses a
284
- record whose units are not declared and drives of two quantities."""
311
+ """The FRF's records as (rows, columns, matrix, filled), assembled
312
+ by `matrix_checks.frf_matrix` as every matrix reading assembles
313
+ them: a row per response channel and a column per drive, (lines,
314
+ rows, columns), with `filled` saying which entries a record
315
+ supplied. Refuses a record whose units are not declared, two
316
+ records of one entry, and drives of two quantities."""
285
317
  from ..units import UNKNOWN
318
+ from .matrix_checks import frf_matrix
286
319
 
287
- rows: list[tuple[str, str]] = []
288
- columns: list[tuple[str, str]] = []
289
- keys = []
320
+ seen: set[tuple[str, str, str]] = set()
290
321
  for i in range(frf.num_records):
291
322
  dimension = str(frf.ordinate_dim[i])
292
323
  if dimension == UNKNOWN:
@@ -295,65 +326,36 @@ def _plant(frf: Frf) -> tuple[list[tuple[str, str]], list[tuple[str, str]],
295
326
  f'{frf.reference_dof[i]} are not declared, so the drives '
296
327
  'it predicts would be in a scale nobody chose; declare '
297
328
  'them first')
298
- row = _channel(frf.response_dof[i], dimension, 0)
299
- column = _channel(frf.reference_dof[i], dimension, 1)
300
- if row not in rows:
301
- rows.append(row)
302
- if column not in columns:
303
- columns.append(column)
304
- key = (rows.index(row), columns.index(column))
305
- if key in keys:
329
+ key = (str(frf.response_dof[i]), str(frf.reference_dof[i]), dimension)
330
+ if key in seen:
306
331
  # two plants for one entry: which one was meant is not in
307
332
  # the data, and taking either would be a guess
308
333
  raise ValueError(
309
334
  f'the FRF holds two records of {frf.response_dof[i]} over '
310
335
  f'{frf.reference_dof[i]}; keep one')
311
- keys.append(key)
336
+ seen.add(key)
337
+ rows, columns, matrix, filled = frf_matrix(frf)
312
338
  drives = sorted({quantity for _dof, quantity in columns})
313
339
  if len(drives) > 1:
314
340
  raise ValueError(
315
341
  f'the FRF is against drives of {" and ".join(drives)}; the '
316
342
  'drives predicted together have to be one quantity')
317
- matrix = np.zeros((len(frf.abscissa), len(rows), len(columns)),
318
- dtype=np.complex128)
319
- filled = np.zeros((len(rows), len(columns)), dtype=bool)
320
- ordinate = np.asarray(frf.ordinate)
321
- for i, (r, c) in enumerate(keys):
322
- matrix[:, r, c] = ordinate[i]
323
- filled[r, c] = True
324
343
  return rows, columns, matrix, filled
325
344
 
326
345
 
327
- def _per_bin(specification: Psd, values: np.ndarray,
328
- lines: np.ndarray) -> np.ndarray:
329
- """A density on lines read at other lines: the value of the bin
330
- each one falls in, not-a-number outside every bin."""
331
- left, right = specification.bin_bounds()
332
- order = np.argsort(left)
333
- left, right, values = left[order], right[order], values[order]
334
- k = np.searchsorted(right, lines, side='right')
335
- inside = k < len(right)
336
- k = np.minimum(k, len(right) - 1)
337
- inside &= left[k] <= lines
338
- out = np.full(len(lines), np.nan, dtype=values.dtype)
339
- out[inside] = values[k[inside]]
340
- return out
341
-
342
-
343
346
  def _read_target(specification: Psd, channels: list[tuple[str, str]],
344
347
  lines: np.ndarray) -> np.ndarray:
345
348
  """The specification's matrix on the control channels, at `lines`.
346
349
 
347
- A curve written at breakpoints is read the way it is drawn: the
348
- autospectra log-log (`compliance.log_interpolate`, not-a-number
349
- outside what is written) and the cross terms magnitude log-log and
350
- phase linear (`author.read_at_lines`), as the authoring sheet reads
351
- them. A density on lines is read per bin. A cross term held only by
352
- its mirror is its mirror's conjugate. Lines where any autospectrum
353
- is not written are not-a-number throughout."""
350
+ Each record is read the way it is drawn, by `compliance.record_at`,
351
+ the reader the matrix comparison uses too: an autospectrum log-log
352
+ between breakpoints with its gaps, a cross term magnitude log-log
353
+ and phase linear with a zero taken as a value, a density on lines
354
+ per bin. A cross term held only by its mirror is its mirror's
355
+ conjugate. Lines where any autospectrum is not written are
356
+ not-a-number throughout."""
354
357
  from ..units import UNKNOWN
355
- from .author import read_at_lines
356
- from .compliance import log_interpolate
358
+ from .compliance import record_at
357
359
 
358
360
  references = (specification.reference_dof
359
361
  if specification.reference_dof is not None
@@ -375,9 +377,8 @@ def _read_target(specification: Psd, channels: list[tuple[str, str]],
375
377
  f'{specification.response_dof[i]}, {references[i]}; '
376
378
  'keep one')
377
379
  held[(row, column)] = i
378
- labels = _labels(channels)
380
+ labels = channel_labels(channels)
379
381
  n = len(channels)
380
- breakpoints = getattr(specification, 'interpolation', 'bin') == 'log_log'
381
382
  ordinate = np.atleast_2d(np.asarray(specification.ordinate))
382
383
  target = np.zeros((len(lines), n, n), dtype=np.complex128)
383
384
  for a in range(n):
@@ -410,21 +411,13 @@ def _read_target(specification: Psd, channels: list[tuple[str, str]],
410
411
  f'{labels[a]} and {labels[b]} are not conjugates '
411
412
  'of each other, so the target between them '
412
413
  'cannot be read; keep one or make them agree')
414
+ read = record_at(specification, record, lines)
413
415
  if a == b:
414
- values = np.real(values)
415
- read = (log_interpolate(lines, specification.abscissa, values)
416
- if breakpoints else
417
- _per_bin(specification, values.astype(float), lines))
416
+ read = np.real(read)
418
417
  with np.errstate(invalid='ignore'):
419
418
  read = np.where(read > 0.0, read, np.nan)
420
- else:
421
- values = values.astype(np.complex128)
422
- read = (read_at_lines(np.asarray(specification.abscissa,
423
- dtype=float), values, lines)
424
- if breakpoints else
425
- _per_bin(specification, values, lines))
426
- if pair not in held:
427
- read = np.conj(read)
419
+ elif pair not in held:
420
+ read = np.conj(read)
428
421
  target[:, a, b] = read
429
422
  target[:, b, a] = np.conj(read)
430
423
  unwritten = ~np.isfinite(np.real(np.einsum('lii->li', target))).all(axis=1)
@@ -518,16 +511,6 @@ def _dominant(frequency: np.ndarray, density: np.ndarray
518
511
  (float(chosen.min()), float(chosen.max())))
519
512
 
520
513
 
521
- def runs(flags: Any) -> list[tuple[int, int]]:
522
- """(first, last) index of each run of consecutive true flags."""
523
- flags = np.asarray(flags, dtype=bool)
524
- if not flags.any():
525
- return []
526
- edges = np.diff(np.concatenate([[0], flags.astype(int), [0]]))
527
- return [(int(a), int(b) - 1) for a, b in
528
- zip(np.flatnonzero(edges == 1), np.flatnonzero(edges == -1))]
529
-
530
-
531
514
  def _amount(value: float, dimension: str,
532
515
  unit_system: UnitSystem | None) -> str:
533
516
  """`value`, in SI, as a number and a unit in `unit_system`, SI's own
@@ -640,6 +623,44 @@ class DrivePrediction:
640
623
  peak_factor: float | None
641
624
  demands: list[Demand] = field(default_factory=list)
642
625
  notes: list[str] = field(default_factory=list)
626
+ #: the rows and the drives as (DOF, quantity), what the ratings are
627
+ #: read against; and how many of `notes` the prediction itself made,
628
+ #: the rest being the ratings', so a rating moved rewrites only those
629
+ row_channels: list[tuple[str, str]] = field(default_factory=list)
630
+ drive_channels: list[tuple[str, str]] = field(default_factory=list)
631
+ plain_notes: int = 0
632
+
633
+ def with_ratings(self, *, drive_limit: float | Mapping[str, float] | None = None,
634
+ shakers: ShakerRating | Mapping[str, ShakerRating] | None = None,
635
+ tables: Mapping[str, str] | None = None,
636
+ limits: Mapping[str, float] | None = None,
637
+ duration: float = DURATION,
638
+ peak_factor: float | None = None) -> DrivePrediction:
639
+ """This prediction judged against other ratings, the drives and
640
+ responses kept: the inverse does not depend on the ratings, so a
641
+ rating moved does not predict again. The ratings are taken
642
+ exactly as `predict_drives` takes them, and one not given here
643
+ is not judged, whatever the prediction was made with.
644
+
645
+ Returns
646
+ -------
647
+ DrivePrediction
648
+ A new prediction; this one is unchanged.
649
+
650
+ Raises
651
+ ------
652
+ ValueError, TypeError
653
+ As `predict_drives` raises for a rating it cannot read.
654
+ """
655
+ from dataclasses import replace
656
+
657
+ out = replace(self, duration=_positive(duration, 'duration'),
658
+ peak_factor=(None if peak_factor is None else
659
+ _positive(peak_factor, 'peak factor')),
660
+ demands=[], notes=list(self.notes[:self.plain_notes]))
661
+ out.demands.extend(_demands(out, out.row_channels, out.drive_channels,
662
+ drive_limit, shakers, tables, limits))
663
+ return out
643
664
 
644
665
  @property
645
666
  def single(self) -> bool:
@@ -796,7 +817,7 @@ def _for_each(given: Any, labels: list[str], what: str) -> dict[str, Any]:
796
817
  return dict.fromkeys(labels, given)
797
818
  out = {}
798
819
  for key, value in given.items():
799
- label = _find(str(key), labels)
820
+ label = str(key) if str(key) in labels else None
800
821
  if label is None:
801
822
  raise ValueError(f'{what} names {key}, which is not a drive of '
802
823
  f'the FRF ({", ".join(labels)})')
@@ -804,13 +825,6 @@ def _for_each(given: Any, labels: list[str], what: str) -> dict[str, Any]:
804
825
  return out
805
826
 
806
827
 
807
- def _find(key: str, labels: list[str]) -> str | None:
808
- """The label `key` names, or None. A label is its channel's DOF, with
809
- the quantity added only where one DOF holds two (`_labels`), so a
810
- DOF names a channel wherever it can."""
811
- return key if key in labels else None
812
-
813
-
814
828
  def predict_drives(frf: Frf, specification: Psd, *,
815
829
  method: str = 'truncated', tolerance: float = TRUNCATION,
816
830
  weight: float = TIKHONOV,
@@ -931,8 +945,8 @@ def predict_drives(frf: Frf, specification: Psd, *,
931
945
  ticker = Ticker(progress)
932
946
  ticker.add(4)
933
947
  rows, columns, matrix, filled = _plant(frf)
934
- row_labels = _labels(rows)
935
- drive_labels = _labels(columns)
948
+ row_labels = channel_labels(rows)
949
+ drive_labels = channel_labels(columns)
936
950
  controls = _control_channels(specification, rows)
937
951
  control_index = [rows.index(key) for key in controls]
938
952
  for r in control_index:
@@ -969,7 +983,9 @@ def predict_drives(frf: Frf, specification: Psd, *,
969
983
  f'FRF is not a number not predicted, first at {where}')
970
984
  ticker.tick()
971
985
  ticker.busy()
972
- gains = np.linalg.svd(plant[judged], compute_uv=False)
986
+ # one decomposition: the condition number, the dead lines and the
987
+ # inverse all read from it
988
+ u, gains, vh = np.linalg.svd(plant[judged], full_matrices=False)
973
989
  if not gains.max() > 0.0:
974
990
  raise ValueError('the FRF is zero at every line the specification '
975
991
  'is written over: the drives reach nothing')
@@ -998,8 +1014,8 @@ def predict_drives(frf: Frf, specification: Psd, *,
998
1014
  inverse = np.full((len(f), len(columns), len(controls)), np.nan,
999
1015
  dtype=np.complex128)
1000
1016
  kept = np.zeros(len(f), dtype=int)
1001
- inverse[judged], kept[judged] = regularized_inverse(
1002
- plant[judged], method=method, tolerance=tolerance, weight=weight)
1017
+ inverse[judged], kept[judged] = inverse_from_svd(
1018
+ u, gains, vh, method=method, tolerance=tolerance, weight=weight)
1003
1019
  ticker.tick()
1004
1020
  drive_csd = np.full((len(f), len(columns), len(columns)), np.nan,
1005
1021
  dtype=np.complex128)
@@ -1035,10 +1051,11 @@ def predict_drives(frf: Frf, specification: Psd, *,
1035
1051
  target=target, control=control, judged=judged, kept=kept,
1036
1052
  condition=condition, method=str(method), tolerance=float(tolerance),
1037
1053
  weight=float(weight), duration=duration, peak_factor=peak_factor,
1038
- notes=notes)
1039
- demands = _demands(prediction, rows, columns, drive_limit, shakers,
1040
- tables, limits)
1041
- prediction.demands.extend(demands)
1054
+ notes=notes, row_channels=list(rows), drive_channels=list(columns),
1055
+ plain_notes=len(notes))
1056
+ prediction = prediction.with_ratings(
1057
+ drive_limit=drive_limit, shakers=shakers, tables=tables,
1058
+ limits=limits, duration=duration, peak_factor=peak_factor)
1042
1059
  ticker.tick()
1043
1060
  return prediction
1044
1061
 
@@ -1129,7 +1146,7 @@ def _demands(prediction: DrivePrediction, rows: list[tuple[str, str]],
1129
1146
  f'{where}')
1130
1147
 
1131
1148
  for key, value in (limits or {}).items():
1132
- found = _find(str(key), prediction.rows)
1149
+ found = str(key) if str(key) in prediction.rows else None
1133
1150
  if found is None:
1134
1151
  raise ValueError(f'a limit is given at {key}, which is not a '
1135
1152
  'channel of the FRF')
@@ -1151,7 +1168,7 @@ def _table(prediction: DrivePrediction, rows: list[tuple[str, str]],
1151
1168
  of a single-input test. Refused unless it is a predicted
1152
1169
  acceleration."""
1153
1170
  if asked is not None:
1154
- found = _find(str(asked), prediction.rows)
1171
+ found = str(asked) if str(asked) in prediction.rows else None
1155
1172
  if found is None:
1156
1173
  raise ValueError(f'the table of the shaker at {label} is '
1157
1174
  f'{asked}, which is not a channel of the FRF')