eigsep_observing 2.5.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.
- eigsep_observing-2.5.0/LICENSE +21 -0
- eigsep_observing-2.5.0/PKG-INFO +79 -0
- eigsep_observing-2.5.0/README.md +45 -0
- eigsep_observing-2.5.0/pyproject.toml +105 -0
- eigsep_observing-2.5.0/setup.cfg +4 -0
- eigsep_observing-2.5.0/src/eigsep_observing/__init__.py +25 -0
- eigsep_observing-2.5.0/src/eigsep_observing/_scripts_util.py +115 -0
- eigsep_observing-2.5.0/src/eigsep_observing/_test_fixtures.py +293 -0
- eigsep_observing-2.5.0/src/eigsep_observing/adc.py +134 -0
- eigsep_observing-2.5.0/src/eigsep_observing/blocks.py +846 -0
- eigsep_observing-2.5.0/src/eigsep_observing/client.py +998 -0
- eigsep_observing-2.5.0/src/eigsep_observing/config/corr_config.yaml +41 -0
- eigsep_observing-2.5.0/src/eigsep_observing/config/dummy_config.yaml +64 -0
- eigsep_observing-2.5.0/src/eigsep_observing/config/live_status_thresholds.yaml +52 -0
- eigsep_observing-2.5.0/src/eigsep_observing/config/obs_config.yaml +79 -0
- eigsep_observing-2.5.0/src/eigsep_observing/config/snap_only.yaml +39 -0
- eigsep_observing-2.5.0/src/eigsep_observing/config/test_config.yaml +39 -0
- eigsep_observing-2.5.0/src/eigsep_observing/config/wiring.yaml +52 -0
- eigsep_observing-2.5.0/src/eigsep_observing/contract_tests/__init__.py +12 -0
- eigsep_observing-2.5.0/src/eigsep_observing/contract_tests/test_key_uniqueness.py +58 -0
- eigsep_observing-2.5.0/src/eigsep_observing/contract_tests/test_producer_contracts.py +352 -0
- eigsep_observing-2.5.0/src/eigsep_observing/corr.py +298 -0
- eigsep_observing-2.5.0/src/eigsep_observing/data/eigsep_fengine_1g_v2_3_2024-07-08_1858.fpg +5481 -19
- eigsep_observing-2.5.0/src/eigsep_observing/file_heartbeat.py +82 -0
- eigsep_observing-2.5.0/src/eigsep_observing/fpga.py +1289 -0
- eigsep_observing-2.5.0/src/eigsep_observing/io.py +1765 -0
- eigsep_observing-2.5.0/src/eigsep_observing/keys.py +15 -0
- eigsep_observing-2.5.0/src/eigsep_observing/live_status/__init__.py +20 -0
- eigsep_observing-2.5.0/src/eigsep_observing/live_status/aggregator.py +1113 -0
- eigsep_observing-2.5.0/src/eigsep_observing/live_status/app.py +685 -0
- eigsep_observing-2.5.0/src/eigsep_observing/live_status/calibration.py +76 -0
- eigsep_observing-2.5.0/src/eigsep_observing/live_status/signals.py +263 -0
- eigsep_observing-2.5.0/src/eigsep_observing/live_status/snap_probe.py +61 -0
- eigsep_observing-2.5.0/src/eigsep_observing/live_status/static/css/dashboard.css +248 -0
- eigsep_observing-2.5.0/src/eigsep_observing/live_status/static/js/dashboard.js +809 -0
- eigsep_observing-2.5.0/src/eigsep_observing/live_status/templates/index.html +99 -0
- eigsep_observing-2.5.0/src/eigsep_observing/live_status/thresholds.py +238 -0
- eigsep_observing-2.5.0/src/eigsep_observing/motion_switch.py +100 -0
- eigsep_observing-2.5.0/src/eigsep_observing/motor_client.py +373 -0
- eigsep_observing-2.5.0/src/eigsep_observing/motor_zeroer.py +172 -0
- eigsep_observing-2.5.0/src/eigsep_observing/obs_config_owner.py +108 -0
- eigsep_observing-2.5.0/src/eigsep_observing/observer.py +620 -0
- eigsep_observing-2.5.0/src/eigsep_observing/plot.py +225 -0
- eigsep_observing-2.5.0/src/eigsep_observing/run_tag.py +161 -0
- eigsep_observing-2.5.0/src/eigsep_observing/scripts/__init__.py +0 -0
- eigsep_observing-2.5.0/src/eigsep_observing/scripts/fpga_init.py +204 -0
- eigsep_observing-2.5.0/src/eigsep_observing/scripts/no_switch_observation.py +190 -0
- eigsep_observing-2.5.0/src/eigsep_observing/scripts/observe.py +201 -0
- eigsep_observing-2.5.0/src/eigsep_observing/scripts/panda_observe.py +162 -0
- eigsep_observing-2.5.0/src/eigsep_observing/scripts/vna_position_sweep.py +196 -0
- eigsep_observing-2.5.0/src/eigsep_observing/snap_reinit.py +104 -0
- eigsep_observing-2.5.0/src/eigsep_observing/status_log_handler.py +214 -0
- eigsep_observing-2.5.0/src/eigsep_observing/tempctrl_client.py +362 -0
- eigsep_observing-2.5.0/src/eigsep_observing/testing/__init__.py +12 -0
- eigsep_observing-2.5.0/src/eigsep_observing/testing/client.py +146 -0
- eigsep_observing-2.5.0/src/eigsep_observing/testing/fpga.py +197 -0
- eigsep_observing-2.5.0/src/eigsep_observing/testing/observer.py +34 -0
- eigsep_observing-2.5.0/src/eigsep_observing/testing/utils.py +152 -0
- eigsep_observing-2.5.0/src/eigsep_observing/utils.py +251 -0
- eigsep_observing-2.5.0/src/eigsep_observing/vna.py +475 -0
- eigsep_observing-2.5.0/src/eigsep_observing/vna_calibration.py +95 -0
- eigsep_observing-2.5.0/src/eigsep_observing.egg-info/PKG-INFO +79 -0
- eigsep_observing-2.5.0/src/eigsep_observing.egg-info/SOURCES.txt +97 -0
- eigsep_observing-2.5.0/src/eigsep_observing.egg-info/dependency_links.txt +1 -0
- eigsep_observing-2.5.0/src/eigsep_observing.egg-info/entry_points.txt +6 -0
- eigsep_observing-2.5.0/src/eigsep_observing.egg-info/requires.txt +21 -0
- eigsep_observing-2.5.0/src/eigsep_observing.egg-info/top_level.txt +1 -0
- eigsep_observing-2.5.0/tests/test_adc.py +431 -0
- eigsep_observing-2.5.0/tests/test_casperfpga_api.py +197 -0
- eigsep_observing-2.5.0/tests/test_client.py +2543 -0
- eigsep_observing-2.5.0/tests/test_file_heartbeat.py +110 -0
- eigsep_observing-2.5.0/tests/test_fpga.py +1085 -0
- eigsep_observing-2.5.0/tests/test_integration.py +153 -0
- eigsep_observing-2.5.0/tests/test_io.py +3273 -0
- eigsep_observing-2.5.0/tests/test_live_status_aggregator.py +941 -0
- eigsep_observing-2.5.0/tests/test_live_status_app.py +1293 -0
- eigsep_observing-2.5.0/tests/test_live_status_calibration.py +187 -0
- eigsep_observing-2.5.0/tests/test_live_status_snap_probe.py +51 -0
- eigsep_observing-2.5.0/tests/test_live_status_thresholds.py +314 -0
- eigsep_observing-2.5.0/tests/test_motion_switch.py +194 -0
- eigsep_observing-2.5.0/tests/test_motor_client.py +392 -0
- eigsep_observing-2.5.0/tests/test_motor_scripts.py +416 -0
- eigsep_observing-2.5.0/tests/test_motor_zeroer.py +199 -0
- eigsep_observing-2.5.0/tests/test_obs_config_owner.py +123 -0
- eigsep_observing-2.5.0/tests/test_obs_config_uploaders.py +198 -0
- eigsep_observing-2.5.0/tests/test_observer.py +1710 -0
- eigsep_observing-2.5.0/tests/test_record_metadata.py +156 -0
- eigsep_observing-2.5.0/tests/test_record_vna.py +93 -0
- eigsep_observing-2.5.0/tests/test_redis.py +1017 -0
- eigsep_observing-2.5.0/tests/test_republish_header.py +166 -0
- eigsep_observing-2.5.0/tests/test_run_tag.py +202 -0
- eigsep_observing-2.5.0/tests/test_scripts_util.py +131 -0
- eigsep_observing-2.5.0/tests/test_snap_reinit.py +131 -0
- eigsep_observing-2.5.0/tests/test_tempctrl_client.py +455 -0
- eigsep_observing-2.5.0/tests/test_tempctrl_manual.py +133 -0
- eigsep_observing-2.5.0/tests/test_utils.py +516 -0
- eigsep_observing-2.5.0/tests/test_vna_calibration.py +142 -0
- eigsep_observing-2.5.0/tests/test_vna_helper.py +272 -0
- eigsep_observing-2.5.0/tests/test_vna_manual.py +142 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 EIGSEP
|
|
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,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eigsep_observing
|
|
3
|
+
Version: 2.5.0
|
|
4
|
+
Summary: Control code for EIGSEP observing.
|
|
5
|
+
Author-email: Christian Hellum Bye <cbh@berkeley.edu>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Intended Audience :: Science/Research
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: h5py
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: pyyaml
|
|
17
|
+
Requires-Dist: flask
|
|
18
|
+
Requires-Dist: plotly
|
|
19
|
+
Requires-Dist: picohost>=3.6.0
|
|
20
|
+
Requires-Dist: eigsep-vna>=1.3
|
|
21
|
+
Requires-Dist: eigsep_redis>=2.3.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: ruff; extra == "dev"
|
|
24
|
+
Requires-Dist: fakeredis; extra == "dev"
|
|
25
|
+
Requires-Dist: pyserial-mock; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-timeout; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-xdist; extra == "dev"
|
|
30
|
+
Provides-Extra: interactive
|
|
31
|
+
Requires-Dist: matplotlib; extra == "interactive"
|
|
32
|
+
Requires-Dist: ipython; extra == "interactive"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# EIGSEP Observing
|
|
36
|
+
|
|
37
|
+
[](https://codecov.io/gh/EIGSEP/eigsep_observing)
|
|
38
|
+
|
|
39
|
+
Control code needed to take EIGSEP data.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install -e ".[dev]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Pulls [`eigsep_redis`](https://github.com/EIGSEP/eigsep_redis) as a
|
|
48
|
+
sibling runtime dependency (Redis transport + bus primitives, also
|
|
49
|
+
consumed by `picohost`).
|
|
50
|
+
|
|
51
|
+
### Hardware dependency
|
|
52
|
+
|
|
53
|
+
Talking to the SNAP board requires
|
|
54
|
+
[casperfpga](https://github.com/EIGSEP/casperfpga), which is **not** on PyPI
|
|
55
|
+
and must be installed from source. It is a lazy optional import, so the test
|
|
56
|
+
suite and any dummy-mode / panda-side install does not need it. On the ground
|
|
57
|
+
computer that actually drives the correlator, install the pinned version from
|
|
58
|
+
`hardware-requirements.txt`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install -r hardware-requirements.txt
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
See that file for the current tag (currently **v0.6.0**).
|
|
65
|
+
|
|
66
|
+
## Scripts
|
|
67
|
+
|
|
68
|
+
Observing loops and the startup flow live in `OPERATIONS.md`. Motor
|
|
69
|
+
operations run through `PicoManager` via Redis, so the manager service
|
|
70
|
+
stays up during scans.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Az/el beam scan
|
|
74
|
+
python scripts/motor_control.py [--dummy] [--el_first] [--count N] \
|
|
75
|
+
[--pause_s S] [--sleep_s S]
|
|
76
|
+
|
|
77
|
+
# Interactive zeroing UI (curses)
|
|
78
|
+
python scripts/motor_manual.py [--dummy] [--deg D]
|
|
79
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# EIGSEP Observing
|
|
2
|
+
|
|
3
|
+
[](https://codecov.io/gh/EIGSEP/eigsep_observing)
|
|
4
|
+
|
|
5
|
+
Control code needed to take EIGSEP data.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e ".[dev]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Pulls [`eigsep_redis`](https://github.com/EIGSEP/eigsep_redis) as a
|
|
14
|
+
sibling runtime dependency (Redis transport + bus primitives, also
|
|
15
|
+
consumed by `picohost`).
|
|
16
|
+
|
|
17
|
+
### Hardware dependency
|
|
18
|
+
|
|
19
|
+
Talking to the SNAP board requires
|
|
20
|
+
[casperfpga](https://github.com/EIGSEP/casperfpga), which is **not** on PyPI
|
|
21
|
+
and must be installed from source. It is a lazy optional import, so the test
|
|
22
|
+
suite and any dummy-mode / panda-side install does not need it. On the ground
|
|
23
|
+
computer that actually drives the correlator, install the pinned version from
|
|
24
|
+
`hardware-requirements.txt`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install -r hardware-requirements.txt
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
See that file for the current tag (currently **v0.6.0**).
|
|
31
|
+
|
|
32
|
+
## Scripts
|
|
33
|
+
|
|
34
|
+
Observing loops and the startup flow live in `OPERATIONS.md`. Motor
|
|
35
|
+
operations run through `PicoManager` via Redis, so the manager service
|
|
36
|
+
stays up during scans.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Az/el beam scan
|
|
40
|
+
python scripts/motor_control.py [--dummy] [--el_first] [--count N] \
|
|
41
|
+
[--pause_s S] [--sleep_s S]
|
|
42
|
+
|
|
43
|
+
# Interactive zeroing UI (curses)
|
|
44
|
+
python scripts/motor_manual.py [--dummy] [--deg D]
|
|
45
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=65", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "eigsep_observing"
|
|
7
|
+
version = "2.5.0"
|
|
8
|
+
description = "Control code for EIGSEP observing."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [{ name="Christian Hellum Bye", email="cbh@berkeley.edu" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Intended Audience :: Science/Research",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Astronomy",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"h5py",
|
|
21
|
+
"numpy",
|
|
22
|
+
"pyyaml",
|
|
23
|
+
"flask",
|
|
24
|
+
"plotly",
|
|
25
|
+
"picohost>=3.6.0",
|
|
26
|
+
"eigsep-vna>=1.3",
|
|
27
|
+
"eigsep_redis>=2.3.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
# Console scripts for the observing entry points. They live under
|
|
32
|
+
# src/eigsep_observing/scripts/ so they ship inside the wheel and
|
|
33
|
+
# resolve to /<venv>/bin on install — required by downstream image
|
|
34
|
+
# builds that install from a wheelhouse and have no source checkout.
|
|
35
|
+
# eigsep-fpga-init / eigsep-observe back the backend systemd units;
|
|
36
|
+
# eigsep-panda is operator-launched (loops control physical actuators
|
|
37
|
+
# and self-recover from per-iteration faults — auto-restart adds risk
|
|
38
|
+
# without buying liveness). See deploy/systemd/README.md.
|
|
39
|
+
eigsep-fpga-init = "eigsep_observing.scripts.fpga_init:main"
|
|
40
|
+
eigsep-observe = "eigsep_observing.scripts.observe:main"
|
|
41
|
+
eigsep-panda = "eigsep_observing.scripts.panda_observe:main"
|
|
42
|
+
# Alt-mode panda observers (see scripts/CLAUDE.md): structurally
|
|
43
|
+
# panda_observe-like, mutually exclusive with eigsep-panda. main() takes
|
|
44
|
+
# (transport, args) so tests can drive it directly; cli() is the no-arg
|
|
45
|
+
# entry-point shim that builds the transport from --dummy and forwards.
|
|
46
|
+
eigsep-no-switch-observation = "eigsep_observing.scripts.no_switch_observation:cli"
|
|
47
|
+
eigsep-vna-position-sweep = "eigsep_observing.scripts.vna_position_sweep:cli"
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
dev = [
|
|
51
|
+
"ruff",
|
|
52
|
+
"fakeredis",
|
|
53
|
+
"pyserial-mock",
|
|
54
|
+
"pytest",
|
|
55
|
+
"pytest-cov",
|
|
56
|
+
"pytest-timeout",
|
|
57
|
+
"pytest-xdist",
|
|
58
|
+
]
|
|
59
|
+
interactive = [
|
|
60
|
+
"matplotlib",
|
|
61
|
+
"ipython",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.setuptools]
|
|
65
|
+
include-package-data = true
|
|
66
|
+
|
|
67
|
+
[tool.setuptools.packages.find]
|
|
68
|
+
where = ["src"]
|
|
69
|
+
|
|
70
|
+
[tool.setuptools.package-data]
|
|
71
|
+
"eigsep_observing" = [
|
|
72
|
+
"config/*.yaml",
|
|
73
|
+
"data/*.fpg",
|
|
74
|
+
"live_status/templates/*.html",
|
|
75
|
+
"live_status/static/css/*.css",
|
|
76
|
+
"live_status/static/js/*.js",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
testpaths = ["tests", "src/eigsep_observing/contract_tests"]
|
|
81
|
+
pythonpath = ["src"]
|
|
82
|
+
addopts = [
|
|
83
|
+
"--cov=eigsep_observing",
|
|
84
|
+
"--cov-report=term-missing",
|
|
85
|
+
"--cov-report=xml",
|
|
86
|
+
"--junitxml=junit.xml",
|
|
87
|
+
"-o junit_family=legacy",
|
|
88
|
+
"--timeout=60",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
[tool.coverage.run]
|
|
92
|
+
omit = [
|
|
93
|
+
"src/eigsep_observing/contract_tests/*",
|
|
94
|
+
"src/eigsep_observing/_test_fixtures.py",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.ruff]
|
|
98
|
+
line-length = 79
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint]
|
|
101
|
+
extend-ignore = ["E203"]
|
|
102
|
+
|
|
103
|
+
[tool.ruff.lint.per-file-ignores]
|
|
104
|
+
"src/*/__init__.py" = ["F401"]
|
|
105
|
+
"scripts/*.py" = ["E402"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
__author__ = "Christian Hellum Bye"
|
|
2
|
+
__version__ = "2.5.0"
|
|
3
|
+
|
|
4
|
+
from .client import PandaClient
|
|
5
|
+
from .motion_switch import MotionSwitchCoordinator
|
|
6
|
+
from .observer import EigObserver
|
|
7
|
+
from .fpga import EigsepFpga
|
|
8
|
+
from .motor_client import MotorClient
|
|
9
|
+
from .motor_zeroer import MotorZeroer
|
|
10
|
+
from .status_log_handler import StatusStreamHandler
|
|
11
|
+
from .tempctrl_client import TempCtrlClient
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from . import testing
|
|
15
|
+
except ImportError as e:
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
# Use a named logger (not the root convenience function) so the
|
|
19
|
+
# warning does NOT trigger logging.basicConfig() and silently
|
|
20
|
+
# install a stderr StreamHandler on the root logger — that would
|
|
21
|
+
# defeat configure_eig_logger(console=False) callers downstream.
|
|
22
|
+
logging.getLogger(__name__).warning(
|
|
23
|
+
f"Could not import testing module: {e}, use pip install .[dev] to "
|
|
24
|
+
"install the required dependencies for testing if needed."
|
|
25
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Shared helpers for the standalone manual test scripts under ``scripts/``.
|
|
2
|
+
|
|
3
|
+
Three responsibilities, all deliberately small:
|
|
4
|
+
|
|
5
|
+
- :func:`build_transport` mirrors the ``--dummy`` bootstrap used by
|
|
6
|
+
the multi-pico manual scripts (``motor_manual.py`` / ``motor_control.py``
|
|
7
|
+
/ ``potmon_manual.py`` / ...) so every per-app manual script can
|
|
8
|
+
share one well-tested transport path.
|
|
9
|
+
- :func:`build_transport_bare` is the equivalent for bring-up scripts
|
|
10
|
+
that build their *own* minimal producer surface and explicitly do
|
|
11
|
+
not want a ``DummyPandaClient`` masquerading on the transport in
|
|
12
|
+
dummy mode (see ``scripts/CLAUDE.md`` for the contract).
|
|
13
|
+
- :func:`require_pico` is a structural availability check the manual
|
|
14
|
+
scripts run before issuing any command, so an operator who forgot to
|
|
15
|
+
start ``pico-manager.service`` (or who flashed the wrong pico) gets a
|
|
16
|
+
one-line actionable error instead of a silent ``send_command`` no-op.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
from eigsep_redis import Transport
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_transport(
|
|
31
|
+
dummy: bool,
|
|
32
|
+
*,
|
|
33
|
+
host: str = "localhost",
|
|
34
|
+
real_port: int = 6379,
|
|
35
|
+
dummy_port: int = 6380,
|
|
36
|
+
) -> Transport:
|
|
37
|
+
"""Return a :class:`Transport` matching the manual-script convention.
|
|
38
|
+
|
|
39
|
+
Real mode just constructs a ``Transport(host, real_port)``.
|
|
40
|
+
|
|
41
|
+
Dummy mode constructs a transport against a separate fakeredis port
|
|
42
|
+
(``dummy_port``, conventionally 6380), resets the fakeredis state,
|
|
43
|
+
and attaches a fully-emulated ``DummyPandaClient`` to the transport
|
|
44
|
+
so every dummy pico (motor, rfswitch, tempctrl, potmon, imu_el,
|
|
45
|
+
imu_az, lidar) is registered and emitting status before the caller
|
|
46
|
+
starts issuing proxy commands. The client is stashed on
|
|
47
|
+
``transport._dummy_client`` to keep it alive — without that
|
|
48
|
+
reference Python would garbage-collect the embedded PicoManager
|
|
49
|
+
threads and the proxy would see ``is_available=False``.
|
|
50
|
+
|
|
51
|
+
Bring-up scripts that build their own minimal producer surface
|
|
52
|
+
(e.g. ``vna_manual.py`` / ``record_vna.py``) want
|
|
53
|
+
:func:`build_transport_bare` instead — the auto-attached
|
|
54
|
+
``DummyPandaClient`` here violates the no-PandaClient-masquerade
|
|
55
|
+
rule documented in ``scripts/CLAUDE.md`` and competes with the
|
|
56
|
+
script's own dummy-pico bootstrap.
|
|
57
|
+
"""
|
|
58
|
+
if dummy:
|
|
59
|
+
logger.warning("Running in DUMMY mode, no hardware will be used.")
|
|
60
|
+
from .testing import DummyPandaClient
|
|
61
|
+
|
|
62
|
+
transport = Transport(host=host, port=dummy_port)
|
|
63
|
+
transport.reset()
|
|
64
|
+
transport._dummy_client = DummyPandaClient(transport=transport)
|
|
65
|
+
return transport
|
|
66
|
+
return Transport(host=host, port=real_port)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_transport_bare(
|
|
70
|
+
dummy: bool,
|
|
71
|
+
*,
|
|
72
|
+
host: str = "localhost",
|
|
73
|
+
real_port: int = 6379,
|
|
74
|
+
dummy_port: int = 6380,
|
|
75
|
+
) -> Transport:
|
|
76
|
+
"""Return a :class:`Transport` with no client attached.
|
|
77
|
+
|
|
78
|
+
The bring-up-script variant of :func:`build_transport`: same port
|
|
79
|
+
convention, same fakeredis reset on dummy, but explicitly does
|
|
80
|
+
**not** instantiate a ``DummyPandaClient`` on the transport.
|
|
81
|
+
|
|
82
|
+
Bring-up scripts (``vna_manual.py``, ``record_vna.py``, etc.) build
|
|
83
|
+
their own minimal producer surface (see ``scripts/CLAUDE.md``) and
|
|
84
|
+
spin up only the dummy picos they actually need. Attaching a
|
|
85
|
+
``DummyPandaClient`` from this helper would (a) start a heartbeat
|
|
86
|
+
thread that collides with whatever the script is meant to test and
|
|
87
|
+
(b) double-register dummy picos if the script also calls
|
|
88
|
+
``start_dummy_pico_manager`` directly.
|
|
89
|
+
"""
|
|
90
|
+
if dummy:
|
|
91
|
+
logger.warning("Running in DUMMY mode, no hardware will be used.")
|
|
92
|
+
transport = Transport(host=host, port=dummy_port)
|
|
93
|
+
transport.reset()
|
|
94
|
+
return transport
|
|
95
|
+
return Transport(host=host, port=real_port)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def require_pico(proxy, *, hint_script: str = "pico_preflight.py") -> None:
|
|
99
|
+
"""Exit with a clear message if ``proxy``'s heartbeat is missing.
|
|
100
|
+
|
|
101
|
+
Manual scripts go through :class:`picohost.proxy.PicoProxy`, which
|
|
102
|
+
silently returns ``None`` from :meth:`send_command` when the
|
|
103
|
+
targeted pico's heartbeat is absent. That's the right behavior for
|
|
104
|
+
long-running observing loops, but a manual operator running a
|
|
105
|
+
bring-up script needs to know immediately that the pico-manager
|
|
106
|
+
service isn't reachable.
|
|
107
|
+
"""
|
|
108
|
+
if proxy.is_available:
|
|
109
|
+
return
|
|
110
|
+
sys.stderr.write(
|
|
111
|
+
f"ERROR: pico {proxy.name!r} is not registered with PicoManager.\n"
|
|
112
|
+
f" - Is pico-manager.service running on the panda?\n"
|
|
113
|
+
f" - Was the pico flashed? Run scripts/{hint_script} to verify.\n"
|
|
114
|
+
)
|
|
115
|
+
raise SystemExit(2)
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Golden fixtures for the eigsep_observing test suite and for the
|
|
2
|
+
producer-contract tests shipped under ``eigsep_observing.contract_tests``.
|
|
3
|
+
|
|
4
|
+
These constants and helpers are deliberately constructed to mirror the
|
|
5
|
+
shape and types of real production data. See the "Testing philosophy"
|
|
6
|
+
section in CLAUDE.md: fixtures should look like what producers actually
|
|
7
|
+
emit so tests catch contract drift, and any deviations should be called
|
|
8
|
+
out explicitly.
|
|
9
|
+
|
|
10
|
+
They live under ``src/`` (rather than in ``tests/conftest.py``) so the
|
|
11
|
+
producer-contract suite can ship inside the installed wheel — the
|
|
12
|
+
eigsep-field CLI runs it via ``pytest --pyargs
|
|
13
|
+
eigsep_observing.contract_tests`` on nodes that only have the wheel
|
|
14
|
+
(the Pi), not the test tree. The leading underscore marks this module
|
|
15
|
+
as private: it is not part of the supported public API of
|
|
16
|
+
eigsep_observing and its shape can change without a deprecation cycle.
|
|
17
|
+
The in-repo ``tests/conftest.py`` re-exports from here so existing
|
|
18
|
+
``from conftest import HEADER`` imports in ``test_io.py`` keep working
|
|
19
|
+
unchanged.
|
|
20
|
+
|
|
21
|
+
These are kept as plain module-level constants rather than
|
|
22
|
+
``@pytest.fixture`` functions because they are referenced from inside
|
|
23
|
+
nested data structures (e.g. ``CORR_METADATA``) and from helper module
|
|
24
|
+
imports — both of which the parameter-injection style does not support
|
|
25
|
+
without significant test rewrites.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
from picohost.base import PicoPeltier
|
|
30
|
+
from picohost.testing import TempCtrlEmulator
|
|
31
|
+
|
|
32
|
+
# One corr file accumulates NTIMES integrations, each of duration
|
|
33
|
+
# INTEGRATION_TIME seconds. FILE_TIME = NTIMES * INTEGRATION_TIME is the
|
|
34
|
+
# wall-clock duration of the file and is included in HEADER so the
|
|
35
|
+
# relationship is explicit at the fixture level (rather than being two
|
|
36
|
+
# independently-set numbers that can drift apart).
|
|
37
|
+
NTIMES = 60
|
|
38
|
+
INTEGRATION_TIME = 1.0 # seconds
|
|
39
|
+
FILE_TIME = NTIMES * INTEGRATION_TIME # seconds
|
|
40
|
+
|
|
41
|
+
# HEADER mimics EigsepFpga.header: the static-configuration portion of a
|
|
42
|
+
# corr file. Units match corr_config.yaml: sample_rate is in MHz (NOT Hz).
|
|
43
|
+
#
|
|
44
|
+
# ``pol_delay`` is a nested dict (one key per pol-pair) matching the
|
|
45
|
+
# shape emitted by the real header property — not three flat keys.
|
|
46
|
+
# ``wiring`` is the hardware manifest (split out of the old ``rf_chain``
|
|
47
|
+
# key in corr_config.yaml); a single antenna with no ``pam:`` block is
|
|
48
|
+
# included so consumers exercise the PAM-absent code path.
|
|
49
|
+
HEADER = {
|
|
50
|
+
"dtype": ">i4",
|
|
51
|
+
"acc_bins": 2,
|
|
52
|
+
"avg_even_odd": True,
|
|
53
|
+
"nchan": 1024,
|
|
54
|
+
"fpg_file": "fpg_files/eigsep_fengine.fpg",
|
|
55
|
+
"fpg_version": [0, 0],
|
|
56
|
+
"corr_acc_len": 2**28,
|
|
57
|
+
"corr_scalar": 2**9,
|
|
58
|
+
"pol_delay": {"01": 0, "23": 0, "45": 0},
|
|
59
|
+
"fft_shift": 0x00FF,
|
|
60
|
+
"sample_rate": 500.0, # MHz, matching corr_config.yaml convention
|
|
61
|
+
"adc_gain": 4,
|
|
62
|
+
"wiring": {
|
|
63
|
+
"snap_id": "C000069",
|
|
64
|
+
"ants": {
|
|
65
|
+
"viv1-N": {
|
|
66
|
+
"fem": {"id": 32, "pol": "N"},
|
|
67
|
+
"snap": {"input": 2, "label": "N4"},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
"sync_time": 1748732903.4203713,
|
|
72
|
+
"integration_time": INTEGRATION_TIME,
|
|
73
|
+
"file_time": FILE_TIME,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Schema-conformant raw IMU reading (as emitted by a pico and pushed into
|
|
77
|
+
# stream:imu_el by picohost). Mirrors the BNO085 UART RVC payload
|
|
78
|
+
# introduced in picohost 1.0.0: yaw/pitch/roll orientation in degrees and
|
|
79
|
+
# accel_x/y/z in m/s². Used to build CORR_METADATA entries and by tests
|
|
80
|
+
# that feed raw stream data into File.add_data.
|
|
81
|
+
IMU_READING = {
|
|
82
|
+
"sensor_name": "imu_el",
|
|
83
|
+
"status": "update",
|
|
84
|
+
"app_id": 3,
|
|
85
|
+
"yaw": 0.0,
|
|
86
|
+
"pitch": 0.0,
|
|
87
|
+
"roll": 0.0,
|
|
88
|
+
"accel_x": 0.0,
|
|
89
|
+
"accel_y": 0.0,
|
|
90
|
+
"accel_z": 9.81,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _imu_avg_entry(yaw):
|
|
95
|
+
"""One per-sample IMU entry as avg_metadata would emit it.
|
|
96
|
+
|
|
97
|
+
All numeric fields are float and take the float→mean reduction in
|
|
98
|
+
``_avg_sensor_values``. ``yaw`` is the per-sample varying axis used
|
|
99
|
+
by tests that need to assert on a non-constant float field.
|
|
100
|
+
"""
|
|
101
|
+
return {
|
|
102
|
+
"sensor_name": "imu_el",
|
|
103
|
+
"status": "update",
|
|
104
|
+
"app_id": 3,
|
|
105
|
+
"yaw": yaw,
|
|
106
|
+
"pitch": 0.0,
|
|
107
|
+
"roll": 0.0,
|
|
108
|
+
"accel_x": 0.0,
|
|
109
|
+
"accel_y": 0.0,
|
|
110
|
+
"accel_z": 9.81,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _lidar_avg_entry(distance_m):
|
|
115
|
+
"""One per-sample lidar entry as avg_metadata would emit it."""
|
|
116
|
+
return {
|
|
117
|
+
"sensor_name": "lidar",
|
|
118
|
+
"status": "update",
|
|
119
|
+
"app_id": 4,
|
|
120
|
+
"distance_m": distance_m,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def tempctrl_post_handler_reading(stream_name):
|
|
125
|
+
"""One per-channel tempctrl reading after ``_peltier_redis_handler``.
|
|
126
|
+
|
|
127
|
+
The firmware/emulator emits one combined ``sensor_name="tempctrl"``
|
|
128
|
+
dict per status tick; ``PicoPeltier._peltier_redis_handler`` fans
|
|
129
|
+
that into two ``writer.add(...)`` calls — one per channel — and is
|
|
130
|
+
the boundary that produces the actual Redis-stream shapes. Composing
|
|
131
|
+
the emulator with the handler here means every test fixture downstream
|
|
132
|
+
is anchored to the real producer output rather than drifting on
|
|
133
|
+
hand-typed steady-state values.
|
|
134
|
+
"""
|
|
135
|
+
pel = PicoPeltier.__new__(PicoPeltier)
|
|
136
|
+
captured = []
|
|
137
|
+
pel._base_redis_handler = lambda d: captured.append(dict(d))
|
|
138
|
+
pel._peltier_redis_handler(TempCtrlEmulator().get_status())
|
|
139
|
+
for entry in captured:
|
|
140
|
+
if entry.get("sensor_name") == stream_name:
|
|
141
|
+
return entry
|
|
142
|
+
raise AssertionError(
|
|
143
|
+
f"_peltier_redis_handler did not emit a {stream_name!r} entry; "
|
|
144
|
+
f"got sensor_names={[e.get('sensor_name') for e in captured]}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _tempctrl_channel_entry(sensor_name, t_now, timestamp):
|
|
149
|
+
"""One per-sample tempctrl channel (``tempctrl_lna`` or ``tempctrl_load``).
|
|
150
|
+
|
|
151
|
+
Steady-state fields are derived from
|
|
152
|
+
``tempctrl_post_handler_reading`` so the fixture stays anchored to
|
|
153
|
+
real ``TempCtrlEmulator`` + ``PicoPeltier._peltier_redis_handler``
|
|
154
|
+
output. Only ``T_now`` and ``timestamp`` are per-sample test inputs;
|
|
155
|
+
everything else (``T_target``, drive flags, watchdog) is the
|
|
156
|
+
emulator's steady-state value.
|
|
157
|
+
"""
|
|
158
|
+
entry = tempctrl_post_handler_reading(sensor_name)
|
|
159
|
+
entry["T_now"] = t_now
|
|
160
|
+
entry["timestamp"] = timestamp
|
|
161
|
+
return entry
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _potmon_avg_entry(pot_el_voltage):
|
|
165
|
+
"""One per-sample potmon entry as avg_metadata would emit it.
|
|
166
|
+
|
|
167
|
+
Mirrors the post-``_pot_redis_handler`` shape that lands in Redis:
|
|
168
|
+
raw voltages plus the flattened cal slope/intercept and the derived
|
|
169
|
+
angle. All-scalar per the picohost scalar-only contract; the cal
|
|
170
|
+
fields are de-facto invariants for the lifetime of a stream.
|
|
171
|
+
|
|
172
|
+
A *calibrated* reading is used here so every cal/angle field is a
|
|
173
|
+
real float, exercising the float→mean reduction in
|
|
174
|
+
``_avg_sensor_values``. The uncalibrated-stream case (cal/angle
|
|
175
|
+
fields all ``None``) is a first-class producer state — see the
|
|
176
|
+
``potmon`` schema comment in ``io.py`` — but it is intentionally
|
|
177
|
+
not exercised by this golden fixture because it would force the
|
|
178
|
+
round-trip assertion to special-case ``None`` survivors and obscure
|
|
179
|
+
the steady-state contract this fixture is meant to pin. Tests that
|
|
180
|
+
need to cover the uncalibrated path should build their own samples.
|
|
181
|
+
Same rationale as ``_potmon_post_handler_reading`` in
|
|
182
|
+
``test_producer_contracts.py``.
|
|
183
|
+
"""
|
|
184
|
+
return {
|
|
185
|
+
"sensor_name": "potmon",
|
|
186
|
+
"status": "update",
|
|
187
|
+
"app_id": 2,
|
|
188
|
+
"pot_el_voltage": pot_el_voltage,
|
|
189
|
+
"pot_az_voltage": 1.5,
|
|
190
|
+
"pot_el_cal_slope": 100.0,
|
|
191
|
+
"pot_el_cal_intercept": -50.0,
|
|
192
|
+
"pot_az_cal_slope": 200.0,
|
|
193
|
+
"pot_az_cal_intercept": -100.0,
|
|
194
|
+
"pot_el_angle": 100.0 * pot_el_voltage - 50.0,
|
|
195
|
+
"pot_az_angle": 200.0 * 1.5 - 100.0,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
ERROR_INTEGRATION_INDEX = 30
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _imu_errored_integration_entry(yaw):
|
|
203
|
+
return {**_imu_avg_entry(yaw), "status": "error"}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
CORR_METADATA = {
|
|
207
|
+
"imu_el": [
|
|
208
|
+
_imu_avg_entry(0.001 * i)
|
|
209
|
+
if i != ERROR_INTEGRATION_INDEX
|
|
210
|
+
else _imu_errored_integration_entry(0.001 * i)
|
|
211
|
+
for i in range(NTIMES)
|
|
212
|
+
],
|
|
213
|
+
"imu_az": [
|
|
214
|
+
{**_imu_avg_entry(0.002 * i), "sensor_name": "imu_az", "app_id": 6}
|
|
215
|
+
for i in range(NTIMES)
|
|
216
|
+
],
|
|
217
|
+
"lidar": [_lidar_avg_entry(1.5 + 0.001 * i) for i in range(NTIMES)],
|
|
218
|
+
"potmon": [_potmon_avg_entry(1.5 + 0.001 * i) for i in range(NTIMES)],
|
|
219
|
+
"tempctrl_lna": [
|
|
220
|
+
_tempctrl_channel_entry("tempctrl_lna", 30.0 + 0.01 * i, 1.0 + i)
|
|
221
|
+
for i in range(NTIMES)
|
|
222
|
+
],
|
|
223
|
+
"tempctrl_load": [
|
|
224
|
+
_tempctrl_channel_entry("tempctrl_load", 25.0 + 0.01 * i, 1.0 + i)
|
|
225
|
+
for i in range(NTIMES)
|
|
226
|
+
],
|
|
227
|
+
"rfswitch": (
|
|
228
|
+
["RFANT"] * 20 # steady state
|
|
229
|
+
+ ["UNKNOWN"] * 5 # transition window
|
|
230
|
+
+ ["RFNOFF"] * 20 # new steady state
|
|
231
|
+
+ [None] * 5 # sensor dropout (gap-fill pad in _insert_sample)
|
|
232
|
+
+ ["RFNOFF"] * 10 # recovery
|
|
233
|
+
),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# VNA_METADATA mirrors the flat ``{key: value}`` dict returned by
|
|
237
|
+
# ``MetadataSnapshotReader.get()`` — the snapshot path used by the VNA
|
|
238
|
+
# code in ``PandaClient.measure_s11``. Values are whatever the producer
|
|
239
|
+
# last pushed via ``MetadataWriter.add``:
|
|
240
|
+
# - picohost pushes the raw sensor dict for each sensor key
|
|
241
|
+
# - ``MetadataWriter.add`` auto-appends a ``{key}_ts`` Unix-seconds float
|
|
242
|
+
# - misc. scalars (e.g. ``corr_sync_time``) go in as floats
|
|
243
|
+
# There is NO averaging on this path; unlike CORR_METADATA, values are
|
|
244
|
+
# scalars or nested dicts, never per-sample lists.
|
|
245
|
+
_SNAPSHOT_TS = 1775997296.789012
|
|
246
|
+
VNA_METADATA = {
|
|
247
|
+
"imu_el": IMU_READING,
|
|
248
|
+
"imu_el_ts": _SNAPSHOT_TS,
|
|
249
|
+
"imu_az": {**IMU_READING, "sensor_name": "imu_az", "app_id": 6},
|
|
250
|
+
"imu_az_ts": _SNAPSHOT_TS,
|
|
251
|
+
"lidar": {
|
|
252
|
+
"sensor_name": "lidar",
|
|
253
|
+
"status": "update",
|
|
254
|
+
"app_id": 4,
|
|
255
|
+
"distance_m": 1.52,
|
|
256
|
+
},
|
|
257
|
+
"lidar_ts": _SNAPSHOT_TS,
|
|
258
|
+
"potmon": {
|
|
259
|
+
"sensor_name": "potmon",
|
|
260
|
+
"status": "update",
|
|
261
|
+
"app_id": 2,
|
|
262
|
+
"pot_el_voltage": 1.5,
|
|
263
|
+
"pot_az_voltage": 1.5,
|
|
264
|
+
"pot_el_cal_slope": 100.0,
|
|
265
|
+
"pot_el_cal_intercept": -50.0,
|
|
266
|
+
"pot_az_cal_slope": 200.0,
|
|
267
|
+
"pot_az_cal_intercept": -100.0,
|
|
268
|
+
"pot_el_angle": 100.0,
|
|
269
|
+
"pot_az_angle": 200.0,
|
|
270
|
+
},
|
|
271
|
+
"potmon_ts": _SNAPSHOT_TS,
|
|
272
|
+
"rfswitch": {
|
|
273
|
+
"sensor_name": "rfswitch",
|
|
274
|
+
"status": "update",
|
|
275
|
+
"app_id": 5,
|
|
276
|
+
"sw_state": 3,
|
|
277
|
+
"sw_state_name": "VNAS",
|
|
278
|
+
},
|
|
279
|
+
"rfswitch_ts": _SNAPSHOT_TS,
|
|
280
|
+
"corr_sync_time": 1748732903.4203713,
|
|
281
|
+
"corr_sync_time_ts": _SNAPSHOT_TS,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
S11_HEADER = {
|
|
285
|
+
"fstart": 1e6,
|
|
286
|
+
"fstop": 250e6,
|
|
287
|
+
"npoints": 1000,
|
|
288
|
+
"ifbw": 100,
|
|
289
|
+
"power_dBm": 0,
|
|
290
|
+
"freqs": np.linspace(1e6, 250e6, 1000),
|
|
291
|
+
"mode": "ant",
|
|
292
|
+
"metadata_snapshot_unix": 1748734379.905014,
|
|
293
|
+
}
|