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.
Files changed (29) hide show
  1. audace_display-0.1.0/LICENSE +21 -0
  2. audace_display-0.1.0/PKG-INFO +225 -0
  3. audace_display-0.1.0/README.md +194 -0
  4. audace_display-0.1.0/pyproject.toml +49 -0
  5. audace_display-0.1.0/setup.cfg +4 -0
  6. audace_display-0.1.0/src/audace_display/__init__.py +47 -0
  7. audace_display-0.1.0/src/audace_display/__main__.py +9 -0
  8. audace_display-0.1.0/src/audace_display/_errors.py +12 -0
  9. audace_display-0.1.0/src/audace_display/channels.py +103 -0
  10. audace_display-0.1.0/src/audace_display/cli.py +789 -0
  11. audace_display-0.1.0/src/audace_display/decimate.py +162 -0
  12. audace_display-0.1.0/src/audace_display/demod.py +196 -0
  13. audace_display-0.1.0/src/audace_display/plotting.py +190 -0
  14. audace_display-0.1.0/src/audace_display/plotting_qt.py +237 -0
  15. audace_display-0.1.0/src/audace_display/processing.py +165 -0
  16. audace_display-0.1.0/src/audace_display/reader.py +250 -0
  17. audace_display-0.1.0/src/audace_display.egg-info/PKG-INFO +225 -0
  18. audace_display-0.1.0/src/audace_display.egg-info/SOURCES.txt +27 -0
  19. audace_display-0.1.0/src/audace_display.egg-info/dependency_links.txt +1 -0
  20. audace_display-0.1.0/src/audace_display.egg-info/entry_points.txt +2 -0
  21. audace_display-0.1.0/src/audace_display.egg-info/requires.txt +10 -0
  22. audace_display-0.1.0/src/audace_display.egg-info/top_level.txt +1 -0
  23. audace_display-0.1.0/tests/test_channels.py +48 -0
  24. audace_display-0.1.0/tests/test_cli.py +157 -0
  25. audace_display-0.1.0/tests/test_decimate.py +113 -0
  26. audace_display-0.1.0/tests/test_demod.py +93 -0
  27. audace_display-0.1.0/tests/test_plotting_qt.py +118 -0
  28. audace_display-0.1.0/tests/test_processing.py +67 -0
  29. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,9 @@
1
+ """Enables ``python -m audace_display``."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+ from .cli import main
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
@@ -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