visualdynamics 0.1.0a2__tar.gz → 0.1.0a4__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.0a2/src/visualdynamics.egg-info → visualdynamics-0.1.0a4}/PKG-INFO +16 -13
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/README.md +14 -12
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/pyproject.toml +8 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/__init__.py +4 -4
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/author.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/averaging.py +9 -9
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/channel_table.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/compliance.py +6 -6
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/data.py +19 -19
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/detect.py +7 -7
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/fem.py +22 -22
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/filters.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/geometry.py +12 -12
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/kurtosis.py +7 -7
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/merge.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/modal_fit.py +17 -17
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/octave.py +19 -19
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/replication.py +11 -11
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/report.py +28 -28
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/rigid.py +16 -16
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/shapes.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/shocks.py +13 -13
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/sine.py +10 -10
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/tables.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/transform.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/unit_choices.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/wavelet.py +174 -39
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/deform.py +9 -9
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/demo/drone.py +48 -48
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/demo/plate.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/__init__.py +2 -2
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/app_icon.py +6 -6
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/author_panel.py +2 -2
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/averaging_panel.py +8 -8
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/console.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/disclaimer.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/drawer_tab.py +6 -6
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/filter_panel.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/glyphs.py +2 -2
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/icons.py +19 -19
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/main_window.py +86 -84
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/object_tables.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/panes.py +14 -14
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/preferences.py +2 -2
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/project_tree.py +9 -9
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/record_grid.py +11 -11
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/report_editor.py +4 -4
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/rigid_panel.py +6 -6
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/settings_panel.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/shock_panel.py +7 -7
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/stage_drag.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/tables.py +10 -10
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/truncate_panel.py +2 -2
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/wavelet_panel.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/windows.py +2 -2
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/__init__.py +2 -2
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/excel.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/exodus.py +16 -10
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/native.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/rattlesnake.py +8 -4
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/rattlesnake_specification.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/sep005.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/step.py +4 -4
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/stl.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/threemf.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/names.py +2 -2
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/plot/__init__.py +77 -69
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/plot/averaging.py +8 -8
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/plot/bands.py +5 -5
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/plot/bars.py +15 -15
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/plot/filtering.py +6 -6
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/plot/scalogram.py +10 -16
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/plot/shocks.py +2 -2
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/plot/truncation.py +3 -3
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/project.py +13 -13
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/report/__init__.py +41 -52
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/report/page.py +45 -45
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/theme.py +29 -20
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/units.py +79 -4
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/update.py +27 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/__init__.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/animate.py +9 -9
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/banded.py +4 -4
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/geometry.py +22 -22
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/mac_bars.py +11 -11
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/marks.py +18 -18
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/paired.py +2 -2
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/pick.py +5 -5
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/rigid.py +4 -4
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/scalogram.py +9 -15
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/sinespec.py +9 -9
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/waterfall.py +5 -5
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4/src/visualdynamics.egg-info}/PKG-INFO +16 -13
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics.egg-info/requires.txt +1 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/LICENSE +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/setup.cfg +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/__main__.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/compatibility.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/__init__.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/correlate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/entities.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/matches.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/photos.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/srs.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/truncate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/core/validate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/decimate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/demo/__init__.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/editors.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/octave_panel.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/gui/toolbars.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/adf.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/exporters.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/femap.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/nastran.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/photo_files.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/punch.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/report_template.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/sdynpy_data.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/sdynpy_npz.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/sniffing.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/io/unv.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/py.typed +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/report/markdown.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/rotate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics/viz/_quiet.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics.egg-info/SOURCES.txt +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics.egg-info/entry_points.txt +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a4}/src/visualdynamics.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: visualdynamics
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a4
|
|
4
4
|
Summary: Structural Dynamics Visualization: a units-aware structural dynamics analysis toolset
|
|
5
5
|
Author-email: Brandon Zwink <bzwink@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -22,6 +22,7 @@ Requires-Python: >=3.12
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: numpy
|
|
25
|
+
Requires-Dist: certifi
|
|
25
26
|
Requires-Dist: scipy
|
|
26
27
|
Requires-Dist: h5py
|
|
27
28
|
Requires-Dist: netCDF4
|
|
@@ -55,7 +56,7 @@ Dynamic: license-file
|
|
|
55
56
|
|
|
56
57
|
[](https://github.com/visualdynamics/visualdynamics/actions/workflows/ci.yml)
|
|
57
58
|
[](https://github.com/astral-sh/ruff)
|
|
58
|
-
[](LICENSE)
|
|
59
60
|
[](pyproject.toml)
|
|
60
61
|
|
|
61
62
|
Units-aware structural dynamics analysis toolset. Geometry, test data (time
|
|
@@ -84,8 +85,8 @@ latest versions rather than pinned.
|
|
|
84
85
|
|
|
85
86
|
**[Documentation](https://visualdynamics.org/documentation/)** —
|
|
86
87
|
getting started, guides, and a generated API reference, a page of
|
|
87
|
-
[visualdynamics.org](https://visualdynamics.org)
|
|
88
|
-
|
|
88
|
+
[visualdynamics.org](https://visualdynamics.org) rebuilt with every
|
|
89
|
+
release; the [source](docs/README.md) is in the tree and GitHub
|
|
89
90
|
renders it well enough to read. The application itself, and its
|
|
90
91
|
downloads, are at [visualdynamics.org](https://visualdynamics.org).
|
|
91
92
|
To build the site locally:
|
|
@@ -115,7 +116,7 @@ What it does today: import sdynpy/exodus/UNV/Rattlesnake files (declaring
|
|
|
115
116
|
units where the source is silent), write a specification back out as
|
|
116
117
|
the target Rattlesnake loads, view everything as grids, plots, CMIF
|
|
117
118
|
and coherence maps, animate shapes and time data on geometry, mark
|
|
118
|
-
measurement DOFs with
|
|
119
|
+
measurement DOFs with labeled axis-colored arrows, **fit modal models to
|
|
119
120
|
FRFs interactively** (residual CMIF, one mode at a time, MAC beside the
|
|
120
121
|
table), compare shape sets by cross-MAC with phase-aligned overlay
|
|
121
122
|
animation, compute averaged spectra and PSDs from multi-average time
|
|
@@ -146,9 +147,11 @@ editable installs record it too, so a move wants those rewritten and
|
|
|
146
147
|
|
|
147
148
|
## Install
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
The packaged application — one download per platform, nothing else
|
|
151
|
+
to install — is on the [downloads
|
|
152
|
+
page](https://visualdynamics.org/downloads). The library is on PyPI
|
|
153
|
+
as `visualdynamics` (`pip install visualdynamics`, Python 3.12 or
|
|
154
|
+
newer). To work on it, install from a clone:
|
|
152
155
|
|
|
153
156
|
```bash
|
|
154
157
|
pip install -e .
|
|
@@ -177,8 +180,8 @@ application: it runs the checkout's own interpreter, so an edit to the
|
|
|
177
180
|
source is live on the next launch, and it points at that checkout by
|
|
178
181
|
absolute path. Move the repository and run it again.
|
|
179
182
|
|
|
180
|
-
The distribution name and the import
|
|
181
|
-
|
|
183
|
+
The distribution name and the import are the same word. The project
|
|
184
|
+
was called `vibe` until August 2026,
|
|
182
185
|
which could never have been published under that name — `vibe` belongs
|
|
183
186
|
to an unrelated package on PyPI.
|
|
184
187
|
|
|
@@ -201,10 +204,10 @@ satisfy.
|
|
|
201
204
|
|
|
202
205
|
Help that is not code is welcome too — advice, review, measurements, a
|
|
203
206
|
bug report that turns out to be a real one. It is recorded in
|
|
204
|
-
[
|
|
207
|
+
[ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md), which is thanks rather
|
|
205
208
|
than authorship.
|
|
206
209
|
|
|
207
|
-
##
|
|
210
|
+
## License
|
|
208
211
|
|
|
209
212
|
Copyright (c) 2026 Brandon Zwink. Visual Dynamics is free software
|
|
210
213
|
under the [GNU General Public License, version 3 or any later
|
|
@@ -217,5 +220,5 @@ or in what the software tells you about it.
|
|
|
217
220
|
Other terms are available from the copyright holder
|
|
218
221
|
(contact@visualdynamics.org). Contributions are accepted under
|
|
219
222
|
[CLA.md](CLA.md), which is what keeps that possible; see
|
|
220
|
-
[CONTRIBUTING.md](CONTRIBUTING.md). Third-party
|
|
223
|
+
[CONTRIBUTING.md](CONTRIBUTING.md). Third-party licenses are listed in
|
|
221
224
|
[NOTICE.md](NOTICE.md).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/visualdynamics/visualdynamics/actions/workflows/ci.yml)
|
|
4
4
|
[](https://github.com/astral-sh/ruff)
|
|
5
|
-
[](LICENSE)
|
|
6
6
|
[](pyproject.toml)
|
|
7
7
|
|
|
8
8
|
Units-aware structural dynamics analysis toolset. Geometry, test data (time
|
|
@@ -31,8 +31,8 @@ latest versions rather than pinned.
|
|
|
31
31
|
|
|
32
32
|
**[Documentation](https://visualdynamics.org/documentation/)** —
|
|
33
33
|
getting started, guides, and a generated API reference, a page of
|
|
34
|
-
[visualdynamics.org](https://visualdynamics.org)
|
|
35
|
-
|
|
34
|
+
[visualdynamics.org](https://visualdynamics.org) rebuilt with every
|
|
35
|
+
release; the [source](docs/README.md) is in the tree and GitHub
|
|
36
36
|
renders it well enough to read. The application itself, and its
|
|
37
37
|
downloads, are at [visualdynamics.org](https://visualdynamics.org).
|
|
38
38
|
To build the site locally:
|
|
@@ -62,7 +62,7 @@ What it does today: import sdynpy/exodus/UNV/Rattlesnake files (declaring
|
|
|
62
62
|
units where the source is silent), write a specification back out as
|
|
63
63
|
the target Rattlesnake loads, view everything as grids, plots, CMIF
|
|
64
64
|
and coherence maps, animate shapes and time data on geometry, mark
|
|
65
|
-
measurement DOFs with
|
|
65
|
+
measurement DOFs with labeled axis-colored arrows, **fit modal models to
|
|
66
66
|
FRFs interactively** (residual CMIF, one mode at a time, MAC beside the
|
|
67
67
|
table), compare shape sets by cross-MAC with phase-aligned overlay
|
|
68
68
|
animation, compute averaged spectra and PSDs from multi-average time
|
|
@@ -93,9 +93,11 @@ editable installs record it too, so a move wants those rewritten and
|
|
|
93
93
|
|
|
94
94
|
## Install
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
The packaged application — one download per platform, nothing else
|
|
97
|
+
to install — is on the [downloads
|
|
98
|
+
page](https://visualdynamics.org/downloads). The library is on PyPI
|
|
99
|
+
as `visualdynamics` (`pip install visualdynamics`, Python 3.12 or
|
|
100
|
+
newer). To work on it, install from a clone:
|
|
99
101
|
|
|
100
102
|
```bash
|
|
101
103
|
pip install -e .
|
|
@@ -124,8 +126,8 @@ application: it runs the checkout's own interpreter, so an edit to the
|
|
|
124
126
|
source is live on the next launch, and it points at that checkout by
|
|
125
127
|
absolute path. Move the repository and run it again.
|
|
126
128
|
|
|
127
|
-
The distribution name and the import
|
|
128
|
-
|
|
129
|
+
The distribution name and the import are the same word. The project
|
|
130
|
+
was called `vibe` until August 2026,
|
|
129
131
|
which could never have been published under that name — `vibe` belongs
|
|
130
132
|
to an unrelated package on PyPI.
|
|
131
133
|
|
|
@@ -148,10 +150,10 @@ satisfy.
|
|
|
148
150
|
|
|
149
151
|
Help that is not code is welcome too — advice, review, measurements, a
|
|
150
152
|
bug report that turns out to be a real one. It is recorded in
|
|
151
|
-
[
|
|
153
|
+
[ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md), which is thanks rather
|
|
152
154
|
than authorship.
|
|
153
155
|
|
|
154
|
-
##
|
|
156
|
+
## License
|
|
155
157
|
|
|
156
158
|
Copyright (c) 2026 Brandon Zwink. Visual Dynamics is free software
|
|
157
159
|
under the [GNU General Public License, version 3 or any later
|
|
@@ -164,5 +166,5 @@ or in what the software tells you about it.
|
|
|
164
166
|
Other terms are available from the copyright holder
|
|
165
167
|
(contact@visualdynamics.org). Contributions are accepted under
|
|
166
168
|
[CLA.md](CLA.md), which is what keeps that possible; see
|
|
167
|
-
[CONTRIBUTING.md](CONTRIBUTING.md). Third-party
|
|
169
|
+
[CONTRIBUTING.md](CONTRIBUTING.md). Third-party licenses are listed in
|
|
168
170
|
[NOTICE.md](NOTICE.md).
|
|
@@ -38,6 +38,13 @@ classifiers = [
|
|
|
38
38
|
]
|
|
39
39
|
dependencies = [
|
|
40
40
|
"numpy",
|
|
41
|
+
# MPL-2.0, which is file-level copyleft on certifi's own files and
|
|
42
|
+
# nothing more for a program that ships them unmodified; already in
|
|
43
|
+
# every package as netCDF4's dependency. Declared because the update
|
|
44
|
+
# check imports it: the packaged OpenSSL's compiled-in certificate
|
|
45
|
+
# directory exists on nobody's machine, and this is the trust store
|
|
46
|
+
# it falls back to (2026-09-15).
|
|
47
|
+
"certifi",
|
|
41
48
|
# BSD-3-Clause. Runtime, not test-only: the low-pass, the
|
|
42
49
|
# integration, the filter view's response curve and the sine matched
|
|
43
50
|
# filter all call it. It was declared a test dependency for a long
|
|
@@ -85,7 +92,7 @@ visualdynamics-gui = "visualdynamics.gui:main"
|
|
|
85
92
|
[project.optional-dependencies]
|
|
86
93
|
# scipy is also a runtime dependency (see `dependencies`); the suite
|
|
87
94
|
# additionally uses it as an independent cross-check of the spectral
|
|
88
|
-
#
|
|
95
|
+
# math, which is a different job from calling it — where a method is
|
|
89
96
|
# checked against scipy rather than delegated to it, the check is only
|
|
90
97
|
# worth having because the implementation is our own.
|
|
91
98
|
# sdypy-sep005 is the sdypy project's own SEP 005 validator (MIT),
|
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
|
7
7
|
# itself must pick the same one. Left alone in an environment that
|
|
8
8
|
# also has PyQt5/PyQt6, pyqtgraph binds to one of those and every
|
|
9
9
|
# brush and widget visualdynamics hands it is a foreign object — which is how
|
|
10
|
-
# `data.plot()` from a plain script died on a
|
|
10
|
+
# `data.plot()` from a plain script died on a color it could not
|
|
11
11
|
# read. `QT_API` is the same question asked by qtpy, which is how
|
|
12
12
|
# pyvistaqt picks. Both are set *only if unset*: a session that has
|
|
13
13
|
# already chosen (sdynpy sets QT_API=pyqt5 at import) is left alone
|
|
@@ -16,7 +16,7 @@ from typing import Any
|
|
|
16
16
|
_os.environ.setdefault('PYQTGRAPH_QT_LIB', 'PySide6')
|
|
17
17
|
_os.environ.setdefault('QT_API', 'pyside6')
|
|
18
18
|
|
|
19
|
-
# Qt Data Visualization is offered under GPLv3 or a commercial
|
|
19
|
+
# Qt Data Visualization is offered under GPLv3 or a commercial license
|
|
20
20
|
# and nothing else — no LGPL (its own licensing page, read 2026-08-20).
|
|
21
21
|
# Nothing here uses it, but qtpy imports it at startup for a
|
|
22
22
|
# Windows-Qt5 compatibility alias, inside a suppress(ImportError) — so
|
|
@@ -76,7 +76,7 @@ from .units import (
|
|
|
76
76
|
#: alpha, said in the version itself so the dialog, the About text, the
|
|
77
77
|
#: installers and latest.json all say the same thing, and the first
|
|
78
78
|
#: non-alpha release is offered as an upgrade (Brandon, 2026-09-11)
|
|
79
|
-
__version__ = '0.1.
|
|
79
|
+
__version__ = '0.1.0a4'
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
|
|
@@ -125,7 +125,7 @@ def _spawn_gui(paths=(), spawn=None):
|
|
|
125
125
|
# the child is a fresh interpreter, but it inherits this one's
|
|
126
126
|
# environment — including whatever chose a Qt here. Both are
|
|
127
127
|
# overridden, not defaulted: sdynpy sets QT_API=pyqt5 at import,
|
|
128
|
-
# and a child that
|
|
128
|
+
# and a child that honors it crashes mixing bindings.
|
|
129
129
|
environment = dict(_os.environ, PYQTGRAPH_QT_LIB='PySide6',
|
|
130
130
|
QT_API='pyside6')
|
|
131
131
|
launcher = ('import sys, visualdynamics; '
|
|
@@ -87,7 +87,7 @@ class SpecificationDraft:
|
|
|
87
87
|
bands: One entry per channel: `{'warning': [(below, above)
|
|
88
88
|
per segment], 'abort': [...], 'symmetric': bool, 'uniform':
|
|
89
89
|
bool}`, the bands in decibels either side of the target,
|
|
90
|
-
a segment being the span between two
|
|
90
|
+
a segment being the span between two neighboring
|
|
91
91
|
breakpoints. A channel's own constraints: `symmetric`
|
|
92
92
|
holds above at minus below, `uniform` holds every segment
|
|
93
93
|
at one band (Brandon, 2026-09-06: each channel its own
|
|
@@ -154,7 +154,7 @@ class SpecificationDraft:
|
|
|
154
154
|
) -> SpecificationDraft:
|
|
155
155
|
"""A starter at a shape set's modal coordinates `M1 … Mn`, each
|
|
156
156
|
channel's quantity by the transform's unit rule — a unit rigid
|
|
157
|
-
set's rotations per radian, a mass-
|
|
157
|
+
set's rotations per radian, a mass-normalized set's modal
|
|
158
158
|
quantity. A set with no mass unit gives its coordinates no
|
|
159
159
|
unit, and a specification in nothing is refused.
|
|
160
160
|
|
|
@@ -945,7 +945,7 @@ class SpecificationDraft:
|
|
|
945
945
|
|
|
946
946
|
def with_breakpoint(self, frequency: float) -> SpecificationDraft:
|
|
947
947
|
"""The same draft with a breakpoint at `frequency`, its levels
|
|
948
|
-
read off the power law between its
|
|
948
|
+
read off the power law between its neighbors — or the end
|
|
949
949
|
breakpoint's, past the ends.
|
|
950
950
|
|
|
951
951
|
Parameters
|
|
@@ -43,7 +43,7 @@ _ALIASES = {'uniform': 'boxcar', 'rectangle': 'boxcar', 'none': 'boxcar',
|
|
|
43
43
|
'blackman harris': 'blackmanharris'}
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
#: how a frame is
|
|
46
|
+
#: how a frame is leveled before it is windowed. 'none' is the
|
|
47
47
|
#: convention here and in sdynpy — the oracle pins it — while scipy's
|
|
48
48
|
#: welch removes each segment's mean by default; offering the choice
|
|
49
49
|
#: is scipy parity (Brandon, 2026-08-29). 'mean' subtracts the
|
|
@@ -54,7 +54,7 @@ DETRENDS = ('none', 'mean', 'linear')
|
|
|
54
54
|
_DETREND_ALIASES = {'constant': 'mean', 'off': 'none'}
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def
|
|
57
|
+
def normalize_detrend(name: str | None) -> str:
|
|
58
58
|
"""The key for a detrend named any of the ways it might be."""
|
|
59
59
|
if name is None:
|
|
60
60
|
return 'none'
|
|
@@ -67,7 +67,7 @@ def normalise_detrend(name: str | None) -> str:
|
|
|
67
67
|
return key
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
def
|
|
70
|
+
def normalize_window(name: str | None) -> str:
|
|
71
71
|
"""The key for a window named any of the ways a file might name it."""
|
|
72
72
|
if name is None:
|
|
73
73
|
return 'boxcar'
|
|
@@ -114,14 +114,14 @@ def window_shape(name: str | None, length: int,
|
|
|
114
114
|
trade reversed: the tables were verified identical to scipy's to
|
|
115
115
|
machine epsilon at every length that can hold a frame before being
|
|
116
116
|
deleted, and the sdynpy oracle now checks the windows through the
|
|
117
|
-
whole spectral pipeline on every run. The one
|
|
117
|
+
whole spectral pipeline on every run. The one behavioral change is
|
|
118
118
|
a one-sample window, which reads 1.0 where the old taper read its
|
|
119
119
|
own first point — and a one-sample frame cannot exist
|
|
120
120
|
(`Averaging` requires two).
|
|
121
121
|
"""
|
|
122
122
|
from scipy.signal import get_window
|
|
123
123
|
|
|
124
|
-
key =
|
|
124
|
+
key = normalize_window(name)
|
|
125
125
|
if length < 1:
|
|
126
126
|
raise ValueError('a window needs at least one sample')
|
|
127
127
|
if key in WINDOW_PARAMETERS:
|
|
@@ -186,8 +186,8 @@ class Averaging:
|
|
|
186
186
|
frames: int = 1
|
|
187
187
|
start: float = 0.0
|
|
188
188
|
detrend: str = 'none'
|
|
189
|
-
#: the
|
|
190
|
-
#: beta — None for every window that takes none. A
|
|
189
|
+
#: the parameterized windows' own number — tukey's alpha, kaiser's
|
|
190
|
+
#: beta — None for every window that takes none. A parameterized
|
|
191
191
|
#: window given None adopts its documented default, so a stored
|
|
192
192
|
#: Averaging always carries the number it was computed with.
|
|
193
193
|
window_parameter: float | None = None
|
|
@@ -206,9 +206,9 @@ class Averaging:
|
|
|
206
206
|
object.__setattr__(self, 'overlap', float(self.overlap))
|
|
207
207
|
object.__setattr__(self, 'frames', int(self.frames))
|
|
208
208
|
object.__setattr__(self, 'start', float(self.start))
|
|
209
|
-
object.__setattr__(self, 'window',
|
|
209
|
+
object.__setattr__(self, 'window', normalize_window(self.window))
|
|
210
210
|
object.__setattr__(self, 'detrend',
|
|
211
|
-
|
|
211
|
+
normalize_detrend(self.detrend))
|
|
212
212
|
settings = WINDOW_PARAMETERS.get(self.window)
|
|
213
213
|
if settings is None:
|
|
214
214
|
if self.window_parameter is not None:
|
|
@@ -483,7 +483,7 @@ class ChannelTable:
|
|
|
483
483
|
The coordinate as the table has it, '101Z+'.
|
|
484
484
|
new : str
|
|
485
485
|
The coordinate to give it: a node number then a direction,
|
|
486
|
-
|
|
486
|
+
normalized the way every DOF is, and refused when it is not
|
|
487
487
|
one — a table row may lack a node or a direction, but a
|
|
488
488
|
correction typed by a person is whole.
|
|
489
489
|
quantity : str, optional
|
|
@@ -129,7 +129,7 @@ def log_log_area(frequencies: ArrayLike, values: ArrayLike,
|
|
|
129
129
|
# It matters because a specification is no longer always a
|
|
130
130
|
# handful of breakpoints spanning decades. The PSD of a
|
|
131
131
|
# transient target is a Specification with four thousand
|
|
132
|
-
# lines a quarter-hertz apart, and
|
|
132
|
+
# lines a quarter-hertz apart, and neighboring lines two
|
|
133
133
|
# decades apart over a frequency ratio of 1.00025 give an
|
|
134
134
|
# exponent near twenty thousand: a**n overflowed, the
|
|
135
135
|
# subtraction went inf - inf, and every channel's RMS error
|
|
@@ -215,7 +215,7 @@ def outside(lines: ArrayLike, values: ArrayLike,
|
|
|
215
215
|
|
|
216
216
|
At the two ends it cannot be. A bin straddling the edge is covered
|
|
217
217
|
by the specification for part of its width and by nothing for the
|
|
218
|
-
rest, and reading the limit at the bin's
|
|
218
|
+
rest, and reading the limit at the bin's center either invents a
|
|
219
219
|
requirement past the edge or drops a bin that is mostly inside. So
|
|
220
220
|
a cut bin is judged on the part that is covered: the power the
|
|
221
221
|
measurement holds over that stretch, `G` times the covered width,
|
|
@@ -275,7 +275,7 @@ def rms(frequencies: ArrayLike, values: ArrayLike) -> float:
|
|
|
275
275
|
|
|
276
276
|
The widths come from the whole axis before any line is dropped, so
|
|
277
277
|
a line that says nothing contributes nothing rather than having its
|
|
278
|
-
bin quietly widened onto its
|
|
278
|
+
bin quietly widened onto its neighbors. Bridging a gap would be
|
|
279
279
|
assuming what is in it.
|
|
280
280
|
"""
|
|
281
281
|
frequencies = np.asarray(frequencies, dtype=float)
|
|
@@ -417,7 +417,7 @@ def detect_scale_db(specification: Specification, measured: DataArray,
|
|
|
417
417
|
# not a statement about the commanded level — the broken-channel
|
|
418
418
|
# test pins that such a channel cannot answer alone, and it must
|
|
419
419
|
# not veto alone either. 2 dB of slack both ways lets a slightly
|
|
420
|
-
# hot control sit under the commanded level without
|
|
420
|
+
# hot control sit under the commanded level without canceling a
|
|
421
421
|
# real run-up.
|
|
422
422
|
plausible = [value for value in per_channel if value >= -2]
|
|
423
423
|
if plausible and min(plausible) < candidate - 2:
|
|
@@ -546,7 +546,7 @@ def compare(specification: Specification, measured: DataArray,
|
|
|
546
546
|
return {'lines': 0}
|
|
547
547
|
# the bins the spectrum actually has. An octave-band one carries
|
|
548
548
|
# its own — geometric, from a standard — and reading them off the
|
|
549
|
-
#
|
|
549
|
+
# centers would be a hair out at every band and wrong at the ends.
|
|
550
550
|
own = getattr(measured, 'bin_widths', None)
|
|
551
551
|
|
|
552
552
|
# A measured line stands for its whole bin, half a width either
|
|
@@ -684,7 +684,7 @@ def compare_all(specification: Specification, measured: DataArray,
|
|
|
684
684
|
|
|
685
685
|
#: The RMS dB deviation an SRS comparison is shaded past, either side.
|
|
686
686
|
#: The same three decibels the level error either side of a
|
|
687
|
-
#: specification uses, because it is the same
|
|
687
|
+
#: specification uses, because it is the same judgment in the same
|
|
688
688
|
#: unit — how far a spectrum sits from what was asked for, and
|
|
689
689
|
#: over-testing and under-testing are not the same fault.
|
|
690
690
|
SRS_ERROR_DB = 3.0
|
|
@@ -426,7 +426,7 @@ class DataArray:
|
|
|
426
426
|
|
|
427
427
|
A channel is a coordinate *and* a quantity — a drive point
|
|
428
428
|
carries a load cell and an accelerometer at one DOF — and the
|
|
429
|
-
rename is the channel's: a force
|
|
429
|
+
rename is the channel's: a force labeled at the wrong node
|
|
430
430
|
moves without taking the accelerometer at that node with it,
|
|
431
431
|
and the other is changed explicitly if it should be (Brandon,
|
|
432
432
|
2026-09-06). The channel moves wherever a record wears it, as
|
|
@@ -441,7 +441,7 @@ class DataArray:
|
|
|
441
441
|
old : str
|
|
442
442
|
The coordinate as it is, '101Z+'.
|
|
443
443
|
new : str
|
|
444
|
-
The coordinate to give it;
|
|
444
|
+
The coordinate to give it; normalized the way every DOF is
|
|
445
445
|
('101Z' is '101Z+'), and refused when it is not one.
|
|
446
446
|
quantity : str, optional
|
|
447
447
|
Which channel at `old`: 'acceleration', 'force' … as
|
|
@@ -764,7 +764,7 @@ class DataArray:
|
|
|
764
764
|
Returns
|
|
765
765
|
-------
|
|
766
766
|
str
|
|
767
|
-
The record's DOF, plus whatever tells it from its
|
|
767
|
+
The record's DOF, plus whatever tells it from its neighbors.
|
|
768
768
|
"""
|
|
769
769
|
if self.reference_dof is not None:
|
|
770
770
|
return f'{self.response_dof[i]}/{self.reference_dof[i]}'
|
|
@@ -946,7 +946,7 @@ class DataArray:
|
|
|
946
946
|
|
|
947
947
|
def plot_waterfall(self, records: Sequence[int] | None = None,
|
|
948
948
|
**kwargs: Any) -> Any:
|
|
949
|
-
"""The records spread along a depth axis,
|
|
949
|
+
"""The records spread along a depth axis, colored by level —
|
|
950
950
|
the plot bar's 3-D reading, scripted. `screenshot=` renders
|
|
951
951
|
headless to a file; without it a window of the app's own 3-D
|
|
952
952
|
pane opens.
|
|
@@ -1166,7 +1166,7 @@ class TimeHistory(DataArray):
|
|
|
1166
1166
|
for f in range(averaging.frames):
|
|
1167
1167
|
at = first + f * averaging.hop
|
|
1168
1168
|
frame = self.ordinate[i][at:at + n]
|
|
1169
|
-
#
|
|
1169
|
+
# leveled before it is windowed, exactly where
|
|
1170
1170
|
# scipy's welch does it; 'none' is the default and
|
|
1171
1171
|
# the sdynpy convention, which the oracle pins
|
|
1172
1172
|
if averaging.detrend == 'mean':
|
|
@@ -1267,7 +1267,7 @@ class TimeHistory(DataArray):
|
|
|
1267
1267
|
above the content is the reach-for-first case; the filter view
|
|
1268
1268
|
offers high- and band-pass beside it.
|
|
1269
1269
|
|
|
1270
|
-
A
|
|
1270
|
+
A judgment, not a detection — nothing in the record says where
|
|
1271
1271
|
its content stops being signal. A tenth of the rate is where
|
|
1272
1272
|
the integrate/differentiate round trip was measured at ~2% RMS
|
|
1273
1273
|
(core.filters), and it sits below the mounted-resonance range a
|
|
@@ -2122,7 +2122,7 @@ class Frf(DataArray):
|
|
|
2122
2122
|
Parameters
|
|
2123
2123
|
----------
|
|
2124
2124
|
shapes : ShapeSet, optional
|
|
2125
|
-
A modal fit, drawn as the
|
|
2125
|
+
A modal fit, drawn as the synthesized CMIF over the measured one.
|
|
2126
2126
|
**kwargs
|
|
2127
2127
|
Passed through to the plotting layer.
|
|
2128
2128
|
|
|
@@ -2281,10 +2281,10 @@ def density_ratio(signal: Any, floor: Any):
|
|
|
2281
2281
|
class Psd(DataArray):
|
|
2282
2282
|
#: the width of the band each line stands for, when the lines are
|
|
2283
2283
|
#: not evenly spaced. `None` for a narrowband spectrum, where the
|
|
2284
|
-
#: bins are the midpoints between
|
|
2284
|
+
#: bins are the midpoints between neighbors and inferring them is
|
|
2285
2285
|
#: exact. An octave-band spectrum sets it: its bins are geometric,
|
|
2286
|
-
#: its edges are a standard's and not its
|
|
2287
|
-
#: them off the
|
|
2286
|
+
#: its edges are a standard's and not its neighbors', and reading
|
|
2287
|
+
#: them off the centers would be a hair out at every band and wrong
|
|
2288
2288
|
#: at the two ends.
|
|
2289
2289
|
"""Power spectral density: the declared unit is the engineering unit
|
|
2290
2290
|
whose square-per-Hz the values are in (declare 'g' for g^2/Hz).
|
|
@@ -2405,7 +2405,7 @@ class Psd(DataArray):
|
|
|
2405
2405
|
operating deflection shape: the dominant eigenvector of the
|
|
2406
2406
|
cross-spectral matrix per line, each channel's phase relative
|
|
2407
2407
|
to the others. An autospectrum set has no phase, so it shows
|
|
2408
|
-
the envelope instead: two copies deflected ±sqrt(PSD),
|
|
2408
|
+
the envelope instead: two copies deflected ±sqrt(PSD), color
|
|
2409
2409
|
reading dB below the loudest node at any line. Defaults to the
|
|
2410
2410
|
strongest line; `frequency` picks another, `quantity` which
|
|
2411
2411
|
measurement deflects.
|
|
@@ -2483,10 +2483,10 @@ class Psd(DataArray):
|
|
|
2483
2483
|
|
|
2484
2484
|
#: the width of the band each line stands for, when the lines are
|
|
2485
2485
|
#: not evenly spaced. None for a narrowband spectrum, whose bins
|
|
2486
|
-
#: are the midpoints between
|
|
2486
|
+
#: are the midpoints between neighbors and exactly inferable. An
|
|
2487
2487
|
#: octave-band spectrum sets it: its bins are geometric, its edges
|
|
2488
|
-
#: are a standard's rather than its
|
|
2489
|
-
#: off the
|
|
2488
|
+
#: are a standard's rather than its neighbors', and reading them
|
|
2489
|
+
#: off the centers is a hair out at every band and plainly wrong at
|
|
2490
2490
|
#: the two ends.
|
|
2491
2491
|
bandwidth: np.ndarray | None = None
|
|
2492
2492
|
|
|
@@ -2512,7 +2512,7 @@ class Psd(DataArray):
|
|
|
2512
2512
|
mean-square content that falls in it, divided by its own width,
|
|
2513
2513
|
so the area under the spectrum — and therefore the RMS it
|
|
2514
2514
|
carries — is unchanged. Reading the narrowband curve at each
|
|
2515
|
-
band
|
|
2515
|
+
band center would throw away everything between the centers.
|
|
2516
2516
|
|
|
2517
2517
|
The band grid is absolute (see `visualdynamics.core.octave`), so this
|
|
2518
2518
|
needs no specification to be told about: the frequency range
|
|
@@ -2544,7 +2544,7 @@ class Psd(DataArray):
|
|
|
2544
2544
|
'octave bands need a spectrum with frequency lines above '
|
|
2545
2545
|
'zero; this one has ' + str(positive.size))
|
|
2546
2546
|
# the range the bands have to span is the range the *bins*
|
|
2547
|
-
# cover, not the range the
|
|
2547
|
+
# cover, not the range the centers do. A line at 0.5 Hz stands
|
|
2548
2548
|
# for a bin reaching down to 0.25, and a grid that started at
|
|
2549
2549
|
# the line would leave that half outside every band — which is
|
|
2550
2550
|
# a small loss of area and an unnecessary one.
|
|
@@ -2554,10 +2554,10 @@ class Psd(DataArray):
|
|
|
2554
2554
|
if low is None else float(low)
|
|
2555
2555
|
high = float(beyond.max()) if high is None else float(high)
|
|
2556
2556
|
per_octave = PER_OCTAVE if per_octave is None else int(per_octave)
|
|
2557
|
-
|
|
2557
|
+
centers, widths, bounds = bands(low, high, per_octave)
|
|
2558
2558
|
banded = resample(frequencies, self.ordinate, bounds, widths,
|
|
2559
2559
|
source=self.bin_bounds())
|
|
2560
|
-
out = Psd(
|
|
2560
|
+
out = Psd(centers, banded,
|
|
2561
2561
|
response_dof=list(self.response_dof),
|
|
2562
2562
|
reference_dof=(None if self.reference_dof is None
|
|
2563
2563
|
else list(self.reference_dof)),
|
|
@@ -2577,7 +2577,7 @@ class Psd(DataArray):
|
|
|
2577
2577
|
def bin_widths(self) -> np.ndarray:
|
|
2578
2578
|
"""The width of every line's own bin.
|
|
2579
2579
|
|
|
2580
|
-
Its own when it has one, the midpoints between
|
|
2580
|
+
Its own when it has one, the midpoints between neighbors when
|
|
2581
2581
|
it does not — so a caller integrating a spectrum never has to
|
|
2582
2582
|
ask which kind it is holding.
|
|
2583
2583
|
"""
|
|
@@ -6,7 +6,7 @@ environment stopped. Averaging the lot mixes them into one PSD that
|
|
|
6
6
|
describes none of them. This works out which stretch to use.
|
|
7
7
|
|
|
8
8
|
The record is compressed to one number per hop before anything else is
|
|
9
|
-
decided — a level, in dB — and every
|
|
9
|
+
decided — a level, in dB — and every judgment after that is made on
|
|
10
10
|
that short series. That is what keeps it quick on the sets where it
|
|
11
11
|
matters: three hundred channels of a ten-minute run cost one pass, and
|
|
12
12
|
the rest is arithmetic on a few thousand points.
|
|
@@ -15,11 +15,11 @@ Two things make the compression trustworthy rather than merely small:
|
|
|
15
15
|
|
|
16
16
|
- the consensus is the median across channels, not the mean, so a
|
|
17
17
|
channel that drops out or spikes or sits forty dB above its
|
|
18
|
-
|
|
18
|
+
neighbors is one vote and not the answer;
|
|
19
19
|
- the mean is removed within each hop, so a DC offset or a slow drift
|
|
20
20
|
is not read as level.
|
|
21
21
|
|
|
22
|
-
Nothing
|
|
22
|
+
Nothing normalizes the levels against each other or centers the series.
|
|
23
23
|
Both were tried: the median across channels already does that work, and
|
|
24
24
|
the threshold sweep walks every bin edge from the top, so scaling a
|
|
25
25
|
whole record by a million moves the bins together and lands on the same
|
|
@@ -73,7 +73,7 @@ TOLERANCE = 2.0
|
|
|
73
73
|
#: still sets it.
|
|
74
74
|
TOP_QUANTILE = 95.0
|
|
75
75
|
|
|
76
|
-
#: how many times a run is re-
|
|
76
|
+
#: how many times a run is re-centered on itself and grown again. Three
|
|
77
77
|
#: is enough for anything seen: the first pass from a fragment's own
|
|
78
78
|
#: median, the second from the median of what that reached, and the
|
|
79
79
|
#: third to confirm it has stopped moving.
|
|
@@ -176,7 +176,7 @@ def hop_levels(ordinate: ArrayLike, hop: int,
|
|
|
176
176
|
block = ordinate[:, first * hop:last * hop].reshape(
|
|
177
177
|
channels, last - first, hop)
|
|
178
178
|
mean = block.mean(axis=2)
|
|
179
|
-
# einsum contracts without
|
|
179
|
+
# einsum contracts without materializing the squares
|
|
180
180
|
power[:, first:last] = (
|
|
181
181
|
np.einsum('cks,cks->ck', block, block) / hop - mean * mean)
|
|
182
182
|
|
|
@@ -218,7 +218,7 @@ def _grow(level, first, last, sigma, tolerance=TOLERANCE):
|
|
|
218
218
|
Repeated until it settles, because the first median is the median of
|
|
219
219
|
a fragment and a fragment of a wandering plateau sits wherever the
|
|
220
220
|
threshold happened to cut it. Grown once from a median near the top
|
|
221
|
-
of the wander it reaches only the upper half; re-
|
|
221
|
+
of the wander it reaches only the upper half; re-centered on what it
|
|
222
222
|
then holds, it reaches the rest.
|
|
223
223
|
"""
|
|
224
224
|
for _ in range(GROW_PASSES):
|
|
@@ -241,7 +241,7 @@ def plateau(level: np.ndarray, sigma: float, want_hops: int,
|
|
|
241
241
|
tolerance: float = TOLERANCE) -> tuple[int, int]:
|
|
242
242
|
"""The highest level the record holds for `want_hops` hops.
|
|
243
243
|
|
|
244
|
-
Level alone is the wrong thing to
|
|
244
|
+
Level alone is the wrong thing to maximize. A ten-second overload in
|
|
245
245
|
the middle of a two-minute test is the highest level in the record
|
|
246
246
|
and the last thing anyone wants to average; sought that way it wins,
|
|
247
247
|
and returns a fifth of the frames the test itself would have. So
|