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.
@@ -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