visualdynamics 0.1.0a2__tar.gz → 0.1.0a3__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.0a3}/PKG-INFO +2 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/pyproject.toml +7 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/__init__.py +1 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/wavelet.py +134 -22
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/main_window.py +8 -6
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/plot/__init__.py +7 -4
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/report/__init__.py +7 -13
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/update.py +27 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3/src/visualdynamics.egg-info}/PKG-INFO +2 -1
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics.egg-info/requires.txt +1 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/LICENSE +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/README.md +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/setup.cfg +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/__main__.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/compatibility.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/__init__.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/author.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/averaging.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/channel_table.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/compliance.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/correlate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/data.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/detect.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/entities.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/fem.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/filters.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/geometry.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/kurtosis.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/matches.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/merge.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/modal_fit.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/octave.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/photos.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/replication.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/report.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/rigid.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/shapes.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/shocks.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/sine.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/srs.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/tables.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/transform.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/truncate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/unit_choices.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/core/validate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/decimate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/deform.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/demo/__init__.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/demo/drone.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/demo/plate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/__init__.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/app_icon.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/author_panel.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/averaging_panel.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/console.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/disclaimer.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/drawer_tab.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/editors.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/filter_panel.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/glyphs.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/icons.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/object_tables.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/octave_panel.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/panes.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/preferences.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/project_tree.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/record_grid.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/report_editor.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/rigid_panel.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/settings_panel.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/shock_panel.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/stage_drag.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/tables.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/toolbars.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/truncate_panel.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/wavelet_panel.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/windows.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/__init__.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/adf.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/excel.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/exodus.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/exporters.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/femap.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/nastran.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/native.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/photo_files.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/punch.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/rattlesnake.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/report_template.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/sdynpy_data.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/sdynpy_npz.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/sep005.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/sniffing.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/step.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/stl.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/threemf.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/unv.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/names.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/plot/averaging.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/plot/bands.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/plot/bars.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/plot/filtering.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/plot/scalogram.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/plot/shocks.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/plot/truncation.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/project.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/py.typed +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/report/markdown.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/report/page.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/rotate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/theme.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/units.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/__init__.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/_quiet.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/animate.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/banded.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/geometry.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/mac_bars.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/marks.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/paired.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/pick.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/rigid.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/scalogram.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/sinespec.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/viz/waterfall.py +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics.egg-info/SOURCES.txt +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics.egg-info/entry_points.txt +0 -0
- {visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/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.0a3
|
|
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
|
|
@@ -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
|
|
@@ -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.0a3'
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
|
|
@@ -112,6 +112,25 @@ OMEGA0 = 6.0
|
|
|
112
112
|
#: looks continuous without computing scales nobody can see apart.
|
|
113
113
|
PER_OCTAVE = 12
|
|
114
114
|
|
|
115
|
+
#: bytes one band of the transform may hold in each of its working
|
|
116
|
+
#: arrays. The one-shot form keeps three arrays of (scales, padded
|
|
117
|
+
#: length) complex values alive at once — the daughters, their product
|
|
118
|
+
#: with the spectrum, and the inverse — and on a five-minute record at
|
|
119
|
+
#: 16 kHz that is 121 rows of 4.9M samples, 27 GB, which is how the
|
|
120
|
+
#: desktop app died (Brandon, 2026-09-15). Cut into bands of rows that
|
|
121
|
+
#: fit this budget, the peak is three of these plus the answer,
|
|
122
|
+
#: whatever the record's length; the rows are independent, so the
|
|
123
|
+
#: cut costs nothing but the loop. 256 MB is 3.4M samples a row on
|
|
124
|
+
#: complex128: a minute at 16 kHz is still one band.
|
|
125
|
+
BAND_BYTES = 256 * 2**20
|
|
126
|
+
|
|
127
|
+
#: columns a picture-sized reading holds time to. A screen is a few
|
|
128
|
+
#: thousand pixels across and a surface of this width is half a million
|
|
129
|
+
#: vertices at the default grid — the 3-D stage's own budget is ten
|
|
130
|
+
#: million — so nothing a reader could see is lost, and a five-minute
|
|
131
|
+
#: record arrives as 60 KB rather than 4 GB.
|
|
132
|
+
COLUMNS = 4096
|
|
133
|
+
|
|
115
134
|
|
|
116
135
|
def fourier_factor(omega0: float = OMEGA0) -> float:
|
|
117
136
|
"""Seconds of Fourier period per second of wavelet scale.
|
|
@@ -199,6 +218,14 @@ def scalogram(values: ArrayLike, sample_rate: float,
|
|
|
199
218
|
the record's FFT and one inverse, rather than a convolution per
|
|
200
219
|
scale in time.
|
|
201
220
|
|
|
221
|
+
Computed a band of scales at a time (`BAND_BYTES`), because every
|
|
222
|
+
scale at once is three arrays of (scales, padded length) complex
|
|
223
|
+
values and a long record turns that into tens of gigabytes. The
|
|
224
|
+
answer here is still the whole thing — one complex row per
|
|
225
|
+
frequency, as long as the record — which for a long record is
|
|
226
|
+
itself gigabytes; a picture wants `scalogram_peaks`, which reduces
|
|
227
|
+
each band as it lands and never holds the whole.
|
|
228
|
+
|
|
202
229
|
Normalised so that **the magnitude is the record's own amplitude**:
|
|
203
230
|
a 2 g tone reads 2 wherever it sits on the frequency axis, so the
|
|
204
231
|
colour bar carries the record's units and a ridge's height means
|
|
@@ -238,8 +265,69 @@ def scalogram(values: ArrayLike, sample_rate: float,
|
|
|
238
265
|
nothing to keep and a ridge's phase is what an instantaneous
|
|
239
266
|
frequency would be read from.
|
|
240
267
|
"""
|
|
241
|
-
|
|
268
|
+
record, wanted, samples = _prepared(values, sample_rate, frequencies)
|
|
269
|
+
out = np.empty((wanted.size, samples), dtype=complex)
|
|
270
|
+
for rows, band in _bands(record, sample_rate, wanted, omega0):
|
|
271
|
+
out[rows] = band
|
|
272
|
+
return out
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def scalogram_peaks(values: ArrayLike, sample_rate: float,
|
|
276
|
+
frequencies: ArrayLike, omega0: float = OMEGA0, *,
|
|
277
|
+
columns: int = COLUMNS) -> tuple[np.ndarray, np.ndarray]:
|
|
278
|
+
"""The scalogram's magnitude, held to at most `columns` of time.
|
|
279
|
+
|
|
280
|
+
What a picture is drawn from: the view, the scripting plot and the
|
|
281
|
+
report all want the magnitude at about a screen's worth of columns,
|
|
282
|
+
and a million-sample record is not that. Time is cut into equal
|
|
283
|
+
slices and each column is the **largest** magnitude in its slice —
|
|
284
|
+
peak-hold, the same reading `decimate.peak_decimate` gives a curve
|
|
285
|
+
— because a scalogram's story is its ridges and a stride would land
|
|
286
|
+
between the very samples a transient's energy lives in, while a
|
|
287
|
+
mean would halve it. Under the budget nothing is held: the reading
|
|
288
|
+
is the magnitude itself, exactly.
|
|
289
|
+
|
|
290
|
+
Computed band by band and reduced as each band lands, so the whole
|
|
291
|
+
transform is never in memory at once: this is what a long record
|
|
292
|
+
goes through, and it costs `BAND_BYTES` a working array whatever
|
|
293
|
+
the length.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
values, sample_rate, frequencies, omega0
|
|
298
|
+
As for `scalogram`.
|
|
299
|
+
columns : int
|
|
300
|
+
The most columns to hand back.
|
|
242
301
|
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
times, magnitude
|
|
305
|
+
`times` is each column's clock in seconds from the record's
|
|
306
|
+
start — the centre of its slice, so the first and last columns
|
|
307
|
+
sit half a slice inside the record's ends — and `magnitude` is
|
|
308
|
+
``(len(frequencies), len(times))``, real.
|
|
309
|
+
"""
|
|
310
|
+
record, wanted, samples = _prepared(values, sample_rate, frequencies)
|
|
311
|
+
step = max(1, -(-samples // int(columns)))
|
|
312
|
+
kept = -(-samples // step)
|
|
313
|
+
held = np.empty((wanted.size, kept), dtype=float)
|
|
314
|
+
# the ragged end is a slice like any other, kept rather than dropped:
|
|
315
|
+
# a burst in the last few samples is still a burst. Padding with
|
|
316
|
+
# zero cannot win a maximum of magnitudes.
|
|
317
|
+
pad = kept * step - samples
|
|
318
|
+
for rows, band in _bands(record, sample_rate, wanted, omega0):
|
|
319
|
+
magnitude = np.abs(band)
|
|
320
|
+
if pad:
|
|
321
|
+
magnitude = np.pad(magnitude, ((0, 0), (0, pad)))
|
|
322
|
+
held[rows] = magnitude.reshape(len(magnitude), kept, step).max(axis=2)
|
|
323
|
+
edges = np.arange(kept + 1) * step
|
|
324
|
+
edges[-1] = samples
|
|
325
|
+
times = (edges[:-1] + edges[1:] - 1) / 2.0 / float(sample_rate)
|
|
326
|
+
return times, held
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _prepared(values, sample_rate, frequencies):
|
|
330
|
+
"""The record and the frequencies, checked once for both readings."""
|
|
243
331
|
record = np.asarray(values)
|
|
244
332
|
if np.iscomplexobj(record):
|
|
245
333
|
raise ValueError('a scalogram is of a real record; this one is '
|
|
@@ -257,7 +345,18 @@ def scalogram(values: ArrayLike, sample_rate: float,
|
|
|
257
345
|
f'frequencies must lie above zero and below Nyquist '
|
|
258
346
|
f'({nyquist:g} Hz); asked for '
|
|
259
347
|
f'{wanted.min():g} to {wanted.max():g} Hz')
|
|
348
|
+
return record, wanted, samples
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _bands(record, sample_rate, wanted, omega0):
|
|
352
|
+
"""The transform, a band of rows at a time: yields (row slice, coefficients).
|
|
353
|
+
|
|
354
|
+
Each band's coefficients are already cut back to the record's
|
|
355
|
+
length; the caller stacks them or reduces them as it likes.
|
|
356
|
+
"""
|
|
357
|
+
from scipy.fft import fft, ifft, next_fast_len
|
|
260
358
|
|
|
359
|
+
samples = record.size
|
|
261
360
|
step = 1.0 / float(sample_rate)
|
|
262
361
|
scales = scale_for(wanted, omega0)
|
|
263
362
|
|
|
@@ -287,27 +386,40 @@ def scalogram(values: ArrayLike, sample_rate: float,
|
|
|
287
386
|
# angular frequencies of the FFT bins, negative above Nyquist — the
|
|
288
387
|
# Morlet is analytic, so the negative half is what gets discarded
|
|
289
388
|
omega = 2.0 * np.pi * np.fft.fftfreq(length, step)
|
|
290
|
-
|
|
291
|
-
#
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
389
|
+
# the analytic half, taken once: the Gaussian below is zero on the
|
|
390
|
+
# negative frequencies, so they need not be raised to it at all
|
|
391
|
+
positive = omega > 0.0
|
|
392
|
+
omega_pos = omega[positive]
|
|
393
|
+
|
|
394
|
+
rows_a_band = max(1, BAND_BYTES // (length * 16))
|
|
395
|
+
for first in range(0, scales.size, rows_a_band):
|
|
396
|
+
rows = slice(first, min(first + rows_a_band, scales.size))
|
|
397
|
+
# (band, samples), one row per frequency
|
|
398
|
+
scaled = scales[rows, None] * omega_pos[None, :]
|
|
399
|
+
# The Gaussian, analytic (the negative half of the spectrum is
|
|
400
|
+
# what makes a wavelet complex), times two. The two is the whole
|
|
401
|
+
# normalisation: a real tone of amplitude A puts A/2 in each
|
|
402
|
+
# half of its spectrum, the analytic wavelet keeps one of them,
|
|
403
|
+
# and the peak of the Morlet's own transform is 1 where it is
|
|
404
|
+
# tuned — so twice that reads back A. No sqrt(scale) anywhere,
|
|
405
|
+
# which is exactly the difference from the unit-energy
|
|
406
|
+
# convention.
|
|
407
|
+
product = np.zeros((rows.stop - rows.start, length), dtype=complex)
|
|
408
|
+
product[:, positive] = (
|
|
409
|
+
2.0 * np.exp(-0.5 * (scaled - omega0) ** 2) * spectrum[positive])
|
|
410
|
+
del scaled
|
|
411
|
+
# workers=-1: the inverse is one transform per scale along axis
|
|
412
|
+
# 1 and they do not depend on each other, so this is the one
|
|
413
|
+
# place a thread each is free. Measured 2.7x on a 512 000-sample
|
|
414
|
+
# record at 97 scales, and bit-identical — scipy.fft takes the
|
|
415
|
+
# argument, numpy.fft has nowhere to put it, which is most of
|
|
416
|
+
# why the transform is computed through scipy at all.
|
|
417
|
+
# `overwrite_x`: the product is scaffolding, and letting the
|
|
418
|
+
# inverse work in place is one array fewer a band.
|
|
419
|
+
transformed = ifft(product, axis=1, workers=-1, overwrite_x=True)
|
|
420
|
+
del product
|
|
421
|
+
# back to the record: the pad was scaffolding, not signal
|
|
422
|
+
yield rows, transformed[:, :samples]
|
|
311
423
|
|
|
312
424
|
|
|
313
425
|
def ridge(coefficients: ArrayLike, frequencies: ArrayLike) -> np.ndarray:
|
|
@@ -11733,9 +11733,13 @@ class MainWindow(QMainWindow):
|
|
|
11733
11733
|
frequencies = frequencies[frequencies < rate / 2.0]
|
|
11734
11734
|
if frequencies.size < 2:
|
|
11735
11735
|
return 'no frequencies this record can carry in that range'
|
|
11736
|
-
|
|
11736
|
+
# the picture-sized reading, never the whole transform: a long
|
|
11737
|
+
# record's coefficients are gigabytes and its surface was what
|
|
11738
|
+
# crashed the app (Brandon, 2026-09-15). Each column is its
|
|
11739
|
+
# slice's peak, so a transient's ridge is not strided past.
|
|
11740
|
+
clock, magnitude = wavelet_maths.scalogram_peaks(
|
|
11737
11741
|
values, rate, frequencies, settings['omega0'])
|
|
11738
|
-
|
|
11742
|
+
clock = clock + float(history.abscissa[0])
|
|
11739
11743
|
|
|
11740
11744
|
colors = resolve_theme(self.theme_name)
|
|
11741
11745
|
us = self.unit_system
|
|
@@ -11758,8 +11762,7 @@ class MainWindow(QMainWindow):
|
|
|
11758
11762
|
plotter.set_background(colors['scene_background'],
|
|
11759
11763
|
top=colors['scene_background_top'])
|
|
11760
11764
|
add_scalogram(
|
|
11761
|
-
plotter, magnitude,
|
|
11762
|
-
frequencies, theme=self.theme_name,
|
|
11765
|
+
plotter, magnitude, clock, frequencies, theme=self.theme_name,
|
|
11763
11766
|
omega0=settings['omega0'],
|
|
11764
11767
|
time_label=f'time [{us.label_html("time")}]',
|
|
11765
11768
|
level_label=f'{history.ordinate_dim[channel]}'
|
|
@@ -11774,8 +11777,7 @@ class MainWindow(QMainWindow):
|
|
|
11774
11777
|
self.data_pane.graphics.clear()
|
|
11775
11778
|
plot = self.data_pane.graphics.addPlot(row=0, col=0)
|
|
11776
11779
|
scalogram_image(
|
|
11777
|
-
plot, magnitude,
|
|
11778
|
-
colors, label=history.ordinate_dim[channel],
|
|
11780
|
+
plot, magnitude, clock, frequencies, colors, label=history.ordinate_dim[channel],
|
|
11779
11781
|
units=units, omega0=settings['omega0'],
|
|
11780
11782
|
time_label=f'time [{us.label_html("time")}]')
|
|
11781
11783
|
|
|
@@ -2169,15 +2169,18 @@ def plot_scalogram(history: Any, channel: int = 0, *,
|
|
|
2169
2169
|
raise ValueError('no frequencies this record can carry in that '
|
|
2170
2170
|
f'range; it reaches {rate / 2.0:g} Hz')
|
|
2171
2171
|
width = wavelet_maths.OMEGA0 if omega0 is None else omega0
|
|
2172
|
-
|
|
2173
|
-
|
|
2172
|
+
# held to a picture's width, each column its slice's peak — the
|
|
2173
|
+
# app's own reading, and the one a long record survives
|
|
2174
|
+
clock, magnitude = wavelet_maths.scalogram_peaks(
|
|
2175
|
+
values, rate, frequencies, width)
|
|
2176
|
+
clock = clock + float(history.abscissa[0])
|
|
2174
2177
|
colors = resolve_theme(theme)
|
|
2175
2178
|
|
|
2176
2179
|
def build(widget: Any) -> Any:
|
|
2177
2180
|
widget.clear()
|
|
2178
2181
|
scalogram_image(
|
|
2179
|
-
widget.addPlot(row=0, col=0), magnitude,
|
|
2180
|
-
|
|
2182
|
+
widget.addPlot(row=0, col=0), magnitude, clock, frequencies,
|
|
2183
|
+
colors,
|
|
2181
2184
|
label=history.ordinate_dim[channel],
|
|
2182
2185
|
units=history.ordinate_unit[channel] or '', omega0=width)
|
|
2183
2186
|
|
|
@@ -862,21 +862,15 @@ def _scalogram_figure(block, source, us, caption):
|
|
|
862
862
|
frequencies = frequencies[frequencies < rate / 2.0]
|
|
863
863
|
if frequencies.size < 2:
|
|
864
864
|
return None
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
#
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
columns = magnitude.shape[1]
|
|
865
|
+
# peak-held down to the columns a figure can carry, by the reading
|
|
866
|
+
# the app's view draws from: the ridges are the story, and a stride
|
|
867
|
+
# would land between the very samples a transient's energy lives in
|
|
868
|
+
times, magnitude = wavelet.scalogram_peaks(
|
|
869
|
+
values, rate, frequencies, columns=SCALOGRAM_COLUMNS)
|
|
871
870
|
note = ''
|
|
872
|
-
if
|
|
873
|
-
step = -(-columns // SCALOGRAM_COLUMNS)
|
|
874
|
-
pad = (-columns) % step
|
|
875
|
-
held = np.pad(magnitude, ((0, 0), (0, pad)), constant_values=0.0)
|
|
876
|
-
magnitude = held.reshape(magnitude.shape[0], -1, step).max(axis=2)
|
|
871
|
+
if magnitude.shape[1] < len(values):
|
|
877
872
|
note = (f'time thinned to {magnitude.shape[1]} columns of '
|
|
878
|
-
f'{
|
|
879
|
-
times = np.linspace(0.0, duration, magnitude.shape[1])
|
|
873
|
+
f'{len(values)}, keeping each bin\'s peak')
|
|
880
874
|
|
|
881
875
|
sx, sy, _sz = STAGE
|
|
882
876
|
z1 = float(magnitude.max()) or 1.0
|
|
@@ -22,6 +22,7 @@ holding page's copy). Its shape:
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
import json
|
|
25
|
+
import ssl
|
|
25
26
|
from typing import Any
|
|
26
27
|
from urllib.request import Request, urlopen
|
|
27
28
|
|
|
@@ -68,6 +69,30 @@ def newer(available: str, running: str = __version__) -> bool:
|
|
|
68
69
|
return parse_version(available) > parse_version(running)
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
def trust_store() -> ssl.SSLContext:
|
|
73
|
+
"""The TLS context the check verifies visualdynamics.org with.
|
|
74
|
+
|
|
75
|
+
A packaged build carries its own OpenSSL, and the one the wheels
|
|
76
|
+
bring was built elsewhere: its compiled-in certificate directory
|
|
77
|
+
is a folder in the builder's own home (read off the library with
|
|
78
|
+
`strings`, 2026-09-15) and exists on nobody else's machine, so the
|
|
79
|
+
default context trusts nothing and every HTTPS request fails —
|
|
80
|
+
which the check reported as "could not reach visualdynamics.org"
|
|
81
|
+
on a Mac that was online (Brandon, 2026-09-15). When the default store is empty, the
|
|
82
|
+
Mozilla bundle certifi ships is loaded instead; it is already in
|
|
83
|
+
every package as netCDF4's dependency, and is what the Python
|
|
84
|
+
ecosystem uses for exactly this.
|
|
85
|
+
"""
|
|
86
|
+
context = ssl.create_default_context()
|
|
87
|
+
if not context.get_ca_certs():
|
|
88
|
+
try:
|
|
89
|
+
import certifi
|
|
90
|
+
context.load_verify_locations(certifi.where())
|
|
91
|
+
except Exception: # noqa: BLE001, S110 — no bundle either: the fetch fails as before
|
|
92
|
+
pass
|
|
93
|
+
return context
|
|
94
|
+
|
|
95
|
+
|
|
71
96
|
def fetch(url: str = MANIFEST, timeout: float = TIMEOUT) -> dict[str, Any] | None:
|
|
72
97
|
"""The manifest as published, or None when it cannot be had.
|
|
73
98
|
|
|
@@ -79,7 +104,8 @@ def fetch(url: str = MANIFEST, timeout: float = TIMEOUT) -> dict[str, Any] | Non
|
|
|
79
104
|
try:
|
|
80
105
|
request = Request(url, headers={
|
|
81
106
|
'User-Agent': f'VisualDynamics/{__version__}'})
|
|
82
|
-
with urlopen(request, timeout=timeout
|
|
107
|
+
with urlopen(request, timeout=timeout,
|
|
108
|
+
context=trust_store()) as response:
|
|
83
109
|
manifest = json.loads(response.read(64_000).decode('utf-8'))
|
|
84
110
|
except Exception: # noqa: BLE001 — see the docstring: every
|
|
85
111
|
return None # failure here means 'no manifest', never a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: visualdynamics
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a3
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/icons/drawer-tab-shape.svg
RENAMED
|
File without changes
|
{visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/gui/icons/drawer-tab.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics/io/rattlesnake_specification.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{visualdynamics-0.1.0a2 → visualdynamics-0.1.0a3}/src/visualdynamics.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|