audace-display 0.1.0__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.
- audace_display-0.1.0/LICENSE +21 -0
- audace_display-0.1.0/PKG-INFO +225 -0
- audace_display-0.1.0/README.md +194 -0
- audace_display-0.1.0/pyproject.toml +49 -0
- audace_display-0.1.0/setup.cfg +4 -0
- audace_display-0.1.0/src/audace_display/__init__.py +47 -0
- audace_display-0.1.0/src/audace_display/__main__.py +9 -0
- audace_display-0.1.0/src/audace_display/_errors.py +12 -0
- audace_display-0.1.0/src/audace_display/channels.py +103 -0
- audace_display-0.1.0/src/audace_display/cli.py +789 -0
- audace_display-0.1.0/src/audace_display/decimate.py +162 -0
- audace_display-0.1.0/src/audace_display/demod.py +196 -0
- audace_display-0.1.0/src/audace_display/plotting.py +190 -0
- audace_display-0.1.0/src/audace_display/plotting_qt.py +237 -0
- audace_display-0.1.0/src/audace_display/processing.py +165 -0
- audace_display-0.1.0/src/audace_display/reader.py +250 -0
- audace_display-0.1.0/src/audace_display.egg-info/PKG-INFO +225 -0
- audace_display-0.1.0/src/audace_display.egg-info/SOURCES.txt +27 -0
- audace_display-0.1.0/src/audace_display.egg-info/dependency_links.txt +1 -0
- audace_display-0.1.0/src/audace_display.egg-info/entry_points.txt +2 -0
- audace_display-0.1.0/src/audace_display.egg-info/requires.txt +10 -0
- audace_display-0.1.0/src/audace_display.egg-info/top_level.txt +1 -0
- audace_display-0.1.0/tests/test_channels.py +48 -0
- audace_display-0.1.0/tests/test_cli.py +157 -0
- audace_display-0.1.0/tests/test_decimate.py +113 -0
- audace_display-0.1.0/tests/test_demod.py +93 -0
- audace_display-0.1.0/tests/test_plotting_qt.py +118 -0
- audace_display-0.1.0/tests/test_processing.py +67 -0
- audace_display-0.1.0/tests/test_reader.py +89 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Invisensing
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: audace-display
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Visualization of Audace DAS acquisition files (.dat / .hdf5 / .tdms / .sgy).
|
|
5
|
+
Author: Invisensing
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/invisensing-io/audace-display
|
|
8
|
+
Project-URL: Source, https://github.com/invisensing-io/audace-display
|
|
9
|
+
Project-URL: Issues, https://github.com/invisensing-io/audace-display/issues
|
|
10
|
+
Keywords: DAS,fiber-optic-sensing,visualization,audace,invisensing,waterfall
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numpy>=1.21
|
|
23
|
+
Requires-Dist: matplotlib>=3.5
|
|
24
|
+
Requires-Dist: invisensing>=1.1.0
|
|
25
|
+
Provides-Extra: interactive
|
|
26
|
+
Requires-Dist: pyqtgraph>=0.13; extra == "interactive"
|
|
27
|
+
Requires-Dist: PyQt5>=5.15; extra == "interactive"
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# audace-display
|
|
33
|
+
|
|
34
|
+
Visualization of **Audace DAS** acquisition files -- simple, fast, safe.
|
|
35
|
+
|
|
36
|
+
`audace-display` reads any format produced by the Filewriter
|
|
37
|
+
(`.dat`, `.hdf5`, `.tdms`, `.sgy`) via the [`invisensing`](https://pypi.org/project/invisensing/)
|
|
38
|
+
library, automatically detects the file mode, and shows:
|
|
39
|
+
|
|
40
|
+
- a **heatmap** (time x distance waterfall) for **demodulated** files,
|
|
41
|
+
- an **animated oscilloscope** (lines replayed in real time, looping) for **raw**
|
|
42
|
+
files.
|
|
43
|
+
|
|
44
|
+
Reading is **streamed and decimated**: a multi-GB file is reduced to screen
|
|
45
|
+
resolution without ever being fully loaded into memory.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install audace-display # matplotlib (static + interactive + PNG)
|
|
51
|
+
pip install audace-display[interactive] # + pyqtgraph backend (fast interactive)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Dependencies: `numpy`, `matplotlib`, `invisensing` (>= 1.1.0 recommended for the
|
|
55
|
+
O(1) seek and TDMS/SEG-Y streaming; works from 1.0.0). Optional `interactive`
|
|
56
|
+
extra: `pyqtgraph` + `PyQt5`.
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Auto: heatmap if demodulated, oscilloscope if raw -- zero options
|
|
62
|
+
audace-display acquisition.dat
|
|
63
|
+
|
|
64
|
+
# PNG export (server / headless mode, no X11/Wayland)
|
|
65
|
+
audace-display acquisition.dat --save output.png
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
That's all for everyday use. The rest is fine-grained control.
|
|
69
|
+
|
|
70
|
+
## Display (backends)
|
|
71
|
+
|
|
72
|
+
| Mode | Command | For |
|
|
73
|
+
|------|---------|-----|
|
|
74
|
+
| interactive matplotlib | *(default)* | quick look, publication quality |
|
|
75
|
+
| headless PNG | `--save out.png` | server without X11/Wayland, batch scripts |
|
|
76
|
+
| interactive pyqtgraph | `--backend pyqtgraph` | smooth exploration (GPU pan/zoom) on large files |
|
|
77
|
+
|
|
78
|
+
matplotlib stays the default (simple, no heavy dependency). For smooth pan/zoom
|
|
79
|
+
on long acquisitions, `--backend pyqtgraph` is far more responsive (GPU
|
|
80
|
+
re-render + on-the-fly downsampling) -- install the extra
|
|
81
|
+
`pip install audace-display[interactive]`. `--save` always renders the PNG via
|
|
82
|
+
matplotlib, regardless of `--backend`.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
audace-display heatmap acq.dat --backend pyqtgraph # fast viewer
|
|
86
|
+
audace-display acq.dat --backend pyqtgraph # same, in auto mode
|
|
87
|
+
audace-display acq.dat --save out.png # headless PNG
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Raw mode: real-time oscilloscope
|
|
91
|
+
|
|
92
|
+
For a **raw** file, the default is an **animated oscilloscope**: each frame draws
|
|
93
|
+
the current line (pulse) -- amplitude vs distance along the fiber -- and the
|
|
94
|
+
cursor **advances at `trigger_frequency` lines/second** (real time), **looping**.
|
|
95
|
+
At high rates, lines are skipped to stay real-time (the pace stays exact, locked
|
|
96
|
+
to the clock). Optimized: O(1) per-line seek (invisensing >= 1.1.0), "peak"
|
|
97
|
+
decimation of large lines, reused curve, frozen axes. For maximum smoothness,
|
|
98
|
+
`--backend pyqtgraph`.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
audace-display scope acq.dat # real time, looping
|
|
102
|
+
audace-display scope acq.dat --speed 0.25 # 4x slow motion
|
|
103
|
+
audace-display scope acq.dat --fps 60 --backend pyqtgraph
|
|
104
|
+
audace-display scope acq.dat --start-time 1.0 --duration 0.5 # loop over 0.5 s
|
|
105
|
+
audace-display scope acq.dat --line 1000 --save frame.png # 1 frozen line (PNG)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| Option | Effect |
|
|
109
|
+
|--------|--------|
|
|
110
|
+
| `--speed F` | Playback speed (1.0 = real time = `trig_frequency` lines/s) |
|
|
111
|
+
| `--fps N` | Render frames/s (default 30) |
|
|
112
|
+
| `--line N` | Frozen line exported with `--save` (default 0) |
|
|
113
|
+
| `--max-points N` | Max points drawn per line (peak decimation). Default 4000 |
|
|
114
|
+
|
|
115
|
+
Requires `invisensing >= 1.1.0` (O(1) per-line seek).
|
|
116
|
+
|
|
117
|
+
## Subcommands
|
|
118
|
+
|
|
119
|
+
| Command | Role |
|
|
120
|
+
|-----------|------|
|
|
121
|
+
| *(none)* | Auto: heatmap (demodulated) or animated oscilloscope (raw) |
|
|
122
|
+
| `info` | Header metadata + stats of the default channel |
|
|
123
|
+
| `heatmap` | Time x distance waterfall, color = channel |
|
|
124
|
+
| `scope` | Oscilloscope: replays the lines (pulses) in real time, looping |
|
|
125
|
+
| `fft` | Temporal spectrum (FFT along the pulses) at 1+ position(s) |
|
|
126
|
+
| `trace` | 1-D time trace at one position |
|
|
127
|
+
| `demod` | Demodulate via an **external script**, then show as heatmap |
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
audace-display info acquisition.dat
|
|
131
|
+
audace-display heatmap acquisition.dat --channel magnitude --db
|
|
132
|
+
audace-display heatmap acquisition.dat --start-time 0.5 --duration 1.0
|
|
133
|
+
audace-display heatmap acquisition.dat --start-distance 50 --end-distance 200
|
|
134
|
+
audace-display fft acquisition.dat --position 120
|
|
135
|
+
audace-display fft acquisition.dat --position 50,100,150 --db
|
|
136
|
+
audace-display fft acquisition.dat --position-range 100:200 --window blackman
|
|
137
|
+
audace-display trace acquisition.dat --position 100
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Channels per mode
|
|
141
|
+
|
|
142
|
+
| File mode | Valid channels | Default |
|
|
143
|
+
|--------------------|-----------------------------------------------------|-------------|
|
|
144
|
+
| `raw` | `raw` | `raw` |
|
|
145
|
+
| `iq` | `i`, `q`, `magnitude` (\|IQ\|), `phase_wrapped` | `magnitude` |
|
|
146
|
+
| `arctan_magnitude` | `arctan`, `magnitude` (sqrt(I^2+Q^2)) | `magnitude` |
|
|
147
|
+
| `phase` | `phase` | `phase` |
|
|
148
|
+
|
|
149
|
+
Common aliases: `mag`/`amp` -> `magnitude`, `atan` -> `arctan`, `angle` ->
|
|
150
|
+
`phase_wrapped`. **No demodulation** is applied: channels are read as the
|
|
151
|
+
Filewriter wrote them.
|
|
152
|
+
|
|
153
|
+
## Large files (200 MB - 10 GB)
|
|
154
|
+
|
|
155
|
+
The heatmap is computed in a **single streaming pass**: pulses are read in chunks
|
|
156
|
+
and aggregated on the fly into a `--max-time-bins x --max-space-bins` grid
|
|
157
|
+
(2000 x 2000 by default). RAM does not depend on file size. Line plots use a
|
|
158
|
+
**min/max-preserving decimation** (no visual aliasing).
|
|
159
|
+
|
|
160
|
+
| Option | Effect |
|
|
161
|
+
|--------|--------|
|
|
162
|
+
| `--max-time-bins N` | Temporal (X) resolution of the waterfall. Default 2000 |
|
|
163
|
+
| `--max-space-bins N` | Spatial (Y) resolution of the waterfall. Default 2000 |
|
|
164
|
+
| `--reduce {mean,rms,std,peak}` | Temporal aggregation statistic. Default `mean` |
|
|
165
|
+
| `--max-pulses N` | Bound the number of pulses read (default: whole file) |
|
|
166
|
+
| `--subsample-time N` | (fft/trace) keep only one pulse out of N |
|
|
167
|
+
|
|
168
|
+
`.dat` and `.hdf5` stream natively; `.tdms` and `.sgy` also stream with
|
|
169
|
+
`invisensing >= 1.1.0`. `--start-time` seeks directly (O(1) seek) with 1.1.0.
|
|
170
|
+
|
|
171
|
+
## Demodulation via external plugin
|
|
172
|
+
|
|
173
|
+
`audace-display` contains **no demodulation code**: it defines a contract and
|
|
174
|
+
loads a Python script that **you** provide (never published). The result is
|
|
175
|
+
displayed as a heatmap.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
# my_demod.py
|
|
179
|
+
import numpy as np
|
|
180
|
+
|
|
181
|
+
class Demodulator:
|
|
182
|
+
OUTPUT_LABEL = "phase (rad)" # colorbar label (optional)
|
|
183
|
+
IS_ANGULAR = True # diverging colormap (optional)
|
|
184
|
+
|
|
185
|
+
def __init__(self, *, sample_rate, trig_frequency, line_size, meta):
|
|
186
|
+
... # init filters / state
|
|
187
|
+
|
|
188
|
+
def process(self, chunk): # called on contiguous time chunks
|
|
189
|
+
# chunk: (rows, line_size) raw -> (rows, positions) float32
|
|
190
|
+
...
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
audace-display demod raw.dat --script my_demod.py --save demod.png
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Stateless alternative: a function
|
|
198
|
+
`demodulate(chunk, *, sample_rate, trig_frequency, line_size, meta) -> ndarray`.
|
|
199
|
+
Chunks arrive **in time order** (compatible with stateful filters).
|
|
200
|
+
|
|
201
|
+
## Programmatic API
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from invisensing import File
|
|
205
|
+
from audace_display import load_decimated, resolve_channel
|
|
206
|
+
|
|
207
|
+
with File("acquisition.dat") as f:
|
|
208
|
+
_, resolver, label, is_angular = resolve_channel(None, f.mode)
|
|
209
|
+
res = load_decimated(f, lambda raw: resolver(f, raw),
|
|
210
|
+
max_time_bins=1000, max_space_bins=1000)
|
|
211
|
+
# res.data: (n_time, n_space) float32 ; res.t_extent, res.d_extent
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Default behavior
|
|
215
|
+
|
|
216
|
+
- **Interactive display** by default; `--save out.png` for headless mode (the
|
|
217
|
+
`Agg` backend is chosen automatically when there is no display).
|
|
218
|
+
- **Auto colormap**: `viridis` (sequential) for magnitude/raw, `RdBu_r`
|
|
219
|
+
(diverging, centered on 0) for phase/arctan. Override with `--cmap`.
|
|
220
|
+
- **Auto-scaling**: 1/99 percentiles in linear, `median - 30 dB -> max` in dB,
|
|
221
|
+
centered on 0 for angular channels. Override with `--vmin`/`--vmax`.
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# audace-display
|
|
2
|
+
|
|
3
|
+
Visualization of **Audace DAS** acquisition files -- simple, fast, safe.
|
|
4
|
+
|
|
5
|
+
`audace-display` reads any format produced by the Filewriter
|
|
6
|
+
(`.dat`, `.hdf5`, `.tdms`, `.sgy`) via the [`invisensing`](https://pypi.org/project/invisensing/)
|
|
7
|
+
library, automatically detects the file mode, and shows:
|
|
8
|
+
|
|
9
|
+
- a **heatmap** (time x distance waterfall) for **demodulated** files,
|
|
10
|
+
- an **animated oscilloscope** (lines replayed in real time, looping) for **raw**
|
|
11
|
+
files.
|
|
12
|
+
|
|
13
|
+
Reading is **streamed and decimated**: a multi-GB file is reduced to screen
|
|
14
|
+
resolution without ever being fully loaded into memory.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install audace-display # matplotlib (static + interactive + PNG)
|
|
20
|
+
pip install audace-display[interactive] # + pyqtgraph backend (fast interactive)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Dependencies: `numpy`, `matplotlib`, `invisensing` (>= 1.1.0 recommended for the
|
|
24
|
+
O(1) seek and TDMS/SEG-Y streaming; works from 1.0.0). Optional `interactive`
|
|
25
|
+
extra: `pyqtgraph` + `PyQt5`.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Auto: heatmap if demodulated, oscilloscope if raw -- zero options
|
|
31
|
+
audace-display acquisition.dat
|
|
32
|
+
|
|
33
|
+
# PNG export (server / headless mode, no X11/Wayland)
|
|
34
|
+
audace-display acquisition.dat --save output.png
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That's all for everyday use. The rest is fine-grained control.
|
|
38
|
+
|
|
39
|
+
## Display (backends)
|
|
40
|
+
|
|
41
|
+
| Mode | Command | For |
|
|
42
|
+
|------|---------|-----|
|
|
43
|
+
| interactive matplotlib | *(default)* | quick look, publication quality |
|
|
44
|
+
| headless PNG | `--save out.png` | server without X11/Wayland, batch scripts |
|
|
45
|
+
| interactive pyqtgraph | `--backend pyqtgraph` | smooth exploration (GPU pan/zoom) on large files |
|
|
46
|
+
|
|
47
|
+
matplotlib stays the default (simple, no heavy dependency). For smooth pan/zoom
|
|
48
|
+
on long acquisitions, `--backend pyqtgraph` is far more responsive (GPU
|
|
49
|
+
re-render + on-the-fly downsampling) -- install the extra
|
|
50
|
+
`pip install audace-display[interactive]`. `--save` always renders the PNG via
|
|
51
|
+
matplotlib, regardless of `--backend`.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
audace-display heatmap acq.dat --backend pyqtgraph # fast viewer
|
|
55
|
+
audace-display acq.dat --backend pyqtgraph # same, in auto mode
|
|
56
|
+
audace-display acq.dat --save out.png # headless PNG
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Raw mode: real-time oscilloscope
|
|
60
|
+
|
|
61
|
+
For a **raw** file, the default is an **animated oscilloscope**: each frame draws
|
|
62
|
+
the current line (pulse) -- amplitude vs distance along the fiber -- and the
|
|
63
|
+
cursor **advances at `trigger_frequency` lines/second** (real time), **looping**.
|
|
64
|
+
At high rates, lines are skipped to stay real-time (the pace stays exact, locked
|
|
65
|
+
to the clock). Optimized: O(1) per-line seek (invisensing >= 1.1.0), "peak"
|
|
66
|
+
decimation of large lines, reused curve, frozen axes. For maximum smoothness,
|
|
67
|
+
`--backend pyqtgraph`.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
audace-display scope acq.dat # real time, looping
|
|
71
|
+
audace-display scope acq.dat --speed 0.25 # 4x slow motion
|
|
72
|
+
audace-display scope acq.dat --fps 60 --backend pyqtgraph
|
|
73
|
+
audace-display scope acq.dat --start-time 1.0 --duration 0.5 # loop over 0.5 s
|
|
74
|
+
audace-display scope acq.dat --line 1000 --save frame.png # 1 frozen line (PNG)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Option | Effect |
|
|
78
|
+
|--------|--------|
|
|
79
|
+
| `--speed F` | Playback speed (1.0 = real time = `trig_frequency` lines/s) |
|
|
80
|
+
| `--fps N` | Render frames/s (default 30) |
|
|
81
|
+
| `--line N` | Frozen line exported with `--save` (default 0) |
|
|
82
|
+
| `--max-points N` | Max points drawn per line (peak decimation). Default 4000 |
|
|
83
|
+
|
|
84
|
+
Requires `invisensing >= 1.1.0` (O(1) per-line seek).
|
|
85
|
+
|
|
86
|
+
## Subcommands
|
|
87
|
+
|
|
88
|
+
| Command | Role |
|
|
89
|
+
|-----------|------|
|
|
90
|
+
| *(none)* | Auto: heatmap (demodulated) or animated oscilloscope (raw) |
|
|
91
|
+
| `info` | Header metadata + stats of the default channel |
|
|
92
|
+
| `heatmap` | Time x distance waterfall, color = channel |
|
|
93
|
+
| `scope` | Oscilloscope: replays the lines (pulses) in real time, looping |
|
|
94
|
+
| `fft` | Temporal spectrum (FFT along the pulses) at 1+ position(s) |
|
|
95
|
+
| `trace` | 1-D time trace at one position |
|
|
96
|
+
| `demod` | Demodulate via an **external script**, then show as heatmap |
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
audace-display info acquisition.dat
|
|
100
|
+
audace-display heatmap acquisition.dat --channel magnitude --db
|
|
101
|
+
audace-display heatmap acquisition.dat --start-time 0.5 --duration 1.0
|
|
102
|
+
audace-display heatmap acquisition.dat --start-distance 50 --end-distance 200
|
|
103
|
+
audace-display fft acquisition.dat --position 120
|
|
104
|
+
audace-display fft acquisition.dat --position 50,100,150 --db
|
|
105
|
+
audace-display fft acquisition.dat --position-range 100:200 --window blackman
|
|
106
|
+
audace-display trace acquisition.dat --position 100
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Channels per mode
|
|
110
|
+
|
|
111
|
+
| File mode | Valid channels | Default |
|
|
112
|
+
|--------------------|-----------------------------------------------------|-------------|
|
|
113
|
+
| `raw` | `raw` | `raw` |
|
|
114
|
+
| `iq` | `i`, `q`, `magnitude` (\|IQ\|), `phase_wrapped` | `magnitude` |
|
|
115
|
+
| `arctan_magnitude` | `arctan`, `magnitude` (sqrt(I^2+Q^2)) | `magnitude` |
|
|
116
|
+
| `phase` | `phase` | `phase` |
|
|
117
|
+
|
|
118
|
+
Common aliases: `mag`/`amp` -> `magnitude`, `atan` -> `arctan`, `angle` ->
|
|
119
|
+
`phase_wrapped`. **No demodulation** is applied: channels are read as the
|
|
120
|
+
Filewriter wrote them.
|
|
121
|
+
|
|
122
|
+
## Large files (200 MB - 10 GB)
|
|
123
|
+
|
|
124
|
+
The heatmap is computed in a **single streaming pass**: pulses are read in chunks
|
|
125
|
+
and aggregated on the fly into a `--max-time-bins x --max-space-bins` grid
|
|
126
|
+
(2000 x 2000 by default). RAM does not depend on file size. Line plots use a
|
|
127
|
+
**min/max-preserving decimation** (no visual aliasing).
|
|
128
|
+
|
|
129
|
+
| Option | Effect |
|
|
130
|
+
|--------|--------|
|
|
131
|
+
| `--max-time-bins N` | Temporal (X) resolution of the waterfall. Default 2000 |
|
|
132
|
+
| `--max-space-bins N` | Spatial (Y) resolution of the waterfall. Default 2000 |
|
|
133
|
+
| `--reduce {mean,rms,std,peak}` | Temporal aggregation statistic. Default `mean` |
|
|
134
|
+
| `--max-pulses N` | Bound the number of pulses read (default: whole file) |
|
|
135
|
+
| `--subsample-time N` | (fft/trace) keep only one pulse out of N |
|
|
136
|
+
|
|
137
|
+
`.dat` and `.hdf5` stream natively; `.tdms` and `.sgy` also stream with
|
|
138
|
+
`invisensing >= 1.1.0`. `--start-time` seeks directly (O(1) seek) with 1.1.0.
|
|
139
|
+
|
|
140
|
+
## Demodulation via external plugin
|
|
141
|
+
|
|
142
|
+
`audace-display` contains **no demodulation code**: it defines a contract and
|
|
143
|
+
loads a Python script that **you** provide (never published). The result is
|
|
144
|
+
displayed as a heatmap.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# my_demod.py
|
|
148
|
+
import numpy as np
|
|
149
|
+
|
|
150
|
+
class Demodulator:
|
|
151
|
+
OUTPUT_LABEL = "phase (rad)" # colorbar label (optional)
|
|
152
|
+
IS_ANGULAR = True # diverging colormap (optional)
|
|
153
|
+
|
|
154
|
+
def __init__(self, *, sample_rate, trig_frequency, line_size, meta):
|
|
155
|
+
... # init filters / state
|
|
156
|
+
|
|
157
|
+
def process(self, chunk): # called on contiguous time chunks
|
|
158
|
+
# chunk: (rows, line_size) raw -> (rows, positions) float32
|
|
159
|
+
...
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
audace-display demod raw.dat --script my_demod.py --save demod.png
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Stateless alternative: a function
|
|
167
|
+
`demodulate(chunk, *, sample_rate, trig_frequency, line_size, meta) -> ndarray`.
|
|
168
|
+
Chunks arrive **in time order** (compatible with stateful filters).
|
|
169
|
+
|
|
170
|
+
## Programmatic API
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from invisensing import File
|
|
174
|
+
from audace_display import load_decimated, resolve_channel
|
|
175
|
+
|
|
176
|
+
with File("acquisition.dat") as f:
|
|
177
|
+
_, resolver, label, is_angular = resolve_channel(None, f.mode)
|
|
178
|
+
res = load_decimated(f, lambda raw: resolver(f, raw),
|
|
179
|
+
max_time_bins=1000, max_space_bins=1000)
|
|
180
|
+
# res.data: (n_time, n_space) float32 ; res.t_extent, res.d_extent
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Default behavior
|
|
184
|
+
|
|
185
|
+
- **Interactive display** by default; `--save out.png` for headless mode (the
|
|
186
|
+
`Agg` backend is chosen automatically when there is no display).
|
|
187
|
+
- **Auto colormap**: `viridis` (sequential) for magnitude/raw, `RdBu_r`
|
|
188
|
+
(diverging, centered on 0) for phase/arctan. Override with `--cmap`.
|
|
189
|
+
- **Auto-scaling**: 1/99 percentiles in linear, `median - 30 dB -> max` in dB,
|
|
190
|
+
centered on 0 for angular channels. Override with `--vmin`/`--vmax`.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "audace-display"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Visualization of Audace DAS acquisition files (.dat / .hdf5 / .tdms / .sgy)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Invisensing" }]
|
|
14
|
+
keywords = ["DAS", "fiber-optic-sensing", "visualization", "audace", "invisensing", "waterfall"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"numpy>=1.21",
|
|
27
|
+
"matplotlib>=3.5",
|
|
28
|
+
"invisensing>=1.1.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
# Fast interactive backend (smooth pan/zoom). Otherwise: matplotlib (always present).
|
|
33
|
+
interactive = ["pyqtgraph>=0.13", "PyQt5>=5.15"]
|
|
34
|
+
test = ["pytest>=7"]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
audace-display = "audace_display.cli:main"
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/invisensing-io/audace-display"
|
|
41
|
+
Source = "https://github.com/invisensing-io/audace-display"
|
|
42
|
+
Issues = "https://github.com/invisensing-io/audace-display/issues"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
addopts = "-q"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""audace-display -- visualization of Audace DAS acquisition files.
|
|
2
|
+
|
|
3
|
+
Reads any format produced by the Filewriter (``.dat`` / ``.hdf5`` / ``.tdms`` /
|
|
4
|
+
``.sgy``) via the :mod:`invisensing` library, and shows:
|
|
5
|
+
|
|
6
|
+
- a **heatmap** (time x distance waterfall) for demodulated files,
|
|
7
|
+
- an **animated oscilloscope** for raw files,
|
|
8
|
+
|
|
9
|
+
with automatic mode detection. *Streaming* decimation to handle multi-GB files
|
|
10
|
+
in bounded RAM.
|
|
11
|
+
|
|
12
|
+
CLI: ``audace-display FILE`` (auto) or subcommands
|
|
13
|
+
``info`` / ``heatmap`` / ``fft`` / ``trace`` / ``scope`` / ``demod``.
|
|
14
|
+
|
|
15
|
+
Programmatic API (stable):
|
|
16
|
+
|
|
17
|
+
from audace_display import load_decimated, load_columns, resolve_channel
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from ._errors import AudaceDisplayError
|
|
22
|
+
from .channels import resolve_channel, CHANNELS, DEFAULT_CHANNEL
|
|
23
|
+
from .reader import (
|
|
24
|
+
load_decimated,
|
|
25
|
+
load_columns,
|
|
26
|
+
position_step_m,
|
|
27
|
+
time_window_pulses,
|
|
28
|
+
DecimatedResult,
|
|
29
|
+
)
|
|
30
|
+
from .demod import load_demodulator, DemodPlugin
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AudaceDisplayError",
|
|
36
|
+
"resolve_channel",
|
|
37
|
+
"CHANNELS",
|
|
38
|
+
"DEFAULT_CHANNEL",
|
|
39
|
+
"load_decimated",
|
|
40
|
+
"load_columns",
|
|
41
|
+
"position_step_m",
|
|
42
|
+
"time_window_pulses",
|
|
43
|
+
"DecimatedResult",
|
|
44
|
+
"load_demodulator",
|
|
45
|
+
"DemodPlugin",
|
|
46
|
+
"__version__",
|
|
47
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Single error type for audace-display.
|
|
2
|
+
|
|
3
|
+
Any user-facing error condition (unreadable file, unknown channel, empty slice,
|
|
4
|
+
invalid demod plugin...) raises :class:`AudaceDisplayError`. The CLI catches it,
|
|
5
|
+
prints the message and exits with a non-zero code, without a noisy traceback.
|
|
6
|
+
Other exceptions propagate (a bug to fix).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AudaceDisplayError(Exception):
|
|
12
|
+
"""Expected error, to be presented cleanly to the user."""
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Registry of displayable *channels*.
|
|
2
|
+
|
|
3
|
+
A channel is a 2-D numpy view ``(rows, positions)`` of a file's content for a
|
|
4
|
+
given mode. The *resolver* converts the raw buffer returned by
|
|
5
|
+
:meth:`invisensing.File.read_lines` (i16 / i32 / f32, possibly interleaved) into
|
|
6
|
+
a physical ``float32`` value.
|
|
7
|
+
|
|
8
|
+
No demodulation here: we only **read** channels already written by the
|
|
9
|
+
Filewriter (raw, I/Q, arctan+magnitude, phase). ``phase_wrapped`` (``arg(I+jQ)``)
|
|
10
|
+
is just a *view* of already-produced IQ data, not a demodulation.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Callable, Optional
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from invisensing import Mode
|
|
18
|
+
|
|
19
|
+
from ._errors import AudaceDisplayError
|
|
20
|
+
|
|
21
|
+
# Resolver: (File, raw_buffer) -> ndarray (rows, positions) float32
|
|
22
|
+
Resolver = Callable[[object, np.ndarray], np.ndarray]
|
|
23
|
+
|
|
24
|
+
# Common aliases -> canonical name
|
|
25
|
+
ALIASES: dict[str, str] = {
|
|
26
|
+
# raw ADC
|
|
27
|
+
"raw": "raw", "adc": "raw",
|
|
28
|
+
# IQ
|
|
29
|
+
"i": "i", "q": "q",
|
|
30
|
+
# magnitude (= |IQ| or sqrt(I^2+Q^2) depending on mode)
|
|
31
|
+
"magnitude": "magnitude", "mag": "magnitude",
|
|
32
|
+
"amp": "magnitude", "amplitude": "magnitude", "|iq|": "magnitude",
|
|
33
|
+
# arctan (ARCTAN_MAGNITUDE mode)
|
|
34
|
+
"arctan": "arctan", "atan": "arctan",
|
|
35
|
+
# phase wrapped from IQ (!= demodulated phase of PHASE mode)
|
|
36
|
+
"phase_wrapped": "phase_wrapped", "wrapped": "phase_wrapped",
|
|
37
|
+
"angle": "phase_wrapped",
|
|
38
|
+
# demodulated phase (PHASE mode)
|
|
39
|
+
"phase": "phase", "p": "phase", "phi": "phase",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _ch_raw(f, buf: np.ndarray) -> np.ndarray:
|
|
44
|
+
"""Raw ADC codes -> volts via the header `range`."""
|
|
45
|
+
return buf.astype(np.float32) * np.float32(f.range / 32768.0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _ch_magnitude(f, buf: np.ndarray) -> np.ndarray:
|
|
49
|
+
"""Magnitude in volts. `|IQ|` in IQ mode, `sqrt(I^2+Q^2)` in ArctanMag."""
|
|
50
|
+
if f.mode is Mode.IQ:
|
|
51
|
+
return np.abs(f.get_iq_volts(buf)).astype(np.float32)
|
|
52
|
+
return f.get_magnitude_volts(buf)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _ch_phase_wrapped(f, buf: np.ndarray) -> np.ndarray:
|
|
56
|
+
"""`arg(I+jQ)` in radians in [-pi, +pi[ from an IQ file."""
|
|
57
|
+
return np.angle(f.get_iq_volts(buf)).astype(np.float32)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# canonical -> (valid modes, resolver, colorbar/y label, angular?)
|
|
61
|
+
CHANNELS: dict[str, tuple[set, Resolver, str, bool]] = {
|
|
62
|
+
"raw": ({Mode.RAW}, _ch_raw, "ADC (V)", False),
|
|
63
|
+
"i": ({Mode.IQ}, lambda f, b: f.get_i_volts(b), "I (V)", False),
|
|
64
|
+
"q": ({Mode.IQ}, lambda f, b: f.get_q_volts(b), "Q (V)", False),
|
|
65
|
+
"magnitude": ({Mode.IQ, Mode.ARCTAN_MAGNITUDE}, _ch_magnitude, "|signal| (V)", False),
|
|
66
|
+
"phase_wrapped": ({Mode.IQ}, _ch_phase_wrapped, "arg(I+jQ) (rad)", True),
|
|
67
|
+
"arctan": ({Mode.ARCTAN_MAGNITUDE}, lambda f, b: f.get_arctan_radians(b), "arctan(Q/I) (rad)", True),
|
|
68
|
+
"phase": ({Mode.PHASE}, lambda f, b: f.get_phase(b), "phase (rad)", True),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
DEFAULT_CHANNEL: dict[Mode, str] = {
|
|
72
|
+
Mode.RAW: "raw",
|
|
73
|
+
Mode.IQ: "magnitude",
|
|
74
|
+
Mode.ARCTAN_MAGNITUDE: "magnitude",
|
|
75
|
+
Mode.PHASE: "phase",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def resolve_channel(name: Optional[str], mode: Mode) -> tuple[str, Resolver, str, bool]:
|
|
80
|
+
"""Resolve a user name into ``(canonical, resolver, label, is_angular)``.
|
|
81
|
+
|
|
82
|
+
``name=None`` -> the mode's default channel. Raises
|
|
83
|
+
:class:`AudaceDisplayError` if the name is unknown or unavailable for the
|
|
84
|
+
file's mode.
|
|
85
|
+
"""
|
|
86
|
+
if name is None:
|
|
87
|
+
canonical = DEFAULT_CHANNEL[mode]
|
|
88
|
+
else:
|
|
89
|
+
canonical = ALIASES.get(name.lower())
|
|
90
|
+
if canonical is None:
|
|
91
|
+
known = ", ".join(sorted(set(ALIASES.values())))
|
|
92
|
+
raise AudaceDisplayError(f"unknown channel '{name}'. Known: {known}")
|
|
93
|
+
|
|
94
|
+
valid_modes, resolver, label, is_angular = CHANNELS[canonical]
|
|
95
|
+
if mode not in valid_modes:
|
|
96
|
+
for_mode = ", ".join(
|
|
97
|
+
sorted(k for k, (modes, *_rest) in CHANNELS.items() if mode in modes)
|
|
98
|
+
)
|
|
99
|
+
raise AudaceDisplayError(
|
|
100
|
+
f"channel '{canonical}' unavailable in mode {mode.value}. "
|
|
101
|
+
f"Available for this mode: {for_mode}"
|
|
102
|
+
)
|
|
103
|
+
return canonical, resolver, label, is_angular
|