diff --git a/README.md b/README.md index e1204001a65ecfdc0e62d244514f859c7db5cd9a..afa1c4339e31944a78489beb85be46f6bceacab7 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ Next change the version in the following places: # Release Notes * 0.22.0 Migrate execution environment to nomad +* 0.21.3 Added DigitalBeam.Antenna_Usage_Mask_R to expose antennas used in beamforming * 0.21.2 Removed deprecated "Boot" device (use StationManager now) * 0.21.1 Implement multi project integration downstream pipeline * 0.21.0 Use radians instead of degrees when interpreting pointings @@ -143,38 +144,38 @@ Next change the version in the following places: * 0.20.3 Fix application of Field_Attenuation_R * 0.20.2 Support only one parent in hierarchies * 0.20.1 Create an abstract AntennaMapper class which implements behavior of both AntennaToSdpMapper - and AntennaToRecvMapper + and AntennaToRecvMapper * 0.20.0 Complete implementation of station-state transitions in StationManager device. - Unified power management under power_hardware_on/off(), dropping prepare_hardware(), - disable_hardware(). - Replaced device.warm_boot() by device.boot(). + Unified power management under power_hardware_on/off(), dropping prepare_hardware(), + disable_hardware(). + Replaced device.warm_boot() by device.boot(). * 0.19.0 Ensure requirements.txt are installed when using pip install * 0.18.3 Many configuration fixes in tango device configs, Fixed APS & EC device port mapping, - fixed variable initialization in several devices, Fixed XST device going into - fault state, prevent UDP packet loss and verify UDP buffer size for XSTs, - Fixed several tests due to use of numpy.array in properties, Implement control hierarchy, - Version pin PyASN, Fix code coverage for PyTango devices, Fix beam tracker not starting again - after being stopped. + fixed variable initialization in several devices, Fixed XST device going into + fault state, prevent UDP packet loss and verify UDP buffer size for XSTs, + Fixed several tests due to use of numpy.array in properties, Implement control hierarchy, + Version pin PyASN, Fix code coverage for PyTango devices, Fix beam tracker not starting again + after being stopped. * 0.18.2 Fix documentation links in README * 0.18.1 Various improvements including: better error handling for commands and - resolving a configuration issue related to beamlets + resolving a configuration issue related to beamlets * 0.18.0 Expose attribute related to SDP rings such as `FPGA_bf_ring_nof_transport_hops_RW_default` and - `FPGA_ring_use_cable_to_next_rn_RW_default` + `FPGA_ring_use_cable_to_next_rn_RW_default` * 0.17.1 Ensure OPCUA devices reconnect automatically if the connection is lost * 0.17.0 Add Power Hierarchy state transition * 0.16.2 Add Power_Parent and Parent_Children properties in LOFAR devices * 0.16.1 AntennaField: Do not put device in FAULT if an attribute cannot be read/written. - AntennaField: Avoid archiving HBA-specific attributes for LBA fields. + AntennaField: Avoid archiving HBA-specific attributes for LBA fields. * 0.16.0 Observation: Removed antenna mask from specification - DigitalBeam: Removed beamlet and antenna selection + DigitalBeam: Removed beamlet and antenna selection * 0.15.0 Split `recv` device into `rcu2h` and `rcu2l` and - split `recv-sim` translator into `rcu2h-sim` and `rcu2l-sim` + split `recv-sim` translator into `rcu2h-sim` and `rcu2l-sim` * 0.14.0 Create async device base and make tilebeam and digitalbeam async device servers, - allowing for cooperative multitasking and preventing issues with beamtracking. + allowing for cooperative multitasking and preventing issues with beamtracking. * 0.13.1 Upgrade PyTango to 9.4.x and ensure it is installed through requirements.txt * 0.13.0 Remove all `archiver-timescale`, `hdbppts-cm`, `hdbppts-es` functionalities * 0.12.1 Add `AbstractHierarchy` and `AbstractHierarchyDevice` classes and - functionality + functionality * 0.12.0 Add `Calibration_SDP_Subband_Weights_<XXX>MHz_R` attributes to implement HDF5 calibration tables * 0.11.2 Fix sleep duration in archiver test * 0.11.1 Fix event unsubscription in TemperatureManager @@ -182,8 +183,8 @@ Next change the version in the following places: * 0.10.0 Add `AntennaToSdpMapper` and fpga_sdp_info_* mapped attributes in `Antennafield` device * 0.9.0 Statistics writer: moved the whole functionality to lofar-station-client repository * 0.8.0 Statistics writer: HDF5 format overhaul (removed `values`, added and moved attributes), - Statistics writer: Added `--field` parameter to record statistics of a specific AntennaField, - AntennaField: Added `RCU_DTH_on_R`, `RCU_DTH_freq_R(W)`, `RCU_band_select_R`, `RCU_attenuator_dB_R`. + Statistics writer: Added `--field` parameter to record statistics of a specific AntennaField, + AntennaField: Added `RCU_DTH_on_R`, `RCU_DTH_freq_R(W)`, `RCU_band_select_R`, `RCU_attenuator_dB_R`. * 0.7.2 Added `sdp.subband_frequency_R`, `antennafield.Frequency_Band_RW`, and support for spectral inversion * 0.7.1 Add restore backup configuration for `Configuration` device * 0.7.0 Raised required Python version to 3.10 diff --git a/tangostationcontrol/tangostationcontrol/beam/managers/_digitalbeam.py b/tangostationcontrol/tangostationcontrol/beam/managers/_digitalbeam.py index 1bca73797bcd39ee55065f1fe3353987a26a9007..16e38a6f2804065ecce05b01350cc7b2fe45005f 100644 --- a/tangostationcontrol/tangostationcontrol/beam/managers/_digitalbeam.py +++ b/tangostationcontrol/tangostationcontrol/beam/managers/_digitalbeam.py @@ -26,6 +26,35 @@ class DigitalBeamManager(AbstractBeamManager): self.relative_antenna_positions = None super().__init__() + def antennas_used(self) -> list[bool]: + """ + Return the set of antennas actually used in the beamforming, after + all masks and selectionis have been applied. + """ + + # Use zero weights for antennas that we should not use: + # - if the antenna has no input mapped on the SDP + # - if the antenna is bad (False in Antenna_Usage_Mask_R) + # - if the antenna is not in the antenna set (False in Antenna_Mask_RW) + antenna_usage_mask = self._device.control.read_parent_attribute( + "Antenna_Usage_Mask_R" + ) + antenna_mask = self._device.read_Antenna_Mask_R() + antenna_to_sdp_mapping = self._device.control.read_parent_attribute( + "Antenna_to_SDP_Mapping_R" + ) + + antennas_used = [False] * len(antenna_mask) + + for antenna_nr, (_fpga_nr, input_nr) in enumerate(antenna_to_sdp_mapping): + antennas_used[antenna_nr] = ( + input_nr >= 0 + and antenna_usage_mask[antenna_nr] + and antenna_mask[antenna_nr] + ) + + return antennas_used + @TimeIt() def delays(self, pointing_direction: numpy.array, timestamp: datetime.datetime): """ @@ -74,24 +103,14 @@ class DigitalBeamManager(AbstractBeamManager): beam_weights = self._device.beamlet.calculate_bf_weights(beam_delays.flatten()) beam_weights = beam_weights.reshape(N_pn, A_pn, N_beamlets_ctrl) - # Use zero weights for antennas that we should not use: - # - if the antenna has no input mapped on the SDP - # - if the antenna is bad (False in Antenna_Usage_Mask_R) - # - if the antenna is not in the antenna set (False in Antenna_Mask_RW) - antenna_usage_mask = self._device.control.read_parent_attribute( - "Antenna_Usage_Mask_R" - ) - antenna_mask = self._device.read_Antenna_Mask_R() + # Use zero weights for antennas that we should not use + antennas_used = self.antennas_used() zeroes_for_all_beamlets = numpy.array([0] * N_beamlets_ctrl, dtype=numpy.uint32) for antenna_nr, (fpga_nr, input_nr) in enumerate( self._device.control.read_parent_attribute("Antenna_to_SDP_Mapping_R") ): - if ( - input_nr < 0 - or not antenna_usage_mask[antenna_nr] - or not antenna_mask[antenna_nr] - ): + if not antennas_used[antenna_nr]: beam_weights[fpga_nr, input_nr, :] = zeroes_for_all_beamlets return beam_weights diff --git a/tangostationcontrol/tangostationcontrol/clients/udp_receiver.py b/tangostationcontrol/tangostationcontrol/clients/udp_receiver.py index c8d364c7bd2ea50b46dce5327cccf021082a851a..52170d5431d5b90394fd1b8cf647d9e5ebd07c5a 100644 --- a/tangostationcontrol/tangostationcontrol/clients/udp_receiver.py +++ b/tangostationcontrol/tangostationcontrol/clients/udp_receiver.py @@ -83,11 +83,15 @@ class UDPReceiver(Thread, StatisticsClientThread): ) if self.parameters["recv_buffer_size"] < self.options["recv_buffer_size"]: + # Typically this is the host OS not allowing us to allocate buffers + # of this size. Try increasing it using (as root): + # sysctl -w net.core.rmem_max=$((16*1024*1024)) logger.error( f"OS does not allow requested buffer size. " f"This could result in UDP packet loss. " f'Requested {self.options["recv_buffer_size"]} bytes, ' - f'got {self.parameters["recv_buffer_size"]}.' + f'got {self.parameters["recv_buffer_size"]}. Verify ' + f'if "sysctl net.core.rmem_max" is sufficiently large.' ) # specify what host and port to listen on diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py b/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py index 1d2d8f77ba0c911e3aaa832e4de328207e0cd27d..022263aab3eeeac20823a37a1b50e1368deab4f9 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py @@ -72,9 +72,19 @@ class DigitalBeam(BeamDevice): max_dim_x=MAX_ANTENNA, ) + Antenna_Usage_Mask_R = attribute( + doc="Antennas used for beam forming. Excludes antennas outside " + "the antenna set, broken antennas, unconnected antennas, etc.", + dtype=(bool,), + max_dim_x=MAX_ANTENNA, + fisallowed="is_attribute_access_allowed", + fget=lambda self: self._beam_manager.antennas_used(), + ) + Duration_delays_R = attribute( access=AttrWriteType.READ, dtype=numpy.float64, + fisallowed="is_attribute_access_allowed", fget=lambda self: self._beam_manager.delays.get_statistic(self)["last"] or 0, ) diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py index 1c8f0de551c3d387c7abb1fe7a680541ec6a7bb6..fe1f1093256253ab7757cfe8f40f8a81986c9a61 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py @@ -58,6 +58,7 @@ class Statistics(OPCUADevice): ) # size of UDP receive buffer udp_recv_buffer_size_R = AttributeWrapper( + doc="Buffer size for receiving UDP packets.", comms_id=StatisticsClient, comms_annotation={"type": "udp", "parameter": "recv_buffer_size"}, datatype=numpy.uint64, diff --git a/tangostationcontrol/test/beam/managers/test_digitalbeam_manager.py b/tangostationcontrol/test/beam/managers/test_digitalbeam_manager.py index bbb360bf46e7fd9218266621093d7915c042579d..cda32bb678eb023665a85051f99d9fa43ea3fa23 100644 --- a/tangostationcontrol/test/beam/managers/test_digitalbeam_manager.py +++ b/tangostationcontrol/test/beam/managers/test_digitalbeam_manager.py @@ -116,3 +116,76 @@ class TestDigitalBeamManager(base.TestCase): numpy.testing.assert_array_equal( sut.current_pointing_error, numpy.full(1, 1.05) ) + + +class TestDigitalBeamManagerAntennasUsed(base.TestCase): + def _test_antennas_used( + self, + antenna_to_sdp_mapping: list[list[int]], + antenna_usage_mask: list[bool], + antenna_mask: list[bool], + expected_antennas_used: list[bool], + ): + """Generic test function.""" + + def read_parent_attribute(attr): + match attr: + case "Antenna_to_SDP_Mapping_R": + return antenna_to_sdp_mapping + case "Antenna_Usage_Mask_R": + return antenna_usage_mask + + device_mock = MagicMock() + device_mock.read_Antenna_Mask_R = MagicMock(return_value=antenna_mask) + + device_mock.control = MagicMock() + device_mock.control.read_parent_attribute = MagicMock() + device_mock.control.read_parent_attribute.side_effect = read_parent_attribute + + sut = DigitalBeamManager(device_mock) + self.assertListEqual(sut.antennas_used(), expected_antennas_used) + + def test_antennas_used_single_antenna(self): + # Test simple case: single antenna + self._test_antennas_used( + antenna_to_sdp_mapping=[[0, 0]], + antenna_usage_mask=[True], + antenna_mask=[True], + expected_antennas_used=[True], + ) + + def test_antennas_used_unconnected_antenna(self): + # Exclude antenna because it is not connected + self._test_antennas_used( + antenna_to_sdp_mapping=[[0, -1]], + antenna_usage_mask=[True], + antenna_mask=[True], + expected_antennas_used=[False], + ) + + def test_antennas_used_unusable_antenna(self): + # Exclude antenna because it is not to be used + self._test_antennas_used( + antenna_to_sdp_mapping=[[0, 0]], + antenna_usage_mask=[False], + antenna_mask=[True], + expected_antennas_used=[False], + ) + + def test_antennas_used_unselected_antenna(self): + # Exclude antenna because it is not in the selected antenna set + self._test_antennas_used( + antenna_to_sdp_mapping=[[0, 0]], + antenna_usage_mask=[True], + antenna_mask=[False], + expected_antennas_used=[False], + ) + + def test_antennas_used_multiple_antennas(self): + # All combinations in one go to test multiple antennas + self._test_antennas_used( + antenna_to_sdp_mapping=[[0, 0], [0, 1], [0, 2], [0, -1]], + antenna_usage_mask=[True, False, True, True], + antenna_mask=[True, True, False, True], + expected_antennas_used=[True, False, False, False], + )