From cf8a39ed0ad83c3b94a3194debc24c9e44d23ed6 Mon Sep 17 00:00:00 2001
From: Jan David Mol <mol@astron.nl>
Date: Wed, 2 Aug 2023 09:06:17 +0000
Subject: [PATCH] L2SS-1465: Add digital pointing to BST files, and move
 subband selection from header to BST matrix

---
 README.md                                     |  1 +
 VERSION                                       |  2 +-
 .../statistics/statistics_data.py             |  8 +-
 .../statistics/writer/hdf5.py                 | 29 ++++---
 tests/statistics/test_writer.py               | 81 +++++++++++--------
 tests/test_devices.py                         | 24 +++++-
 6 files changed, 97 insertions(+), 48 deletions(-)

diff --git a/README.md b/README.md
index 442afc5..7218d99 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,7 @@ tox -e debug tests.requests.test_prometheus
 ```
 
 ## Releasenotes
+- 0.15.3 - BSTs: added digital pointing direction, and subband selection
 - 0.15.2 - Statistics filenames: added antennafield, moved subband to the end
 - 0.15.1 - Infer device names and default port from the specified antenna-field name
 - 0.15.0 - Add all_connected method to MultiStationObservation
diff --git a/VERSION b/VERSION
index 4312e0d..1985d91 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.15.2
+0.15.3
diff --git a/lofar_station_client/statistics/statistics_data.py b/lofar_station_client/statistics/statistics_data.py
index aee8de5..80b15c2 100644
--- a/lofar_station_client/statistics/statistics_data.py
+++ b/lofar_station_client/statistics/statistics_data.py
@@ -138,6 +138,12 @@ class StatisticsData(ndarray):
     tile_beam_tracking_enabled: bool = attribute(optional=True)
     """ Whether the tile beam is tracking """
 
+    digital_beam_pointing_direction: str = attribute(optional=True)
+    """ Direction of the digital beam """
+
+    digital_beam_tracking_enabled: bool = attribute(optional=True)
+    """ Whether the digital beam is tracking """
+
     hbat_pwr_on: str = attribute(optional=True)
     """ Elements per hba tile """
 
@@ -203,8 +209,6 @@ class StatisticsFileHeader:
     frequency_band: str = attribute(optional=True)
     """ filter selection """
 
-    subbands: int = attribute(optional=True)
-
     fpga_firmware_version: str = attribute(optional=True)
     fpga_hardware_version: str = attribute(optional=True)
 
diff --git a/lofar_station_client/statistics/writer/hdf5.py b/lofar_station_client/statistics/writer/hdf5.py
index 21c1137..ff609b5 100644
--- a/lofar_station_client/statistics/writer/hdf5.py
+++ b/lofar_station_client/statistics/writer/hdf5.py
@@ -246,14 +246,6 @@ class HDF5Writer(ABC):
             except DevFailed:
                 logger.exception("Failed to read from %s", self.sdp_device.name())
 
-        if self.digitalbeam_device:
-            try:
-                self.file.subbands = self.digitalbeam_device.subband_select_RW
-            except DevFailed:
-                logger.exception(
-                    "Failed to read from %s", self.digitalbeam_device.name()
-                )
-
     def hdf5_matrix_header(self, statistics_packet_header: dict) -> dict:
         """Returns the header fields per statistics matrix."""
 
@@ -442,7 +434,7 @@ class HDF5Writer(ABC):
         if self.tilebeam_device:
             try:
                 matrix.tile_beam_pointing_direction = (
-                    self.tilebeam_device.Pointing_direction_str
+                    self.tilebeam_device.Pointing_direction_str_R
                 )
                 matrix.tile_beam_tracking_enabled = (
                     self.tilebeam_device.Tracking_enabled_R
@@ -588,6 +580,25 @@ class BstHdf5Writer(HDF5Writer):
     def new_collector(self):
         return BSTCollector()
 
+    def hdf5_matrix_header(self, statistics_packet_header: dict) -> dict:
+        header = super().hdf5_matrix_header(statistics_packet_header)
+
+        if self.digitalbeam_device:
+            try:
+                header[
+                    "digital_beam_pointing_direction"
+                ] = self.digitalbeam_device.Pointing_direction_str_R
+                header[
+                    "digital_beam_tracking_enabled"
+                ] = self.digitalbeam_device.Tracking_enabled_R
+                header["subbands"] = self.digitalbeam_device.subband_select_RW
+            except DevFailed:
+                logger.exception(
+                    "Failed to read from %s", self.digitalbeam_device.name()
+                )
+
+        return header
+
     def get_values_matrix(self) -> StatisticsData:
         return (
             self.current_matrix.parameters["bst_values"]
diff --git a/tests/statistics/test_writer.py b/tests/statistics/test_writer.py
index b20654e..308fffd 100644
--- a/tests/statistics/test_writer.py
+++ b/tests/statistics/test_writer.py
@@ -20,6 +20,7 @@ from tests.test_devices import (
     FakeAntennaFieldDeviceProxy,
     FakeOffAntennaFieldDeviceProxy,
     FakeDigitalBeamDeviceProxy,
+    FakeTileBeamDeviceProxy,
     FakeSDPDeviceProxy,
     FakeStationManagerDeviceProxy,
 )
@@ -31,13 +32,15 @@ class TestStatisticsReaderWriter(base.TestCase):
 
     def _mock_get_tango_device(self, tango_disabled, host, device_name):
         """Return our mocked DeviceProxies"""
-        if device_name == "STAT/AntennaField/LBA":
+        if device_name == "STAT/AntennaField/HBA":
             return FakeAntennaFieldDeviceProxy(device_name)
-        if device_name == "STAT/DigitalBeam/LBA":
+        if device_name == "STAT/DigitalBeam/HBA":
             return FakeDigitalBeamDeviceProxy(device_name)
+        if device_name == "STAT/TileBeam/HBA":
+            return FakeTileBeamDeviceProxy(device_name)
         if device_name == "STAT/StationManager/1":
             return FakeStationManagerDeviceProxy
-        if device_name == "STAT/SDP/LBA":
+        if device_name == "STAT/SDP/HBA":
             return FakeSDPDeviceProxy(device_name)
         raise ValueError(
             f"Device not mocked, and thus not available in this test: {device_name}"
@@ -45,13 +48,15 @@ class TestStatisticsReaderWriter(base.TestCase):
 
     def _mock_get_tango_device_off(self, tango_disabled, host, device_name):
         """Return our mocked DeviceProxies that simulate a device that is off"""
-        if device_name == "STAT/AntennaField/LBA":
+        if device_name == "STAT/AntennaField/HBA":
             return FakeOffAntennaFieldDeviceProxy(device_name)
-        if device_name == "STAT/DigitalBeam/LBA":
+        if device_name == "STAT/DigitalBeam/HBA":
             return FakeDigitalBeamDeviceProxy(device_name)
+        if device_name == "STAT/TileBeam/HBA":
+            return FakeTileBeamDeviceProxy(device_name)
         if device_name == "STAT/StationManager/1":
             return FakeStationManagerDeviceProxy
-        if device_name == "STAT/SDP/LBA":
+        if device_name == "STAT/SDP/HBA":
             return FakeSDPDeviceProxy(device_name)
         raise ValueError(
             f"Device not mocked, and thus not available in this test: {device_name}"
@@ -62,7 +67,7 @@ class TestStatisticsReaderWriterSST(TestStatisticsReaderWriter):
     """TestStatistics class for SST-mode"""
 
     def _run_writer_reader(
-        self, tmpdir: str, writer_argv: list, antennafield: str = "LBA"
+        self, tmpdir: str, writer_argv: list, antennafield: str = "HBA"
     ) -> Tuple[StatisticsData, StatisticsFileHeader]:
         """Run the statistics writer with the given arguments,
         and read and return the output."""
@@ -77,6 +82,9 @@ class TestStatisticsReaderWriterSST(TestStatisticsReaderWriter):
             tmpdir,
         ]
 
+        if antennafield != "unknown":
+            default_writer_sys_argv += ["--antennafield", antennafield]
+
         with mock.patch.object(
             entry.sys, "argv", default_writer_sys_argv + writer_argv
         ):
@@ -122,10 +130,7 @@ class TestStatisticsReaderWriterSST(TestStatisticsReaderWriter):
 
     def test_insert_tango_SST_statistics(self):
         with TemporaryDirectory() as tmpdir:
-            writer_argv = [
-                "--antennafield",
-                "LBA",
-            ]
+            writer_argv = []
 
             with mock.patch.object(
                 entry, "_get_tango_device", self._mock_get_tango_device
@@ -145,15 +150,11 @@ class TestStatisticsReaderWriterSST(TestStatisticsReaderWriter):
                 "--no-tango",
             ]
 
-            _ = self._run_writer_reader(tmpdir, writer_argv, antennafield="unknown")
+            _ = self._run_writer_reader(tmpdir, writer_argv, "unknown")
 
     def test_SST_statistics_with_device_in_off(self):
         with TemporaryDirectory() as tmpdir:
-            writer_argv = [
-                "--no-tango",
-                "--antennafield",
-                "LBA",
-            ]
+            writer_argv = []
 
             with mock.patch.object(
                 entry, "_get_tango_device", self._mock_get_tango_device_off
@@ -185,6 +186,8 @@ class TestStatisticsReaderWriterBST(TestStatisticsReaderWriter):
             sys.argv[0],
             "--mode",
             "BST",
+            "--antennafield",
+            "HBA",
             "--file",
             dirname(__file__) + "/SDP_BST_statistics_packets.bin",
             "--output_dir",
@@ -198,13 +201,13 @@ class TestStatisticsReaderWriterBST(TestStatisticsReaderWriter):
                 entry.main()
 
         # check if file was written
-        self.assertTrue(isfile(f"{tmpdir}/BST_2022-05-20-11-08-44_LBA.h5"))
+        self.assertTrue(isfile(f"{tmpdir}/BST_2022-05-20-11-08-44_HBA.h5"))
 
         # default arguments for statistics reader
         default_reader_sys_argv = [
             sys.argv[0],
             "--files",
-            f"{tmpdir}/BST_2022-05-20-11-08-44_LBA.h5",
+            f"{tmpdir}/BST_2022-05-20-11-08-44_HBA.h5",
             "--start_time",
             "2021-09-20#07:40:08.937+00:00",
             "--end_time",
@@ -222,18 +225,22 @@ class TestStatisticsReaderWriterBST(TestStatisticsReaderWriter):
 
     def test_insert_tango_BST_statistics(self):
         with TemporaryDirectory() as tmpdir:
-            writer_argv = [
-                "--antennafield",
-                "LBA",
-            ]
+            writer_argv = []
 
             with mock.patch.object(
                 entry, "_get_tango_device", self._mock_get_tango_device
             ):
-                file_header = self._run_writer_reader(tmpdir, writer_argv)
+                _ = self._run_writer_reader(tmpdir, writer_argv)
 
-            # Test some AntennField attributes, whether they match our mock
-            self.assertListEqual(list(range(0, 488)), file_header.subbands.tolist())
+            # validate HDF5 content
+            with h5py.File(f"{tmpdir}/BST_2022-05-20-11-08-44_HBA.h5") as f:
+                # validate header
+                self.assertIn("station_version", dict(f.attrs))
+                self.assertIn("writer_version", dict(f.attrs))
+                self.assertIn("mode", dict(f.attrs))
+
+                # check for the datasets present in our input data
+                self.assertIn("BST_2022-05-20T11:08:44.999+00:00", dict(f.items()))
 
     def test_bst(self):
         with mock.patch.object(entry, "_get_tango_device", self._mock_get_tango_device):
@@ -242,6 +249,8 @@ class TestStatisticsReaderWriterBST(TestStatisticsReaderWriter):
                     sys.argv[0],
                     "--mode",
                     "BST",
+                    "--antennafield",
+                    "HBA",
                     "--file",
                     dirname(__file__) + "/SDP_BST_statistics_packets.bin",
                     "--output_dir",
@@ -252,7 +261,7 @@ class TestStatisticsReaderWriterBST(TestStatisticsReaderWriter):
                         entry.main()
 
                 # check if file was written
-                self.assertTrue(isfile(f"{tmpdir}/BST_2022-05-20-11-08-44_LBA.h5"))
+                self.assertTrue(isfile(f"{tmpdir}/BST_2022-05-20-11-08-44_HBA.h5"))
 
 
 class TestStatisticsWriterXST(TestStatisticsReaderWriter):
@@ -265,6 +274,8 @@ class TestStatisticsWriterXST(TestStatisticsReaderWriter):
                     sys.argv[0],
                     "--mode",
                     "XST",
+                    "--antennafield",
+                    "HBA",
                     "--file",
                     dirname(__file__) + "/SDP_XST_statistics_packets.bin",
                     "--output_dir",
@@ -276,11 +287,11 @@ class TestStatisticsWriterXST(TestStatisticsReaderWriter):
 
                 # check if file was written
                 self.assertTrue(
-                    isfile(f"{tmpdir}/XST_2021-09-13-13-21-32_LBA_SB102.h5")
+                    isfile(f"{tmpdir}/XST_2021-09-13-13-21-32_HBA_SB102.h5")
                 )
 
                 # validate HDF5 content
-                with h5py.File(f"{tmpdir}/XST_2021-09-13-13-21-32_LBA_SB102.h5") as f:
+                with h5py.File(f"{tmpdir}/XST_2021-09-13-13-21-32_HBA_SB102.h5") as f:
                     # validate header
                     self.assertIn("station_version", dict(f.attrs))
                     self.assertIn("writer_version", dict(f.attrs))
@@ -315,6 +326,8 @@ class TestStatisticsWriterXST(TestStatisticsReaderWriter):
                     sys.argv[0],
                     "--mode",
                     "XST",
+                    "--antennafield",
+                    "HBA",
                     "--file",
                     dirname(__file__)
                     + "/SDP_XST_statistics_packets_multiple_subbands.bin",
@@ -327,10 +340,10 @@ class TestStatisticsWriterXST(TestStatisticsReaderWriter):
 
                 # check if files were written
                 self.assertTrue(
-                    isfile(f"{tmpdir}/XST_2021-09-13-13-21-32_LBA_SB102.h5")
+                    isfile(f"{tmpdir}/XST_2021-09-13-13-21-32_HBA_SB102.h5")
                 )
                 self.assertTrue(
-                    isfile(f"{tmpdir}/XST_2021-09-13-13-21-32_LBA_SB103.h5")
+                    isfile(f"{tmpdir}/XST_2021-09-13-13-21-32_HBA_SB103.h5")
                 )
 
     def test_xst_with_antennafield(self):
@@ -343,7 +356,7 @@ class TestStatisticsWriterXST(TestStatisticsReaderWriter):
                     "--mode",
                     "XST",
                     "--antennafield",
-                    "LBA",
+                    "HBA",
                     "--file",
                     dirname(__file__) + "/SDP_XST_statistics_packets.bin",
                     "--output_dir",
@@ -355,11 +368,11 @@ class TestStatisticsWriterXST(TestStatisticsReaderWriter):
 
                 # check if file was written
                 self.assertTrue(
-                    isfile(f"{tmpdir}/XST_2021-09-13-13-21-32_LBA_SB102.h5")
+                    isfile(f"{tmpdir}/XST_2021-09-13-13-21-32_HBA_SB102.h5")
                 )
 
                 # validate HDF5 content
-                with h5py.File(f"{tmpdir}/XST_2021-09-13-13-21-32_LBA_SB102.h5") as f:
+                with h5py.File(f"{tmpdir}/XST_2021-09-13-13-21-32_HBA_SB102.h5") as f:
                     # check extra header fields provided by the AntennaField
                     self.assertIn("antenna_names", dict(f.attrs))
                     self.assertIn("antenna_quality", dict(f.attrs))
diff --git a/tests/test_devices.py b/tests/test_devices.py
index e11cc9b..fc75572 100644
--- a/tests/test_devices.py
+++ b/tests/test_devices.py
@@ -288,9 +288,9 @@ class FakeAntennaFieldDeviceProxy:
     Antenna_Names_R = ["Aap", "Noot", "Mies"]
     Antenna_Reference_ITRF_R = [[0, 0, 0]] * nr_antennas_R
     Antenna_Quality_str_R = ["OK"] * nr_antennas_R
-    Antenna_Type_R = "LBA"
+    Antenna_Type_R = "HBA"
     Antenna_Usage_Mask_R = [True] * nr_antennas_R
-    Frequency_Band_RW = ["LBA_10_90"] * nr_antennas_R
+    Frequency_Band_RW = ["HBA_110_190"] * nr_antennas_R
     RCU_attenuator_dB_R = [0, 1, 2]
     RCU_band_select_R = [1] * nr_antennas_R
     RCU_DTH_on_R = [False] * nr_antennas_R
@@ -348,12 +348,32 @@ class FakeSDPDeviceProxy:
         return getattr(self, attrname)
 
 
+class FakeTileBeamDeviceProxy:
+    """DeviceProxy that mocks access to a TileBeam device."""
+
+    N_tiles = 48
+
+    Pointing_direction_str_R = ["J2000 (0deg, 0deg)"] * N_tiles
+    Tracking_enabled_R = [True] * N_tiles
+
+    def __init__(self, name):
+        self._name = name
+
+    def name(self):
+        return self._name
+
+    def __getattr__(self, attrname):
+        return getattr(self, attrname)
+
+
 class FakeDigitalBeamDeviceProxy:
     """DeviceProxy that mocks access to an DigitalBeam device."""
 
     N_beamlets_ctrl = 488
 
     subband_select_RW = list(range(0, N_beamlets_ctrl))
+    Pointing_direction_str_R = ["J2000 (0deg, 0deg)"] * N_beamlets_ctrl
+    Tracking_enabled_R = [True] * N_beamlets_ctrl
 
     def __init__(self, name):
         self._name = name
-- 
GitLab