audace-display 0.1.0__py3-none-any.whl
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/__init__.py +47 -0
- audace_display/__main__.py +9 -0
- audace_display/_errors.py +12 -0
- audace_display/channels.py +103 -0
- audace_display/cli.py +789 -0
- audace_display/decimate.py +162 -0
- audace_display/demod.py +196 -0
- audace_display/plotting.py +190 -0
- audace_display/plotting_qt.py +237 -0
- audace_display/processing.py +165 -0
- audace_display/reader.py +250 -0
- audace_display-0.1.0.dist-info/METADATA +225 -0
- audace_display-0.1.0.dist-info/RECORD +17 -0
- audace_display-0.1.0.dist-info/WHEEL +5 -0
- audace_display-0.1.0.dist-info/entry_points.txt +2 -0
- audace_display-0.1.0.dist-info/licenses/LICENSE +21 -0
- audace_display-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|