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