visualdynamics 0.1.0a4__tar.gz → 0.1.0a5__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.0a4/src/visualdynamics.egg-info → visualdynamics-0.1.0a5}/PKG-INFO +2 -2
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/README.md +1 -1
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/pyproject.toml +4 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/__init__.py +2 -2
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/__main__.py +1 -1
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/author.py +25 -9
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/averaging.py +26 -10
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/compliance.py +32 -10
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/data.py +230 -27
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/report.py +168 -42
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/averaging_panel.py +12 -5
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/icons.py +7 -1
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/main_window.py +148 -24
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/report_editor.py +86 -4
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/shock_panel.py +6 -2
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/stage_drag.py +17 -11
- visualdynamics-0.1.0a5/src/visualdynamics/gui/stream_window.py +269 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/truncate_panel.py +25 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/__init__.py +7 -0
- visualdynamics-0.1.0a5/src/visualdynamics/io/matlab.py +386 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/native.py +76 -52
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/rattlesnake.py +527 -86
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/plot/__init__.py +46 -12
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/plot/averaging.py +11 -5
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/plot/bands.py +16 -6
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/plot/shocks.py +12 -6
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/project.py +38 -12
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/report/__init__.py +63 -21
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/report/page.py +19 -5
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/banded.py +37 -5
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/marks.py +19 -11
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/waterfall.py +18 -4
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5/src/visualdynamics.egg-info}/PKG-INFO +2 -2
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics.egg-info/SOURCES.txt +2 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics.egg-info/entry_points.txt +1 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/LICENSE +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/setup.cfg +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/compatibility.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/__init__.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/channel_table.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/correlate.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/detect.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/entities.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/fem.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/filters.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/geometry.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/kurtosis.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/matches.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/merge.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/modal_fit.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/octave.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/photos.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/replication.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/rigid.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/shapes.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/shocks.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/sine.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/srs.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/tables.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/transform.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/truncate.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/unit_choices.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/validate.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/core/wavelet.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/decimate.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/deform.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/demo/__init__.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/demo/drone.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/demo/plate.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/__init__.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/app_icon.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/author_panel.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/console.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/disclaimer.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/drawer_tab.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/editors.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/filter_panel.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/glyphs.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/icons/drawer-tab-shape.svg +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/icons/drawer-tab.svg +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/object_tables.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/octave_panel.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/panes.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/preferences.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/project_tree.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/record_grid.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/rigid_panel.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/settings_panel.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/tables.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/toolbars.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/wavelet_panel.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/gui/windows.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/adf.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/excel.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/exodus.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/exporters.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/femap.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/nastran.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/photo_files.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/punch.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/rattlesnake_specification.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/report_template.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/sdynpy_data.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/sdynpy_npz.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/sdynpy_shapes.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/sep005.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/sniffing.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/step.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/stl.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/threemf.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/io/unv.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/names.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/plot/bars.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/plot/filtering.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/plot/scalogram.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/plot/truncation.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/py.typed +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/report/markdown.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/rotate.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/theme.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/units.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/update.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/__init__.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/_quiet.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/animate.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/geometry.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/mac_bars.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/paired.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/pick.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/rigid.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/scalogram.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics/viz/sinespec.py +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics.egg-info/dependency_links.txt +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/src/visualdynamics.egg-info/requires.txt +0 -0
- {visualdynamics-0.1.0a4 → visualdynamics-0.1.0a5}/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.0a5
|
|
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
|
|
@@ -155,7 +155,7 @@ newer). To work on it, install from a clone:
|
|
|
155
155
|
|
|
156
156
|
```bash
|
|
157
157
|
pip install -e .
|
|
158
|
-
visualdynamics
|
|
158
|
+
visualdynamics # the desktop app
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
```python
|
|
@@ -87,6 +87,10 @@ Source = "https://github.com/visualdynamics/visualdynamics"
|
|
|
87
87
|
Issues = "https://github.com/visualdynamics/visualdynamics/issues"
|
|
88
88
|
|
|
89
89
|
[project.scripts]
|
|
90
|
+
# one window is the whole program, so the package's own name launches
|
|
91
|
+
# it (Brandon, 2026-09-18); the longer spelling stays for anything
|
|
92
|
+
# that learned it from the first alphas
|
|
93
|
+
visualdynamics = "visualdynamics.gui:main"
|
|
90
94
|
visualdynamics-gui = "visualdynamics.gui:main"
|
|
91
95
|
|
|
92
96
|
[project.optional-dependencies]
|
|
@@ -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.0a5'
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
|
|
@@ -103,7 +103,7 @@ def launch_gui(*paths: str, in_process: bool | None = None) -> Any:
|
|
|
103
103
|
if in_process:
|
|
104
104
|
from .gui import main
|
|
105
105
|
|
|
106
|
-
# `main` hands back Qt's exit status, which `visualdynamics
|
|
106
|
+
# `main` hands back Qt's exit status, which the `visualdynamics` command wants as
|
|
107
107
|
# its own and nothing else does — it is always 0, since nothing
|
|
108
108
|
# here ever calls exit() with anything else. Swallowed, so
|
|
109
109
|
# closing the window from a prompt does not answer with a bare
|
|
@@ -67,6 +67,13 @@ STARTER_LEVEL = 1.0
|
|
|
67
67
|
CONSTANT_TOLERANCE = 1e-6
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
#: the grid a dragged or stepped limit lands on, in decibels. A
|
|
71
|
+
#: whole decibel (Brandon, 2026-09-06: "never 1.5") was too coarse
|
|
72
|
+
#: to place a limit where it was wanted (Brandon, 2026-09-18); the
|
|
73
|
+
#: plot's handles snap to the same grid (`plot.bands.STEP`).
|
|
74
|
+
LIMIT_STEP_DB = 0.25
|
|
75
|
+
|
|
76
|
+
|
|
70
77
|
@dataclass
|
|
71
78
|
class SpecificationDraft:
|
|
72
79
|
"""A specification being written, at any channels.
|
|
@@ -128,7 +135,15 @@ class SpecificationDraft:
|
|
|
128
135
|
def __post_init__(self) -> None:
|
|
129
136
|
from .validate import dofs as _dofs
|
|
130
137
|
|
|
131
|
-
|
|
138
|
+
# what the object accepts, the sheet accepts: a virtual point's
|
|
139
|
+
# rows come in numbered '1', '2', '3' with no direction, since
|
|
140
|
+
# the controller's file names them nowhere, and the sheet
|
|
141
|
+
# refused to open on them (Brandon, 2026-09-18). Only an empty
|
|
142
|
+
# name is refused — a channel has to be called something
|
|
143
|
+
self.channels = _dofs(list(self.channels), 'channel',
|
|
144
|
+
allow_unknown=True)
|
|
145
|
+
if any(not name for name in self.channels):
|
|
146
|
+
raise ValueError('every channel needs a name')
|
|
132
147
|
self.dims = [str(d) for d in self.dims]
|
|
133
148
|
self.frequencies = [float(f) for f in self.frequencies]
|
|
134
149
|
self.levels = [[float(v) for v in row] for row in self.levels]
|
|
@@ -773,12 +788,13 @@ class SpecificationDraft:
|
|
|
773
788
|
|
|
774
789
|
def shifted_band(self, kind: str, side: str, segments: Any,
|
|
775
790
|
decibels: float, channels: Any = None,
|
|
776
|
-
step: float =
|
|
791
|
+
step: float = LIMIT_STEP_DB) -> SpecificationDraft:
|
|
777
792
|
"""The same draft with one edge of a band moved — the drag on
|
|
778
793
|
the plot, landed. A symmetric channel moves the other edge to
|
|
779
794
|
match; a uniform channel moves every segment. The edge lands
|
|
780
|
-
on a multiple of `step` (
|
|
781
|
-
|
|
795
|
+
on a multiple of `step` (`LIMIT_STEP_DB`: a quarter decibel,
|
|
796
|
+
Brandon 2026-09-18, from the whole decibel of 2026-09-06), and
|
|
797
|
+
never nearer the target than one step.
|
|
782
798
|
|
|
783
799
|
Parameters
|
|
784
800
|
----------
|
|
@@ -793,7 +809,7 @@ class SpecificationDraft:
|
|
|
793
809
|
channels : list of str, optional
|
|
794
810
|
Which channels; all of them when omitted — the channels
|
|
795
811
|
selected are the channels edited.
|
|
796
|
-
step : float, default
|
|
812
|
+
step : float, default LIMIT_STEP_DB
|
|
797
813
|
The decibel grid the edge lands on.
|
|
798
814
|
|
|
799
815
|
Returns
|
|
@@ -806,7 +822,7 @@ class SpecificationDraft:
|
|
|
806
822
|
|
|
807
823
|
def with_band_edge(self, kind: str, side: str, segments: Any,
|
|
808
824
|
decibels: float, channels: Any = None,
|
|
809
|
-
step: float =
|
|
825
|
+
step: float = LIMIT_STEP_DB) -> SpecificationDraft:
|
|
810
826
|
"""The same draft with one edge of a band set to `decibels` on
|
|
811
827
|
every channel — the drag on the plot, landed (Brandon,
|
|
812
828
|
2026-09-06: a drag from 3 to 4 dB puts every selected channel
|
|
@@ -828,7 +844,7 @@ class SpecificationDraft:
|
|
|
828
844
|
The edge's level, positive above the target.
|
|
829
845
|
channels : list of str, optional
|
|
830
846
|
Which channels; all of them when omitted.
|
|
831
|
-
step : float, default
|
|
847
|
+
step : float, default LIMIT_STEP_DB
|
|
832
848
|
The decibel grid the edge lands on.
|
|
833
849
|
|
|
834
850
|
Returns
|
|
@@ -1285,7 +1301,7 @@ class SpecificationDraft:
|
|
|
1285
1301
|
return below if side == 'lower' else above
|
|
1286
1302
|
|
|
1287
1303
|
def carried(values):
|
|
1288
|
-
return
|
|
1304
|
+
return read_at_lines(old_f, values, lines)
|
|
1289
1305
|
|
|
1290
1306
|
def autos_at(name, i):
|
|
1291
1307
|
# the sheet inside its range, the specification's own
|
|
@@ -1493,7 +1509,7 @@ def uniform_spacing(frequencies: Any) -> float | None:
|
|
|
1493
1509
|
return float(steps[0])
|
|
1494
1510
|
|
|
1495
1511
|
|
|
1496
|
-
def
|
|
1512
|
+
def read_at_lines(old_f: np.ndarray, values: np.ndarray,
|
|
1497
1513
|
lines: np.ndarray) -> np.ndarray:
|
|
1498
1514
|
"""A specification's record read at new lines: its own value where
|
|
1499
1515
|
a line is one of its own, and between them the power law a
|
|
@@ -80,7 +80,8 @@ def normalize_window(name: str | None) -> str:
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def from_span(averaging: Averaging, low: float, high: float,
|
|
83
|
-
sample_rate: float, samples: int
|
|
83
|
+
sample_rate: float, samples: int,
|
|
84
|
+
origin: float = 0.0) -> Averaging:
|
|
84
85
|
"""The averaging a dragged span means, in whole frames.
|
|
85
86
|
|
|
86
87
|
The frame length and the window are the table's; a drag says only
|
|
@@ -91,8 +92,11 @@ def from_span(averaging: Averaging, low: float, high: float,
|
|
|
91
92
|
"""
|
|
92
93
|
rate = sample_rate
|
|
93
94
|
n = averaging.frame_length
|
|
95
|
+
# the drag is on the record's clock; the start counts from the
|
|
96
|
+
# record's beginning (`Averaging.stop` says why they differ)
|
|
97
|
+
low, high = float(low) - origin, float(high) - origin
|
|
94
98
|
last_start = max(samples - n, 0) / rate
|
|
95
|
-
start = min(max(
|
|
99
|
+
start = min(max(low, 0.0), last_start)
|
|
96
100
|
moved = replace(averaging, start=start)
|
|
97
101
|
width = round((float(high) - start) * rate)
|
|
98
102
|
wanted = 1 if width < n else 1 + (width - n) // moved.hop
|
|
@@ -265,16 +269,28 @@ class Averaging:
|
|
|
265
269
|
def start_sample(self, sample_rate: float) -> int:
|
|
266
270
|
return round(self.start * sample_rate)
|
|
267
271
|
|
|
268
|
-
def stop(self, sample_rate: float) -> float:
|
|
269
|
-
"""When the analysis ends, in seconds — derived, never stored.
|
|
270
|
-
|
|
272
|
+
def stop(self, sample_rate: float, origin: float = 0.0) -> float:
|
|
273
|
+
"""When the analysis ends, in seconds — derived, never stored.
|
|
274
|
+
|
|
275
|
+
`origin` is the record's first instant. `start` counts from
|
|
276
|
+
the record's beginning, a sample count in disguise, and stays
|
|
277
|
+
so whatever the clock says; but a record whose clock does not
|
|
278
|
+
begin at zero — a truncation keeps its measured instants, an
|
|
279
|
+
import window reads the run's — is drawn and dragged on that
|
|
280
|
+
clock, and the frames of one that began at 1060 s were being
|
|
281
|
+
drawn at 0 s, off the plot (Brandon, 2026-09-18). Every
|
|
282
|
+
reading in seconds takes the origin here, once.
|
|
283
|
+
"""
|
|
284
|
+
return origin + (self.start_sample(sample_rate) + self.span) / sample_rate
|
|
271
285
|
|
|
272
|
-
def frame_bounds(self, sample_rate: float
|
|
273
|
-
|
|
274
|
-
|
|
286
|
+
def frame_bounds(self, sample_rate: float,
|
|
287
|
+
origin: float = 0.0) -> list[tuple[float, float]]:
|
|
288
|
+
"""[(start, stop)] in seconds on the record's clock, one per
|
|
289
|
+
frame, overlaps included — what the time history plot shades.
|
|
290
|
+
`origin` as in `stop`."""
|
|
275
291
|
first = self.start_sample(sample_rate)
|
|
276
|
-
return [((first + i * self.hop) / sample_rate,
|
|
277
|
-
(first + i * self.hop + self.frame_length) / sample_rate)
|
|
292
|
+
return [(origin + (first + i * self.hop) / sample_rate,
|
|
293
|
+
origin + (first + i * self.hop + self.frame_length) / sample_rate)
|
|
278
294
|
for i in range(self.frames)]
|
|
279
295
|
|
|
280
296
|
def fits(self, samples: int, sample_rate: float) -> bool:
|
|
@@ -152,11 +152,26 @@ def written_band(frequencies: ArrayLike,
|
|
|
152
152
|
|
|
153
153
|
def band_of(specification: Specification,
|
|
154
154
|
record: int = 0) -> tuple[float, float] | None:
|
|
155
|
-
"""(low, high) the specification actually says something over.
|
|
156
|
-
|
|
155
|
+
"""(low, high) the specification actually says something over.
|
|
156
|
+
|
|
157
|
+
A specification on bands says something over each band's whole
|
|
158
|
+
width, so its range is its outer bin edges — not its first and last
|
|
159
|
+
centers, which sit inside them and would shrink every comparison
|
|
160
|
+
against it by half a band at each end.
|
|
161
|
+
"""
|
|
162
|
+
frequencies = np.asarray(specification.abscissa, dtype=float)
|
|
163
|
+
values = np.asarray(np.real(specification.ordinate[record]), dtype=float)
|
|
164
|
+
band = written_band(frequencies, values)
|
|
165
|
+
if band is None or getattr(specification, 'bandwidth', None) is None:
|
|
166
|
+
return band
|
|
167
|
+
left, right = specification.bin_bounds()
|
|
168
|
+
usable = (np.isfinite(frequencies) & (frequencies > 0.0)
|
|
169
|
+
& np.isfinite(values) & (values > 0.0))
|
|
170
|
+
return float(left[usable].min()), float(right[usable].max())
|
|
157
171
|
|
|
158
172
|
|
|
159
|
-
def covered(lines: ArrayLike, low: float, high: float
|
|
173
|
+
def covered(lines: ArrayLike, low: float, high: float,
|
|
174
|
+
widths: ArrayLike | None = None
|
|
160
175
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
161
176
|
"""Each measured bin cut to the part of it a band covers.
|
|
162
177
|
|
|
@@ -171,14 +186,15 @@ def covered(lines: ArrayLike, low: float, high: float
|
|
|
171
186
|
if lines.size < 2:
|
|
172
187
|
empty = np.zeros(lines.shape)
|
|
173
188
|
return empty, empty, empty, np.zeros(lines.shape, dtype=bool)
|
|
174
|
-
|
|
189
|
+
# a banded measurement's bins are its own, geometric ones
|
|
190
|
+
widths = np.gradient(lines) if widths is None else np.asarray(widths)
|
|
175
191
|
start = np.maximum(lines - widths / 2.0, low)
|
|
176
192
|
stop = np.minimum(lines + widths / 2.0, high)
|
|
177
193
|
width = np.clip(stop - start, 0.0, None)
|
|
178
194
|
return start, stop, width, (width > 0.0) & (width < widths * (1.0 - 1e-9))
|
|
179
195
|
|
|
180
196
|
|
|
181
|
-
def _written_area(frequencies, values, low, high, reading):
|
|
197
|
+
def _written_area(frequencies, values, low, high, reading, widths=None):
|
|
182
198
|
"""The area under a written curve, read the way its owner is.
|
|
183
199
|
|
|
184
200
|
A limit belongs to a specification and is written the way the
|
|
@@ -192,7 +208,9 @@ def _written_area(frequencies, values, low, high, reading):
|
|
|
192
208
|
return log_log_area(frequencies, values, low, high)
|
|
193
209
|
from .octave import bin_bounds
|
|
194
210
|
|
|
195
|
-
|
|
211
|
+
# a banded specification carries its own bins — geometric, from a
|
|
212
|
+
# standard — and the midpoints between its centers are not them
|
|
213
|
+
left, right = bin_bounds(np.asarray(frequencies, dtype=float), widths)
|
|
196
214
|
if low is not None:
|
|
197
215
|
left, right = np.maximum(left, low), np.maximum(right, low)
|
|
198
216
|
if high is not None:
|
|
@@ -207,7 +225,9 @@ def _written_area(frequencies, values, low, high, reading):
|
|
|
207
225
|
|
|
208
226
|
def outside(lines: ArrayLike, values: ArrayLike,
|
|
209
227
|
spec_frequencies: ArrayLike, limit_values: ArrayLike,
|
|
210
|
-
over: bool = True, reading: str = 'log_log'
|
|
228
|
+
over: bool = True, reading: str = 'log_log',
|
|
229
|
+
spec_widths: ArrayLike | None = None,
|
|
230
|
+
widths: ArrayLike | None = None) -> np.ndarray:
|
|
211
231
|
"""Which measured lines fell outside one written limit curve.
|
|
212
232
|
|
|
213
233
|
In the middle of the band this is the plain comparison: the density
|
|
@@ -234,10 +254,10 @@ def outside(lines: ArrayLike, values: ArrayLike,
|
|
|
234
254
|
band = written_band(spec_frequencies, limit_values)
|
|
235
255
|
if band is None:
|
|
236
256
|
return out
|
|
237
|
-
start, stop, width, cut = covered(lines, *band)
|
|
257
|
+
start, stop, width, cut = covered(lines, *band, widths)
|
|
238
258
|
for i in np.flatnonzero(cut & real):
|
|
239
259
|
asked = _written_area(spec_frequencies, limit_values,
|
|
240
|
-
start[i], stop[i], reading)
|
|
260
|
+
start[i], stop[i], reading, spec_widths)
|
|
241
261
|
if not np.isfinite(asked):
|
|
242
262
|
continue
|
|
243
263
|
held = values[i] * width[i]
|
|
@@ -516,7 +536,9 @@ def exceedances(specification: Specification, measured: DataArray,
|
|
|
516
536
|
else outside(lines, got, specification.abscissa,
|
|
517
537
|
written[spec_record], over,
|
|
518
538
|
getattr(specification, 'interpolation',
|
|
519
|
-
'log_log')
|
|
539
|
+
'log_log'),
|
|
540
|
+
getattr(specification, 'bandwidth', None),
|
|
541
|
+
getattr(measured, 'bandwidth', None)))
|
|
520
542
|
return marks[0], marks[1]
|
|
521
543
|
|
|
522
544
|
|
|
@@ -58,6 +58,13 @@ from ..units import (
|
|
|
58
58
|
si_factor,
|
|
59
59
|
si_transform,
|
|
60
60
|
)
|
|
61
|
+
|
|
62
|
+
#: how much of an object's ordinate a display conversion holds at
|
|
63
|
+
#: once, in bytes (`DataArray.display_blocks`): a quarter gigabyte is
|
|
64
|
+
#: a few hundred records of a long run, and a block's transients —
|
|
65
|
+
#: the gather, the scaling, the decimator's padding — are bounded by
|
|
66
|
+
#: it whatever the object weighs
|
|
67
|
+
DISPLAY_BLOCK_BYTES = 256 * 2 ** 20
|
|
61
68
|
from .validate import DIRECTION_CODES
|
|
62
69
|
from .validate import dofs as _dofs
|
|
63
70
|
|
|
@@ -853,17 +860,62 @@ class DataArray:
|
|
|
853
860
|
rows = range(self.num_records) if records is None else list(records)
|
|
854
861
|
out = np.empty((len(rows), self.ordinate.shape[1]),
|
|
855
862
|
dtype=self.ordinate.dtype)
|
|
856
|
-
|
|
857
|
-
for
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
out[
|
|
864
|
-
else unit_system.from_si(source, dim))
|
|
863
|
+
# converted a block of records at a time into slices of `out`:
|
|
864
|
+
# gathering every asked-for record and scaling the gather were
|
|
865
|
+
# two whole copies beside the answer, and on a 1.2 GB history
|
|
866
|
+
# the conversion alone peaked at three times its output
|
|
867
|
+
# (measured 2026-09-18, on the way to a 22 GB import that took
|
|
868
|
+
# the application down at 67 GB)
|
|
869
|
+
for start, stop, block in self.display_blocks(unit_system, rows):
|
|
870
|
+
out[start:stop] = block
|
|
865
871
|
return out
|
|
866
872
|
|
|
873
|
+
def display_blocks(self, unit_system: UnitSystem,
|
|
874
|
+
records: Iterable[int] | None = None,
|
|
875
|
+
block_bytes: int | None = None):
|
|
876
|
+
"""`display_ordinate` a block of records at a time.
|
|
877
|
+
|
|
878
|
+
Yields `(start, stop, values)`: positions within `records` and
|
|
879
|
+
the converted rows for them, each block at most `block_bytes`
|
|
880
|
+
(`DISPLAY_BLOCK_BYTES` by default) — so a reading that thins
|
|
881
|
+
every record before it draws it, the 3-D stage above all,
|
|
882
|
+
never holds a converted copy of the whole object. A record
|
|
883
|
+
larger than the block is still one block: a row is not split.
|
|
884
|
+
|
|
885
|
+
Parameters
|
|
886
|
+
----------
|
|
887
|
+
unit_system : UnitSystem
|
|
888
|
+
The units to present the values in.
|
|
889
|
+
records : iterable of int, optional
|
|
890
|
+
Which records to convert. All of them when omitted.
|
|
891
|
+
block_bytes : int, optional
|
|
892
|
+
The most a block may hold, in bytes.
|
|
893
|
+
|
|
894
|
+
Yields
|
|
895
|
+
------
|
|
896
|
+
tuple of (int, int, numpy.ndarray)
|
|
897
|
+
The block's positions among `records`, and its values in
|
|
898
|
+
display units.
|
|
899
|
+
"""
|
|
900
|
+
rows = range(self.num_records) if records is None else list(records)
|
|
901
|
+
limit = DISPLAY_BLOCK_BYTES if block_bytes is None else int(block_bytes)
|
|
902
|
+
row_bytes = max(1, self.ordinate.shape[1] * self.ordinate.dtype.itemsize)
|
|
903
|
+
per_block = max(1, limit // row_bytes)
|
|
904
|
+
for start in range(0, len(rows), per_block):
|
|
905
|
+
chunk = rows[start:start + per_block]
|
|
906
|
+
block = np.empty((len(chunk), self.ordinate.shape[1]),
|
|
907
|
+
dtype=self.ordinate.dtype)
|
|
908
|
+
by_dimension = {}
|
|
909
|
+
for position, record in enumerate(chunk):
|
|
910
|
+
by_dimension.setdefault(self.ordinate_dim[record], []).append(
|
|
911
|
+
(position, record))
|
|
912
|
+
for dim, pairs in by_dimension.items():
|
|
913
|
+
positions = [position for position, _ in pairs]
|
|
914
|
+
source = self.ordinate[[record for _, record in pairs]]
|
|
915
|
+
block[positions] = (source if dim == UNKNOWN
|
|
916
|
+
else unit_system.from_si(source, dim))
|
|
917
|
+
yield start, start + len(chunk), block
|
|
918
|
+
|
|
867
919
|
def __eq__(self, other: object) -> bool:
|
|
868
920
|
if type(self) is not type(other):
|
|
869
921
|
return NotImplemented
|
|
@@ -2535,26 +2587,11 @@ class Psd(DataArray):
|
|
|
2535
2587
|
Psd
|
|
2536
2588
|
The same power, arranged on proportional bands.
|
|
2537
2589
|
"""
|
|
2538
|
-
from .octave import
|
|
2590
|
+
from .octave import resample
|
|
2539
2591
|
|
|
2540
2592
|
frequencies = np.asarray(self.abscissa, dtype=float)
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
raise ValueError(
|
|
2544
|
-
'octave bands need a spectrum with frequency lines above '
|
|
2545
|
-
'zero; this one has ' + str(positive.size))
|
|
2546
|
-
# the range the bands have to span is the range the *bins*
|
|
2547
|
-
# cover, not the range the centers do. A line at 0.5 Hz stands
|
|
2548
|
-
# for a bin reaching down to 0.25, and a grid that started at
|
|
2549
|
-
# the line would leave that half outside every band — which is
|
|
2550
|
-
# a small loss of area and an unnecessary one.
|
|
2551
|
-
reach, beyond = self.bin_bounds()
|
|
2552
|
-
inside = reach[reach > 0.0]
|
|
2553
|
-
low = (float(inside.min()) if inside.size else float(positive.min())) \
|
|
2554
|
-
if low is None else float(low)
|
|
2555
|
-
high = float(beyond.max()) if high is None else float(high)
|
|
2556
|
-
per_octave = PER_OCTAVE if per_octave is None else int(per_octave)
|
|
2557
|
-
centers, widths, bounds = bands(low, high, per_octave)
|
|
2593
|
+
per_octave, centers, widths, bounds = self._octave_grid(
|
|
2594
|
+
per_octave, low, high)
|
|
2558
2595
|
banded = resample(frequencies, self.ordinate, bounds, widths,
|
|
2559
2596
|
source=self.bin_bounds())
|
|
2560
2597
|
out = Psd(centers, banded,
|
|
@@ -2574,6 +2611,32 @@ class Psd(DataArray):
|
|
|
2574
2611
|
out.scale_db = self.scale_db
|
|
2575
2612
|
return out
|
|
2576
2613
|
|
|
2614
|
+
def _octave_grid(self, per_octave, low, high):
|
|
2615
|
+
"""(per_octave, centers, widths, bounds): the bands this spectrum
|
|
2616
|
+
bands onto — one grid, shared by the ordinate and, for a
|
|
2617
|
+
specification, its limits."""
|
|
2618
|
+
from .octave import PER_OCTAVE, bands
|
|
2619
|
+
|
|
2620
|
+
frequencies = np.asarray(self.abscissa, dtype=float)
|
|
2621
|
+
positive = frequencies[frequencies > 0.0]
|
|
2622
|
+
if positive.size < 2:
|
|
2623
|
+
raise ValueError(
|
|
2624
|
+
'octave bands need a spectrum with frequency lines above '
|
|
2625
|
+
'zero; this one has ' + str(positive.size))
|
|
2626
|
+
# the range the bands have to span is the range the *bins*
|
|
2627
|
+
# cover, not the range the centers do. A line at 0.5 Hz stands
|
|
2628
|
+
# for a bin reaching down to 0.25, and a grid that started at
|
|
2629
|
+
# the line would leave that half outside every band — which is
|
|
2630
|
+
# a small loss of area and an unnecessary one.
|
|
2631
|
+
reach, beyond = self.bin_bounds()
|
|
2632
|
+
inside = reach[reach > 0.0]
|
|
2633
|
+
low = (float(inside.min()) if inside.size else float(positive.min())) \
|
|
2634
|
+
if low is None else float(low)
|
|
2635
|
+
high = float(beyond.max()) if high is None else float(high)
|
|
2636
|
+
per_octave = PER_OCTAVE if per_octave is None else int(per_octave)
|
|
2637
|
+
centers, widths, bounds = bands(low, high, per_octave)
|
|
2638
|
+
return per_octave, centers, widths, bounds
|
|
2639
|
+
|
|
2577
2640
|
def bin_widths(self) -> np.ndarray:
|
|
2578
2641
|
"""The width of every line's own bin.
|
|
2579
2642
|
|
|
@@ -2792,6 +2855,146 @@ class Specification(Bounded, Psd):
|
|
|
2792
2855
|
#: law. `compute_psds` says otherwise for one it computed.
|
|
2793
2856
|
interpolation = 'log_log'
|
|
2794
2857
|
|
|
2858
|
+
#: how finely a breakpoint curve's cross terms are read when a band
|
|
2859
|
+
#: integrates them: points per band on a log grid
|
|
2860
|
+
_CROSS_POINTS = 64
|
|
2861
|
+
|
|
2862
|
+
def to_octave(self, per_octave: int | None = None,
|
|
2863
|
+
low: float | None = None,
|
|
2864
|
+
high: float | None = None) -> Specification:
|
|
2865
|
+
"""This specification integrated onto proportional bands, its
|
|
2866
|
+
warning and abort limits with it.
|
|
2867
|
+
|
|
2868
|
+
The same area rule as `Psd.to_octave`, read the way the object
|
|
2869
|
+
is: a specification computed on lines is a density and each
|
|
2870
|
+
line is a bin; one written at breakpoints is a power law
|
|
2871
|
+
between them, and each band takes the exact area under that law
|
|
2872
|
+
(`compliance.log_log_area`) over the part of the band the
|
|
2873
|
+
specification covers, divided by the band's width. The limits
|
|
2874
|
+
are curves written the same way and go through the same rule,
|
|
2875
|
+
so a band's warning line stands in the same relation to its
|
|
2876
|
+
target as the breakpoints did. Cross terms of a breakpoint
|
|
2877
|
+
specification are not power laws (a phase is not), so they are
|
|
2878
|
+
read onto a fine log grid the way the authoring sheet reads
|
|
2879
|
+
them — magnitude log–log, phase straight — and integrated there.
|
|
2880
|
+
|
|
2881
|
+
The result is a density per band (`interpolation` 'bin', the
|
|
2882
|
+
bands' widths carried), which is what a banded measurement is
|
|
2883
|
+
compared against band for band. Brandon, 2026-09-18: the
|
|
2884
|
+
specification and the measurement it judges should be
|
|
2885
|
+
convertible alike, so a compliance can be read on bands at both
|
|
2886
|
+
ends — reversing the earlier reasoning (PLAN.md, "Octave
|
|
2887
|
+
bands") that a written curve needed no banding because the
|
|
2888
|
+
comparison integrates it exactly; it does, and a banded
|
|
2889
|
+
specification is still the object a person asks to see and
|
|
2890
|
+
hand on.
|
|
2891
|
+
|
|
2892
|
+
Parameters
|
|
2893
|
+
----------
|
|
2894
|
+
per_octave : int, optional
|
|
2895
|
+
Bands per octave.
|
|
2896
|
+
low, high : float, optional
|
|
2897
|
+
The band to cover.
|
|
2898
|
+
|
|
2899
|
+
Returns
|
|
2900
|
+
-------
|
|
2901
|
+
Specification
|
|
2902
|
+
The same power and the same limits, arranged on
|
|
2903
|
+
proportional bands.
|
|
2904
|
+
"""
|
|
2905
|
+
from .compliance import log_log_area, written_band
|
|
2906
|
+
from .octave import resample
|
|
2907
|
+
|
|
2908
|
+
frequencies = np.asarray(self.abscissa, dtype=float)
|
|
2909
|
+
rows = np.atleast_2d(self.ordinate)
|
|
2910
|
+
if self.interpolation == 'log_log':
|
|
2911
|
+
# the grid spans what the curve is written over — its first
|
|
2912
|
+
# breakpoint to its last — not bins inferred between
|
|
2913
|
+
# breakpoints, which put the first band at the midpoint of
|
|
2914
|
+
# the first segment and lost everything below it
|
|
2915
|
+
spans = [written_band(frequencies, np.abs(row)) for row in rows]
|
|
2916
|
+
spans = [span for span in spans if span is not None]
|
|
2917
|
+
if not spans:
|
|
2918
|
+
raise ValueError('octave bands need a specification written '
|
|
2919
|
+
'over a range of frequencies above zero')
|
|
2920
|
+
from .octave import PER_OCTAVE, bands
|
|
2921
|
+
|
|
2922
|
+
per_octave = PER_OCTAVE if per_octave is None else int(per_octave)
|
|
2923
|
+
low = min(span[0] for span in spans) if low is None else float(low)
|
|
2924
|
+
high = (max(span[1] for span in spans) if high is None
|
|
2925
|
+
else float(high))
|
|
2926
|
+
centers, widths, bounds = bands(low, high, per_octave)
|
|
2927
|
+
|
|
2928
|
+
def band_real(row):
|
|
2929
|
+
out = np.full(len(centers), np.nan)
|
|
2930
|
+
band = written_band(frequencies, row)
|
|
2931
|
+
if band is None:
|
|
2932
|
+
return out
|
|
2933
|
+
for k in range(len(centers)):
|
|
2934
|
+
lo = max(bounds[k], band[0])
|
|
2935
|
+
hi = min(bounds[k + 1], band[1])
|
|
2936
|
+
if hi > lo:
|
|
2937
|
+
out[k] = log_log_area(frequencies, row, lo, hi) \
|
|
2938
|
+
/ widths[k]
|
|
2939
|
+
return out
|
|
2940
|
+
|
|
2941
|
+
def band_complex(row):
|
|
2942
|
+
from .author import read_at_lines
|
|
2943
|
+
|
|
2944
|
+
out = np.full(len(centers), np.nan, dtype=complex)
|
|
2945
|
+
band = written_band(frequencies, np.abs(row))
|
|
2946
|
+
if band is None:
|
|
2947
|
+
return out
|
|
2948
|
+
for k in range(len(centers)):
|
|
2949
|
+
lo = max(bounds[k], band[0])
|
|
2950
|
+
hi = min(bounds[k + 1], band[1])
|
|
2951
|
+
if hi > lo:
|
|
2952
|
+
grid = np.geomspace(lo, hi, self._CROSS_POINTS)
|
|
2953
|
+
values = read_at_lines(frequencies, row, grid)
|
|
2954
|
+
out[k] = np.trapezoid(values, grid) / widths[k]
|
|
2955
|
+
return out
|
|
2956
|
+
|
|
2957
|
+
banded = np.array([
|
|
2958
|
+
band_complex(row) if np.iscomplexobj(row) and np.any(row.imag)
|
|
2959
|
+
else band_real(np.real(row)) for row in rows])
|
|
2960
|
+
limits = {name: np.array([band_real(row) for row in values])
|
|
2961
|
+
for name, values in self.limits.items()}
|
|
2962
|
+
else:
|
|
2963
|
+
per_octave, centers, widths, bounds = self._octave_grid(
|
|
2964
|
+
per_octave, low, high)
|
|
2965
|
+
source = self.bin_bounds()
|
|
2966
|
+
banded = resample(frequencies, rows, bounds, widths, source=source)
|
|
2967
|
+
limits = {}
|
|
2968
|
+
for name, values in self.limits.items():
|
|
2969
|
+
values = np.atleast_2d(values)
|
|
2970
|
+
# a limit row that is all NaN is a cross term's, which
|
|
2971
|
+
# has no limit of its own; the integration would read
|
|
2972
|
+
# its NaN as nothing and hand back zeros
|
|
2973
|
+
unwritten = ~np.isfinite(values).any(axis=1)
|
|
2974
|
+
out = resample(frequencies, values, bounds, widths,
|
|
2975
|
+
source=source).astype(float)
|
|
2976
|
+
out[unwritten] = np.nan
|
|
2977
|
+
limits[name] = out
|
|
2978
|
+
out = Specification(
|
|
2979
|
+
centers, banded,
|
|
2980
|
+
response_dof=list(self.response_dof),
|
|
2981
|
+
reference_dof=(None if self.reference_dof is None
|
|
2982
|
+
else list(self.reference_dof)),
|
|
2983
|
+
ordinate_dim=list(self.ordinate_dim),
|
|
2984
|
+
ordinate_unit=list(self.ordinate_unit),
|
|
2985
|
+
reference_unit=list(self.reference_unit),
|
|
2986
|
+
dimension_hint=list(self.dimension_hint),
|
|
2987
|
+
block=None if self.block is None else list(self.block),
|
|
2988
|
+
bandwidth=widths,
|
|
2989
|
+
comment=f'1/{per_octave} octave bands',
|
|
2990
|
+
**limits)
|
|
2991
|
+
out.interpolation = 'bin'
|
|
2992
|
+
out.scale_db = self.scale_db
|
|
2993
|
+
constraints = getattr(self, 'band_constraints', None)
|
|
2994
|
+
if constraints:
|
|
2995
|
+
out.band_constraints = dict(constraints)
|
|
2996
|
+
return out
|
|
2997
|
+
|
|
2795
2998
|
|
|
2796
2999
|
class ShockSpecification(Bounded, Srs):
|
|
2797
3000
|
"""What a shock test was controlled to: an SRS, and its band.
|