visualdynamics 0.1.0a44__tar.gz → 0.1.0a45__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.0a44/src/visualdynamics.egg-info → visualdynamics-0.1.0a45}/PKG-INFO +1 -1
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/__init__.py +1 -1
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/data.py +41 -32
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/filters.py +1 -6
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/geometry.py +258 -26
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/mesh.py +46 -11
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/modal_fit.py +48 -15
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/report.py +0 -19
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/sine.py +5 -26
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/sine_tracking.py +5 -3
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/truncate.py +2 -8
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/demo/barc.py +152 -84
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/author_panel.py +0 -1
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/averaging_panel.py +6 -13
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/filter_panel.py +15 -43
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/icons.py +37 -2
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/main_window.py +548 -74
- visualdynamics-0.1.0a45/src/visualdynamics/gui/merge_panel.py +105 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/mesh_panel.py +26 -6
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/octave_panel.py +3 -8
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/panes.py +5 -2
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/report_editor.py +2 -3
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/rigid_panel.py +2 -6
- visualdynamics-0.1.0a45/src/visualdynamics/gui/settings_panel.py +144 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/sine_panel.py +10 -48
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/tables.py +65 -5
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/truncate_panel.py +4 -10
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/wavelet_panel.py +4 -10
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/adf.py +15 -6
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf.py +0 -15
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/__init__.py +4 -3
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/bands.py +0 -1
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/tracking_filter.py +0 -1
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/project.py +36 -9
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/report/__init__.py +12 -25
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/theme.py +2 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/units.py +3 -2
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/banded.py +2 -15
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/geometry.py +70 -30
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/sinespec.py +30 -16
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45/src/visualdynamics.egg-info}/PKG-INFO +1 -1
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics.egg-info/SOURCES.txt +1 -0
- visualdynamics-0.1.0a44/src/visualdynamics/gui/settings_panel.py +0 -52
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/LICENSE +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/README.md +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/pyproject.toml +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/setup.cfg +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/__main__.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/compatibility.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/__init__.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/author.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/averaging.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/channel_table.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/compliance.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/correlate.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/detect.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/entities.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/fem.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/kurtosis.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/matches.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/merge.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/octave.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/photos.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/progress.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/replication.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/rigid.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/shapes.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/shocks.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/snr.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/srs.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/tables.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/transform.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/unit_choices.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/validate.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/core/wavelet.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/decimate.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/deform.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/demo/__init__.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/demo/drone.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/demo/frame.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/demo/plate.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/__init__.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/app_icon.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/ask.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/console.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/disclaimer.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/drawer_tab.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/editors.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/glyphs.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/movie.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/object_tables.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/preferences.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/project_tree.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/record_grid.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/run_reports.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/shock_panel.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/stage_drag.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/stream_window.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/toolbars.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/updater.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/gui/windows.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/__init__.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_objects.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/LICENSE +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/activity_result.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/channel_table.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/data.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/data_physics_random_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/data_physics_shock_response_synthesis_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/data_physics_sine_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/geometry.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/global_analysis_attributes.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/global_test_attributes.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/matrix.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/mechanical_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/modal_test_parameters.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/mode.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/parameter_set.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/point_cloud.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/rattlesnake_random_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/rattlesnake_sysid_parameters.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/rattlesnake_transient_control_parameters.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/response_spectrum.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/scalar.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/unknown.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/vector.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/escdf_specifications/vibration_channel_table.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/excel.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/exodus.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/exporters.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/femap.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/matlab.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/nastran.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/native.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/notes.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/photo_files.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/punch.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/rattlesnake.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/rattlesnake_sine.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/report_template.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/sdynpy_data.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/sdynpy_npz.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/sep005.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/sniffing.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/step.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/stl.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/threemf.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/io/unv.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/names.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/averaging.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/bars.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/filtering.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/scalogram.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/shocks.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/sine_tracking.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/plot/truncation.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/py.typed +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/report/markdown.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/report/page.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/rotate.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/update.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/__init__.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/_quiet.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/animate.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/mac_bars.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/marks.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/paired.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/pick.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/rigid.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/scalogram.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics/viz/waterfall.py +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics.egg-info/entry_points.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics.egg-info/requires.txt +0 -0
- {visualdynamics-0.1.0a44 → visualdynamics-0.1.0a45}/src/visualdynamics.egg-info/top_level.txt +0 -0
|
@@ -90,7 +90,7 @@ from .units import (
|
|
|
90
90
|
#: alpha, said in the version itself so the dialog, the About text, the
|
|
91
91
|
#: installers and latest.json all say the same thing, and the first
|
|
92
92
|
#: non-alpha release is offered as an upgrade (Brandon, 2026-09-11)
|
|
93
|
-
__version__ = '0.1.
|
|
93
|
+
__version__ = '0.1.0a45'
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
|
|
@@ -122,6 +122,12 @@ REAL_FLOOR = 1e-9
|
|
|
122
122
|
def _even_steps(abscissa, what: str) -> float:
|
|
123
123
|
"""The sample step, or a refusal that says which way it is wrong.
|
|
124
124
|
|
|
125
|
+
`what` names the act that needs the step, as the subject of the
|
|
126
|
+
refusal: 'Computing spectra', 'A sample rate'. Every reader of a
|
|
127
|
+
time history's step comes here, the sine extraction included —
|
|
128
|
+
it kept a copy of its own that checked the spacing but not the
|
|
129
|
+
order (2026-10-10).
|
|
130
|
+
|
|
125
131
|
An abscissa is stored as it arrived — uneven spacing and out-of-order
|
|
126
132
|
samples are both real, and the storage refuses neither (see
|
|
127
133
|
`DataArray.abscissa`). What cannot be done on such a record is
|
|
@@ -131,15 +137,17 @@ def _even_steps(abscissa, what: str) -> float:
|
|
|
131
137
|
record was simply in the wrong order.
|
|
132
138
|
"""
|
|
133
139
|
steps = np.diff(abscissa)
|
|
140
|
+
if not len(steps):
|
|
141
|
+
raise ValueError(f'{what} needs at least two time samples')
|
|
134
142
|
if (steps <= 0).any():
|
|
135
143
|
first = int(np.flatnonzero(steps <= 0)[0])
|
|
136
144
|
raise ValueError(
|
|
137
|
-
f'{what}
|
|
145
|
+
f'{what} needs time samples in order; sample {first + 1} is at '
|
|
138
146
|
f'{float(abscissa[first + 1]):g} s, after {float(abscissa[first]):g}')
|
|
139
147
|
if not np.allclose(steps, steps[0], rtol=1e-6, atol=0):
|
|
140
148
|
widest = float(np.abs(steps - steps[0]).max())
|
|
141
149
|
raise ValueError(
|
|
142
|
-
f'{what}
|
|
150
|
+
f'{what} needs evenly spaced time samples; the step varies by '
|
|
143
151
|
f'{widest:g} s across the record')
|
|
144
152
|
return float(steps[0])
|
|
145
153
|
|
|
@@ -440,6 +448,28 @@ class DataArray:
|
|
|
440
448
|
raise ValueError(f'{name} has {len(out)} entries, expected {n}')
|
|
441
449
|
return out
|
|
442
450
|
|
|
451
|
+
def record_fields(self) -> dict[str, Any]:
|
|
452
|
+
"""Each record's description — DOFs, quantity, units, hint,
|
|
453
|
+
comment and block — as the keyword arguments a derived object of
|
|
454
|
+
the same records is made with, every list a copy.
|
|
455
|
+
|
|
456
|
+
A filter, a truncation and an octave banding keep their source's
|
|
457
|
+
records and change only the numbers; this is the one statement
|
|
458
|
+
of what "the same records" carries, where it was written out at
|
|
459
|
+
each (2026-10-10). Override `comment` where the derivation says
|
|
460
|
+
what it did.
|
|
461
|
+
"""
|
|
462
|
+
return {
|
|
463
|
+
'response_dof': list(self.response_dof),
|
|
464
|
+
'reference_dof': (None if self.reference_dof is None
|
|
465
|
+
else list(self.reference_dof)),
|
|
466
|
+
'ordinate_dim': list(self.ordinate_dim),
|
|
467
|
+
'ordinate_unit': list(self.ordinate_unit),
|
|
468
|
+
'reference_unit': list(self.reference_unit),
|
|
469
|
+
'dimension_hint': list(self.dimension_hint),
|
|
470
|
+
'comment': list(self.comment),
|
|
471
|
+
'block': None if self.block is None else list(self.block)}
|
|
472
|
+
|
|
443
473
|
def rename_dof(self, old: str, new: str,
|
|
444
474
|
quantity: str | None = None) -> int:
|
|
445
475
|
"""Give a channel's coordinate a new name, in place.
|
|
@@ -1087,9 +1117,7 @@ class TimeHistory(DataArray):
|
|
|
1087
1117
|
Spectrum with one record per channel, carrying the channel's
|
|
1088
1118
|
own quantity and units.
|
|
1089
1119
|
"""
|
|
1090
|
-
|
|
1091
|
-
raise ValueError('Spectra need at least two time samples')
|
|
1092
|
-
step = _even_steps(self.abscissa, 'Spectra')
|
|
1120
|
+
step = _even_steps(self.abscissa, 'Computing spectra')
|
|
1093
1121
|
groups = {}
|
|
1094
1122
|
for i, dof in enumerate(self.response_dof):
|
|
1095
1123
|
key = (dof, self.ordinate_dim[i], self.ordinate_unit[i],
|
|
@@ -1113,9 +1141,7 @@ class TimeHistory(DataArray):
|
|
|
1113
1141
|
@property
|
|
1114
1142
|
def sample_rate(self) -> float:
|
|
1115
1143
|
"""Samples per second, from the abscissa — which must be even."""
|
|
1116
|
-
|
|
1117
|
-
raise ValueError('a sample rate needs at least two time samples')
|
|
1118
|
-
return 1.0 / _even_steps(self.abscissa, 'a sample rate')
|
|
1144
|
+
return 1.0 / _even_steps(self.abscissa, 'A sample rate')
|
|
1119
1145
|
|
|
1120
1146
|
def channel_key(self, i: int) -> tuple[str, str, str | None]:
|
|
1121
1147
|
"""What makes record `i` the same channel as another.
|
|
@@ -1280,9 +1306,7 @@ class TimeHistory(DataArray):
|
|
|
1280
1306
|
|
|
1281
1307
|
if averaging is None:
|
|
1282
1308
|
averaging = self.averaging
|
|
1283
|
-
|
|
1284
|
-
raise ValueError('PSDs need at least two time samples')
|
|
1285
|
-
step = _even_steps(self.abscissa, 'PSDs')
|
|
1309
|
+
step = _even_steps(self.abscissa, 'Computing PSDs')
|
|
1286
1310
|
fs = 1.0 / step
|
|
1287
1311
|
samples = len(self.abscissa)
|
|
1288
1312
|
if averaging is None:
|
|
@@ -3079,16 +3103,9 @@ class Psd(_Bands, DataArray):
|
|
|
3079
3103
|
banded = resample(frequencies, self.ordinate, bounds, widths,
|
|
3080
3104
|
source=self.bin_bounds())
|
|
3081
3105
|
out = Psd(centers, banded,
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
ordinate_dim=list(self.ordinate_dim),
|
|
3086
|
-
ordinate_unit=list(self.ordinate_unit),
|
|
3087
|
-
reference_unit=list(self.reference_unit),
|
|
3088
|
-
dimension_hint=list(self.dimension_hint),
|
|
3089
|
-
block=None if self.block is None else list(self.block),
|
|
3090
|
-
bandwidth=widths,
|
|
3091
|
-
comment=f'1/{per_octave} octave bands')
|
|
3106
|
+
**{**self.record_fields(),
|
|
3107
|
+
'comment': f'1/{per_octave} octave bands'},
|
|
3108
|
+
bandwidth=widths)
|
|
3092
3109
|
# banding conserves the area and changes nothing about what the
|
|
3093
3110
|
# object is — a held comparison scale included, so the report's
|
|
3094
3111
|
# own banding compares what the narrowband comparison compared
|
|
@@ -3615,17 +3632,9 @@ class Specification(Bounded, Psd):
|
|
|
3615
3632
|
limits[name] = out
|
|
3616
3633
|
out = Specification(
|
|
3617
3634
|
centers, banded,
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
ordinate_dim=list(self.ordinate_dim),
|
|
3622
|
-
ordinate_unit=list(self.ordinate_unit),
|
|
3623
|
-
reference_unit=list(self.reference_unit),
|
|
3624
|
-
dimension_hint=list(self.dimension_hint),
|
|
3625
|
-
block=None if self.block is None else list(self.block),
|
|
3626
|
-
bandwidth=widths,
|
|
3627
|
-
comment=f'1/{per_octave} octave bands',
|
|
3628
|
-
**limits)
|
|
3635
|
+
**{**self.record_fields(),
|
|
3636
|
+
'comment': f'1/{per_octave} octave bands'},
|
|
3637
|
+
bandwidth=widths, **limits)
|
|
3629
3638
|
out.interpolation = 'bin'
|
|
3630
3639
|
out.scale_db = self.scale_db
|
|
3631
3640
|
constraints = getattr(self, 'band_constraints', None)
|
|
@@ -255,12 +255,7 @@ def filtered(data: TimeHistory, filtering: Filtering) -> TimeHistory:
|
|
|
255
255
|
sos = design(filtering, rate)
|
|
256
256
|
result = TimeHistory(
|
|
257
257
|
data.abscissa, threaded_sosfiltfilt(sos, np.real(data.ordinate)),
|
|
258
|
-
|
|
259
|
-
ordinate_dim=list(data.ordinate_dim),
|
|
260
|
-
ordinate_unit=list(data.ordinate_unit),
|
|
261
|
-
dimension_hint=list(data.dimension_hint),
|
|
262
|
-
comment=list(data.comment),
|
|
263
|
-
block=None if data.block is None else list(data.block))
|
|
258
|
+
**data.record_fields())
|
|
264
259
|
return carrying_marks(data, result)
|
|
265
260
|
|
|
266
261
|
|
|
@@ -1203,6 +1203,32 @@ class Geometry:
|
|
|
1203
1203
|
return [int(self.elem_id[i])
|
|
1204
1204
|
for i in np.flatnonzero(self.elem_group == int(group))]
|
|
1205
1205
|
|
|
1206
|
+
def nodes_in(self, groups: Ids) -> list[int]:
|
|
1207
|
+
"""The ids of the nodes the elements of these element groups
|
|
1208
|
+
use, ascending — what a merge scoped to them may touch.
|
|
1209
|
+
|
|
1210
|
+
Parameters
|
|
1211
|
+
----------
|
|
1212
|
+
groups : sequence of int
|
|
1213
|
+
Element groups, by identifier.
|
|
1214
|
+
|
|
1215
|
+
Returns
|
|
1216
|
+
-------
|
|
1217
|
+
list of int
|
|
1218
|
+
The node identifiers.
|
|
1219
|
+
|
|
1220
|
+
Raises
|
|
1221
|
+
------
|
|
1222
|
+
ValueError
|
|
1223
|
+
When a group is not in the geometry.
|
|
1224
|
+
"""
|
|
1225
|
+
groups = [int(g) for g in groups]
|
|
1226
|
+
missing = [g for g in groups if g not in self.group_id.tolist()]
|
|
1227
|
+
if missing:
|
|
1228
|
+
raise ValueError(f'no element group {missing[0]}')
|
|
1229
|
+
return sorted({int(n) for k in np.flatnonzero(
|
|
1230
|
+
np.isin(self.elem_group, groups)) for n in self.elem_conn[k]})
|
|
1231
|
+
|
|
1206
1232
|
def add_group(self, name: str = '', group_id: int | None = None) -> int:
|
|
1207
1233
|
"""Declare an element group. Returns its id.
|
|
1208
1234
|
|
|
@@ -1466,7 +1492,8 @@ class Geometry:
|
|
|
1466
1492
|
self.validate()
|
|
1467
1493
|
return {'nodes': removed_nodes, 'elements': len(orphan_elements)}
|
|
1468
1494
|
|
|
1469
|
-
def coincident_nodes(self, tolerance: float
|
|
1495
|
+
def coincident_nodes(self, tolerance: float,
|
|
1496
|
+
nodes: Ids | None = None) -> dict[int, int]:
|
|
1470
1497
|
"""{node: the node it coincides with}: every node within
|
|
1471
1498
|
`tolerance` of another, mapped to the lowest id among those it is
|
|
1472
1499
|
joined to — the one a merge keeps. Chains join: a within tolerance
|
|
@@ -1477,6 +1504,10 @@ class Geometry:
|
|
|
1477
1504
|
tolerance : float
|
|
1478
1505
|
How close two nodes must be to be one point, as the
|
|
1479
1506
|
coordinates are held: meters once units are defined.
|
|
1507
|
+
nodes : sequence of int, optional
|
|
1508
|
+
Only these nodes are candidates, by id: a node outside them
|
|
1509
|
+
is not merged, nor merged into, however close. Every node
|
|
1510
|
+
when omitted.
|
|
1480
1511
|
|
|
1481
1512
|
Returns
|
|
1482
1513
|
-------
|
|
@@ -1485,10 +1516,13 @@ class Geometry:
|
|
|
1485
1516
|
"""
|
|
1486
1517
|
from scipy.spatial import cKDTree
|
|
1487
1518
|
|
|
1488
|
-
|
|
1519
|
+
rows = (np.arange(len(self.node_id)) if nodes is None
|
|
1520
|
+
else np.flatnonzero(np.isin(self.node_id,
|
|
1521
|
+
[int(n) for n in nodes])))
|
|
1522
|
+
if len(rows) < 2:
|
|
1489
1523
|
return {}
|
|
1490
|
-
pairs = cKDTree(self.node_xyz).query_pairs(float(tolerance))
|
|
1491
|
-
parent = list(range(len(
|
|
1524
|
+
pairs = cKDTree(self.node_xyz[rows]).query_pairs(float(tolerance))
|
|
1525
|
+
parent = list(range(len(rows)))
|
|
1492
1526
|
|
|
1493
1527
|
def root(i: int) -> int:
|
|
1494
1528
|
while parent[i] != i:
|
|
@@ -1501,18 +1535,18 @@ class Geometry:
|
|
|
1501
1535
|
if a != b:
|
|
1502
1536
|
parent[max(a, b)] = min(a, b)
|
|
1503
1537
|
groups: dict[int, list[int]] = {}
|
|
1504
|
-
for i in range(len(
|
|
1538
|
+
for i in range(len(rows)):
|
|
1505
1539
|
groups.setdefault(root(i), []).append(i)
|
|
1506
1540
|
out = {}
|
|
1507
1541
|
for members in groups.values():
|
|
1508
1542
|
if len(members) < 2:
|
|
1509
1543
|
continue
|
|
1510
|
-
ids = sorted(int(self.node_id[i]) for i in members)
|
|
1544
|
+
ids = sorted(int(self.node_id[rows[i]]) for i in members)
|
|
1511
1545
|
for other in ids[1:]:
|
|
1512
1546
|
out[other] = ids[0]
|
|
1513
1547
|
return out
|
|
1514
1548
|
|
|
1515
|
-
def duplicate_elements(self) -> dict[int, int]:
|
|
1549
|
+
def duplicate_elements(self, groups: Ids | None = None) -> dict[int, int]:
|
|
1516
1550
|
"""{element id: the element it is} for every solid element on
|
|
1517
1551
|
exactly the nodes of an earlier one, of the same type — two
|
|
1518
1552
|
bricks filling one cell. The lowest id is the one that stays.
|
|
@@ -1526,12 +1560,27 @@ class Geometry:
|
|
|
1526
1560
|
overlap: the region counted twice, its stiffness and mass
|
|
1527
1561
|
doubled, and every face of each pair drawn as interior, so the
|
|
1528
1562
|
middle of the X vanished from the view (Brandon, 2026-10-02).
|
|
1563
|
+
|
|
1564
|
+
Parameters
|
|
1565
|
+
----------
|
|
1566
|
+
groups : sequence of int, optional
|
|
1567
|
+
Keeps the search to these element groups' elements, by id;
|
|
1568
|
+
every element when omitted.
|
|
1569
|
+
|
|
1570
|
+
Returns
|
|
1571
|
+
-------
|
|
1572
|
+
dict of int to int
|
|
1573
|
+
Each element that fills a cell another already fills, to the
|
|
1574
|
+
element it is.
|
|
1529
1575
|
"""
|
|
1576
|
+
wanted = None if groups is None else {int(g) for g in groups}
|
|
1530
1577
|
first: dict[tuple, int] = {}
|
|
1531
1578
|
out: dict[int, int] = {}
|
|
1532
1579
|
for k, conn in enumerate(self.elem_conn):
|
|
1533
1580
|
if element_family(int(self.elem_type[k])) not in SOLID_FAMILIES:
|
|
1534
1581
|
continue
|
|
1582
|
+
if wanted is not None and int(self.elem_group[k]) not in wanted:
|
|
1583
|
+
continue
|
|
1535
1584
|
key = (int(self.elem_type[k]), tuple(sorted(int(n) for n in conn)))
|
|
1536
1585
|
eid = int(self.elem_id[k])
|
|
1537
1586
|
if key in first:
|
|
@@ -1540,16 +1589,180 @@ class Geometry:
|
|
|
1540
1589
|
first[key] = eid
|
|
1541
1590
|
return out
|
|
1542
1591
|
|
|
1543
|
-
def merge_duplicate_elements(self) -> int:
|
|
1592
|
+
def merge_duplicate_elements(self, groups: Ids | None = None) -> int:
|
|
1544
1593
|
"""Make elements that are one cell one element (`duplicate_elements`):
|
|
1545
1594
|
the later of each pair is removed, the earlier — and its element group —
|
|
1546
|
-
stays.
|
|
1547
|
-
|
|
1595
|
+
stays.
|
|
1596
|
+
|
|
1597
|
+
Parameters
|
|
1598
|
+
----------
|
|
1599
|
+
groups : sequence of int, optional
|
|
1600
|
+
Keeps it to these element groups' elements, by id; every
|
|
1601
|
+
element when omitted.
|
|
1602
|
+
|
|
1603
|
+
Returns
|
|
1604
|
+
-------
|
|
1605
|
+
int
|
|
1606
|
+
How many elements were removed.
|
|
1607
|
+
"""
|
|
1608
|
+
going = self.duplicate_elements(groups)
|
|
1548
1609
|
if going:
|
|
1549
1610
|
self.delete_elements(list(going))
|
|
1550
1611
|
return len(going)
|
|
1551
1612
|
|
|
1552
|
-
def
|
|
1613
|
+
def _merge_candidates(self, groups: Ids | None,
|
|
1614
|
+
exclude: Ids | None) -> list[int] | None:
|
|
1615
|
+
"""The node ids a merge may touch: those of `groups`' elements
|
|
1616
|
+
(every node when None), less `exclude`; None for every node."""
|
|
1617
|
+
nodes = None if groups is None else self.nodes_in(groups)
|
|
1618
|
+
if not exclude:
|
|
1619
|
+
return nodes
|
|
1620
|
+
left_out = {int(n) for n in exclude}
|
|
1621
|
+
base = self.node_id.tolist() if nodes is None else nodes
|
|
1622
|
+
return [int(n) for n in base if int(n) not in left_out]
|
|
1623
|
+
|
|
1624
|
+
def node_spacing(self, groups: Ids | None = None) -> float | None:
|
|
1625
|
+
"""The mesh's finest spacing: the shortest distance between two
|
|
1626
|
+
nodes of one element, zero-length elements passed over — a
|
|
1627
|
+
brick's shortest edge, a quad8's half edge. What Merge Coincident
|
|
1628
|
+
Nodes sizes its default tolerance from (a quarter of it, Brandon,
|
|
1629
|
+
2026-10-11): parts meshed apart sit well inside it, and no two
|
|
1630
|
+
nodes of one element do.
|
|
1631
|
+
|
|
1632
|
+
Parameters
|
|
1633
|
+
----------
|
|
1634
|
+
groups : sequence of int, optional
|
|
1635
|
+
Only these element groups' elements, by id; every element
|
|
1636
|
+
when omitted.
|
|
1637
|
+
|
|
1638
|
+
Returns
|
|
1639
|
+
-------
|
|
1640
|
+
float or None
|
|
1641
|
+
The spacing, as the coordinates are held; None when there is
|
|
1642
|
+
no element with two distinct nodes.
|
|
1643
|
+
"""
|
|
1644
|
+
rows = (range(len(self.elem_conn)) if groups is None
|
|
1645
|
+
else np.flatnonzero(np.isin(self.elem_group,
|
|
1646
|
+
[int(g) for g in groups])))
|
|
1647
|
+
by_count: dict[int, list[np.ndarray]] = {}
|
|
1648
|
+
for k in rows:
|
|
1649
|
+
conn = self.elem_conn[k]
|
|
1650
|
+
if len(conn) > 1:
|
|
1651
|
+
by_count.setdefault(len(conn), []).append(conn)
|
|
1652
|
+
finest = np.inf
|
|
1653
|
+
for count, conns in by_count.items():
|
|
1654
|
+
xyz = self.node_xyz[self.node_index(np.concatenate(conns))]
|
|
1655
|
+
xyz = xyz.reshape(len(conns), count, 3)
|
|
1656
|
+
first, second = np.triu_indices(count, 1)
|
|
1657
|
+
gaps = np.linalg.norm(xyz[:, first] - xyz[:, second], axis=-1)
|
|
1658
|
+
gaps = gaps[gaps > 0.0]
|
|
1659
|
+
if gaps.size:
|
|
1660
|
+
finest = min(finest, float(gaps.min()))
|
|
1661
|
+
return None if not np.isfinite(finest) else finest
|
|
1662
|
+
|
|
1663
|
+
def coincident_spots(self, tolerance: float, groups: Ids | None = None,
|
|
1664
|
+
exclude: Ids | None = None) -> list[list[int]]:
|
|
1665
|
+
"""The places a merge would make one node: each a list of the
|
|
1666
|
+
node ids that coincide there, ascending, so the first is the one
|
|
1667
|
+
that stays. What Merge Coincident Nodes marks in the view before
|
|
1668
|
+
anything is merged, a place at a time, for a person to leave some
|
|
1669
|
+
out (Brandon, 2026-10-11).
|
|
1670
|
+
|
|
1671
|
+
Parameters
|
|
1672
|
+
----------
|
|
1673
|
+
tolerance : float
|
|
1674
|
+
How close two nodes must be to be one, as the coordinates are
|
|
1675
|
+
held: meters once units are defined.
|
|
1676
|
+
groups : sequence of int, optional
|
|
1677
|
+
Only the nodes these element groups' elements use, by id;
|
|
1678
|
+
every node when omitted.
|
|
1679
|
+
exclude : sequence of int, optional
|
|
1680
|
+
Nodes left out, by id: neither merged nor merged into.
|
|
1681
|
+
|
|
1682
|
+
Returns
|
|
1683
|
+
-------
|
|
1684
|
+
list of list of int
|
|
1685
|
+
One list per place, in the order of the node each keeps.
|
|
1686
|
+
"""
|
|
1687
|
+
mapping = self.coincident_nodes(
|
|
1688
|
+
tolerance, self._merge_candidates(groups, exclude))
|
|
1689
|
+
spots: dict[int, list[int]] = {}
|
|
1690
|
+
for going, kept in mapping.items():
|
|
1691
|
+
spots.setdefault(int(kept), [int(kept)]).append(int(going))
|
|
1692
|
+
return [sorted(nodes) for _kept, nodes in sorted(spots.items())]
|
|
1693
|
+
|
|
1694
|
+
def merge_folds(self, tolerance: float, groups: Ids | None = None,
|
|
1695
|
+
exclude: Ids | None = None) -> str | None:
|
|
1696
|
+
"""Why this merge would be refused, or None: the sentence
|
|
1697
|
+
`merge_coincident_nodes` raises when the tolerance would fold an
|
|
1698
|
+
element onto itself — asked ahead, so a pane can say it before
|
|
1699
|
+
its button is pressed.
|
|
1700
|
+
|
|
1701
|
+
Parameters
|
|
1702
|
+
----------
|
|
1703
|
+
tolerance : float
|
|
1704
|
+
As `merge_coincident_nodes`.
|
|
1705
|
+
groups : sequence of int, optional
|
|
1706
|
+
As `merge_coincident_nodes`.
|
|
1707
|
+
exclude : sequence of int, optional
|
|
1708
|
+
As `merge_coincident_nodes`.
|
|
1709
|
+
|
|
1710
|
+
Returns
|
|
1711
|
+
-------
|
|
1712
|
+
str or None
|
|
1713
|
+
"""
|
|
1714
|
+
return self._folding(self.coincident_nodes(
|
|
1715
|
+
tolerance, self._merge_candidates(groups, exclude)), tolerance)
|
|
1716
|
+
|
|
1717
|
+
def merge_collapses(self, tolerance: float, groups: Ids | None = None,
|
|
1718
|
+
exclude: Ids | None = None) -> list[int]:
|
|
1719
|
+
"""The links this merge would remove, by element id: the line
|
|
1720
|
+
elements — a rigid link, a spring, a beam — whose nodes all become
|
|
1721
|
+
one node, which then join nothing. A joint built with rigid
|
|
1722
|
+
links and then merged takes them out, where the merge used to be
|
|
1723
|
+
refused for folding them (Brandon, 2026-10-11).
|
|
1724
|
+
|
|
1725
|
+
Parameters
|
|
1726
|
+
----------
|
|
1727
|
+
tolerance : float
|
|
1728
|
+
As `merge_coincident_nodes`.
|
|
1729
|
+
groups : sequence of int, optional
|
|
1730
|
+
As `merge_coincident_nodes`.
|
|
1731
|
+
exclude : sequence of int, optional
|
|
1732
|
+
As `merge_coincident_nodes`.
|
|
1733
|
+
|
|
1734
|
+
Returns
|
|
1735
|
+
-------
|
|
1736
|
+
list of int
|
|
1737
|
+
"""
|
|
1738
|
+
return self._collapsed(self.coincident_nodes(
|
|
1739
|
+
tolerance, self._merge_candidates(groups, exclude)))
|
|
1740
|
+
|
|
1741
|
+
def _collapsed(self, mapping: dict[int, int]) -> list[int]:
|
|
1742
|
+
if not mapping:
|
|
1743
|
+
return []
|
|
1744
|
+
out = []
|
|
1745
|
+
for i, conn in enumerate(self.elem_conn):
|
|
1746
|
+
if (len(conn) > 1 and ELEMENT_TYPES[int(self.elem_type[i])][2] == 'line'
|
|
1747
|
+
and len({mapping.get(int(n), int(n)) for n in conn}) == 1):
|
|
1748
|
+
out.append(int(self.elem_id[i]))
|
|
1749
|
+
return out
|
|
1750
|
+
|
|
1751
|
+
def _folding(self, mapping: dict[int, int], tolerance: float) -> str | None:
|
|
1752
|
+
gone = set(self._collapsed(mapping))
|
|
1753
|
+
for i, conn in enumerate(self.elem_conn):
|
|
1754
|
+
if int(self.elem_id[i]) in gone:
|
|
1755
|
+
continue
|
|
1756
|
+
renamed = [mapping.get(int(n), int(n)) for n in conn]
|
|
1757
|
+
if len(set(renamed)) < len(renamed):
|
|
1758
|
+
return (f'a tolerance of {tolerance:g} folds element '
|
|
1759
|
+
f'{int(self.elem_id[i])} onto itself — two of its own '
|
|
1760
|
+
'nodes are within it; use a smaller tolerance')
|
|
1761
|
+
return None
|
|
1762
|
+
|
|
1763
|
+
def merge_coincident_nodes(self, tolerance: float,
|
|
1764
|
+
groups: Ids | None = None,
|
|
1765
|
+
exclude: Ids | None = None) -> dict[str, int]:
|
|
1553
1766
|
"""Make nodes that are one point one node: every element and
|
|
1554
1767
|
element naming a node within `tolerance` of another is renamed
|
|
1555
1768
|
to the lowest id among them, and the rest are removed. Plates
|
|
@@ -1558,34 +1771,53 @@ class Geometry:
|
|
|
1558
1771
|
|
|
1559
1772
|
Refused, with the element named, when the tolerance would fold an
|
|
1560
1773
|
element onto itself — two of its own corners within it — since
|
|
1561
|
-
that is a tolerance larger than the mesh, not a coincidence.
|
|
1774
|
+
that is a tolerance larger than the mesh, not a coincidence. A
|
|
1775
|
+
line element whose nodes all become one — a rigid link across
|
|
1776
|
+
the gap the merge closes — is removed instead
|
|
1777
|
+
(`merge_collapses`).
|
|
1562
1778
|
Elements the merge leaves on exactly the same nodes are one cell
|
|
1563
1779
|
filled twice, and are made one (`merge_duplicate_elements`).
|
|
1564
1780
|
|
|
1781
|
+
With `groups`, only what those element groups hold: the nodes
|
|
1782
|
+
their elements use are the candidates, so two groups picked
|
|
1783
|
+
together are tied to each other and to nothing else, and only
|
|
1784
|
+
their elements are made one where they fill a cell twice. A merge
|
|
1785
|
+
over a whole geometry ties every part to every part it touches,
|
|
1786
|
+
which is further than a person usually means (Brandon,
|
|
1787
|
+
2026-10-11: the app offers the merge on element groups only).
|
|
1788
|
+
|
|
1565
1789
|
Parameters
|
|
1566
1790
|
----------
|
|
1567
1791
|
tolerance : float
|
|
1568
1792
|
How close two nodes must be to be one, as the coordinates are
|
|
1569
1793
|
held: meters once units are defined.
|
|
1794
|
+
groups : sequence of int, optional
|
|
1795
|
+
The element groups whose nodes are merged, by id. Every node
|
|
1796
|
+
of the geometry when omitted.
|
|
1797
|
+
exclude : sequence of int, optional
|
|
1798
|
+
Nodes left out of the merge, by id: neither merged nor merged
|
|
1799
|
+
into — the places a person unpicked in the app
|
|
1800
|
+
(`coincident_spots`).
|
|
1570
1801
|
|
|
1571
1802
|
Returns
|
|
1572
1803
|
-------
|
|
1573
1804
|
dict of str to int
|
|
1574
1805
|
'merged', the nodes removed, 'into', the nodes they became,
|
|
1575
|
-
|
|
1576
|
-
|
|
1806
|
+
'duplicates', the elements removed for filling a cell another
|
|
1807
|
+
already filled, and 'links', the line elements removed for
|
|
1808
|
+
joining nodes that became one.
|
|
1577
1809
|
"""
|
|
1578
|
-
mapping = self.coincident_nodes(
|
|
1810
|
+
mapping = self.coincident_nodes(
|
|
1811
|
+
tolerance, self._merge_candidates(groups, exclude))
|
|
1579
1812
|
if not mapping:
|
|
1580
|
-
duplicates = self.merge_duplicate_elements()
|
|
1581
|
-
return {'merged': 0, 'into': 0, 'duplicates': duplicates
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
'nodes are within it; use a smaller tolerance')
|
|
1813
|
+
duplicates = self.merge_duplicate_elements(groups)
|
|
1814
|
+
return {'merged': 0, 'into': 0, 'duplicates': duplicates,
|
|
1815
|
+
'links': 0}
|
|
1816
|
+
refusal = self._folding(mapping, tolerance)
|
|
1817
|
+
if refusal is not None:
|
|
1818
|
+
raise ValueError(refusal)
|
|
1819
|
+
links = self._collapsed(mapping)
|
|
1820
|
+
self.delete_elements(links)
|
|
1589
1821
|
self.elem_conn = [np.asarray([mapping.get(int(n), int(n))
|
|
1590
1822
|
for n in conn], dtype=np.int64)
|
|
1591
1823
|
for conn in self.elem_conn]
|
|
@@ -1593,10 +1825,10 @@ class Geometry:
|
|
|
1593
1825
|
for name in ('node_id', 'node_def_cs', 'node_disp_cs', 'node_color'):
|
|
1594
1826
|
setattr(self, name, getattr(self, name)[keep])
|
|
1595
1827
|
self.node_xyz = self.node_xyz[keep]
|
|
1596
|
-
duplicates = self.merge_duplicate_elements()
|
|
1828
|
+
duplicates = self.merge_duplicate_elements(groups)
|
|
1597
1829
|
self.validate()
|
|
1598
1830
|
return {'merged': len(mapping), 'into': len(set(mapping.values())),
|
|
1599
|
-
'duplicates': duplicates}
|
|
1831
|
+
'duplicates': duplicates, 'links': len(links)}
|
|
1600
1832
|
|
|
1601
1833
|
def delete_coordinate_systems(self, cs_ids: Ids) -> dict[str, int]:
|
|
1602
1834
|
"""Remove coordinate systems, reassigning any node that used them.
|