ndscope 0.22.2__tar.gz → 0.22.4__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.
- {ndscope-0.22.2 → ndscope-0.22.4}/CHANGELOG.md +1 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/PKG-INFO +2 -2
- ndscope-0.22.4/install_wheel +11 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/__version__.py +3 -3
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/cache.py +1 -1
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/configuration/configuration.py +3 -1
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/export.py +103 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/plot.py +19 -3
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/scope.py +65 -21
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/scope.ui +11 -1
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/template.py +6 -1
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/test_util.py +7 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope.egg-info/PKG-INFO +2 -2
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope.egg-info/requires.txt +1 -1
- ndscope-0.22.4/ndscope.egg-info/scm_version.json +8 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/pyproject.toml +1 -1
- ndscope-0.22.2/install_wheel +0 -3
- ndscope-0.22.2/ndscope.egg-info/scm_version.json +0 -8
- {ndscope-0.22.2 → ndscope-0.22.4}/COPYING +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/COPYING-GPL-3 +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/MANIFEST.in +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/NEWS.md +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/README.md +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/RELEASE.md +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/bad.sh +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/bad2.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/bad_legend.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/bad_ndscope.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/bad_times.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/big.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/biglive.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/calib.sh +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/calib.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/calib_arrakis.sh +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/calib_arrakis.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/dev.sh +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/dts1 +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/live.sh +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/live.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/manual_tests/manual_test.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/manual_tests/manual_test_live.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/manual_tests/ndscope-test.txt +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/manual_tests/old_manual_test.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/manual_tests/test_results_2025_10_23.txt +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/maturin.sh +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/__init__.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/__main__.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/_qt.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/axis.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/axisCtrlTemplate.ui +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/base_plot.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/channel_config.ui +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/channel_config_dialog.ui +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/channel_list.ui +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/channel_select.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/channel_select_dialog.ui +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/configuration/__init__.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/const.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/crosshair.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/cursors.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/data.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/data_source_determination.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/dialog.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/dialog_nds_auth.ui +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/dialog_nds_ontape.ui +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/dtt/__init__.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/dtt/channel_trend_id.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/dtt/dtt.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/exceptions.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/kerberos.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/legend.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/markers.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/math_config.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/math_config.ui +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/messages/__init__.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/messages/dtt_messages.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/messages/has_issues.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/messages/message_windows/__init__.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/messages/message_windows/log_list.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/messages/message_windows/message_window.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/messages/messages.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/nds.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/plotMenu.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/primary_view.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/result.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/ruff.toml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/scatterPlotMenu.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/scatter_plot.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/__init__.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test0.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test1-1.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test1-2.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test1-3.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test1.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test2.stp +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test2.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test3.txt +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test3.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test4.xml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/templates/test4.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/test_cache.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/test_cli.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/test_data_flow.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/test_layout.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/test_plot.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/test/test_window.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/trigger.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope/util.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope.egg-info/SOURCES.txt +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope.egg-info/dependency_links.txt +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope.egg-info/entry_points.txt +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope.egg-info/scm_file_list.json +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/ndscope.egg-info/top_level.txt +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/newdaq.sh +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/past.sh +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/past.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/past2.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/pidcheck.py +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/setup.cfg +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/teststand_arrakis.sh +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/teststand_arrakis.yaml +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/trend.sh +0 -0
- {ndscope-0.22.2 → ndscope-0.22.4}/trend.yaml +0 -0
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- Added system to place markers on plots, then jump between markers.
|
|
11
11
|
- Fix bug where arrakis data could be timestamped wrong
|
|
12
12
|
- Add --arrakis (choose default Arrakis data source) and --arrakis-server (specify an arrakis server) options.
|
|
13
|
+
- Added raw data export to WAV sound files (one file per channel), a "play" button on the export tab that plays the visible data, and the export "show" button now opens files with the OS default application on Linux, Mac and Windows.
|
|
13
14
|
|
|
14
15
|
## 0.22.0 - 2026-08-24
|
|
15
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndscope
|
|
3
|
-
Version: 0.22.
|
|
3
|
+
Version: 0.22.4
|
|
4
4
|
Summary: Next-generation NDS time series plotting
|
|
5
5
|
Author-email: Jameson Graef Rollins <jameson.rollins@ligo.org>, Patrick Thomas <patrick.thomas@ligo.org>, Erik von Reis <erik.vonreis@ligo.org>
|
|
6
6
|
Maintainer-email: Jameson Graef Rollins <jameson.rollins@ligo.org>, Patrick Thomas <patrick.thomas@ligo.org>, Erik von Reis <erik.vonreis@ligo.org>
|
|
@@ -18,7 +18,7 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: COPYING
|
|
19
19
|
License-File: COPYING-GPL-3
|
|
20
20
|
Requires-Dist: cairosvg
|
|
21
|
-
Requires-Dist: dttlib
|
|
21
|
+
Requires-Dist: dttlib<0.13.0,>=0.12.2
|
|
22
22
|
Requires-Dist: gpstime
|
|
23
23
|
Requires-Dist: h5py
|
|
24
24
|
Requires-Dist: matplotlib
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
pyver="$(python3 --version | grep -oP '\d+\.\d+' | tr -d '.')"
|
|
4
|
+
wheelpath="../dttlib/wheels/*${pyver}*.whl"
|
|
5
|
+
wheelcount=$( ls ${wheelpath} 2> /dev/null | wc -l )
|
|
6
|
+
if [[ $wheelcount -gt 0 ]]
|
|
7
|
+
then
|
|
8
|
+
pip install "$(ls -tr ${wheelpath} )" --force-reinstall
|
|
9
|
+
else
|
|
10
|
+
echo "no wheel file found for python${pyver}"
|
|
11
|
+
fi
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.22.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 22,
|
|
21
|
+
__version__ = version = '0.22.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 22, 4)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g3d3f44107'
|
|
@@ -808,7 +808,7 @@ class DataCache(QtCore.QObject):
|
|
|
808
808
|
|
|
809
809
|
def guess_invalid_channel(self, msg: str) -> str:
|
|
810
810
|
"""Transform 'Invalid channel name' messages into something more helpful"""
|
|
811
|
-
if "Invalid channel" in msg:
|
|
811
|
+
if "Invalid channel" in msg or ("Unavailable," in msg and "channel(s)" in msg):
|
|
812
812
|
channels = self.channel_names()
|
|
813
813
|
suspect = None
|
|
814
814
|
if len(channels) == 1:
|
|
@@ -73,7 +73,9 @@ def _handle_environment() -> TOMLDocument:
|
|
|
73
73
|
env_config = TOMLDocument()
|
|
74
74
|
|
|
75
75
|
if "NDSSERVER" in os.environ:
|
|
76
|
-
env_config["data_source"] = {
|
|
76
|
+
env_config["data_source"] = {
|
|
77
|
+
"urls": [f"nds://{n}" for n in os.environ["NDSSERVER"].split(",")]
|
|
78
|
+
}
|
|
77
79
|
elif "ARRAKIS_SERVER" in os.environ:
|
|
78
80
|
env_config["data_source"] = {"urls": [os.environ["ARRAKIS_SERVER"]]}
|
|
79
81
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import os
|
|
2
3
|
import io
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
3
7
|
from collections import OrderedDict
|
|
4
8
|
from typing import Optional, Tuple
|
|
5
9
|
import xml.etree.ElementTree as ET
|
|
@@ -8,6 +12,7 @@ import dttlib
|
|
|
8
12
|
import yaml
|
|
9
13
|
import numpy as np
|
|
10
14
|
from qtpy import QtCore
|
|
15
|
+
from qtpy.QtGui import QDesktopServices
|
|
11
16
|
from pyqtgraph import exporters
|
|
12
17
|
import h5py
|
|
13
18
|
|
|
@@ -22,6 +27,7 @@ except:
|
|
|
22
27
|
cairosvg = None
|
|
23
28
|
|
|
24
29
|
from scipy.io import savemat
|
|
30
|
+
from scipy.io import wavfile
|
|
25
31
|
import matplotlib.pyplot as plt
|
|
26
32
|
|
|
27
33
|
from .data import DataBufferDict
|
|
@@ -29,12 +35,15 @@ from .template import get_channel_color
|
|
|
29
35
|
|
|
30
36
|
##################################################
|
|
31
37
|
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
32
40
|
EXPORT_FILTERS = [
|
|
33
41
|
("scene to Portable Network Graphic (*.png)", [".png"]),
|
|
34
42
|
("scene to Scalable Vector Graphic (*.svg)", [".svg"]),
|
|
35
43
|
("scene to Portable Document Format (*.pdf)", [".pdf"]),
|
|
36
44
|
("data to Hierarchical Data Format (*.hdf5 *.h5)", [".hdf5", ".h5"]),
|
|
37
45
|
("data to MATLAB Binary Format (*.mat)", [".mat"]),
|
|
46
|
+
("data to WAV sound files, one per channel (*.wav)", [".wav"]),
|
|
38
47
|
("template to YAML (*.yaml *.yml)", [".yaml", ".yml"]),
|
|
39
48
|
]
|
|
40
49
|
|
|
@@ -83,6 +92,28 @@ def export_dialog(dialog_func, parent, path):
|
|
|
83
92
|
return path
|
|
84
93
|
|
|
85
94
|
|
|
95
|
+
def open_file(path) -> bool:
|
|
96
|
+
"""Open file with the OS default application (Linux, Mac, Windows)
|
|
97
|
+
|
|
98
|
+
Returns True if the request was handed to the OS. Success does
|
|
99
|
+
not guarantee the application started, only that it was launched.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
if QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(os.path.abspath(path))):
|
|
103
|
+
return True
|
|
104
|
+
# fall back to native launchers if Qt couldn't find a handler
|
|
105
|
+
try:
|
|
106
|
+
if sys.platform.startswith("win"):
|
|
107
|
+
os.startfile(path) # type: ignore
|
|
108
|
+
elif sys.platform == "darwin":
|
|
109
|
+
subprocess.Popen(["open", path])
|
|
110
|
+
else:
|
|
111
|
+
subprocess.Popen(["xdg-open", path])
|
|
112
|
+
except (OSError, AttributeError):
|
|
113
|
+
return False
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
86
117
|
def _write_file(data, path):
|
|
87
118
|
with open(path, "wb") as f:
|
|
88
119
|
f.write(data)
|
|
@@ -270,10 +301,82 @@ def export_data_mat(ddict: DataBufferDict, path, start, end, **kwargs):
|
|
|
270
301
|
savemat(path, out)
|
|
271
302
|
|
|
272
303
|
|
|
304
|
+
# lowest sample rate (Hz) written to a WAV header. Slower channels
|
|
305
|
+
# are sped up by a power of two so the result is audible.
|
|
306
|
+
WAV_MIN_RATE_HZ = 8000
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _wav_speedup(rate_hz: float) -> int:
|
|
310
|
+
"""Power of two playback speed-up needed to reach WAV_MIN_RATE_HZ"""
|
|
311
|
+
factor = 1
|
|
312
|
+
while rate_hz * factor < WAV_MIN_RATE_HZ:
|
|
313
|
+
factor *= 2
|
|
314
|
+
return factor
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _wav_samples(data) -> Optional[np.ndarray]:
|
|
318
|
+
"""Convert channel data to float32 audio: mean removed, peak of 1.
|
|
319
|
+
|
|
320
|
+
Returns None if there is nothing to hear (empty, constant, or
|
|
321
|
+
non-finite data).
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
data = np.asarray(data, dtype=np.float64).ravel()
|
|
325
|
+
data = data[np.isfinite(data)]
|
|
326
|
+
if data.size == 0:
|
|
327
|
+
return None
|
|
328
|
+
data = data - data.mean()
|
|
329
|
+
peak = np.max(np.abs(data))
|
|
330
|
+
if peak == 0:
|
|
331
|
+
return None
|
|
332
|
+
return (data / peak).astype(np.float32)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _wav_filename(path, id) -> str:
|
|
336
|
+
base, ext = os.path.splitext(path)
|
|
337
|
+
chan = re.sub(r"[^A-Za-z0-9_.-]", "_", str(id.name))
|
|
338
|
+
stat = re.sub(r"[^A-Za-z0-9_.-]", "_", str(id.trend_stat.data_name()))
|
|
339
|
+
return f"{base}_{chan}_{stat}{ext}"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def export_data_wav(
|
|
343
|
+
ddict: DataBufferDict,
|
|
344
|
+
path,
|
|
345
|
+
start: dttlib.PipInstant,
|
|
346
|
+
end: dttlib.PipInstant,
|
|
347
|
+
**kwargs,
|
|
348
|
+
) -> list:
|
|
349
|
+
"""Save each channel to its own WAV file
|
|
350
|
+
|
|
351
|
+
Files are named <path base>_<channel>_<stat>.wav. Each channel
|
|
352
|
+
is written at its native sample rate, with the mean removed and
|
|
353
|
+
scaled to full range. Channels with sample rates too low to hear
|
|
354
|
+
are sped up by a power of two (see WAV_MIN_RATE_HZ). Returns the
|
|
355
|
+
list of paths written.
|
|
356
|
+
|
|
357
|
+
"""
|
|
358
|
+
if ddict is None:
|
|
359
|
+
raise ValueError("No data to export")
|
|
360
|
+
written = []
|
|
361
|
+
for id, datad in ddict.items():
|
|
362
|
+
samples = _wav_samples(_get_data_slice(datad.original_struct, start, end))
|
|
363
|
+
if samples is None:
|
|
364
|
+
logger.info("wav export: no audible data for %s, skipping", id.name)
|
|
365
|
+
continue
|
|
366
|
+
rate = int(round(datad.sample_rate * _wav_speedup(datad.sample_rate)))
|
|
367
|
+
fpath = _wav_filename(path, id)
|
|
368
|
+
wavfile.write(fpath, rate, samples)
|
|
369
|
+
written.append(fpath)
|
|
370
|
+
if not written:
|
|
371
|
+
raise ValueError("No audible data to export")
|
|
372
|
+
return written
|
|
373
|
+
|
|
374
|
+
|
|
273
375
|
DATA_EXPORT_FUNCS = {
|
|
274
376
|
".hdf5": export_data_hdf5,
|
|
275
377
|
".h5": export_data_hdf5,
|
|
276
378
|
".mat": export_data_mat,
|
|
379
|
+
".wav": export_data_wav,
|
|
277
380
|
}
|
|
278
381
|
|
|
279
382
|
|
|
@@ -28,7 +28,7 @@ from . import cursors
|
|
|
28
28
|
from . import markers
|
|
29
29
|
from .dtt.channel_trend_id import ChannelTrendId
|
|
30
30
|
from .axis import LogAxis
|
|
31
|
-
from typing import Any, List, Tuple, Dict, TYPE_CHECKING, Optional
|
|
31
|
+
from typing import Any, List, Tuple, Dict, TYPE_CHECKING, Optional, Set
|
|
32
32
|
from weakref import WeakSet
|
|
33
33
|
|
|
34
34
|
if TYPE_CHECKING:
|
|
@@ -384,6 +384,7 @@ class CurveView:
|
|
|
384
384
|
self.args = args
|
|
385
385
|
self.kwargs = kwargs
|
|
386
386
|
self.curve.setData(x=x, y=y, *args, **kwargs)
|
|
387
|
+
self.original_t0 = t0
|
|
387
388
|
|
|
388
389
|
def update_t0(self, t0: PipInstant):
|
|
389
390
|
"""
|
|
@@ -391,6 +392,8 @@ class CurveView:
|
|
|
391
392
|
"""
|
|
392
393
|
delta = self.original_t0 - t0
|
|
393
394
|
x = self.x + delta.to_seconds()
|
|
395
|
+
self.x[:] = x
|
|
396
|
+
self.original_t0 = t0
|
|
394
397
|
self.curve.setData(x=x, y=self.y, *self.args, **self.kwargs)
|
|
395
398
|
|
|
396
399
|
|
|
@@ -486,6 +489,7 @@ class NDScopePlot(BasePlot):
|
|
|
486
489
|
|
|
487
490
|
self.has_t_cursors = True
|
|
488
491
|
self.has_y_cursors = True
|
|
492
|
+
self.latest_chan_ids: Set[ChannelTrendId] = set()
|
|
489
493
|
|
|
490
494
|
def __del__(self):
|
|
491
495
|
logger.info("deleting plot %s", str(self))
|
|
@@ -819,10 +823,20 @@ class NDScopePlot(BasePlot):
|
|
|
819
823
|
self.markers.set_t0(t0)
|
|
820
824
|
self._set_t_limits(t0.to_gpst_seconds())
|
|
821
825
|
|
|
826
|
+
# clear the latest chan_id list only when the trends change
|
|
827
|
+
# so that we collect all chan ids drawn for the current trend
|
|
828
|
+
if (
|
|
829
|
+
self.latest_chan_ids
|
|
830
|
+
and next(iter(self.latest_chan_ids)).trend_stat
|
|
831
|
+
!= next(iter(data.buffers.keys())).trend_stat
|
|
832
|
+
):
|
|
833
|
+
self.latest_chan_ids = set()
|
|
822
834
|
for chan_id, curve_view in self.curve_view.items():
|
|
823
835
|
if chan_id not in data:
|
|
824
836
|
continue
|
|
825
837
|
|
|
838
|
+
self.latest_chan_ids.add(chan_id)
|
|
839
|
+
|
|
826
840
|
cd = data[chan_id]
|
|
827
841
|
|
|
828
842
|
# if cd.is_trend:
|
|
@@ -889,8 +903,10 @@ class NDScopePlot(BasePlot):
|
|
|
889
903
|
Shift all curves to reflect a new t0
|
|
890
904
|
needed in case fetch data doesn't ever return anything.
|
|
891
905
|
"""
|
|
892
|
-
for
|
|
893
|
-
curve_view
|
|
906
|
+
for channel_id in self.latest_chan_ids:
|
|
907
|
+
if channel_id in self.curve_view:
|
|
908
|
+
curve_view = self.curve_view[channel_id]
|
|
909
|
+
curve_view.update_t0(t0)
|
|
894
910
|
self.markers.set_t0(t0)
|
|
895
911
|
|
|
896
912
|
def update_results(self, results: dict[str, "Result"]):
|
|
@@ -7,6 +7,7 @@ import copy
|
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
9
|
import subprocess
|
|
10
|
+
import tempfile
|
|
10
11
|
import traceback
|
|
11
12
|
import heapq
|
|
12
13
|
from time import sleep
|
|
@@ -216,6 +217,11 @@ class NDScope(*load_ui("scope.ui")):
|
|
|
216
217
|
# flag to indicate that we are in a base state with no
|
|
217
218
|
# channels declared
|
|
218
219
|
self._base = True
|
|
220
|
+
# paths written by multi-file exports, keyed by requested path
|
|
221
|
+
self._export_files: Dict[str, List[str]] = {}
|
|
222
|
+
# scratch directory for play_sound, created on first use and
|
|
223
|
+
# removed when the scope is garbage collected or exits
|
|
224
|
+
self._sound_dir: Optional[tempfile.TemporaryDirectory] = None
|
|
219
225
|
|
|
220
226
|
# FIXME: HACK: this is an attempt to bypass the following bug:
|
|
221
227
|
#
|
|
@@ -412,6 +418,7 @@ class NDScope(*load_ui("scope.ui")):
|
|
|
412
418
|
self.exportPath.returnPressed.connect(self._export_save_handler)
|
|
413
419
|
self.exportSaveButton.clicked.connect(self._export_save_handler)
|
|
414
420
|
self.exportShowButton.clicked.connect(self._export_show_handler)
|
|
421
|
+
self.exportPlayButton.clicked.connect(self.play_sound)
|
|
415
422
|
self.exportButton2.clicked.connect(self.snapshot)
|
|
416
423
|
|
|
417
424
|
##########
|
|
@@ -458,6 +465,7 @@ class NDScope(*load_ui("scope.ui")):
|
|
|
458
465
|
self.exportShowButton.setIcon(
|
|
459
466
|
self.style().standardIcon(QStyle.SP_FileDialogContentsView) # type: ignore
|
|
460
467
|
)
|
|
468
|
+
self.exportPlayButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) # type: ignore
|
|
461
469
|
# self.exportButton2.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton))
|
|
462
470
|
|
|
463
471
|
##########
|
|
@@ -875,12 +883,13 @@ class NDScope(*load_ui("scope.ui")):
|
|
|
875
883
|
if y_range:
|
|
876
884
|
plot.set_y_range(y_range)
|
|
877
885
|
|
|
878
|
-
if
|
|
886
|
+
if x_range:
|
|
879
887
|
plot.set_x_range(x_range)
|
|
880
888
|
|
|
881
889
|
plot.lock_aspect_ratio(lock_aspect_ratio)
|
|
882
890
|
|
|
883
891
|
self.set_font(self.font())
|
|
892
|
+
plot.showGrid(alpha=self.gridAlphaSelect.value())
|
|
884
893
|
|
|
885
894
|
plot.row_stretch_factor = row_stretch_factor
|
|
886
895
|
plot.col_stretch_factor = col_stretch_factor
|
|
@@ -991,6 +1000,7 @@ class NDScope(*load_ui("scope.ui")):
|
|
|
991
1000
|
plot.set_y_range(y_range)
|
|
992
1001
|
|
|
993
1002
|
self.set_font(self.font())
|
|
1003
|
+
plot.showGrid(alpha=self.gridAlphaSelect.value())
|
|
994
1004
|
|
|
995
1005
|
plot.row_stretch_factor = row_stretch_factor
|
|
996
1006
|
plot.col_stretch_factor = col_stretch_factor
|
|
@@ -1441,8 +1451,7 @@ class NDScope(*load_ui("scope.ui")):
|
|
|
1441
1451
|
"""Stop online mode"""
|
|
1442
1452
|
logger.info("STOP")
|
|
1443
1453
|
self.data.online_stop()
|
|
1444
|
-
|
|
1445
|
-
self.last_cmd = ("fetch", {"t0": self.t0, "window": self.get_window()})
|
|
1454
|
+
self.fetch(t0=self.t0, window=self.get_window())
|
|
1446
1455
|
|
|
1447
1456
|
def _data_request(self, force=False, snapshot=False, singleshot=False):
|
|
1448
1457
|
"""
|
|
@@ -1471,7 +1480,8 @@ class NDScope(*load_ui("scope.ui")):
|
|
|
1471
1480
|
elif self.data.online:
|
|
1472
1481
|
extend = PipDuration.from_sec(0)
|
|
1473
1482
|
else:
|
|
1474
|
-
extend = (end - start) /
|
|
1483
|
+
extend = (end - start) / 4
|
|
1484
|
+
self._offline_ui()
|
|
1475
1485
|
|
|
1476
1486
|
self.data.request(
|
|
1477
1487
|
trend, (start - extend, end + extend), force, snapshot, singleshot
|
|
@@ -1612,12 +1622,6 @@ class NDScope(*load_ui("scope.ui")):
|
|
|
1612
1622
|
|
|
1613
1623
|
# SLOT
|
|
1614
1624
|
def data_retrieve_done(self, _tag: str, msg: Message | None = None, active=False):
|
|
1615
|
-
if self.data.empty:
|
|
1616
|
-
self._base_ui()
|
|
1617
|
-
elif self.data.online:
|
|
1618
|
-
self._online_ui()
|
|
1619
|
-
else:
|
|
1620
|
-
self._offline_ui()
|
|
1621
1625
|
if msg is not None:
|
|
1622
1626
|
error = msg.short_msg
|
|
1623
1627
|
else:
|
|
@@ -2429,7 +2433,8 @@ class NDScope(*load_ui("scope.ui")):
|
|
|
2429
2433
|
def export(self, path=None):
|
|
2430
2434
|
"""Export plot scene, data, or template to file.
|
|
2431
2435
|
|
|
2432
|
-
Supports PNG, SVG, PDF image formats, HDF5
|
|
2436
|
+
Supports PNG, SVG, PDF image formats, HDF5, MAT and WAV (one file per
|
|
2437
|
+
channel) for data, and YAML
|
|
2433
2438
|
for template. A file select dialog will be presented if a
|
|
2434
2439
|
path is not supplied.
|
|
2435
2440
|
|
|
@@ -2488,12 +2493,15 @@ class NDScope(*load_ui("scope.ui")):
|
|
|
2488
2493
|
return
|
|
2489
2494
|
|
|
2490
2495
|
try:
|
|
2491
|
-
export_func(*args, **kwargs)
|
|
2496
|
+
result = export_func(*args, **kwargs)
|
|
2492
2497
|
except Exception as e:
|
|
2493
2498
|
logger.info(traceback.format_exc())
|
|
2494
2499
|
set_error(tag, f"Export failed: {str(e)}")
|
|
2495
2500
|
return
|
|
2496
2501
|
|
|
2502
|
+
if isinstance(result, list):
|
|
2503
|
+
# exporters that write multiple files return their paths
|
|
2504
|
+
self._export_files[path] = result
|
|
2497
2505
|
ftype = ext[1:].upper()
|
|
2498
2506
|
set_info(tag, f"Exported {obj} to {ftype}: {path}")
|
|
2499
2507
|
self._export_complete.emit()
|
|
@@ -2505,21 +2513,57 @@ class NDScope(*load_ui("scope.ui")):
|
|
|
2505
2513
|
def _export_show_handler(self):
|
|
2506
2514
|
tag = "export"
|
|
2507
2515
|
path = self.exportPath.text()
|
|
2508
|
-
if not path or not os.path.exists(path):
|
|
2516
|
+
if not path or not (os.path.exists(path) or path in self._export_files):
|
|
2509
2517
|
set_error(tag, "No path, or no file at path. Export first.")
|
|
2510
2518
|
return
|
|
2511
2519
|
ext = os.path.splitext(path)[1]
|
|
2512
2520
|
if ext in [".hdf5", ".h5"]:
|
|
2513
2521
|
export.matplot_h5(path)
|
|
2514
2522
|
else:
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
)
|
|
2522
|
-
|
|
2523
|
+
# multi-file exports (e.g. wav) open their first file
|
|
2524
|
+
files = self._export_files.get(path)
|
|
2525
|
+
target = files[0] if files else path
|
|
2526
|
+
if not os.path.exists(target):
|
|
2527
|
+
set_error(tag, f"No file at {target}. Export first.")
|
|
2528
|
+
elif not export.open_file(target):
|
|
2529
|
+
set_error(tag, f"Could not open {target} with the default application.")
|
|
2530
|
+
elif files and len(files) > 1:
|
|
2531
|
+
set_info(tag, f"Opened {target} (1 of {len(files)} files)")
|
|
2532
|
+
|
|
2533
|
+
def play_sound(self):
|
|
2534
|
+
"""Export visible raw data to a temporary WAV file and play it
|
|
2535
|
+
|
|
2536
|
+
One file is written per channel, but only the first is played.
|
|
2537
|
+
|
|
2538
|
+
"""
|
|
2539
|
+
tag = "play"
|
|
2540
|
+
data_block = self.get_current_data_block()
|
|
2541
|
+
if data_block is None:
|
|
2542
|
+
set_error(tag, "No data to play.")
|
|
2543
|
+
return
|
|
2544
|
+
if self._sound_dir is None:
|
|
2545
|
+
self._sound_dir = tempfile.TemporaryDirectory(prefix="ndscope_")
|
|
2546
|
+
window = self.get_window()
|
|
2547
|
+
start = self.t0 + window[0]
|
|
2548
|
+
end = self.t0 + window[1]
|
|
2549
|
+
try:
|
|
2550
|
+
files = export.export_data_wav(
|
|
2551
|
+
data_block,
|
|
2552
|
+
os.path.join(self._sound_dir.name, "temporary.wav"),
|
|
2553
|
+
start,
|
|
2554
|
+
end,
|
|
2555
|
+
)
|
|
2556
|
+
except Exception as e:
|
|
2557
|
+
logger.info(traceback.format_exc())
|
|
2558
|
+
set_error(tag, f"Could not create sound file: {str(e)}")
|
|
2559
|
+
return
|
|
2560
|
+
if not export.open_file(files[0]):
|
|
2561
|
+
set_error(tag, f"Could not open {files[0]} with the default application.")
|
|
2562
|
+
return
|
|
2563
|
+
msg = f"Playing {os.path.basename(files[0])}"
|
|
2564
|
+
if len(files) > 1:
|
|
2565
|
+
msg += f" (1 of {len(files)} channels)"
|
|
2566
|
+
set_info(tag, msg)
|
|
2523
2567
|
|
|
2524
2568
|
def snapshot(self):
|
|
2525
2569
|
"""Take snapshot of scene (PNG) and copy to clipboard"""
|
|
@@ -1810,7 +1810,7 @@
|
|
|
1810
1810
|
</sizepolicy>
|
|
1811
1811
|
</property>
|
|
1812
1812
|
<property name="text">
|
|
1813
|
-
<string>Export scene image (PNG, SVG, PDF), data (HDF5, MAT), or layout (YAML).
|
|
1813
|
+
<string>Export scene image (PNG, SVG, PDF), data (HDF5, MAT, WAV), or layout (YAML).
|
|
1814
1814
|
Format determined by file name extension (e.g. ".png" etc.).</string>
|
|
1815
1815
|
</property>
|
|
1816
1816
|
<property name="wordWrap">
|
|
@@ -1867,6 +1867,16 @@ Format determined by file name extension (e.g. ".png" etc.).</string>
|
|
|
1867
1867
|
</property>
|
|
1868
1868
|
</widget>
|
|
1869
1869
|
</item>
|
|
1870
|
+
<item>
|
|
1871
|
+
<widget class="QPushButton" name="exportPlayButton">
|
|
1872
|
+
<property name="toolTip">
|
|
1873
|
+
<string>export visible raw data to a temporary WAV file and play it (first channel only)</string>
|
|
1874
|
+
</property>
|
|
1875
|
+
<property name="text">
|
|
1876
|
+
<string>play</string>
|
|
1877
|
+
</property>
|
|
1878
|
+
</widget>
|
|
1879
|
+
</item>
|
|
1870
1880
|
</layout>
|
|
1871
1881
|
</item>
|
|
1872
1882
|
</layout>
|
|
@@ -241,6 +241,7 @@ def get_channel_color(channel: str, color=None):
|
|
|
241
241
|
|
|
242
242
|
"""
|
|
243
243
|
global _CHANNEL_COLORS
|
|
244
|
+
global _PEN_GEN
|
|
244
245
|
if color:
|
|
245
246
|
if isinstance(color, str):
|
|
246
247
|
if color[0] == "#":
|
|
@@ -267,6 +268,10 @@ def get_channel_color(channel: str, color=None):
|
|
|
267
268
|
)
|
|
268
269
|
_CHANNEL_COLORS[channel] = rgb
|
|
269
270
|
elif channel not in _CHANNEL_COLORS:
|
|
271
|
+
if _PEN_GEN is None:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
"reset_colors() must be called once before calling g_channel_color()"
|
|
274
|
+
)
|
|
270
275
|
color = _PEN_GEN.__next__()
|
|
271
276
|
_CHANNEL_COLORS[channel] = color
|
|
272
277
|
return color_RGB_to_24bit(_CHANNEL_COLORS[channel])
|
|
@@ -504,6 +509,7 @@ def template_from_chans(chan_layout):
|
|
|
504
509
|
chans = []
|
|
505
510
|
r = 0
|
|
506
511
|
c = 0
|
|
512
|
+
template = _new_template()
|
|
507
513
|
for chan in chan_layout + ["."]:
|
|
508
514
|
if chan in [",", "."]:
|
|
509
515
|
layout.append(
|
|
@@ -522,7 +528,6 @@ def template_from_chans(chan_layout):
|
|
|
522
528
|
else:
|
|
523
529
|
chans.append({chan: _new_curve(chan)})
|
|
524
530
|
channels.append(chan)
|
|
525
|
-
template = _new_template()
|
|
526
531
|
template["window-title"] = " ".join(channels)
|
|
527
532
|
template["plots"] = layout
|
|
528
533
|
logger.debug("created template from channel list")
|
|
@@ -285,6 +285,13 @@ class TestResolveDataSource:
|
|
|
285
285
|
result = self.resolve_data_source("--nds", "test.server.com:31200")
|
|
286
286
|
assert "test.server.com" in result
|
|
287
287
|
|
|
288
|
+
def test_multiple_ndsserver(self):
|
|
289
|
+
"""only first nds server should be used"""
|
|
290
|
+
env = {"NDSSERVER": "nds.test:8088,nds2.test:8088"}
|
|
291
|
+
with patch.dict(os.environ, env, clear=True):
|
|
292
|
+
result = self.resolve_data_source()
|
|
293
|
+
assert result == "nds://nds.test:8088"
|
|
294
|
+
|
|
288
295
|
def test_alias_cit(self):
|
|
289
296
|
"""'cit' alias should resolve to caltech server"""
|
|
290
297
|
result = self.resolve_data_source("--nds", "cit")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndscope
|
|
3
|
-
Version: 0.22.
|
|
3
|
+
Version: 0.22.4
|
|
4
4
|
Summary: Next-generation NDS time series plotting
|
|
5
5
|
Author-email: Jameson Graef Rollins <jameson.rollins@ligo.org>, Patrick Thomas <patrick.thomas@ligo.org>, Erik von Reis <erik.vonreis@ligo.org>
|
|
6
6
|
Maintainer-email: Jameson Graef Rollins <jameson.rollins@ligo.org>, Patrick Thomas <patrick.thomas@ligo.org>, Erik von Reis <erik.vonreis@ligo.org>
|
|
@@ -18,7 +18,7 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: COPYING
|
|
19
19
|
License-File: COPYING-GPL-3
|
|
20
20
|
Requires-Dist: cairosvg
|
|
21
|
-
Requires-Dist: dttlib
|
|
21
|
+
Requires-Dist: dttlib<0.13.0,>=0.12.2
|
|
22
22
|
Requires-Dist: gpstime
|
|
23
23
|
Requires-Dist: h5py
|
|
24
24
|
Requires-Dist: matplotlib
|
ndscope-0.22.2/install_wheel
DELETED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|