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.
Files changed (99) hide show
  1. eigsep_observing-2.5.0/LICENSE +21 -0
  2. eigsep_observing-2.5.0/PKG-INFO +79 -0
  3. eigsep_observing-2.5.0/README.md +45 -0
  4. eigsep_observing-2.5.0/pyproject.toml +105 -0
  5. eigsep_observing-2.5.0/setup.cfg +4 -0
  6. eigsep_observing-2.5.0/src/eigsep_observing/__init__.py +25 -0
  7. eigsep_observing-2.5.0/src/eigsep_observing/_scripts_util.py +115 -0
  8. eigsep_observing-2.5.0/src/eigsep_observing/_test_fixtures.py +293 -0
  9. eigsep_observing-2.5.0/src/eigsep_observing/adc.py +134 -0
  10. eigsep_observing-2.5.0/src/eigsep_observing/blocks.py +846 -0
  11. eigsep_observing-2.5.0/src/eigsep_observing/client.py +998 -0
  12. eigsep_observing-2.5.0/src/eigsep_observing/config/corr_config.yaml +41 -0
  13. eigsep_observing-2.5.0/src/eigsep_observing/config/dummy_config.yaml +64 -0
  14. eigsep_observing-2.5.0/src/eigsep_observing/config/live_status_thresholds.yaml +52 -0
  15. eigsep_observing-2.5.0/src/eigsep_observing/config/obs_config.yaml +79 -0
  16. eigsep_observing-2.5.0/src/eigsep_observing/config/snap_only.yaml +39 -0
  17. eigsep_observing-2.5.0/src/eigsep_observing/config/test_config.yaml +39 -0
  18. eigsep_observing-2.5.0/src/eigsep_observing/config/wiring.yaml +52 -0
  19. eigsep_observing-2.5.0/src/eigsep_observing/contract_tests/__init__.py +12 -0
  20. eigsep_observing-2.5.0/src/eigsep_observing/contract_tests/test_key_uniqueness.py +58 -0
  21. eigsep_observing-2.5.0/src/eigsep_observing/contract_tests/test_producer_contracts.py +352 -0
  22. eigsep_observing-2.5.0/src/eigsep_observing/corr.py +298 -0
  23. eigsep_observing-2.5.0/src/eigsep_observing/data/eigsep_fengine_1g_v2_3_2024-07-08_1858.fpg +5481 -19
  24. eigsep_observing-2.5.0/src/eigsep_observing/file_heartbeat.py +82 -0
  25. eigsep_observing-2.5.0/src/eigsep_observing/fpga.py +1289 -0
  26. eigsep_observing-2.5.0/src/eigsep_observing/io.py +1765 -0
  27. eigsep_observing-2.5.0/src/eigsep_observing/keys.py +15 -0
  28. eigsep_observing-2.5.0/src/eigsep_observing/live_status/__init__.py +20 -0
  29. eigsep_observing-2.5.0/src/eigsep_observing/live_status/aggregator.py +1113 -0
  30. eigsep_observing-2.5.0/src/eigsep_observing/live_status/app.py +685 -0
  31. eigsep_observing-2.5.0/src/eigsep_observing/live_status/calibration.py +76 -0
  32. eigsep_observing-2.5.0/src/eigsep_observing/live_status/signals.py +263 -0
  33. eigsep_observing-2.5.0/src/eigsep_observing/live_status/snap_probe.py +61 -0
  34. eigsep_observing-2.5.0/src/eigsep_observing/live_status/static/css/dashboard.css +248 -0
  35. eigsep_observing-2.5.0/src/eigsep_observing/live_status/static/js/dashboard.js +809 -0
  36. eigsep_observing-2.5.0/src/eigsep_observing/live_status/templates/index.html +99 -0
  37. eigsep_observing-2.5.0/src/eigsep_observing/live_status/thresholds.py +238 -0
  38. eigsep_observing-2.5.0/src/eigsep_observing/motion_switch.py +100 -0
  39. eigsep_observing-2.5.0/src/eigsep_observing/motor_client.py +373 -0
  40. eigsep_observing-2.5.0/src/eigsep_observing/motor_zeroer.py +172 -0
  41. eigsep_observing-2.5.0/src/eigsep_observing/obs_config_owner.py +108 -0
  42. eigsep_observing-2.5.0/src/eigsep_observing/observer.py +620 -0
  43. eigsep_observing-2.5.0/src/eigsep_observing/plot.py +225 -0
  44. eigsep_observing-2.5.0/src/eigsep_observing/run_tag.py +161 -0
  45. eigsep_observing-2.5.0/src/eigsep_observing/scripts/__init__.py +0 -0
  46. eigsep_observing-2.5.0/src/eigsep_observing/scripts/fpga_init.py +204 -0
  47. eigsep_observing-2.5.0/src/eigsep_observing/scripts/no_switch_observation.py +190 -0
  48. eigsep_observing-2.5.0/src/eigsep_observing/scripts/observe.py +201 -0
  49. eigsep_observing-2.5.0/src/eigsep_observing/scripts/panda_observe.py +162 -0
  50. eigsep_observing-2.5.0/src/eigsep_observing/scripts/vna_position_sweep.py +196 -0
  51. eigsep_observing-2.5.0/src/eigsep_observing/snap_reinit.py +104 -0
  52. eigsep_observing-2.5.0/src/eigsep_observing/status_log_handler.py +214 -0
  53. eigsep_observing-2.5.0/src/eigsep_observing/tempctrl_client.py +362 -0
  54. eigsep_observing-2.5.0/src/eigsep_observing/testing/__init__.py +12 -0
  55. eigsep_observing-2.5.0/src/eigsep_observing/testing/client.py +146 -0
  56. eigsep_observing-2.5.0/src/eigsep_observing/testing/fpga.py +197 -0
  57. eigsep_observing-2.5.0/src/eigsep_observing/testing/observer.py +34 -0
  58. eigsep_observing-2.5.0/src/eigsep_observing/testing/utils.py +152 -0
  59. eigsep_observing-2.5.0/src/eigsep_observing/utils.py +251 -0
  60. eigsep_observing-2.5.0/src/eigsep_observing/vna.py +475 -0
  61. eigsep_observing-2.5.0/src/eigsep_observing/vna_calibration.py +95 -0
  62. eigsep_observing-2.5.0/src/eigsep_observing.egg-info/PKG-INFO +79 -0
  63. eigsep_observing-2.5.0/src/eigsep_observing.egg-info/SOURCES.txt +97 -0
  64. eigsep_observing-2.5.0/src/eigsep_observing.egg-info/dependency_links.txt +1 -0
  65. eigsep_observing-2.5.0/src/eigsep_observing.egg-info/entry_points.txt +6 -0
  66. eigsep_observing-2.5.0/src/eigsep_observing.egg-info/requires.txt +21 -0
  67. eigsep_observing-2.5.0/src/eigsep_observing.egg-info/top_level.txt +1 -0
  68. eigsep_observing-2.5.0/tests/test_adc.py +431 -0
  69. eigsep_observing-2.5.0/tests/test_casperfpga_api.py +197 -0
  70. eigsep_observing-2.5.0/tests/test_client.py +2543 -0
  71. eigsep_observing-2.5.0/tests/test_file_heartbeat.py +110 -0
  72. eigsep_observing-2.5.0/tests/test_fpga.py +1085 -0
  73. eigsep_observing-2.5.0/tests/test_integration.py +153 -0
  74. eigsep_observing-2.5.0/tests/test_io.py +3273 -0
  75. eigsep_observing-2.5.0/tests/test_live_status_aggregator.py +941 -0
  76. eigsep_observing-2.5.0/tests/test_live_status_app.py +1293 -0
  77. eigsep_observing-2.5.0/tests/test_live_status_calibration.py +187 -0
  78. eigsep_observing-2.5.0/tests/test_live_status_snap_probe.py +51 -0
  79. eigsep_observing-2.5.0/tests/test_live_status_thresholds.py +314 -0
  80. eigsep_observing-2.5.0/tests/test_motion_switch.py +194 -0
  81. eigsep_observing-2.5.0/tests/test_motor_client.py +392 -0
  82. eigsep_observing-2.5.0/tests/test_motor_scripts.py +416 -0
  83. eigsep_observing-2.5.0/tests/test_motor_zeroer.py +199 -0
  84. eigsep_observing-2.5.0/tests/test_obs_config_owner.py +123 -0
  85. eigsep_observing-2.5.0/tests/test_obs_config_uploaders.py +198 -0
  86. eigsep_observing-2.5.0/tests/test_observer.py +1710 -0
  87. eigsep_observing-2.5.0/tests/test_record_metadata.py +156 -0
  88. eigsep_observing-2.5.0/tests/test_record_vna.py +93 -0
  89. eigsep_observing-2.5.0/tests/test_redis.py +1017 -0
  90. eigsep_observing-2.5.0/tests/test_republish_header.py +166 -0
  91. eigsep_observing-2.5.0/tests/test_run_tag.py +202 -0
  92. eigsep_observing-2.5.0/tests/test_scripts_util.py +131 -0
  93. eigsep_observing-2.5.0/tests/test_snap_reinit.py +131 -0
  94. eigsep_observing-2.5.0/tests/test_tempctrl_client.py +455 -0
  95. eigsep_observing-2.5.0/tests/test_tempctrl_manual.py +133 -0
  96. eigsep_observing-2.5.0/tests/test_utils.py +516 -0
  97. eigsep_observing-2.5.0/tests/test_vna_calibration.py +142 -0
  98. eigsep_observing-2.5.0/tests/test_vna_helper.py +272 -0
  99. 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
+ [![codecov](https://codecov.io/gh/EIGSEP/eigsep_observing/graph/badge.svg?token=GK8ZZOJ57W)](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
+ [![codecov](https://codecov.io/gh/EIGSEP/eigsep_observing/graph/badge.svg?token=GK8ZZOJ57W)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ }