From c954c1ea52e848c788d88acb9272ac952f6f2f53 Mon Sep 17 00:00:00 2001 From: Jan David Mol <mol@astron.nl> Date: Fri, 7 Oct 2022 10:42:13 +0200 Subject: [PATCH] L2SS-1006: Move Antenna_Type as a property of AntennaField instead of SDP --- CDB/stations/DTS_ConfigDb.json | 3 + CDB/stations/DTS_Outside_ConfigDb.json | 9 ++- .../devices/antennafield.py | 43 ++++++++++-- .../devices/sdp/beamlet.py | 14 ++-- .../tangostationcontrol/devices/sdp/sdp.py | 68 ++++++++++++------- .../devices/test_device_antennafield.py | 14 ++++ .../default/devices/test_device_beamlet.py | 7 +- .../devices/test_device_digitalbeam.py | 8 +-- .../devices/test_device_observation.py | 2 +- .../recv_cluster/test_recv_cluster.py | 8 +++ .../test/devices/test_beamlet_device.py | 19 +++--- 11 files changed, 144 insertions(+), 51 deletions(-) diff --git a/CDB/stations/DTS_ConfigDb.json b/CDB/stations/DTS_ConfigDb.json index 65e87c4f5..f2c36a66f 100644 --- a/CDB/stations/DTS_ConfigDb.json +++ b/CDB/stations/DTS_ConfigDb.json @@ -147,6 +147,9 @@ "AntennaField": { "STAT/AntennaField/1": { "properties": { + "Antenna_Type": [ + "HBA" + ], "RECV_devices": [ "STAT/RECV/1" ], diff --git a/CDB/stations/DTS_Outside_ConfigDb.json b/CDB/stations/DTS_Outside_ConfigDb.json index 485fd4388..ce52a70b9 100644 --- a/CDB/stations/DTS_Outside_ConfigDb.json +++ b/CDB/stations/DTS_Outside_ConfigDb.json @@ -172,6 +172,9 @@ "AntennaField": { "STAT/AntennaField/2": { "properties": { + "Antenna_Type": [ + "HBA" + ], "RECV_devices": [ "STAT/RECV/1" ], @@ -229,6 +232,9 @@ }, "STAT/AntennaField/1": { "properties": { + "Antenna_Type": [ + "LBA" + ], "RECV_devices": [ "STAT/RECV/1" ], @@ -344,9 +350,6 @@ "SDP": { "STAT/SDP/1": { "properties": { - "AntennaType": [ - "LBA" - ], "OPC_Server_Name": [ "10.99.0.250" ], diff --git a/tangostationcontrol/tangostationcontrol/devices/antennafield.py b/tangostationcontrol/tangostationcontrol/devices/antennafield.py index 72f1993fe..8cb16b7fd 100644 --- a/tangostationcontrol/tangostationcontrol/devices/antennafield.py +++ b/tangostationcontrol/tangostationcontrol/devices/antennafield.py @@ -121,6 +121,13 @@ class AntennaField(lofar_device): # ----- Antenna properties + Antenna_Type = device_property( + doc="Type of antenna in this field (LBA or HBA)", + dtype='DevString', + mandatory=False, + default_value = "LBA" + ) + Antenna_Needs_Power = device_property( doc="Whether to provide power to each antenna (False for noise sources)", dtype='DevVarBooleanArray', @@ -229,6 +236,9 @@ class AntennaField(lofar_device): default_value=[] ) + Antenna_Type_R = attribute(doc='The type of antenna in this field (LBA or HBA).', + dtype=str) + Antenna_Names_R = attribute(access=AttrWriteType.READ, dtype=(str,), max_dim_x=MAX_NUMBER_OF_HBAT) Antenna_Quality_R = attribute(doc='The quality of each antenna. 0=OK, 1=SUSPICIOUS, 2=BROKEN, 3=BEYOND_REPAIR.', @@ -308,6 +318,9 @@ class AntennaField(lofar_device): doc='Number of Antennas in this field', dtype=numpy.int32) + def read_Antenna_Type_R(self): + return self.Antenna_Type + def read_Antenna_Names_R(self): return self.Antenna_Names @@ -468,7 +481,16 @@ class AntennaField(lofar_device): @log_exceptions() def _prepare_hardware(self): - usage_mask = self.read_attribute('Antenna_Usage_Mask_R') + # Configure the devices that process our antennas + self.configure_recv() + self.configure_sdp() + + # -------- + # Commands + # -------- + @command() + def configure_recv(self): + """ Configure RECV to process our antennas. """ # Disable controlling the tiles that fall outside the mask # WARN: Needed in configure_for_initialise but Tango does not allow to write attributes in INIT state @@ -477,9 +499,22 @@ class AntennaField(lofar_device): # Turn on power to antennas that need it (and due to the ANT_mask, that we're using) self.proxy.write_attribute('RCU_PWR_ANT_on_RW', self.Antenna_Needs_Power) - # -------- - # Commands - # -------- + @command() + def configure_sdp(self): + """ Configure SDP to process our antennas. """ + + # Upload which antenna type we're using + + # read-modify-write on [fpga][(input, polarisation)] + sdp_antenna_type = numpy.array(self.sdp_proxy.antenna_type_RW, dtype=object) + for fpga_nr, input_nr in self.read_attribute("Antenna_to_SDP_Mapping_R"): + # set for x polarisation + sdp_antenna_type[fpga_nr, input_nr * 2 + 0] = self.Antenna_Type + # set for y polarisation + sdp_antenna_type[fpga_nr, input_nr * 2 + 1] = self.Antenna_Type + + self.sdp_proxy.antenna_type_RW = tuple(sdp_antenna_type) + @command(dtype_in=DevVarFloatArray, dtype_out=DevVarLongArray) def calculate_HBAT_bf_delay_steps(self, delays: numpy.ndarray): num_tiles = self.read_nr_antennas_R() diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py index e09ca0968..44532e9c4 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py @@ -318,14 +318,14 @@ class Beamlet(opcua_device): return bf_weights.reshape(orig_shape) @staticmethod - def _subband_frequencies(subbands: numpy.ndarray, clock: int, nyquist_zone: int) -> numpy.ndarray: + def _subband_frequencies(subbands: numpy.ndarray, clock: int, nyquist_zones: numpy.ndarray) -> numpy.ndarray: """ Obtain the frequencies of each subband, given a clock and an antenna type. """ subband_width = clock / 1024 - base_subband = nyquist_zone * 512 + base_subbands = nyquist_zones * 512 # broadcast clock across frequencies - frequencies = (subbands + base_subband) * subband_width + frequencies = (subbands + base_subbands) * subband_width return frequencies @@ -338,7 +338,13 @@ class Beamlet(opcua_device): # obtain which subband is selected for each input and beamlet beamlet_subbands = self.read_attribute("FPGA_beamlet_subband_select_RW") # (fpga_nr, [input_nr][pol][beamlet_nr]) - return self._subband_frequencies(beamlet_subbands, self.sdp_proxy.clock_RW, self.sdp_proxy.nyquist_zone_R) + nyquist_zones = self.sdp_proxy.nyquist_zone_R # (fpga_nr, [input_nr][pol]) + + # repeat nyquist zone for all beamlets, to match the shape of beamlet_subbands + nyquist_zones = numpy.repeat(nyquist_zones, self.N_BEAMLETS_CTRL, axis=1) + + # compute the frequency of each beamlet for each input + return self._subband_frequencies(beamlet_subbands, self.sdp_proxy.clock_RW, nyquist_zones) @staticmethod def _calculate_bf_weights(delays: numpy.ndarray, beamlet_frequencies: numpy.ndarray): diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py index 88191f1cc..64d6b9461 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py @@ -32,13 +32,6 @@ class SDP(opcua_device): # Device Properties # ----------------- - AntennaType = device_property( - doc='Antenna type (LBA or HBA) we control', - dtype='DevString', - mandatory=False, - default_value = "HBA" - ) - TR_fpga_mask_RW_default = device_property( dtype='DevVarBooleanArray', mandatory=False, @@ -186,15 +179,36 @@ class SDP(opcua_device): FPGA_bst_offload_bsn_R = attribute_wrapper(comms_annotation=["FPGA_bst_offload_bsn_R"], datatype=numpy.int64, dims=(N_pn, N_beamsets_ctrl)) - antenna_type_R = attribute(doc='Type of antenna (LBA or HBA) attached to the FPGAs', - dtype=str, fget=lambda self: self.AntennaType) - nyquist_zone_R = attribute(doc='Nyquist zone of the input frequencies', - dtype=numpy.uint32, fisallowed="is_attribute_access_allowed", + antenna_type_RW = attribute(doc='Type of antenna (LBA or HBA) attached to each input of the FPGAs', + dtype=((str,),), max_dim_y=N_pn, max_dim_x=S_pn, + access=AttrWriteType.READ_WRITE, fisallowed="is_attribute_access_allowed") + nyquist_zone_R = attribute(doc='Nyquist zone of each input.', + dtype=((numpy.uint32,),), max_dim_y=N_pn, max_dim_x=S_pn, + fisallowed="is_attribute_access_allowed", polling_period=1000, abs_change=1) clock_RW = attribute(doc='Configured sampling clock (Hz)', dtype=numpy.uint32, access=AttrWriteType.READ_WRITE, fisallowed="is_attribute_access_allowed", polling_period=1000, abs_change=1) + def read_antenna_type_RW(self): + return self._antenna_type + + def write_antenna_type_RW(self, value): + # use numpy for easy processing + value = numpy.array(value) + + # validate shape + if value.shape != (self.N_pn, self.S_pn): + raise ValueError(f"Dimension mismatch. Expected ({self.N_pn}, {self.S_pn}), got {value.shape}.") + + # validate content + for val in value.flatten(): + if val not in ["LBA", "HBA"]: + raise ValueError(f"Unsupported antenna type: {val}. Must be one of [LBA, HBA].") + + # adopt new value + self._antenna_type = value + def _nyquist_zone(self, clock): """ Return the Nyquist zone for the given clock (in Hz). @@ -203,7 +217,7 @@ class SDP(opcua_device): NOTE: Only 160 and 200 MHz clocks are supported. """ - # (AntennaType, clockMHz) -> Nyquist zone + # (antenna type, clockMHz) -> Nyquist zone nyquist_zones = { ("LBA", 160): 0, ("LBA", 200): 0, @@ -211,17 +225,17 @@ class SDP(opcua_device): ("HBA", 200): 2, } - try: - return nyquist_zones[(self.AntennaType), clock // 1000000] - except KeyError: - raise ValueError(f"Could not determine Nyquist zone for antenna type {self.AntennaType} with clock {clock} Hz") + def antenna_type_to_nyquist_zone(antenna_type): + try: + return nyquist_zones[(antenna_type, clock // 1_000_000)] + except KeyError: + # sane default + return 0 + + return numpy.vectorize(antenna_type_to_nyquist_zone)(self._antenna_type) def read_nyquist_zone_R(self): - try: - return self._nyquist_zone(self.read_attribute("clock_RW")) - except ValueError: - # Supply a sane default for computations in tests until L2SDP-725 allows us to read back the set clock - return 0 + return self._nyquist_zone(self.read_attribute("clock_RW")) def read_clock_RW(self): # We can only return a single value, so we assume the FPGA is configured coherently. Which is something @@ -240,8 +254,8 @@ class SDP(opcua_device): # Tell all FPGAs to use this clock self.proxy.FPGA_pps_expected_cnt_RW = [clock] * self.N_pn - # Also update the packet headers - self.proxy.FPGA_sdp_info_nyquist_sampling_zone_index_RW = [self._nyquist_zone(clock)] * self.N_pn + # Also update the packet headers. We assume the first Nyquist zone of each FPGA is representative + self.proxy.FPGA_sdp_info_nyquist_sampling_zone_index_RW = self._nyquist_zone(clock)[:,0] # ---------- # Summarising Attributes @@ -274,6 +288,14 @@ class SDP(opcua_device): # overloaded functions # -------- + def configure_for_initialise(self): + super().configure_for_initialise() + + # Store which type of antenna is connected to each input. + # + # We need to be told this by AntennaField, through configure_for_antennafield. + self._antenna_type = numpy.array([["LBA"] * self.S_pn] * self.N_pn, dtype=str) + def _prepare_hardware(self): # FPGAs that are actually reachable and we care about wait_for = ~(self.read_attribute("TR_fpga_communication_error_R")) & self.read_attribute("TR_fpga_mask_R") diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py index d8e72fc1f..24e2c1191 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py @@ -24,9 +24,11 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): "Power_to_RECV_mapping": [1, 1, 1, 0] + [-1] * 92 }) self.recv_proxy = self.setup_recv_proxy() + self.sdp_proxy = self.setup_sdp_proxy() self.addCleanup(self.restore_antennafield) self.addCleanup(self.shutdown_recv) + self.addCleanup(self.shutdown_sdp) def restore_antennafield(self): self.proxy.put_property({ @@ -40,6 +42,11 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): recv_proxy = TestDeviceProxy("STAT/RECV/1") recv_proxy.off() + @staticmethod + def shutdown_sdp(): + sdp_proxy = TestDeviceProxy("STAT/SDP/1") + sdp_proxy.off() + def setup_recv_proxy(self): # setup RECV recv_proxy = TestDeviceProxy("STAT/RECV/1") @@ -48,6 +55,13 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): recv_proxy.set_defaults() return recv_proxy + def setup_sdp_proxy(self): + # setup SDP + sdp_proxy = TestDeviceProxy("STAT/SDP/1") + sdp_proxy.off() + sdp_proxy.warm_boot() + return sdp_proxy + def test_property_recv_devices_has_one_receiver(self): result = self.proxy.get_property("RECV_devices") self.assertSequenceEqual(result["RECV_devices"], ["STAT/RECV/1"]) diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py index 108491bca..6aa67918d 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py @@ -30,12 +30,17 @@ class TestDeviceBeamlet(AbstractTestBases.TestDeviceBase): super().test_device_read_all_attributes() - def setup_sdp(self): + def setup_sdp(self, antenna_type="HBA", clock=200_000_000): # setup SDP, on which this device depends sdp_proxy = TestDeviceProxy("STAT/SDP/1") sdp_proxy.off() sdp_proxy.warm_boot() sdp_proxy.set_defaults() + + # setup the frequencies as expected in the test + sdp_proxy.antenna_type_RW = [[antenna_type] * 12] * 16 + sdp_proxy.clock_RW = clock + return sdp_proxy def test_pointing_to_zenith(self): diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py index 3c48ad0ad..9a7d22306 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py @@ -87,8 +87,8 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): TestDeviceProxy.test_device_turn_off, self.antennafield_iden ) - self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok) self.sdp_proxy = self.setup_sdp_proxy() + self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok) self.beamlet_proxy = self.initialise_beamlet_proxy() self.beamlet_proxy.on() @@ -126,8 +126,8 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): TestDeviceProxy.test_device_turn_off, self.antennafield_iden ) - self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok) self.sdp_proxy = self.setup_sdp_proxy() + self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok) self.beamlet_proxy = self.initialise_beamlet_proxy() self.beamlet_proxy.subband_select_RW = numpy.array(list(range(317)) + [316] + list(range(318,488)), dtype=numpy.uint32) @@ -160,8 +160,8 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): TestDeviceProxy.test_device_turn_off, self.antennafield_iden ) - self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok) self.setup_sdp_proxy() + self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok) self.proxy.initialise() self.proxy.Tracking_enabled_RW = False @@ -190,8 +190,8 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): TestDeviceProxy.test_device_turn_off, self.antennafield_iden ) - self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok) self.setup_sdp_proxy() + self.setup_antennafield_proxy(self.antenna_qualities_ok, self.antenna_use_ok) self.proxy.initialise() self.proxy.Tracking_enabled_RW = False diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py index fb931b7dc..46c77246e 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py @@ -37,9 +37,9 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): super().setUp("STAT/Observation/1") self.VALID_JSON = TestObservationBase.VALID_JSON self.recv_proxy = self.setup_recv_proxy() + self.sdp_proxy = self.setup_sdp_proxy() self.antennafield_proxy = self.setup_antennafield_proxy() self.beamlet_proxy = self.setup_beamlet_proxy() - self.sdp_proxy = self.setup_sdp_proxy() self.digitalbeam_proxy = self.setup_digitalbeam_proxy() self.tilebeam_proxy = self.setup_tilebeam_proxy() diff --git a/tangostationcontrol/tangostationcontrol/integration_test/recv_cluster/test_recv_cluster.py b/tangostationcontrol/tangostationcontrol/integration_test/recv_cluster/test_recv_cluster.py index 6c7aad92b..92db0ffbf 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/recv_cluster/test_recv_cluster.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/recv_cluster/test_recv_cluster.py @@ -35,6 +35,14 @@ class TestRecvCluster(base.IntegrationTestCase): antenna_field_proxies = [] recv_proxies = [] + # SDP must be ready before AntennaField + sdp_proxy = TestDeviceProxy("STAT/SDP/1") + sdp_proxy.off() + self.assertTrue(sdp_proxy.state() is DevState.OFF) + sdp_proxy.warm_boot() + sdp_proxy.set_defaults() + self.assertTrue(sdp_proxy.state() is DevState.ON) + # Beam / Recv 1,2,3,4 for i in range(1, 5): recv_proxies.append(TestDeviceProxy(f"STAT/RECV/{i}")) diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py index e7b82ec29..ff43c4ada 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py @@ -97,38 +97,35 @@ class TestBeamletDevice(base.TestCase): [0, 1, 102], ]) - clocks = numpy.array([ - 200 * 1000000, - 160 * 1000000 - ]) - - subband_width = 200e6 / 1024 + nyquist_zones_0 = numpy.zeros(subbands.shape) + nyquist_zones_1 = numpy.ones(subbands.shape) + nyquist_zones_2 = numpy.ones(subbands.shape) * 2 # for reference values, see https://proxy.lofar.eu/rtsm/tests/ - lba_frequencies = Beamlet._subband_frequencies(subbands, 160 * 1000000, 0) + lba_frequencies = Beamlet._subband_frequencies(subbands, 160 * 1000000, nyquist_zones_0) self.assertAlmostEqual(lba_frequencies[0][0], 0.0000000e6) self.assertAlmostEqual(lba_frequencies[0][1], 0.1562500e6) self.assertAlmostEqual(lba_frequencies[0][2], 15.9375000e6) - lba_frequencies = Beamlet._subband_frequencies(subbands, 200 * 1000000, 0) + lba_frequencies = Beamlet._subband_frequencies(subbands, 200 * 1000000, nyquist_zones_0) self.assertAlmostEqual(lba_frequencies[0][0], 0.0000000e6) self.assertAlmostEqual(lba_frequencies[0][1], 0.1953125e6) self.assertAlmostEqual(lba_frequencies[0][2], 19.9218750e6) # Nyquist zone 1 is not used in 160 MHz - hba_low_frequencies = Beamlet._subband_frequencies(subbands, 200 * 1000000, 1) + hba_low_frequencies = Beamlet._subband_frequencies(subbands, 200 * 1000000, nyquist_zones_1) self.assertAlmostEqual(hba_low_frequencies[0][0], 100.0000000e6) self.assertAlmostEqual(hba_low_frequencies[0][1], 100.1953125e6) self.assertAlmostEqual(hba_low_frequencies[0][2], 119.9218750e6) - hba_high_frequencies = Beamlet._subband_frequencies(subbands, 160 * 1000000, 2) + hba_high_frequencies = Beamlet._subband_frequencies(subbands, 160 * 1000000, nyquist_zones_2) self.assertAlmostEqual(hba_high_frequencies[0][0], 160.0000000e6) self.assertAlmostEqual(hba_high_frequencies[0][1], 160.1562500e6) self.assertAlmostEqual(hba_high_frequencies[0][2], 175.9375000e6) - hba_high_frequencies = Beamlet._subband_frequencies(subbands, 200 * 1000000, 2) + hba_high_frequencies = Beamlet._subband_frequencies(subbands, 200 * 1000000, nyquist_zones_2) self.assertAlmostEqual(hba_high_frequencies[0][0], 200.0000000e6) self.assertAlmostEqual(hba_high_frequencies[0][1], 200.1953125e6) self.assertAlmostEqual(hba_high_frequencies[0][2], 219.9218750e6) -- GitLab