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