From 27a850febe821b3075b9d3ac47ded69fbe3d824d Mon Sep 17 00:00:00 2001 From: Jan David Mol <mol@astron.nl> Date: Tue, 8 Aug 2023 10:53:15 +0000 Subject: [PATCH] Resolve L2SS-1470 "Set rcu band for both polarisations" --- README.md | 1 + tangostationcontrol/VERSION | 2 +- .../devices/test_device_antennafield.py | 39 ++++++++++------ .../devices/test_device_observation.py | 11 +++-- .../tangostationcontrol/common/calibration.py | 46 +++++++++---------- .../devices/antennafield.py | 45 ++++++++++-------- .../devices/base_device_classes/mapper.py | 17 +++++-- .../devices/observation.py | 6 +-- .../test/common/test_calibration.py | 2 +- .../test/devices/test_antennafield_device.py | 4 +- 10 files changed, 101 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 5cfefc5ca..bf330a2d0 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ Next change the version in the following places: # Release Notes +* 0.20.5 Manage both polarisations in RCU_band_select_R(W), Antenna_Loss_R, and Frequency_Band_RW * 0.20.4 Collapse AbstractHierarchyDevice and AbstractHierarchy into one class * 0.20.3 Fix application of Field_Attenuation_R * 0.20.2 Support only one parent in hierarchies diff --git a/tangostationcontrol/VERSION b/tangostationcontrol/VERSION index 6dd46024a..1b619f348 100644 --- a/tangostationcontrol/VERSION +++ b/tangostationcontrol/VERSION @@ -1 +1 @@ -0.20.4 +0.20.5 diff --git a/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py b/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py index 6411150bb..d79c50d30 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py @@ -29,7 +29,12 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): super().setUp("STAT/AntennaField/HBA0") self.proxy.put_property( { - "Power_to_RECV_mapping": [1, 1, 1, 0] + [-1] * ((CS001_TILES * 2) - 4), + "Power_to_RECV_mapping": numpy.array( + [[1, x * 2 + 0] for x in range(MAX_ANTENNA // 2)] + ).flatten(), + "Control_to_RECV_mapping": numpy.array( + [[1, x * 2 + 1] for x in range(MAX_ANTENNA // 2)] + ).flatten(), } ) self.recv_proxy = self.setup_recv_proxy() @@ -300,10 +305,12 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): """Verify small RECV device attribute changed all inputs mapped""" mapping_properties = { - "Power_to_RECV_mapping": [-1, -1] * CS001_TILES, - "Control_to_RECV_mapping": - # [1, 0, 1, 1, 1, 2, 1, x ... 1, 95] - numpy.array([[1, x] for x in range(0, MAX_ANTENNA)]).flatten(), + "Power_to_RECV_mapping": numpy.array( # X maps on power + [[1, x * 2 + 1] for x in range(MAX_ANTENNA // 2)] + ).flatten(), + "Control_to_RECV_mapping": numpy.array( # Y maps on control + [[1, x * 2 + 0] for x in range(MAX_ANTENNA // 2)] + ).flatten(), } antennafield_proxy = self.proxy @@ -311,22 +318,23 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): antennafield_proxy.put_property(mapping_properties) antennafield_proxy.boot() - self.recv_proxy.write_attribute( - "RCU_band_select_RW", [[False] * N_rcu_inp] * N_rcu - ) + self.recv_proxy.write_attribute("RCU_band_select_RW", [[0] * N_rcu_inp] * N_rcu) try: antennafield_proxy.write_attribute( - "RCU_band_select_RW", [True] * MAX_ANTENNA + # [X, Y] + "RCU_band_select_RW", + [[1, 2]] * MAX_ANTENNA, ) numpy.testing.assert_equal( - numpy.array([[True] * N_rcu_inp] * N_rcu), - self.recv_proxy.read_attribute("RCU_band_select_RW").value, + # [Power, Control] + numpy.array([2, 1] * (MAX_ANTENNA // 2)), + self.recv_proxy.read_attribute("RCU_band_select_RW").value.flatten(), ) finally: # Always restore recv again self.recv_proxy.write_attribute( - "RCU_band_select_RW", [[False] * N_rcu_inp] * N_rcu + "RCU_band_select_RW", [[0] * N_rcu_inp] * N_rcu ) # Verify device did not enter FAULT state @@ -339,7 +347,8 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): for antenna_type in ["HBA"]: properties = { "Antenna_Type": [antenna_type], - "Control_to_RECV_mapping": [1, 1, 1, 0] + [-1, -1] * (CS001_TILES - 2), + "Control_to_RECV_mapping": [1, 1] + [-1, -1] * (CS001_TILES - 1), + "Power_to_RECV_mapping": [1, 0] + [-1, -1] * (CS001_TILES - 1), "Antenna_to_SDP_Mapping": [0, 1, 0, 0] + [-1, -1] * (CS001_TILES - 2), } @@ -356,7 +365,7 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): self.sdp_proxy.write_attribute("nyquist_zone_RW", [[0] * S_pn] * N_pn) antennafield_proxy.write_attribute( - "Frequency_Band_RW", [band.name] * CS001_TILES + "Frequency_Band_RW", [[band.name, band.name]] * CS001_TILES ) # Wait for Tango attributes updating time.sleep(1) @@ -390,6 +399,6 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): # test whether reading back results in the same band for our inputs numpy.testing.assert_equal( numpy.array([band.name, band.name]), - antennafield_proxy.read_attribute("Frequency_Band_RW").value[:2], + antennafield_proxy.read_attribute("Frequency_Band_RW").value[0], err_msg=f"{band.name}", ) diff --git a/tangostationcontrol/integration_test/default/devices/test_device_observation.py b/tangostationcontrol/integration_test/default/devices/test_device_observation.py index f6b304da8..634b0f222 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_observation.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_observation.py @@ -112,12 +112,14 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): def setup_antennafield_proxy(self): # setup AntennaField antennafield_proxy = TestDeviceProxy("STAT/AntennaField/HBA0") - control_mapping = [[1, i] for i in range(CS001_TILES)] + power_mapping = [[1, i * 2 + 0] for i in range(CS001_TILES)] + control_mapping = [[1, i * 2 + 1] for i in range(CS001_TILES)] antenna_qualities = numpy.array([AntennaQuality.OK] * MAX_ANTENNA) antenna_use = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA) antennafield_proxy.put_property( { "Antenna_Type": ["HBA"], + "Power_to_RECV_mapping": numpy.array(power_mapping).flatten(), "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(), "Antenna_to_SDP_Mapping": self.ANTENNA_TO_SDP_MAPPING, "Antenna_Quality": antenna_qualities, @@ -241,16 +243,17 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): self.setup_stationmanager_proxy() self.setup_recv_proxy() antennafield_proxy = self.setup_antennafield_proxy() - antennafield_proxy.RCU_band_select_RW = [0] * CS001_TILES + antennafield_proxy.RCU_band_select_RW = [[0, 0]] * CS001_TILES self.assertListEqual( - antennafield_proxy.RCU_band_select_RW.tolist(), [0] * CS001_TILES + antennafield_proxy.RCU_band_select_RW.tolist(), + [[0, 0]] * CS001_TILES, ) self.proxy.off() self.proxy.observation_settings_RW = self.VALID_JSON self.proxy.Initialise() self.proxy.On() expected_bands = [ - 2 + [2, 2] ] * CS001_TILES # we request every antenna to be in this band, regardless of mask self.assertListEqual( antennafield_proxy.RCU_band_select_RW.tolist(), expected_bands diff --git a/tangostationcontrol/tangostationcontrol/common/calibration.py b/tangostationcontrol/tangostationcontrol/common/calibration.py index 1c98b7dbc..9cf13f644 100644 --- a/tangostationcontrol/tangostationcontrol/common/calibration.py +++ b/tangostationcontrol/tangostationcontrol/common/calibration.py @@ -120,13 +120,8 @@ class CalibrationManager: is_hba = antenna_type.upper() != "LBA" - for antenna_nr, rcu_band in enumerate(rcu_bands): - fpga_nr, input_nr = antenna_to_sdp_mapping[antenna_nr] - antenna_name = antenna_names[antenna_nr] - - if input_nr == -1: - # skip unconnected antennas - continue + def get_antenna_calibration(antenna_name: str, rcu_band: int): + """Return the calibration values for the given antenna and RCU band.""" calibration_filename = os.path.join( self._tmp_dir.name, @@ -143,19 +138,29 @@ class CalibrationManager: f"Expected calibration table for station {self._station_name}, but got {table.observation_station} in calibration file {calibration_filename}" ) - if antenna_name not in table.antennas: + try: + return table.antennas[antenna_name] + except KeyError: raise ValueError( f"Could not find calibration values for field {antennafield_name} antenna {antenna_name} (index {antenna_nr}) in calibration file {calibration_filename}" ) - # set weights, converted from complex to packed uint32 - fpga_subband_weights[fpga_nr, input_nr, 0] = complex_to_weights( - table.antennas[antenna_name].x - ) + for antenna_nr, (rcu_band_x, rcu_band_y) in enumerate(rcu_bands): + fpga_nr, input_nr = antenna_to_sdp_mapping[antenna_nr] + antenna_name = antenna_names[antenna_nr] - fpga_subband_weights[fpga_nr, input_nr, 1] = complex_to_weights( - table.antennas[antenna_name].y - ) + if input_nr == -1: + # skip unconnected antennas + continue + + # set weights, converted from complex to packed uint32 + fpga_subband_weights[fpga_nr, input_nr, 0] = complex_to_weights( + get_antenna_calibration(antenna_name, rcu_band_x).x + ) + + fpga_subband_weights[fpga_nr, input_nr, 1] = complex_to_weights( + get_antenna_calibration(antenna_name, rcu_band_y).y + ) # TODO(L2SS-1312): This should use atomic_read_modify_write sdp.FPGA_subband_weights_RW = fpga_subband_weights.reshape( @@ -266,14 +271,7 @@ def calibrate_RCU_attenuator_dB(antenna_field: DeviceProxy): rcu_attenuator_db += antenna_field.Field_Attenuation_R # apply on antenna field - antenna_field.RCU_attenuator_dB_RW = ( - numpy.tile( - numpy.array(rcu_attenuator_db, dtype=numpy.int64).flatten(), - N_pol, - ) - .reshape(N_pol, antenna_field.nr_antennas_R) - .T - ) + antenna_field.RCU_attenuator_dB_RW = rcu_attenuator_db.astype(numpy.int64) def loss_compensation(losses_dB: numpy.ndarray): @@ -300,7 +298,7 @@ def loss_compensation(losses_dB: numpy.ndarray): # correct for the coarse loss by dampening the signals to line up. input_attenuation_integer_dB = ( - max(signal_attenuation_integer_dB) - signal_attenuation_integer_dB + numpy.max(signal_attenuation_integer_dB) - signal_attenuation_integer_dB ) # compute the remainder, as a scaling factor diff --git a/tangostationcontrol/tangostationcontrol/devices/antennafield.py b/tangostationcontrol/tangostationcontrol/devices/antennafield.py index b83f34e6b..3477e4787 100644 --- a/tangostationcontrol/tangostationcontrol/devices/antennafield.py +++ b/tangostationcontrol/tangostationcontrol/devices/antennafield.py @@ -371,9 +371,10 @@ class AntennaField(LOFARDevice): return numpy.array(self.RECV_Devices) Frequency_Band_RW = attribute( - doc="The selected frequency band of each antenna.", - dtype=(str,), - max_dim_x=MAX_ANTENNA, + doc="The selected frequency band of each polarisation of each antenna.", + dtype=((str,),), + max_dim_x=N_pol, + max_dim_y=MAX_ANTENNA, access=AttrWriteType.READ_WRITE, ) @@ -393,9 +394,10 @@ class AntennaField(LOFARDevice): unit="s", ) Antenna_Cables_Loss_R = attribute( - doc="Loss caused by the cable between antenna and RCU, in dB.", - dtype=(numpy.float64,), - max_dim_x=MAX_ANTENNA, + doc="Loss caused by the cable between antenna and RCU, in dB, for either polarisation.", + dtype=((numpy.float64,),), + max_dim_x=N_pol, + max_dim_y=MAX_ANTENNA, unit="dB", ) @@ -510,13 +512,15 @@ class AntennaField(LOFARDevice): ) RCU_band_select_R = MappedAttribute( "RCU_band_select_R", - dtype=(numpy.int64,), - max_dim_x=MAX_ANTENNA, + dtype=((numpy.int64,),), + max_dim_x=N_pol, + max_dim_y=MAX_ANTENNA, ) RCU_band_select_RW = MappedAttribute( "RCU_band_select_RW", - dtype=(numpy.int64,), - max_dim_x=MAX_ANTENNA, + dtype=((numpy.int64,),), + max_dim_x=N_pol, + max_dim_y=MAX_ANTENNA, access=AttrWriteType.READ_WRITE, ) RCU_attenuator_dB_R = MappedAttribute( @@ -699,7 +703,7 @@ class AntennaField(LOFARDevice): def read_Frequency_Band_RW(self): antenna_type = self.Antenna_Type - # fetch settings from RECV + # fetch settings from RECV, use X polarisation for reference rcu_band_select = self.read_attribute("RCU_band_select_RW") # fetch settings from SDP @@ -733,20 +737,20 @@ class AntennaField(LOFARDevice): ) if ( - bands[val].clock != bands[value[0]].clock - ): # NB: "value[0] in bands" holds at this point + bands[val].clock != bands[value[0, 0]].clock + ): # NB: "value[0,0] in bands" holds at this point raise ValueError( f"All frequency bands must use the same clock. \ These do not: {val} and {value[0, 0]}." ) - # apply settings on RECV + # convert into settings for RECV self.proxy.RCU_band_select_RW = numpy.vectorize( lambda band: bands[band].rcu_band )(value) # apply settings on SDP - self.sdpfirmware_proxy.clock_RW = bands[value[0]].clock + self.sdpfirmware_proxy.clock_RW = bands[value[0, 0]].clock # read-modify-write on [fpga][(input, polarisation)] sdp_nyquist_zone = numpy.full((N_pn, A_pn * N_pol), None) @@ -759,11 +763,11 @@ class AntennaField(LOFARDevice): # set for x polarisation sdp_nyquist_zone[fpga_nr, input_nr * 2 + 0] = bands[ - value[antenna_nr] + value[antenna_nr, 0] ].nyquist_zone # set for y polarisation sdp_nyquist_zone[fpga_nr, input_nr * 2 + 1] = bands[ - value[antenna_nr] + value[antenna_nr, 1] ].nyquist_zone self.atomic_read_modify_write_attribute( @@ -790,9 +794,12 @@ class AntennaField(LOFARDevice): # Return 0 loss for them instead. return numpy.array( [ - cable_types[cable].get_loss(self.Antenna_Type, rcu_band) + [ + cable_types[cable].get_loss(self.Antenna_Type, rcu_band[0]), + cable_types[cable].get_loss(self.Antenna_Type, rcu_band[1]), + ] if recv > 0 - else 0 + else [0, 0] for recv, cable, rcu_band in zip(recvs, self.Antenna_Cables, rcu_bands) ] ) diff --git a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/mapper.py b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/mapper.py index efddb1bd2..064bba36a 100644 --- a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/mapper.py +++ b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/mapper.py @@ -328,8 +328,12 @@ class AntennaToRecvMapper(AntennaMapper): "HBAT_PWR_on_RW": value_map_ant_32_bool, "RCU_PWR_ANT_on_R": value_map_ant_bool, "RCU_PWR_ANT_on_RW": value_map_ant_bool, - "RCU_band_select_R": numpy.zeros(number_of_antennas, dtype=numpy.int64), - "RCU_band_select_RW": numpy.zeros(number_of_antennas, dtype=numpy.int64), + "RCU_band_select_R": numpy.zeros( + (number_of_antennas, N_pol), dtype=numpy.int64 + ), + "RCU_band_select_RW": numpy.zeros( + (number_of_antennas, N_pol), dtype=numpy.int64 + ), "RCU_attenuator_dB_R": numpy.zeros( (number_of_antennas, N_pol), dtype=numpy.int64 ), @@ -354,6 +358,8 @@ class AntennaToRecvMapper(AntennaMapper): self._reshape_attributes_in = mapped_dimensions self._reshape_attributes_in["RCU_attenuator_dB_R"] = ((MAX_ANTENNA * N_pol),) self._reshape_attributes_in["RCU_attenuator_dB_RW"] = ((MAX_ANTENNA * N_pol),) + self._reshape_attributes_in["RCU_band_select_R"] = ((MAX_ANTENNA * N_pol),) + self._reshape_attributes_in["RCU_band_select_RW"] = ((MAX_ANTENNA * N_pol),) self._reshape_attributes_in["RCU_PCB_ID_R"] = (MAX_ANTENNA,) self._reshape_attributes_in["RCU_PCB_version_R"] = (MAX_ANTENNA,) @@ -366,6 +372,8 @@ class AntennaToRecvMapper(AntennaMapper): # Attributes which need to be reshaped with a copy of their values, # because RECV original dimension < AntennaField mapped dimension self._fill_attributes_in = { + "RCU_band_select_R": N_pol, + "RCU_band_select_RW": N_pol, "RCU_attenuator_dB_R": N_pol, "RCU_attenuator_dB_RW": N_pol, "RCU_PCB_ID_R": N_rcu_inp, @@ -375,7 +383,8 @@ class AntennaToRecvMapper(AntennaMapper): # Attributes which need to be reshaped with removing a part of their duplicated # values because RECV original dimension < Antennafield mapped dimension self._empty_attributes_out = { - "RCU_attenuator_dB_RW": (N_rcu * N_rcu_inp, N_pol) + "RCU_band_select_RW": (N_rcu * N_rcu_inp, N_pol), + "RCU_attenuator_dB_RW": (N_rcu * N_rcu_inp, N_pol), } def _init_masked_value_write(self, mapped_dimensions: dict): @@ -398,6 +407,8 @@ class AntennaToRecvMapper(AntennaMapper): double_mapping = [ "RCU_attenuator_dB_R", "RCU_attenuator_dB_RW", + "RCU_band_select_R", + "RCU_band_select_RW", "RCU_PCB_ID_R", "RCU_PCB_version_R", ] diff --git a/tangostationcontrol/tangostationcontrol/devices/observation.py b/tangostationcontrol/tangostationcontrol/devices/observation.py index 1af565875..af767e7cc 100644 --- a/tangostationcontrol/tangostationcontrol/devices/observation.py +++ b/tangostationcontrol/tangostationcontrol/devices/observation.py @@ -16,6 +16,7 @@ from tangostationcontrol.common.constants import ( MAX_ANTENNA, N_beamlets_ctrl, N_point_prop, + N_pol, ) from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python @@ -313,9 +314,8 @@ class Observation(LOFARDevice): AntennaField device """ - # convert numpy.array(dtype=str) to list[str] to avoid crash in DeviceProxy - # see https://gitlab.com/tango-controls/pytango/-/issues/492 - return (numpy.array([filter_name] * MAX_ANTENNA).tolist(),) + nr_antennas = self.antennafield_proxy.nr_antennas_R + return (numpy.array([[filter_name] * N_pol] * nr_antennas),) def _apply_saps_subbands(self, sap_subbands: list): """Convert an array of subbands into the correct format for Beamlet device""" diff --git a/tangostationcontrol/test/common/test_calibration.py b/tangostationcontrol/test/common/test_calibration.py index 159c088f7..40cb48f5f 100644 --- a/tangostationcontrol/test/common/test_calibration.py +++ b/tangostationcontrol/test/common/test_calibration.py @@ -74,7 +74,7 @@ class TestCalibrationManager(base.TestCase): ).reshape(-1, 2), Antenna_Names_R=[f"T{n + 1}" for n in range(2)], Antenna_Type_R="HBA", - RCU_band_select_RW=numpy.array([1, 2]), + RCU_band_select_RW=numpy.array([[1, 1], [2, 2]]), **{"name.return_value": "Stat/AntennaField/TEST"}, ) subband_weights = numpy.array([[SDP_UNIT_WEIGHT] * S_pn * N_subbands] * N_pn) diff --git a/tangostationcontrol/test/devices/test_antennafield_device.py b/tangostationcontrol/test/devices/test_antennafield_device.py index da91e7d26..12edfafef 100644 --- a/tangostationcontrol/test/devices/test_antennafield_device.py +++ b/tangostationcontrol/test/devices/test_antennafield_device.py @@ -208,7 +208,7 @@ class TestAntennaToRecvMapper(base.TestCase): } mapper = AntennaToRecvMapper(recv_mapping, RECV_MAPPED_ATTRS, 3) receiver_values = [[0] * MAX_ANTENNA, [0] * MAX_ANTENNA, [0] * MAX_ANTENNA] - expected = [0] * DEFAULT_N_HBA_TILES + expected = [[0, 0]] * DEFAULT_N_HBA_TILES actual = mapper.map_read("RCU_band_select_RW", receiver_values) numpy.testing.assert_equal(expected, actual) @@ -711,7 +711,7 @@ class TestAntennaToRecvMapper(base.TestCase): } mapper = AntennaToRecvMapper(recv_mapping, RECV_MAPPED_ATTRS, 1) - set_values = [1, 0] + [None] * (DEFAULT_N_HBA_TILES - 2) + set_values = [[1, 1], [0, 0]] + [[None, None]] * (DEFAULT_N_HBA_TILES - 2) expected = [[[0, 1, None]] + [[None, None, None]] * (N_rcu - 1)] actual = mapper.map_write("RCU_band_select_RW", set_values) numpy.testing.assert_equal(expected, actual) -- GitLab