diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d9ac77aa5ec97f22b96c152ed3a2a7d37d99c75b..89caf1660c68b93b00f763479d85fc0b51dce948 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -115,7 +115,7 @@ docker_build_image: - if: $CI_PIPELINE_SOURCE == "merge_request_event" changes: - docker-compose/$IMAGE.yml - - docker-compose/$IMAGE/* + - docker-compose/$IMAGE/**/* - docker-compose/.env - if: ($CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH) || $CI_COMMIT_TAG script: diff --git a/CDB/stations/hba_core.json b/CDB/stations/hba_core.json index 480d5ac4f28eae7380a7b321a29d407ab2bb826c..051bf2968c5836827c4611d4258879f37052bb7e 100644 --- a/CDB/stations/hba_core.json +++ b/CDB/stations/hba_core.json @@ -163,6 +163,12 @@ ], "Antenna_Type": [ "HBA" + ], + "HBAT_single_element_selection_GENERIC_201512": [ + "0", "10", "4", "3", "14", "0", + "5", "5", "3", "13", "10", "3", + "12", "2", "7", "15", "6", "14", + "7", "5", "7", "9", "0", "15" ] } }, @@ -182,6 +188,12 @@ ], "Antenna_Type": [ "HBA" + ], + "HBAT_single_element_selection_GENERIC_201512": [ + "0", "10", "4", "3", "14", "0", + "5", "5", "3", "13", "10", "3", + "12", "2", "7", "15", "6", "14", + "7", "5", "7", "9", "0", "15" ] } } diff --git a/CDB/stations/hba_remote.json b/CDB/stations/hba_remote.json index 8f47468f858d741c5c638c1bfa76af13ecf2936d..1c31f41f71a0863157e5a7327a8c69253f683060 100644 --- a/CDB/stations/hba_remote.json +++ b/CDB/stations/hba_remote.json @@ -100,6 +100,16 @@ ], "Antenna_Type": [ "HBA" + ], + "HBAT_single_element_selection_GENERIC_201512": [ + "0", "13", "12", "4", "11", "11", + "7", "8", "2", "7", "11", "2", + "10", "2", "6", "3", "8", "3", + "1", "7", "1", "15", "13", "1", + "11", "1", "12", "7", "10", "15", + "8", "2", "12", "13", "9", "13", + "4", "5", "5", "12", "5", "5", + "9", "11", "15", "12", "2", "15" ] } } diff --git a/docker-compose/http-json-schemas/definitions/dithering.json b/docker-compose/http-json-schemas/definitions/dithering.json new file mode 100644 index 0000000000000000000000000000000000000000..87a6077d24f4bd8878ba0a66ba1572417ce44adb --- /dev/null +++ b/docker-compose/http-json-schemas/definitions/dithering.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "description": "Settings for adding dithering to the signal to increase its linearity", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "power": { + "type": "number", + "description": "Power of dithering signal, in dBm", + "minimum": -25.0, + "maximum": -4.0, + "default": -4.0 + }, + "frequency": { + "type": "number", + "description": "Frequency of dithering signal, in Hz", + "default": 102000000 + } + } +} diff --git a/docker-compose/http-json-schemas/definitions/hba.json b/docker-compose/http-json-schemas/definitions/hba.json new file mode 100644 index 0000000000000000000000000000000000000000..5d1a319fc18c58dc45e720cfbe1d616d7fcb3d88 --- /dev/null +++ b/docker-compose/http-json-schemas/definitions/hba.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "required": [ + "tile_beam" + ], + "properties": { + "tile_beam": { + "$ref": "pointing.json" + }, + "DAB_filter": { + "type": "boolean", + "default": false, + "description": "Enable hardware filter on DAB frequencies" + }, + "element_selection": { + "type": "string", + "default": "ALL", + "description": "Which element(s) to enable in each tile", + "enum": [ + "ALL", + "GENERIC_201512" + ] + } + } +} diff --git a/docker-compose/http-json-schemas/definitions/observation-settings.json b/docker-compose/http-json-schemas/definitions/observation-settings.json index 50a6590f3d0fc671121ffa867349585b56d4d4ec..e6bdf42a3787cd05d819df380871b480c96bd3fc 100644 --- a/docker-compose/http-json-schemas/definitions/observation-settings.json +++ b/docker-compose/http-json-schemas/definitions/observation-settings.json @@ -52,6 +52,9 @@ "SPARSE_ODD" ] }, + "dithering": { + "$ref": "dithering.json" + }, "filter": { "type": "string", "enum": [ @@ -71,13 +74,13 @@ "$ref": "sap.json" } }, - "tile_beam": { - "$ref": "pointing.json" - }, "first_beamlet": { "type": "number", "default": 0, "minimum": 0 + }, + "HBA": { + "$ref": "hba.json" } } } diff --git a/tangostationcontrol/docs/source/observing.rst b/tangostationcontrol/docs/source/observing.rst index a207accf044f52ff808feac09e2912611d1535b1..6586a25efbaa2b52fa5bd4bf2f35070a09fb4e19 100644 --- a/tangostationcontrol/docs/source/observing.rst +++ b/tangostationcontrol/docs/source/observing.rst @@ -15,11 +15,22 @@ To observe with a station, you must construct the observation's specifications, "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_210_250", + "dithering": { + "enabled": true, + "power": -4.0, + "frequency": 102000000 + }, "SAPs": [{ "subbands": [10, 20, 30], - "pointing": { "angle1": 1.5, "angle2": 0, "direction_type": "J2000" } + "pointing": { "angle1": 1.0, "angle2": 0, "direction_type": "J2000" } + }, { + "subbands": [40, 50, 60], + "pointing": { "angle1": 2.0, "angle2": 0, "direction_type": "J2000" } }], - "tile_beam": { "angle1": 1.5, "angle2": 0, "direction_type": "J2000" } + "HBA": { + "DAB_filter": true, + "tile_beam": { "angle1": 1.5, "angle2": 0, "direction_type": "J2000" } + } } import json @@ -28,32 +39,40 @@ To observe with a station, you must construct the observation's specifications, The above specification contains the following parameters: -+--------------------+-----------------------------------------------------------------------------------------+ -| Parameter | Description | -+====================+=========================================================================================+ -| ``observation_id`` | User-specified unique reference to this observation. | -+--------------------+-----------------------------------------------------------------------------------------+ -| ``start_time`` | automatically start observing when this timestamp is reached. | -+--------------------+-----------------------------------------------------------------------------------------+ -| ``stop_time`` | automatically stop observing when this timestamp is reached. | -+--------------------+-----------------------------------------------------------------------------------------+ -| ``antenna_field`` | Which antenna field to use (LBA, HBA, HBA0, HBA1). | -+--------------------+-----------------------------------------------------------------------------------------+ -| ``antenna_set`` | Which subset of antennas to use (ALL, INNER, OUTER, EVEN, ODD). | -+--------------------+-----------------------------------------------------------------------------------------+ -| ``filter`` | Which band filter to use (LBA_10_90, LBA_30_70, HBA_110_190, HBA_170_230, HBA_210_250). | -+--------------------+-----------------------------------------------------------------------------------------+ -| ``SAPs`` | List of pointings and frequencies (subbands) to track and beam form. | -+--------------------+-----------------------------------------------------------------------------------------+ -| ``tile_beam`` | Pointing to track with the HBA tiles (optional). | -+--------------------+-----------------------------------------------------------------------------------------+ ++-------------------------+-----------------------------------------------------------------------------------------+ +| Parameter | Description | ++=========================+=========================================================================================+ +| ``observation_id`` | User-specified unique reference to this observation. | ++-------------------------+-----------------------------------------------------------------------------------------+ +| ``start_time`` | automatically start observing when this timestamp is reached. (optional) | ++-------------------------+-----------------------------------------------------------------------------------------+ +| ``stop_time`` | automatically stop observing when this timestamp is reached. | ++-------------------------+-----------------------------------------------------------------------------------------+ +| ``antenna_field`` | Which antenna field to use (LBA, HBA, HBA0, HBA1). | ++-------------------------+-----------------------------------------------------------------------------------------+ +| ``antenna_set`` | Which subset of antennas to use (ALL, INNER, OUTER, EVEN, ODD). | ++-------------------------+-----------------------------------------------------------------------------------------+ +| ``filter`` | Which band filter to use (LBA_10_90, LBA_30_70, HBA_110_190, HBA_170_230, HBA_210_250). | ++-------------------------+-----------------------------------------------------------------------------------------+ +| ``dithering.enabled`` | Whether to add analog dithering noise to increase linearity. (optional) | ++-------------------------+-----------------------------------------------------------------------------------------+ +| ``dithering.power`` | Power (in dB) to apply for dithering (-4.0 to -25.0). (optional) | ++-------------------------+-----------------------------------------------------------------------------------------+ +| ``dithering.frequency`` | Dithering frequency (in Hz). (optional) | ++-------------------------+-----------------------------------------------------------------------------------------+ +| ``SAPs`` | List of pointings and frequencies (subbands) to track and beam form. | ++-------------------------+-----------------------------------------------------------------------------------------+ +| ``HBA.DAB_filter`` | Enable the analog filter on the RCUs for DAB radio frequencies. (optional) | ++-------------------------+-----------------------------------------------------------------------------------------+ +| ``HBA.tile_beam`` | Pointing to track with the HBA tiles (optional). (specify for HBA) | ++-------------------------+-----------------------------------------------------------------------------------------+ This will configure the specified antenna field (f.e. ``HBA``) as follows: * ``STAT/DigitalBeam/HBA`` is configured to beam form the antennas in the specified ``antenna_set``, track all pointings given in ``SAPs[x].pointing``, and produce beamlets for all subbands in ``SAPs[x].subbands``. The beamlets mirror the subbands in the order in which they are specified, * The ``observation_id`` is used to annotate the beamlet data produced by this observation, * ``STAT/AntennaField/HBA`` is configured to use the specified ``filter`` for the RCUs, -* ``STAT/TileBeam/HBA`` is configured to beam form all tiles, tracking the given ``tile_beam`` pointing. +* ``STAT/TileBeam/HBA`` is configured to beam form all HBA tiles, tracking the given ``tile_beam`` pointing. Observation Output ```````````````````````` diff --git a/tangostationcontrol/integration_test/default/devices/test_device_observation.py b/tangostationcontrol/integration_test/default/devices/test_device_observation.py index a21df5d396b4f976b66c3bece1796ee3f9ce5009..6aa3527b937681a7d0dc6718f84ef6bc7b61e195 100644 --- a/tangostationcontrol/integration_test/default/devices/test_device_observation.py +++ b/tangostationcontrol/integration_test/default/devices/test_device_observation.py @@ -9,6 +9,8 @@ from tango import DevState, DevFailed from tangostationcontrol.common.constants import ( N_beamlets_ctrl, + N_elements, + N_pol, MAX_ANTENNA, CS001_TILES, ) @@ -132,6 +134,9 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): "010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101", "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", ], + "HBAT_single_element_selection_GENERIC_201512": [ + str(n % 16) for n in range(CS001_TILES) + ], } ) antennafield_proxy.off() @@ -230,12 +235,18 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): ) ] * len(data["SAPs"][0]["subbands"]) tile_beam = [ - str(data["tile_beam"]["direction_type"]), - f"{data['tile_beam']['angle1']}rad", - f"{data['tile_beam']['angle2']}rad", + str(data["HBA"]["tile_beam"]["direction_type"]), + f"{data['HBA']['tile_beam']['angle1']}rad", + f"{data['HBA']['tile_beam']['angle2']}rad", ] first_beamlet = data["first_beamlet"] + dithering = data["dithering"]["enabled"] + dithering_power = data["dithering"]["power"] + dithering_frequency = data["dithering"]["frequency"] + + dab_filter = data["HBA"]["DAB_filter"] + self.proxy.off() self.proxy.observation_settings_RW = self.VALID_JSON self.proxy.Initialise() @@ -248,9 +259,14 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): self.assertEqual(filter_, self.proxy.filter_R) self.assertListEqual(saps_subband, self.proxy.saps_subband_R.tolist()) self.assertListEqual(saps_pointing, list(self.proxy.saps_pointing_R)) - self.assertListEqual(tile_beam, list(self.proxy.tile_beam_R)) + self.assertEqual(dab_filter, self.proxy.HBA_DAB_filter_R) + self.assertListEqual(tile_beam, list(self.proxy.HBA_tile_beam_R)) self.assertEqual(first_beamlet, self.proxy.first_beamlet_R) + self.assertEqual(dithering, self.proxy.dithering_enabled_R) + self.assertEqual(dithering_power, self.proxy.dithering_power_R) + self.assertEqual(dithering_frequency, self.proxy.dithering_frequency_R) + def test_apply_antennafield_settings(self): """Test that attribute filter is correctly applied""" self.setup_stationmanager_proxy() @@ -338,3 +354,35 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): tilebeam_directions, ["J2000", "0.0261799rad", "0rad"], ) + + def test_apply_element_selection(self): + # failing + """Test that attribute element_selection is correctly applied""" + antennafield_proxy = self.setup_antennafield_proxy() + antennafield_proxy.HBAT_PWR_on_RW = numpy.ones( + (CS001_TILES, N_elements * N_pol), dtype=bool + ) + antennafield_proxy.HBAT_PWR_LNA_on_RW = numpy.zeros( + (CS001_TILES, N_elements * N_pol), dtype=bool + ) + + self.proxy.off() + self.proxy.observation_settings_RW = self.VALID_JSON + self.proxy.Initialise() + self.proxy.On() + + # construct an array with "True" at the expected spots + expected = numpy.zeros((CS001_TILES, N_elements, N_pol), dtype=bool) + for n in range(CS001_TILES): + expected[n, n % 16, :] = True + + # collapse all values for a tile into a single dimension, + # like HBAT_PWR_(LNA_)on_RW will return + expected = expected.reshape(CS001_TILES, -1) + + self.assertListEqual( + antennafield_proxy.HBAT_PWR_on_RW.tolist(), expected.tolist() + ) + self.assertListEqual( + antennafield_proxy.HBAT_PWR_LNA_on_RW.tolist(), expected.tolist() + ) diff --git a/tangostationcontrol/tangostationcontrol/configuration/__init__.py b/tangostationcontrol/tangostationcontrol/configuration/__init__.py index de4c16c013f4e80a96ea95465e47a454c7902bec..735a5ee255aa6d660458612587af18bb5f01110a 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/__init__.py +++ b/tangostationcontrol/tangostationcontrol/configuration/__init__.py @@ -4,9 +4,13 @@ from .observation_settings import ObservationSettings from .pointing import Pointing from .sap import Sap +from .hba import HBA +from .dithering import Dithering __all__ = [ "ObservationSettings", "Pointing", "Sap", + "HBA", + "Dithering", ] diff --git a/tangostationcontrol/tangostationcontrol/configuration/_json_parser.py b/tangostationcontrol/tangostationcontrol/configuration/_json_parser.py index a922f9e82a409e50df8f9826d455e8de72ab26e7..026be5be9c924a59591fe72717d8e0ec7e37e917 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/_json_parser.py +++ b/tangostationcontrol/tangostationcontrol/configuration/_json_parser.py @@ -7,11 +7,17 @@ from jsonschema.exceptions import ValidationError def _from_json_hook_t(primary: Type): - from tangostationcontrol.configuration import Pointing, Sap, ObservationSettings + from tangostationcontrol.configuration import ( + Pointing, + Sap, + ObservationSettings, + HBA, + Dithering, + ) def actual_hook(json_dct): primary_ex = None - for t in [Pointing, Sap, ObservationSettings]: + for t in [Pointing, Sap, HBA, Dithering, ObservationSettings]: try: t.get_validator().validate(json_dct) except ValidationError as ex: diff --git a/tangostationcontrol/tangostationcontrol/configuration/configuration_base.py b/tangostationcontrol/tangostationcontrol/configuration/configuration_base.py index eb5fcc3928e3d7b8ea36f4aee1ce4f4e57b2a23f..3b879ff2af4145204b9796788bdb7fa817c70278 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/configuration_base.py +++ b/tangostationcontrol/tangostationcontrol/configuration/configuration_base.py @@ -57,6 +57,8 @@ class _ConfigurationBase(ABC): @staticmethod def _class_to_url(cls_name): + cls_name = cls_name.replace("HBA", "hba") + cls_name = cls_name.replace("LBA", "lba") return re.sub(r"(?<!^)(?=[A-Z])", "-", cls_name).lower() @classmethod diff --git a/tangostationcontrol/tangostationcontrol/configuration/dithering.py b/tangostationcontrol/tangostationcontrol/configuration/dithering.py new file mode 100644 index 0000000000000000000000000000000000000000..750a6e463358e34b56151baf001e46ce24638530 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/configuration/dithering.py @@ -0,0 +1,35 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +from tangostationcontrol.configuration.configuration_base import _ConfigurationBase + + +class Dithering(_ConfigurationBase): + VALIDATOR = None + + def __init__( + self, + enabled: bool, + power: float | None, + frequency: float | None, + ): + self.enabled = enabled + self.power = power + self.frequency = frequency + + def __iter__(self): + yield from { + "enabled": self.enabled, + }.items() + if self.power is not None: + yield "power", self.power + if self.frequency is not None: + yield "frequency", self.frequency + + @staticmethod + def to_object(json_dct) -> "Dithering": + return Dithering( + json_dct["enabled"], + json_dct.get("power"), + json_dct.get("frequency"), + ) diff --git a/tangostationcontrol/tangostationcontrol/configuration/hba.py b/tangostationcontrol/tangostationcontrol/configuration/hba.py new file mode 100644 index 0000000000000000000000000000000000000000..ecd65aecf06c074edf124a194fc0ba7d2f5c91eb --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/configuration/hba.py @@ -0,0 +1,35 @@ +# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +from tangostationcontrol.configuration.configuration_base import _ConfigurationBase +from tangostationcontrol.configuration.pointing import Pointing + + +class HBA(_ConfigurationBase): + def __init__( + self, + tile_beam: Pointing, + DAB_filter: bool | None, + element_selection: str | None = "ALL", + ): + self.tile_beam = tile_beam + self.DAB_filter = DAB_filter + self.element_selection = element_selection + + def __iter__(self): + yield from { + "tile_beam": dict(self.tile_beam), + }.items() + + if self.DAB_filter is not None: + yield "DAB_filter", self.DAB_filter + if self.element_selection is not None: + yield "element_selection", self.element_selection + + @staticmethod + def to_object(json_dct) -> "HBA": + return HBA( + json_dct["tile_beam"], + json_dct.get("DAB_filter"), + json_dct.get("element_selection"), + ) diff --git a/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py b/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py index b8f49369f062899c95c2b95369ab9a506acaf4dd..40d64922187fb1bcec7cb7c30578e0922d1ce1ae 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py +++ b/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py @@ -5,7 +5,8 @@ from datetime import datetime from typing import Sequence from tangostationcontrol.configuration.configuration_base import _ConfigurationBase -from tangostationcontrol.configuration.pointing import Pointing +from tangostationcontrol.configuration.dithering import Dithering +from tangostationcontrol.configuration.hba import HBA from tangostationcontrol.configuration.sap import Sap @@ -19,9 +20,10 @@ class ObservationSettings(_ConfigurationBase): antenna_set: str, filter: str, SAPs: Sequence[Sap], - tile_beam: Pointing | None = None, + HBA: HBA | None = None, first_beamlet: int = 0, lead_time: float | None = None, + dithering: Dithering | None = None, ): self.observation_id = observation_id self.start_time = start_time @@ -30,9 +32,10 @@ class ObservationSettings(_ConfigurationBase): self.antenna_set = antenna_set self.filter = filter self.SAPs = SAPs - self.tile_beam = tile_beam + self.HBA = HBA self.first_beamlet = first_beamlet self.lead_time = lead_time + self.dithering = dithering def __iter__(self): yield "observation_id", self.observation_id @@ -45,11 +48,13 @@ class ObservationSettings(_ConfigurationBase): "filter": self.filter, "SAPs": [dict(s) for s in self.SAPs], }.items() - if self.tile_beam: - yield "tile_beam", dict(self.tile_beam) + if self.HBA: + yield "HBA", dict(self.HBA) yield "first_beamlet", self.first_beamlet if self.lead_time is not None: yield "lead_time", self.lead_time + if self.dithering is not None: + yield "dithering", dict(self.dithering) @staticmethod def to_object(json_dct) -> "ObservationSettings": @@ -63,7 +68,8 @@ class ObservationSettings(_ConfigurationBase): json_dct["antenna_set"], json_dct["filter"], json_dct["SAPs"], - json_dct["tile_beam"] if "tile_beam" in json_dct else None, - json_dct["first_beamlet"] if "first_beamlet" in json_dct else 0, - json_dct["lead_time"] if "lead_time" in json_dct else None, + json_dct.get("HBA"), + json_dct.get("first_beamlet", 0), + json_dct.get("lead_time"), + json_dct.get("dithering"), ) diff --git a/tangostationcontrol/tangostationcontrol/devices/antennafield.py b/tangostationcontrol/tangostationcontrol/devices/antennafield.py index d4b8dc33666f22b94b4f02e7f8b42e30a9a2a1ba..f195d15eb4785fc35ee34bf527c7ead63c0c769c 100644 --- a/tangostationcontrol/tangostationcontrol/devices/antennafield.py +++ b/tangostationcontrol/tangostationcontrol/devices/antennafield.py @@ -54,6 +54,7 @@ from tangostationcontrol.devices.base_device_classes.mapper import ( MappedAttribute, AntennaToRecvMapper, AntennaToSdpMapper, + RecvDeviceWalker, ) logger = logging.getLogger() @@ -241,6 +242,8 @@ class AntennaField(LOFARDevice): default_value=2015.5, ) + # ----- HBA properties + HBAT_PQR_rotation_angles_deg = device_property( doc='Rotation of each tile in the PQ plane ("horizontal") in degrees.', dtype="DevVarFloatArray", @@ -270,6 +273,39 @@ class AntennaField(LOFARDevice): default_value=HBATAntennaOffsets.HBAT1_BASE_ANTENNA_OFFSETS.flatten(), ) + # see also https://github.com/lofar-astron/lofarimaging/blob/9672b52bb9be8f3405e6e3f85701175bdc4bf211/lofarimaging/singlestationutil.py#L43 + HBAT_single_element_selection_GENERIC_201512 = device_property( + doc="Which element to select when using a single dipole per tile in the 'GENERIC_201512' strategy", + dtype="DevVarLongArray", + mandatory=False, + default_value=[ + 0, + 10, + 4, + 3, + 14, + 0, + 5, + 5, + 3, + 13, + 10, + 3, + 12, + 2, + 7, + 15, + 6, + 14, + 7, + 5, + 7, + 9, + 0, + 15, + ], + ) + # ----- SDP mapping Antenna_to_SDP_Mapping = device_property( @@ -370,6 +406,27 @@ class AntennaField(LOFARDevice): def RECV_Devices_R(self): return numpy.array(self.RECV_Devices) + @attribute( + access=AttrWriteType.READ, + dtype=((numpy.float64,),), + max_dim_x=3, + max_dim_y=3, + ) + def PQR_to_ETRS_rotation_matrix_R(self): + return numpy.array( + self.PQR_to_ETRS_rotation_matrix, dtype=numpy.float64 + ).reshape(3, 3) + + @attribute( + access=AttrWriteType.READ, + dtype=(numpy.int32,), + max_dim_x=MAX_ANTENNA, + ) + def HBAT_single_element_selection_GENERIC_201512_R(self): + return numpy.array( + self.HBAT_single_element_selection_GENERIC_201512, dtype=numpy.int32 + ) + Frequency_Band_RW = attribute( doc="The selected frequency band of each polarisation of each antenna.", dtype=((str,),), @@ -1088,6 +1145,26 @@ class AntennaField(LOFARDevice): return result_values.flatten() + @command() + @DebugIt() + @only_in_states(DEFAULT_COMMAND_STATES) + def RCU_DTH_on(self): + walker = RecvDeviceWalker( + self.read_attribute("Control_to_RECV_mapping_R"), + self.read_attribute("Antenna_Usage_Mask_R"), + ) + walker.walk_receivers(self.recv_proxies, lambda recv: recv.RCU_DTH_on()) + + @command() + @DebugIt() + @only_in_states(DEFAULT_COMMAND_STATES) + def RCU_DTH_off(self): + walker = RecvDeviceWalker( + self.read_attribute("Control_to_RECV_mapping_R"), + self.read_attribute("Antenna_Usage_Mask_R"), + ) + walker.walk_receivers(self.recv_proxies, lambda recv: recv.RCU_DTH_off()) + # ---------- # Run server diff --git a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/mapper.py b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/mapper.py index 064bba36a3b77b9f06961eda7d5ea1267f4ada35..1af7d34c06f5a59725d1a04a483a0e542902e04d 100644 --- a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/mapper.py +++ b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/mapper.py @@ -6,10 +6,11 @@ """ from enum import Enum from typing import List, Optional, Dict +from collections.abc import Callable import numpy # PyTango imports -from tango import AttrWriteType +from tango import AttrWriteType, DeviceProxy from tango.server import attribute from tangostationcontrol.common.constants import ( @@ -421,3 +422,54 @@ class AntennaToRecvMapper(AntennaMapper): else: value_mapper[attr] = [self._control_mapping] return value_mapper + + +class RecvDeviceWalker(object): + """Walks over child devices with the appropriate mask set to affect the hardware + under the parent's control..""" + + def __init__( + self, control_to_recv_mapping: numpy.ndarray, antenna_usage_mask: list[bool] + ): + self.control_to_recv_mapping = control_to_recv_mapping + self.antenna_usage_mask = antenna_usage_mask + + def recv_ant_masks(self) -> numpy.ndarray: + """Return the antenna mask for the control inputs of the antennas enabled in Antenna_Usage_Mask_R.""" + + nr_recv_devices = max(self.control_to_recv_mapping[:, 0]) + + recv_ant_masks = numpy.zeros((nr_recv_devices, N_rcu, N_rcu_inp), dtype=bool) + + for use, (recv, rcu_input) in zip( + self.antenna_usage_mask, self.control_to_recv_mapping + ): + if not use: + continue + if recv <= 0: + continue + + recv_ant_masks[recv - 1][rcu_input // N_rcu_inp][ + rcu_input % N_rcu_inp + ] = True + + return recv_ant_masks + + def walk_receivers( + self, + recv_proxies: list[DeviceProxy], + visitor_func: Callable[[DeviceProxy], None], + ): + """Walk over all RECV devices with the mask set for our antennas.""" + ant_masks = self.recv_ant_masks() + + for recv_proxy, ant_mask in zip(recv_proxies, ant_masks): + # configure RECV to address only our antennas. Save existing mask. + old_mask = recv_proxy.ANT_mask_RW + recv_proxy.ANT_mask_RW = ant_mask + + try: + visitor_func(recv_proxy) + finally: + # restore mask + recv_proxy.ANT_mask_RW = old_mask diff --git a/tangostationcontrol/tangostationcontrol/devices/observation.py b/tangostationcontrol/tangostationcontrol/devices/observation.py index d195f5480962ebfe5469f35e81f9a25c617e0a21..9e35080a3028bc119d14c556f28262dcc26d6c95 100644 --- a/tangostationcontrol/tangostationcontrol/devices/observation.py +++ b/tangostationcontrol/tangostationcontrol/devices/observation.py @@ -16,6 +16,7 @@ from tangostationcontrol.common.constants import ( DEFAULT_POLLING_PERIOD, MAX_ANTENNA, N_beamlets_ctrl, + N_elements, N_point_prop, N_pol, ) @@ -108,6 +109,41 @@ class Observation(LOFARDevice): def antenna_set_R(self): return self._observation_settings.antenna_set + @attribute( + doc="Whether to add dithering to the signal to increase its linearity", + dtype=bool, + fisallowed="is_attribute_access_allowed", + ) + def dithering_enabled_R(self): + try: + return self._observation_settings.dithering.enabled or False + except AttributeError: + return False + + @attribute( + doc="Power of dithering signal, in dBm", + unit="dBm", + dtype=numpy.float64, + fisallowed="is_attribute_access_allowed", + ) + def dithering_power_R(self): + try: + return self._observation_settings.dithering.power or -4.0 + except AttributeError: + return -4.0 + + @attribute( + doc="Frequency of dithering signal, in Hz", + unit="Hz", + dtype=numpy.int64, + fisallowed="is_attribute_access_allowed", + ) + def dithering_frequency_R(self): + try: + return self._observation_settings.dithering.frequency or 102_000_000 + except AttributeError: + return 102_000_000 + @attribute( doc="Which band filter to use for all antennas", dtype=str, @@ -147,16 +183,27 @@ class Observation(LOFARDevice): ) return saps_pointing + @attribute( + doc="Beamlet index of the FPGA output, at which to start mapping the beamlets of this observation.", + dtype=numpy.uint64, + fisallowed="is_attribute_access_allowed", + ) + def first_beamlet_R(self): + return self._observation_settings.first_beamlet + @attribute( doc="Which pointing to beamform all HBA tiles to (if any).", dtype=(str,), max_dim_x=N_point_prop, ) - def tile_beam_R(self): - if self._observation_settings.tile_beam is None: + def HBA_tile_beam_R(self): + try: + if self._observation_settings.HBA.tile_beam is None: + return None + except AttributeError: return None - pointing_direction = self._observation_settings.tile_beam + pointing_direction = self._observation_settings.HBA.tile_beam return [ str(pointing_direction.direction_type), f"{pointing_direction.angle1}rad", @@ -164,12 +211,26 @@ class Observation(LOFARDevice): ] @attribute( - doc="Beamlet index of the FPGA output, at which to start mapping the beamlets of this observation.", - dtype=numpy.uint64, + doc="Whether to enable the DAB filter", + dtype=bool, fisallowed="is_attribute_access_allowed", ) - def first_beamlet_R(self): - return self._observation_settings.first_beamlet + def HBA_DAB_filter_R(self): + try: + return self._observation_settings.HBA.DAB_filter or False + except AttributeError: + return False + + @attribute( + doc="Which element(s) to enable in each tile", + dtype=str, + fisallowed="is_attribute_access_allowed", + ) + def HBA_element_selection_R(self): + try: + return self._observation_settings.HBA.element_selection or "ALL" + except AttributeError: + return "ALL" observation_settings_RW = attribute(dtype=str, access=AttrWriteType.READ_WRITE) @@ -265,7 +326,7 @@ class Observation(LOFARDevice): f"{util.get_ds_inst_name()}/DigitalBeam/{antennafield}" ) - if self._has_tilebeam(): + if self._is_HBA(): # Set a reference of TileBeam device that is correlated to this device self.tilebeam_proxy = create_device_proxy( f"{util.get_ds_inst_name()}/TileBeam/{antennafield}" @@ -288,6 +349,20 @@ class Observation(LOFARDevice): ) self.antennafield_proxy.Frequency_Band_RW = frequency_band + # Apply dithering configuration + if self.read_attribute("dithering_enabled_R"): + self.antennafield_proxy.RCU_DTH_freq_RW = [ + self.read_attribute("dithering_frequency_R") + ] * self.antennafield_proxy.nr_antennas_R + + self.antennafield_proxy.RCU_DTH_PWR_RW = [ + self.read_attribute("dithering_power_R") + ] * self.antennafield_proxy.nr_antennas_R + + self.antennafield_proxy.RCU_DTH_on() + else: + self.antennafield_proxy.RCU_DTH_off() + # Apply Beamlet configuration self.beamlet_proxy.subband_select_RW = self._apply_saps_subbands( self.read_attribute("saps_subband_R") @@ -296,16 +371,33 @@ class Observation(LOFARDevice): self.read_attribute("saps_pointing_R") ) - # Apply Tile Beam pointing direction - # NB: This is applied to all tiles, not just the ones in the selected - # antenna set. - if self._has_tilebeam(): - tile_beam = self.read_attribute("tile_beam_R") + if self._is_HBA(): + # Apply Tile Beam pointing direction + # NB: These settings are applied to all tiles, not just the ones + # in the selected antenna set. + tile_beam = self.read_attribute("HBA_tile_beam_R") self.tilebeam_proxy.Pointing_direction_RW = [ tuple(tile_beam) ] * self.antennafield_proxy.nr_antennas_R + # Apply DAB filter setting + DAB_filter = self.read_attribute("HBA_DAB_filter_R") + + self.antennafield_proxy.RCU_DAB_filter_on_RW = [ + DAB_filter + ] * self.antennafield_proxy.nr_antennas_R + + # Apply HBAT element selection + element_selection = self.read_attribute("HBA_element_selection_R") + + self.antennafield_proxy.HBAT_PWR_LNA_on_RW = self._apply_element_selection( + element_selection + ) + self.antennafield_proxy.HBAT_PWR_on_RW = self._apply_element_selection( + element_selection + ) + @log_exceptions() def _stop_observation(self): """Tear down station resources we used.""" @@ -332,7 +424,7 @@ class Observation(LOFARDevice): self._observation_settings = None raise - def _has_tilebeam(self): + def _is_HBA(self): """Return whether this observation should control a TileBeam device.""" return self._observation_settings.antenna_field.startswith("HBA") @@ -370,6 +462,29 @@ class Observation(LOFARDevice): """Convert the observation id value into the correct format for Antennafield device""" return numpy.array([observation_id] * MAX_ANTENNA, dtype=numpy.uint32) + def _apply_element_selection(self, element_selection: str): + """Convert the element selection strategy to a boolean array per element per polarisation per tile.""" + + nr_antennas = self.antennafield_proxy.nr_antennas_R + + selection = numpy.zeros((nr_antennas, N_elements, N_pol), dtype=bool) + + if element_selection == "ALL": + selection[:, :, :] = True + elif element_selection == "FIRST": + selection[:, 0, :] = True + elif element_selection == "GENERIC_201512": + # select a specific element for each tile + element_per_tile = ( + self.antennafield_proxy.HBAT_single_element_selection_GENERIC_201512_R + ) + for antenna, element in enumerate(element_per_tile): + selection[antenna, element, :] = True + else: + raise ValueError(f"Unsupported element selection: {element_selection}") + + return selection.reshape(nr_antennas, N_elements * N_pol) + # ---------- # Run server diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py index 03dc58103d247e26978d36bea41fa0e3ca8bde9e..37e863b4e4f1e11e027082872c0d9b97d56a6d8c 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py @@ -16,8 +16,17 @@ class TestObservationBase: "angle1": 0.0261799, "angle2": 0, "direction_type": "J2000" } }], - "tile_beam": - { "angle1": 0.0261799, "angle2": 0, "direction_type": "J2000" }, + "dithering": { + "enabled": true, + "power": -10.0, + "frequency": 123000000 + }, + "HBA": { + "tile_beam": + { "angle1": 0.0261799, "angle2": 0, "direction_type": "J2000" }, + "DAB_filter": true, + "element_selection": "GENERIC_201512" + }, "first_beamlet": 0 } """ diff --git a/tangostationcontrol/test/configuration/_mock_requests.py b/tangostationcontrol/test/configuration/_mock_requests.py index 314c1b40d9cfad27f73c380b53d98d4b2971cbd7..d89e75cb5c66a1939f4f6f8a3ea49be4595a2747 100644 --- a/tangostationcontrol/test/configuration/_mock_requests.py +++ b/tangostationcontrol/test/configuration/_mock_requests.py @@ -72,6 +72,64 @@ SAP_SCHEMA = """ } """ +HBA_SCHEMA = """ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "required": [ + "tile_beam" + ], + "properties": { + "tile_beam": { + "$ref": "pointing.json" + }, + "dab_filter": { + "type": "boolean", + "default": false, + "description": "Enable hardware filter on DAB frequencies" + }, + "element_selection": { + "type": "string", + "default": "ALL", + "description": "Which element(s) to enable in each tile", + "enum": [ + "ALL", + "GENERIC_201512" + ] + } + } +} +""" + +DITHERING_SCHEMA = """ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "description": "Settings for adding dithering to the signal to increase its linearity", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "power": { + "type": "number", + "description": "Power of dithering signal, in dBm", + "minimum": -25.0, + "maximum": -4.0, + "default": -4.0 + }, + "frequency": { + "type": "number", + "description": "Frequency of dithering signal, in Hz", + "default": 102000000 + } + } +} +""" + OBSERVATION_SETTINGS_SCHEMA = """ { "$schema": "http://json-schema.org/draft-07/schema", @@ -89,18 +147,58 @@ OBSERVATION_SETTINGS_SCHEMA = """ "type": "number", "minimum": 1 }, + "start_time": { + "type": "string", + "format": "date-time", + "default": "1970-01-01T00:00:00" + }, "stop_time": { "type": "string", "format": "date-time" }, + "lead_time": { + "type": "number", + "description": "Number of seconds to start before the provided start time, to account for initialising the on-line signal chain, and for possibly negative geometrical delay compensation.", + "default": 2.0, + "minimum": 0 + }, "antenna_field": { - "type": "string" + "default": "HBA", + "description": "Antenna field to use", + "type": "string", + "enum": [ + "LBA", + "HBA", + "HBA0", + "HBA1" + ] }, "antenna_set": { - "type": "string" + "default": "ALL", + "description": "Fields & antennas to use", + "type": "string", + "enum": [ + "ALL", + "INNER", + "OUTER", + "SPARSE_EVEN", + "SPARSE_ODD" + ] + }, + "dithering": { + "$ref": "dithering.json" }, "filter": { - "type": "string" + "type": "string", + "enum": [ + "LBA_10_90", + "LBA_10_70", + "LBA_30_90", + "LBA_30_70", + "HBA_170_230", + "HBA_110_190", + "HBA_210_250" + ] }, "SAPs": { "type": "array", @@ -109,12 +207,13 @@ OBSERVATION_SETTINGS_SCHEMA = """ "$ref": "sap.json" } }, - "tile_beam": { - "$ref": "pointing.json" - }, "first_beamlet": { "type": "number", + "default": 0, "minimum": 0 + }, + "HBA": { + "$ref": "hba.json" } } } @@ -137,6 +236,10 @@ def mocked_requests_get(*args, **kwargs): return MockResponse(POINTING_SCHEMA, 200) elif args[0] == "http://http-json-schemas/sap.json": return MockResponse(SAP_SCHEMA, 200) + elif args[0] == "http://http-json-schemas/hba.json": + return MockResponse(HBA_SCHEMA, 200) + elif args[0] == "http://http-json-schemas/dithering.json": + return MockResponse(DITHERING_SCHEMA, 200) elif args[0] == "http://http-json-schemas/observation-settings.json": return MockResponse(OBSERVATION_SETTINGS_SCHEMA, 200) diff --git a/tangostationcontrol/test/configuration/test_observation_settings.py b/tangostationcontrol/test/configuration/test_observation_settings.py index 049ec2416b806181ff9149542d4edff1d2668140..4f876563126bbe5e3c670bbe15a1d211bae18a62 100644 --- a/tangostationcontrol/test/configuration/test_observation_settings.py +++ b/tangostationcontrol/test/configuration/test_observation_settings.py @@ -4,6 +4,7 @@ from datetime import datetime from unittest import mock +import json import requests from jsonschema.exceptions import ValidationError, RefResolutionError @@ -19,77 +20,77 @@ class TestObservationSettings(base.TestCase): sut = ObservationSettings.from_json( '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' '"antenna_field": "HBA", ' - '"antenna_set": "ALL", "filter": "filter_settings",' + '"antenna_set": "ALL", "filter": "HBA_110_190",' '"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}]}' ) self.assertEqual(sut.observation_id, 3) self.assertEqual(sut.stop_time, datetime.fromisoformat("2012-04-23T18:25:43")) self.assertEqual(sut.antenna_set, "ALL") - self.assertEqual(sut.filter, "filter_settings") + self.assertEqual(sut.filter, "HBA_110_190") self.assertEqual(len(sut.SAPs), 1) sut = ObservationSettings.from_json( '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' '"antenna_field": "HBA", ' - '"antenna_set": "ALL", "filter": "filter_settings",' + '"antenna_set": "ALL", "filter": "HBA_110_190",' '"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],' - '"tile_beam": {"angle1":2.2, "angle2": 3.1, "direction_type":"MOON"} }' + '"HBA": { "tile_beam": {"angle1":2.2, "angle2": 3.1, "direction_type":"MOON"} } }' ) - self.assertEqual(sut.tile_beam.angle1, 2.2) - self.assertEqual(sut.tile_beam.angle2, 3.1) - self.assertEqual(sut.tile_beam.direction_type, "MOON") + self.assertEqual(sut.HBA.tile_beam.angle1, 2.2) + self.assertEqual(sut.HBA.tile_beam.angle2, 3.1) + self.assertEqual(sut.HBA.tile_beam.direction_type, "MOON") sut = ObservationSettings.from_json( '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' '"antenna_field": "HBA", ' - '"antenna_set": "ALL", "filter": "filter_settings",' + '"antenna_set": "ALL", "filter": "HBA_110_190",' '"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],' - '"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}' + '"HBA": {"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}, "first_beamlet": 2}' ) self.assertEqual(sut.first_beamlet, 2) def test_from_json_type_missmatch(self, _): - for json in [ + for json_str in [ # observation_id - '{"observation_id": "3", "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "filter_settings","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', + '{"observation_id": "3", "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', # stop_time - '{"observation_id": 3, "stop_time": "test", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "filter_settings","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', + '{"observation_id": 3, "stop_time": "test", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', # antenna_set - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": 4, "filter": "filter_settings","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', + '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": 4, "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', # filter - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": 1,"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', + '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": 1,"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": 2}', # SAPs - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "filter_settings","SAPs": {"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}},"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', - # '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "filter_settings","SAPs": [1],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', - # '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "filter_settings","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],"tile_beam": 1, "first_beamlet": 2}', + '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": {"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}, "first_beamlet": 2}', # first_beamlet - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "filter_settings","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": "2"}', + '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "first_beamlet": "2"}', ]: - with self.assertRaises((ValidationError, ValueError), msg=f"{json}"): - ObservationSettings.from_json(json) + with self.assertRaises((ValidationError, ValueError), msg=f"{json_str}"): + ObservationSettings.from_json(json_str) def test_from_json_missing_fields(self, _): - for json in [ - # observation_id - '{"stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "filter_settings","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', - # stop_time - '{"observation_id": 3, "antenna_field": "HBA", "antenna_set": "ALL", "filter": "filter_settings","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', - # antenna_field - '{"observation_id": 3, "antenna_set": "ALL", "stop_time": "2012-04-23T18:25:43", "filter": "filter_settings","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', - # antenna_set - '{"observation_id": 3, "antenna_field": "HBA", "stop_time": "2012-04-23T18:25:43", "filter": "filter_settings","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', - # filter - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', - # SAPs - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "filter_settings","tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', - # SAPs (empty list) - '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "filter_settings","SAPs": [],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', - ]: - with self.assertRaises((ValidationError, ValueError), msg=f"{json}"): - ObservationSettings.from_json(json) + complete_json = '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", "antenna_field": "HBA", "antenna_set": "ALL", "filter": "HBA_110_190","SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}], "HBA": {"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}, "first_beamlet": 2}' + + for field in ( + "observation_id", + "stop_time", + "antenna_field", + "antenna_set", + "filter", + "SAPs", + ): + # construct a JSON string with the provided field missing + json_dict = json.loads(complete_json) + del json_dict[field] + json_str = json.dumps(json_dict) + + # trigger validation error + with self.assertRaises( + (ValidationError, ValueError), msg=f"Omitted field {field}: {json_str}" + ): + ObservationSettings.from_json(json_str) def test_to_json(self, _): sut = ObservationSettings( @@ -122,15 +123,15 @@ class TestObservationSettings(base.TestCase): ObservationSettings.from_json( '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' '"antenna_field": "HBA", ' - '"antenna_set": "ALL", "filter": "filter_settings",' + '"antenna_set": "ALL", "filter": "HBA_110_190",' '"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}]}' ) self.assertEqual(5, mock_get.call_count) def test_throw_wrong_instance(self, _): - for json in [ + for json_str in [ '{"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}', '{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}', ]: with self.assertRaises(ValidationError): - ObservationSettings.from_json(json) + ObservationSettings.from_json(json_str) diff --git a/tangostationcontrol/test/devices/test_antennafield_device.py b/tangostationcontrol/test/devices/test_antennafield_device.py index 12edfafefe60b49e1a0f72b03bfd96fb614d1329..0ce70d37a2b9af165372f63fc2bed2ef9e370ae7 100644 --- a/tangostationcontrol/test/devices/test_antennafield_device.py +++ b/tangostationcontrol/test/devices/test_antennafield_device.py @@ -30,6 +30,7 @@ from tangostationcontrol.devices.base_device_classes.mapper import ( MappingKeys, AntennaToRecvMapper, AntennaToSdpMapper, + RecvDeviceWalker, ) logger = logging.getLogger() @@ -1106,3 +1107,137 @@ class TestAntennafieldDevice(device_base.DeviceTestCase): ) as proxy: expected = [True] * int(MAX_ANTENNA / 2) + [False] * int(MAX_ANTENNA / 2) numpy.testing.assert_equal(expected, proxy.antenna_set_to_mask("INNER")) + + +class TestRecvDeviceWalker(base.TestCase): + class MockDeviceProxy: + """Mock of DeviceProxy that simulates recv.ANT_mask_RW.""" + + @property + def ANT_mask_RW(self): + return self.ant_mask + + @ANT_mask_RW.setter + def ANT_mask_RW(self, value): + self.ant_mask = value + + def __init__(self, index): + self.visited = False + self.index = index + + # fill with a value we don't use, to make sure + # there will be a difference when set + self.ant_mask = numpy.array([[False, True, True]] * N_rcu) + + # save the value we originally use + self.original_ant_mask = self.ant_mask + + def test_recv_masks_identity_mapping(self): + """Test whether a straight setup works.""" + + control_to_recv_mapping = numpy.array([[1, 0], [1, 1], [1, 2]]) + antenna_usage_mask = numpy.array([True, True, True]) + + sut = RecvDeviceWalker(control_to_recv_mapping, antenna_usage_mask) + + expected = numpy.array( + [[[True, True, True]] + [[False, False, False]] * (N_rcu - 1)] + ) + numpy.testing.assert_equal(expected, sut.recv_ant_masks()) + + def test_recv_masks_antenna_usage_mask(self): + """Test whether the antenna_usage_mask is respected.""" + + control_to_recv_mapping = numpy.array([[1, 0], [1, 1], [1, 2]]) + antenna_usage_mask = numpy.array([False, True, False]) + + sut = RecvDeviceWalker(control_to_recv_mapping, antenna_usage_mask) + + expected = numpy.array( + [[[False, True, False]] + [[False, False, False]] * (N_rcu - 1)] + ) + numpy.testing.assert_equal(expected, sut.recv_ant_masks()) + + def test_recv_masks_control_to_recv_mapping(self): + """Test whether control_to_recv_mapping is respected.""" + + control_to_recv_mapping = numpy.array([[1, 0], [2, 1], [1, 2]]) + antenna_usage_mask = numpy.array([True, True, True]) + + sut = RecvDeviceWalker(control_to_recv_mapping, antenna_usage_mask) + + expected = numpy.array( + [ + [[True, False, True]] + [[False, False, False]] * (N_rcu - 1), + [[False, True, False]] + [[False, False, False]] * (N_rcu - 1), + ] + ) + numpy.testing.assert_equal(expected, sut.recv_ant_masks()) + + def test_walk_receivers(self): + """Test walk_receivers on multiple recv_proxies.""" + + control_to_recv_mapping = numpy.array([[1, 0], [2, 1], [1, 2]]) + antenna_usage_mask = numpy.array([True, True, True]) + + sut = RecvDeviceWalker(control_to_recv_mapping, antenna_usage_mask) + + recv_proxies = [TestRecvDeviceWalker.MockDeviceProxy(n + 1) for n in range(2)] + + def visitor(recv_proxy): + recv_proxy.visited = True + + # is our mask set correctly? + if recv_proxy.index == 1: + expected = numpy.array( + [[True, False, True]] + [[False, False, False]] * (N_rcu - 1) + ) + elif recv_proxy.index == 2: + expected = numpy.array( + [[False, True, False]] + [[False, False, False]] * (N_rcu - 1) + ) + + numpy.testing.assert_equal(expected, recv_proxy.ANT_mask_RW) + + sut.walk_receivers(recv_proxies, visitor) + + # make sure both recv_proxies were visited + self.assertTrue(recv_proxies[0].visited) + self.assertTrue(recv_proxies[1].visited) + + # make sure both masks were restored + numpy.testing.assert_equal( + recv_proxies[0].original_ant_mask, recv_proxies[0].ant_mask + ) + numpy.testing.assert_equal( + recv_proxies[1].original_ant_mask, recv_proxies[1].ant_mask + ) + + def test_walk_receivers_restores_mask_on_exception(self): + """Test whether walk_receivers() also restores the recv_proxy.ANT_mask_RW + if the visitor function throws.""" + control_to_recv_mapping = numpy.array([[1, 0], [2, 1], [1, 2]]) + antenna_usage_mask = numpy.array([True, True, True]) + + sut = RecvDeviceWalker(control_to_recv_mapping, antenna_usage_mask) + + recv_proxies = [TestRecvDeviceWalker.MockDeviceProxy(n + 1) for n in range(2)] + + class MyException(Exception): + """A exception noone can raise but us.""" + + pass + + def visitor(recv_proxy): + raise MyException("foo") + + with self.assertRaises(MyException): + sut.walk_receivers(recv_proxies, visitor) + + # make sure no mask was disturbed + numpy.testing.assert_equal( + recv_proxies[0].original_ant_mask, recv_proxies[0].ant_mask + ) + numpy.testing.assert_equal( + recv_proxies[1].original_ant_mask, recv_proxies[1].ant_mask + )