diff --git a/CDB/stations/dummy_positions_ConfigDb.json b/CDB/stations/dummy_positions_ConfigDb.json index 7758a30d8ad45a0b67effe7b38f5a4812025929c..020ebe51e6854b5150fd548afb4460d3784858ba 100644 --- a/CDB/stations/dummy_positions_ConfigDb.json +++ b/CDB/stations/dummy_positions_ConfigDb.json @@ -5,6 +5,20 @@ "AntennaField": { "STAT/AntennaField/HBA": { "properties": { + "Antenna_Sets": [ + "INNER", + "OUTER", + "SPARSE_EVEN", + "SPARSE_ODD", + "ALL" + ], + "Antenna_Set_Masks":[ + "111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000", + "000000000000000000000000000000000000000000000000111111111111111111111111111111111111111111111111", + "101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010", + "010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101", + "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + ], "Control_to_RECV_mapping": [ "1", "0", "1", "1", diff --git a/docker-compose/schemas/definitions/observation-settings.json b/docker-compose/schemas/definitions/observation-settings.json index baccdd06971932f02d1a21b206e1bb674125ab20..97c778620b6e1a5ad38643ab0dc56ee34a563100 100644 --- a/docker-compose/schemas/definitions/observation-settings.json +++ b/docker-compose/schemas/definitions/observation-settings.json @@ -4,6 +4,7 @@ "required": [ "observation_id", "stop_time", + "antenna_set", "antenna_mask", "filter", "SAPs" @@ -17,6 +18,18 @@ "type": "string", "format": "date-time" }, + "antenna_set": { + "default": "ALL", + "description": "Fields & antennas to use", + "type": "string", + "enum": [ + "ALL", + "INNER", + "OUTER", + "SPARSE_EVEN", + "SPARSE_ODD" + ] + }, "antenna_mask": { "type": "array", "uniqueItems": true, @@ -53,4 +66,4 @@ "minimum": 0 } } -} \ No newline at end of file +} diff --git a/tangostationcontrol/tangostationcontrol/common/observation_controller.py b/tangostationcontrol/tangostationcontrol/common/observation_controller.py index a95fe31d28d1988fac928d1e96aeb784e0a53443..a0bcf8a9a12e131a709fc23e269077a5582c14aa 100644 --- a/tangostationcontrol/tangostationcontrol/common/observation_controller.py +++ b/tangostationcontrol/tangostationcontrol/common/observation_controller.py @@ -4,6 +4,7 @@ import logging import time from datetime import datetime +from typing import List from tango import DevFailed, DevState, Except, Util, EventType, DeviceProxy from tangostationcontrol.common.lofar_logging import log_exceptions @@ -43,7 +44,8 @@ class RunningObservation(object): self._tango_util: Util = Util.instance() def create_tango_device(self): - logger.info(f"Create device: {self.device_name}") + """Instatiate an Observation Device""" + logger.info("Create device: %s", self.device_name) try: # Create the Observation device and instantiate it. self._tango_util.create_device(self.class_name, f"{self.device_name}") @@ -55,11 +57,15 @@ class RunningObservation(object): ): # and self.is_observation_running(self.observation_id) is False: self._tango_util.delete_device(self.class_name, self.device_name) - error_string = f"Cannot create the Observation device {self.device_name} because it is already present in the Database but it is not running. Try to re-run the start_observation command" + error_string = f"Cannot create the Observation device {self.device_name} \ + because it is already present in the Database but it is not running. \ + Try to re-run the start_observation command" logger.exception(error_string) Except.re_throw_exception(ex, "DevFailed", error_string, __name__) else: - error_string = f"Cannot create the Observation device instance {self.device_name} for ID={self.observation_id}. This means that the observation did not start." + error_string = f"Cannot create the Observation device instance \ + {self.device_name} for ID={self.observation_id}. \ + This means that the observation did not start." logger.exception(error_string) Except.re_throw_exception(ex, "DevFailed", error_string, __name__) @@ -91,7 +97,7 @@ class RunningObservation(object): self.attribute_name.split("/")[-1], EventType.PERIODIC_EVENT, cb ) logger.info( - f"Successfully started an observation with ID={self.observation_id}." + "Successfully started an observation with ID=%s.", self.observation_id ) def shutdown(self): @@ -100,7 +106,10 @@ class RunningObservation(object): self._device_proxy.ping() except DevFailed: logger.warning( - f"The device for the Observation with ID={self.observation_id} has unexpectedly already disappeared. It is advised to check the logs up to 10s prior to this message to see what happened." + "The device for the Observation with ID=%s \ + has unexpectedly already disappeared. It is advised to check \ + the logs up to 10s prior to this message to see what happened.", + {self.observation_id}, ) else: # Unsubscribe from the subscribed event. @@ -127,11 +136,17 @@ class RunningObservation(object): # Check if the observation object is really in OFF state. if stopped: logger.info( - f"Successfully stopped the observation with ID={self.observation_id}" + "Successfully stopped the observation with ID=%s", + {self.observation_id}, ) else: logger.warning( - f"Could not shut down the Observation device ( {self.device_name} ) for observation ID={self.observation_id}. This means that there is a chance for a memory leak. Will continue anyway and forcefully delete the Observation object." + "Could not shut down the Observation device ( %s ) \ + for observation ID=%s. This means that there is a \ + chance for a memory leak. Will continue anyway and forcefully delete \ + the Observation object.", + {self.device_name}, + {self.observation_id}, ) # Finally remove the device object from the Tango DB. @@ -139,13 +154,16 @@ class RunningObservation(object): self._tango_util.delete_device(self.class_name, self.device_name) except DevFailed: logger.warning( - f"Something went wrong when the device {self.device_name} was removed from the Tango DB. There is nothing that can be done about this here at this moment but you should check the Tango DB yourself." + "Something went wrong when the device %s \ + was removed from the Tango DB. There is nothing that can be done \ + about this here at this moment but you should check the Tango DB yourself.", + {self.device_name}, ) class ObservationController(object): @property - def running_observations(self) -> [int]: + def running_observations(self) -> List[int]: return list(self._running_observations.keys()) def __init__(self, tango_domain: str): @@ -172,7 +190,11 @@ class ObservationController(object): if event.err: # Something is fishy with this event. logger.warning( - f"The Observation device {event.device} sent an event but the event signals an error. It is advised to check the logs for any indication that something went wrong in that device. Event data={event}" + "The Observation device %s sent an event but the event \ + signals an error. It is advised to check the logs for any indication \ + that something went wrong in that device. Event data=%s", + {event.device}, + {event}, ) return @@ -184,7 +206,11 @@ class ObservationController(object): if not running_obs: # No obs is running??? logger.warning( - f"Received an observation_running event for the observation with ID={obs_id}. According to the records in ObservationControl, this observation is not supposed to run. Please check previous logs, especially around the time an observation with this ID was started. Will continue and ignore this event." + "Received an observation_running event for the observation with ID=%s. \ + According to the records in ObservationControl, this observation is \ + not supposed to run. Please check previous logs, especially around the time \ + an observation with this ID was started. Will continue and ignore this event.", + {obs_id}, ) return @@ -203,16 +229,25 @@ class ObservationController(object): obs = running_obs[obs_id] self.stop_observation(obs_id) else: - # The observation that we are trying to process is not part of the running_obs dictionary + # The observation that we are trying to process is not part + # of the running_obs dictionary logger.warning( - f"Received an observation_running event for the observation with ID={obs_id}. According to the records in ObservationControl, this observation is not supposed to run. Please check previous logs, especially around the time an observation with this ID was started. Will continue and ignore this event." + "Received an observation_running event for the observation with ID=%s. \ + According to the records in ObservationControl, this observation is \ + not supposed to run. Please check previous logs, especially around \ + the time an observation with this ID was started. \ + Will continue and ignore this event.", + {obs_id}, ) return def start_observation(self, settings: ObservationSettings): + """Create a new Observation Device and start an observation""" # Check further properties that cannot be validated through a JSON schema if settings.stop_time <= datetime.now(): - error = f'Cannot start an observation with ID={settings.observation_id} because the parameter stop_time parameter value="{settings.stop_time}" is invalid. Set a stop_time parameter later in time than the start time.' + error = f'Cannot start an observation with ID={settings.observation_id} \ + because the parameter stop_time parameter value="{settings.stop_time}" \ + is invalid. Set a stop_time parameter later in time than the start time.' Except.throw_exception("IllegalCommand", error, __name__) obs = RunningObservation(self._tango_domain, settings) @@ -223,7 +258,11 @@ class ObservationController(object): except DevFailed as ex: # Remove the device again. self._tango_util.delete_device(obs.class_name, obs.device_name) - error_string = f"Cannot access the Observation device instance for observation ID={obs.observation_id} with device class name={obs.class_name} and device instance name={obs.device_name}. This means that the observation likely did not start but certainly cannot be controlled and/or forcefully be stopped." + error_string = f"Cannot access the Observation device instance for observation \ + ID={obs.observation_id} with device class name={obs.class_name} and \ + device instance name={obs.device_name}. \ + This means that the observation likely did not start \ + but certainly cannot be controlled and/or forcefully be stopped." logger.exception(error_string) Except.re_throw_exception(ex, "DevFailed", error_string, __name__) @@ -235,15 +274,19 @@ class ObservationController(object): obs.subscribe(self.observation_running_callback) except DevFailed as ex: self._tango_util.delete_device(obs.class_name, obs.device_name) - error_string = "Cannot access the Observation device instance for observation ID=%s with device class name=%s and device instance name=%s. This means that the observation cannot be controlled and/or forcefully be stopped." + error_string = "Cannot access the Observation device instance for observation ID=%s \ + with device class name=%s and device instance name=%s. This means that the observation \ + cannot be controlled and/or forcefully be stopped." logger.exception( error_string, obs.observation_id, obs.class_name, obs.device_name ) Except.re_throw_exception(ex, "DevFailed", error_string, __name__) def stop_observation(self, obs_id): + """Stop the observation with the given ID""" if self.is_observation_running(obs_id) is False: - error = f"Cannot stop an observation with ID={obs_id}, because the observation is not running." + error = f"Cannot stop an observation with ID={obs_id}, \ + because the observation is not running." Except.throw_exception("IllegalCommand", error, __name__) # Fetch the obs data and remove it from the dict of @@ -252,5 +295,6 @@ class ObservationController(object): observation.shutdown() def stop_all_observations(self): + """Stop all running observations""" for obs_id in self._running_observations.copy().keys(): self.stop_observation(obs_id) diff --git a/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py b/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py index bcffad6971279c66e905b649a8615b28bff176e8..e94c84c03538a15d8ee396138d94ff5e7f193243 100644 --- a/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py +++ b/tangostationcontrol/tangostationcontrol/configuration/observation_settings.py @@ -14,6 +14,7 @@ class ObservationSettings(_ConfigurationBase): self, observation_id: int, stop_time: datetime, + antenna_set: str, antenna_mask: Sequence[int], filter: str, SAPs: Sequence[Sap], @@ -22,6 +23,7 @@ class ObservationSettings(_ConfigurationBase): ): self.observation_id = observation_id self.stop_time = stop_time + self.antenna_set = antenna_set self.antenna_mask = antenna_mask self.filter = filter self.SAPs = SAPs @@ -32,6 +34,7 @@ class ObservationSettings(_ConfigurationBase): yield from { "observation_id": self.observation_id, "stop_time": self.stop_time.isoformat(), + "antenna_set": self.antenna_set, "antenna_mask": self.antenna_mask, "filter": self.filter, "SAPs": [dict(s) for s in self.SAPs], @@ -45,6 +48,7 @@ class ObservationSettings(_ConfigurationBase): return ObservationSettings( json_dct["observation_id"], datetime.fromisoformat(json_dct["stop_time"]), + json_dct["antenna_set"], json_dct["antenna_mask"], json_dct["filter"], json_dct["SAPs"], diff --git a/tangostationcontrol/tangostationcontrol/devices/antennafield.py b/tangostationcontrol/tangostationcontrol/devices/antennafield.py index d53366e796abca91325dbd0abd5f6ef6d07e80e1..6af1697be694d2467d099f7fdc027a60c85e744c 100644 --- a/tangostationcontrol/tangostationcontrol/devices/antennafield.py +++ b/tangostationcontrol/tangostationcontrol/devices/antennafield.py @@ -150,6 +150,20 @@ class AntennaField(LOFARDevice): default_value=[f"Antenna{n + 1}" for n in range(MAX_ANTENNA)], ) + # ----- Antenna set + + Antenna_Sets = device_property( + doc="String representation of officially offered set of antennas", + dtype="DevVarStringArray", + mandatory=False, + ) + + Antenna_Set_Masks = device_property( + doc="String encoding of the corresponding antenna masks for the antennafield", + dtype="DevVarStringArray", + mandatory=False, + ) + # ----- Antenna states Antenna_Quality = device_property( @@ -371,7 +385,20 @@ class AntennaField(LOFARDevice): 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_ANTENNA + access=AttrWriteType.READ, + dtype=(str,), + max_dim_x=MAX_ANTENNA, + ) + Antenna_Set_RW = attribute( + doc="Name of the Antenna set", + dtype=str, + access=AttrWriteType.READ_WRITE, + ) + Antenna_Mask_RW = attribute( + doc="Mask values translated from the AntennaSet name", + dtype=(bool,), + max_dim_x=MAX_ANTENNA, + access=AttrWriteType.READ_WRITE, ) Antenna_to_SDP_Mapping_R = attribute( doc="To which (fpga, input) pair each antenna is connected. " "-1=unconnected.", @@ -682,6 +709,40 @@ class AntennaField(LOFARDevice): def read_Antenna_Names_R(self): return self.Antenna_Names + def read_Antenna_Set_RW(self): + return self._antenna_set + + def write_Antenna_Set_RW(self, antenna_set: str): + if antenna_set not in self.Antenna_Sets: + raise ValueError( + f"Unsupported antenna set : {antenna_set}. Must be one of {self.Antenna_Sets}" + ) + self._antenna_set = antenna_set + self._antenna_mask = self._translate_antenna_set_to_antenna_mask( + self._antenna_set + ) + + def read_Antenna_Mask_RW(self): + return self._antenna_mask + + def write_Antenna_Mask_RW(self, antenna_mask: list): + self._antenna_mask = antenna_mask + + def _translate_antenna_set_to_antenna_mask(self, antenna_set: str) -> numpy.ndarray: + try: + # Retrieve antenna_set entry position + antenna_index = list(self.Antenna_Sets).index(antenna_set) + # Return the corresponding antenna_mask array + return numpy.array( + [int(x) for x in list(self.Antenna_Set_Masks[antenna_index])], + dtype=bool, + ) + except ValueError as exc: + raise Exception( + f"Unsupported antenna mask with the following antenna set: {antenna_set}. \ + Must be one of {self.Antenna_Sets}" + ) from exc + def read_Frequency_Band_RW(self): antenna_type = self.Antenna_Type @@ -714,14 +775,16 @@ class AntennaField(LOFARDevice): if bands[val].antenna_type != self.Antenna_Type: raise ValueError( - f"Unsupported frequency band for our antenna type: {val} is for {bands[val].antenna_type}, but we are {self.Antenna_Type}." + f"Unsupported frequency band for our antenna type: {val} \ + is for {bands[val].antenna_type}, but we are {self.Antenna_Type}." ) if ( bands[val].clock != bands[value[0]].clock ): # NB: "value[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]}." + f"All frequency bands must use the same clock. \ + These do not: {val} and {value[0,0]}." ) # apply settings on RECV @@ -1159,6 +1222,10 @@ class AntennaField(LOFARDevice): self.__setup_all_proxies() self.__setup_recv_mapper() self.__setup_sdp_mapper() + self._antenna_set = "ALL" + self._antenna_mask = self._translate_antenna_set_to_antenna_mask( + self._antenna_set + ) @log_exceptions() def _prepare_hardware(self): diff --git a/tangostationcontrol/tangostationcontrol/devices/observation.py b/tangostationcontrol/tangostationcontrol/devices/observation.py index d234754a6b76a55fde7a9bcddf92bba6fda177fa..9ca844b2c0fdb0bbed7b68b2375ab1d676a9aa78 100644 --- a/tangostationcontrol/tangostationcontrol/devices/observation.py +++ b/tangostationcontrol/tangostationcontrol/devices/observation.py @@ -51,9 +51,7 @@ class Observation(LOFARDevice): ) observation_id_R = attribute(dtype=numpy.int64, access=AttrWriteType.READ) stop_time_R = attribute(dtype=numpy.float64, access=AttrWriteType.READ) - antenna_mask_R = attribute( - dtype=(numpy.int64,), max_dim_x=MAX_ANTENNA, access=AttrWriteType.READ - ) + antenna_set_R = attribute(dtype=str, access=AttrWriteType.READ) filter_R = attribute(dtype=str, access=AttrWriteType.READ) saps_subband_R = attribute( dtype=((numpy.uint32,),), @@ -73,6 +71,9 @@ class Observation(LOFARDevice): first_beamlet_R = attribute(dtype=numpy.int64, access=AttrWriteType.READ) observation_settings_RW = attribute(dtype=str, access=AttrWriteType.READ_WRITE) + antenna_mask_RW = attribute( + dtype=(numpy.int64,), max_dim_x=MAX_ANTENNA, access=AttrWriteType.READ_WRITE + ) def __init__(self, cl, name): super().__init__(cl, name) @@ -82,6 +83,7 @@ class Observation(LOFARDevice): self.tilebeam_proxy: Optional[DeviceProxy] = None self._observation_settings: Optional[ObservationSettings] = None self._num_inputs: int = 0 + self._antenna_mask = [] def init_device(self): """Setup some class member variables for observation state""" @@ -110,7 +112,8 @@ class Observation(LOFARDevice): # Set a reference of AntennaField device that is correlated to this device util = Util.instance() # TODO(Stefano): set a proper policy for the devices instance number - # It cannot be inherited from the Observation instance number (i.e. Observation_id) + # It cannot be inherited from the Observation instance number + # (i.e. Observation_id) self.antennafield_proxy = DeviceProxy( f"{util.get_ds_inst_name()}/AntennaField/HBA" ) @@ -134,9 +137,10 @@ class Observation(LOFARDevice): self._num_inputs = self.digitalbeam_proxy.antenna_select_RW.shape[0] logger.info( - f"The observation with ID={self._observation_settings.observation_id} is " - "configured. It will begin as soon as On() is called and it is" - f"supposed to stop at {self._observation_settings.stop_time}" + "The observation with ID=%s is configured." + "It will begin as soon as On() is called and it is supposed to stop at %s.", + self._observation_settings.observation_id, + self._observation_settings.stop_time, ) def configure_for_off(self): @@ -145,8 +149,12 @@ class Observation(LOFARDevice): super().configure_for_off() logger.info( - f"Stopped the observation with ID=" - f"{self._observation_settings.observation_id if self._observation_settings else None}." + "Stopped the observation with ID=%s.", + { + self._observation_settings.observation_id + if self._observation_settings + else None + }, ) def configure_for_on(self): @@ -159,9 +167,14 @@ class Observation(LOFARDevice): self._apply_observation_id(self._observation_settings.observation_id) ) + # Apply Antenna Set + self.antennafield_proxy.Antenna_Set_RW = self._observation_settings.antenna_set + # Apply Antenna Mask and Filter + self.write_antenna_mask_RW(self._observation_settings.antenna_mask) + ant_mask, frequency_band = self._apply_antennafield_settings( - self.read_antenna_mask_R(), self.read_filter_R() + self.read_antenna_mask_RW(), self.read_filter_R() ) self.antennafield_proxy.ANT_mask_RW = ant_mask self.antennafield_proxy.Frequency_Band_RW = frequency_band @@ -174,7 +187,7 @@ class Observation(LOFARDevice): self.read_saps_pointing_R() ) self.digitalbeam_proxy.antenna_select_RW = self._apply_saps_antenna_select( - self.read_antenna_mask_R() + self._observation_settings.antenna_mask ) # Apply Tile Beam pointing direction @@ -185,7 +198,8 @@ class Observation(LOFARDevice): ] * self.antennafield_proxy.nr_antennas_R logger.info( - f"Started the observation with ID={self._observation_settings.observation_id}." + "Started the observation with ID=%s.", + {self._observation_settings.observation_id}, ) @only_when_on() @@ -205,9 +219,9 @@ class Observation(LOFARDevice): @only_in_states([DevState.STANDBY, DevState.ON]) @fault_on_error() @log_exceptions() - def read_antenna_mask_R(self): - """Return the antenna_mask_R attribute.""" - return self._observation_settings.antenna_mask + def read_antenna_set_R(self): + """Return the antenna_set_R attribute.""" + return self._observation_settings.antenna_set @only_in_states([DevState.STANDBY, DevState.ON]) @fault_on_error() @@ -268,10 +282,23 @@ class Observation(LOFARDevice): """No validation on configuring parameters as task of control device""" try: self._observation_settings = ObservationSettings.from_json(parameters) - except ValidationError as e: + except ValidationError: self._observation_settings = None # Except.throw_exception("IllegalCommand", e.message, __name__) + @fault_on_error() + @log_exceptions() + def read_antenna_mask_RW(self): + """Return current observation_parameters string""" + return self._antenna_mask + + @only_in_states([DevState.STANDBY, DevState.ON]) + @fault_on_error() + @log_exceptions() + def write_antenna_mask_RW(self, requested_antenna_mask: list): + """Write validated antenna mask values""" + self._antenna_mask = self._validate_antenna_mask(requested_antenna_mask) + @only_when_on() @fault_on_error() @log_exceptions() @@ -302,8 +329,7 @@ class Observation(LOFARDevice): AntennaField device """ ANT_mask_RW = [False] * MAX_ANTENNA - for a in antenna_mask: - ANT_mask_RW[a] = True + ANT_mask_RW = numpy.logical_or(ANT_mask_RW, antenna_mask) # convert numpy.array(dtype=str) to list[str] to avoid crash in DeviceProxy # see https://gitlab.com/tango-controls/pytango/-/issues/492 @@ -344,6 +370,22 @@ 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 _validate_antenna_mask(self, requested_antenna_mask: list): + """Perform the validation steps for antenna_set and antenna_mask settings""" + antenna_mask = self.antennafield_proxy.Antenna_Mask_RW + antenna_usage_mask = self.antennafield_proxy.Antenna_Usage_Mask_R + # Bitwise AND of default antennafield mask and usable antennas + valid_antenna_mask = numpy.logical_and(antenna_mask, antenna_usage_mask) + # Bitwise AND of validate antenna mask and user-requested antennas + if len(requested_antenna_mask) == 0: + # Empty array = all antennas + return valid_antenna_mask + selected_antenna_mask = [0] * len(valid_antenna_mask) + for ant in requested_antenna_mask: + # select antenna only if it belongs to the valid antenna set + selected_antenna_mask[ant] = numpy.logical_and(1, valid_antenna_mask[ant]) + return selected_antenna_mask + # ---------- # Run server diff --git a/tangostationcontrol/tangostationcontrol/devices/observation_control.py b/tangostationcontrol/tangostationcontrol/devices/observation_control.py index 97f26d1c2aa4c6afdaee882d39789efdbb570e06..668e399f0034ca21a0c06ea879f7e93da79f11b8 100644 --- a/tangostationcontrol/tangostationcontrol/devices/observation_control.py +++ b/tangostationcontrol/tangostationcontrol/devices/observation_control.py @@ -4,7 +4,15 @@ import logging import numpy -from tango import Except, DevState, AttrWriteType, DebugIt, Util, DevBoolean, DevString +from tango import ( + Except, + DevState, + AttrWriteType, + DebugIt, + Util, + DevBoolean, + DevString, +) from tango.server import Device, command, attribute from tangostationcontrol.common import ObservationController from tangostationcontrol.common.entrypoint import entry @@ -25,19 +33,23 @@ __all__ = ["ObservationControl", "main"] @device_logging_to_python() class ObservationControl(LOFARDevice): """Observation Control Device Server for LOFAR2.0 - The ObservationControl Tango device controls the instantiation of a Tango Dynamic Device from the Observation class. + The ObservationControl Tango device controls the instantiation of a Tango Dynamic Device + from the Observation class. ObservationControl then keeps a record of the Observation devices and if they are still alive. - At the end of an observation ObservationControl checks if the respective Observation device has stopped its - execution and releases it. If the Observation device has not stopped its execution yet, it is attempted to - forcefully stop the execution of the Observation device. + At the end of an observation ObservationControl checks if the respective + Observation device has stopped its execution and releases it. If the Observation device + has not stopped its execution yet, it is attempted to forcefully stop the execution + of the Observation device. - The Observation devices are responsible for the "real" execution of an observation. They get references to the - hardware devices that are needed to set values in the relevant Control Points. The Observation device performs only - a check if enough parameters are available to perform the set-up. + The Observation devices are responsible for the "real" execution of an observation. + They get references to the hardware devices that are needed to set values in the + relevant Control Points. The Observation device performs only a check if enough parameters + are available to perform the set-up. Essentially this is what happens: - Somebody calls ObservationControl.start_observation(parameters). Then ObservationControl will perform: + Somebody calls ObservationControl.start_observation(parameters). + Then ObservationControl will perform: - Creates a new instance of an Observation device in the Tango DB - Call Initialise(parameters) - Wait for initialise to return @@ -47,7 +59,8 @@ class ObservationControl(LOFARDevice): - Subscribe to the Observation.running MP's periodic event - Register the observation in the dict self.running_observations[ID] - The Observation updates the MP every second with the current time - - The callback gets called periodically. It checks if MP value > stop (stored in the dict under the obs IDS. + - The callback gets called periodically. + It checks if MP value > stop (stored in the dict under the obs IDS. By this it can determine if the observation is done. - if MP value > observation end - Remove observation ID from running obs dict @@ -125,13 +138,15 @@ class ObservationControl(LOFARDevice): # the parameters are not sufficient. if obs_id < 1: # Do not execute - error = f"Cannot stop an observation with ID={obs_id}, because the observation ID is invalid." + error = f"Cannot stop an observation with ID={obs_id}, \ + because the observation ID is invalid." Except.throw_exception("IllegalCommand", error, __name__) elif self._observation_controller.is_observation_running(obs_id) is False: - error = f"Cannot stop an observation with ID={obs_id}, because the observation is not running." + error = f"Cannot stop an observation with ID={obs_id}, \ + because the observation is not running." Except.throw_exception("IllegalCommand", error, __name__) - logger.info(f"Stopping the observation with ID={obs_id}.") + logger.info("Stopping the observation with ID=%s.", obs_id) self._observation_controller.stop_observation(obs_id) @@ -154,7 +169,8 @@ class ObservationControl(LOFARDevice): # Parameter check, do not execute if obs_id is invalid if obs_id < 1: # Do not execute - error = f"Cannot check if an observation with ID={obs_id} is running, because the observation ID is invalid" + error = f"Cannot check if an observation with ID={obs_id} is running, \ + because the observation ID is invalid" Except.throw_exception("IllegalCommand", error, __name__) return self._observation_controller.is_observation_running(obs_id) 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 348dd3936acee336f5584536e6537449a2601b64..d9b88a20c69e5edae4cf5ce7f0d918a5b8e65c93 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py @@ -127,6 +127,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): self.beamlet_proxy = self.setup_beamlet_proxy() self.digitalbeam_proxy = self.setup_digitalbeam_proxy() self.tilebeam_proxy = self.setup_tilebeam_proxy() + self._antenna_mask = [1, 1, 1] + [0] * 6 + [1] + [0] * (MAX_ANTENNA - 10) def setup_recv_proxy(self): # setup RECV @@ -156,6 +157,14 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): "Antenna_to_SDP_Mapping": self.ANTENNA_TO_SDP_MAPPING, "Antenna_Quality": antenna_qualities, "Antenna_Use": antenna_use, + "Antenna_Sets": ["INNER", "OUTER", "SPARSE_EVEN", "SPARSE_ODD", "ALL"], + "Antenna_Set_Masks": [ + "111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000", + "000000000000000000000000000000000000000000000000111111111111111111111111111111111111111111111111", + "101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010", + "010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101", + "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + ], } ) antennafield_proxy.off() @@ -220,6 +229,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): data = loads(self.VALID_JSON) stop_timestamp = datetime.fromisoformat(data["stop_time"]).timestamp() observation_id = data["observation_id"] + antenna_set = data["antenna_set"] antenna_mask = data["antenna_mask"] filter = data["filter"] num_saps = len(data["SAPs"]) @@ -247,7 +257,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): self.assertEqual(DevState.ON, self.proxy.state()) self.assertEqual(stop_timestamp, self.proxy.stop_time_R) self.assertEqual(observation_id, self.proxy.observation_id_R) - self.assertListEqual(antenna_mask, self.proxy.antenna_mask_R.tolist()) + self.assertEqual(antenna_set, self.proxy.antenna_set_R) 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)) diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py index c10df47d01dc8d3ba5b77e26c9e8ebaf6f5da86a..6e6afddef7b4faa8cd6a630785c23e83bdc4ee08 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py @@ -146,6 +146,7 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): control_mapping = [[1, i] for i in range(DEFAULT_N_HBA_TILES)] antennafield_proxy.put_property( { + "Antenna_Set": "ALL", "RECV_devices": ["STAT/RECV/1"], "Power_to_RECV_mapping": numpy.array(control_mapping).flatten(), "Antenna_to_SDP_Mapping": self.ANTENNA_TO_SDP_MAPPING, @@ -313,3 +314,12 @@ class TestObservationControlDevice(AbstractTestBases.TestDeviceBase): # Test false self.assertFalse(self.proxy.is_observation_running(12345)) self.assertFalse(self.proxy.is_observation_running(54321)) + + def test_check_and_convert_parameters_invalid_antenna_set(self): + """Test invalid antenna set name""" + parameters = json.loads(self.VALID_JSON) + parameters["antenna_set"] = "ZZZ" + self.on_device_assert(self.proxy) + self.assertRaises( + DevFailed, self.proxy.start_observation, json.dumps(parameters) + ) 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 3d472908474edac3875405639e695da17185a683..fc4881b28a93d1ebebb5920a71358e9c981ee54b 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/recv_cluster/test_recv_cluster.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/recv_cluster/test_recv_cluster.py @@ -63,6 +63,25 @@ class TestRecvCluster(base.IntegrationTestCase): "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(), "Antenna_Quality": antenna_qualities, "Antenna_Use": antenna_use, + "Antenna_Sets": [ + "INNER", + "OUTER", + "SPARSE_EVEN", + "SPARSE_ODD", + "ALL", + ], + "Antenna_Set_Masks": [ + "111111111111111111111111111111111111111111111111" + + "000000000000000000000000000000000000000000000000", + "000000000000000000000000000000000000000000000000" + + "111111111111111111111111111111111111111111111111", + "1010101010101010101010101010101010101010101010101" + + "01010101010101010101010101010101010101010101010", + "0101010101010101010101010101010101010101010101010" + + "10101010101010101010101010101010101010101010101", + "11111111111111111111111111111111111111111111111111" + + "1111111111111111111111111111111111111111111111", + ], } ) proxy.off() diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_observation_controller.py b/tangostationcontrol/tangostationcontrol/test/common/test_observation_controller.py index 75908dacea942049132dd8d6e5c9af16fdd1b84c..c2d7997c06167b5abfb4beb4aabb228c1f173c02 100644 --- a/tangostationcontrol/tangostationcontrol/test/common/test_observation_controller.py +++ b/tangostationcontrol/tangostationcontrol/test/common/test_observation_controller.py @@ -16,6 +16,8 @@ from tangostationcontrol.test import base @mock.patch("tango.Util.instance") class TestObservationController(base.TestCase): + """Test Observation Controller main operations""" + def test_is_any_observation_running(self, _): sut = ObservationController("DMR") self.assertFalse(sut.is_any_observation_running()) @@ -38,6 +40,7 @@ class TestRunningObservation(base.TestCase): SETTINGS = ObservationSettings( 5, datetime.fromisoformat("2022-10-26T11:35:54.704150"), + "ALL", [3, 2, 1], "filter settings", [Sap([3, 2], Pointing(1.2, 2.1, "LMN")), Sap([1], Pointing(3.3, 4.4, "MOON"))], diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/_mock_requests.py b/tangostationcontrol/tangostationcontrol/test/configuration/_mock_requests.py index 38eb7412c7f296db0811f088ec97fe13e2ea52e4..df19c68f338c540791a29e888aa8c886af62a25d 100644 --- a/tangostationcontrol/tangostationcontrol/test/configuration/_mock_requests.py +++ b/tangostationcontrol/tangostationcontrol/test/configuration/_mock_requests.py @@ -79,6 +79,7 @@ OBSERVATION_SETTINGS_SCHEMA = """ "required": [ "observation_id", "stop_time", + "antenna_set", "antenna_mask", "filter", "SAPs" @@ -92,6 +93,9 @@ OBSERVATION_SETTINGS_SCHEMA = """ "type": "string", "format": "date-time" }, + "antenna_set": { + "type": "string" + }, "antenna_mask": { "type": "array", "minItems": 1, diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/test_observation_settings.py b/tangostationcontrol/tangostationcontrol/test/configuration/test_observation_settings.py index 2e63d0c0b8ae36e781371cc8e41ea98e739f8fd7..cba385096eff77a90d799f75da18f7f537df6638 100644 --- a/tangostationcontrol/tangostationcontrol/test/configuration/test_observation_settings.py +++ b/tangostationcontrol/tangostationcontrol/test/configuration/test_observation_settings.py @@ -16,19 +16,20 @@ class TestObservationSettings(base.TestCase): def test_from_json(self, _): sut = ObservationSettings.from_json( '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' - '"antenna_mask": [3, 2, 1], "filter": "filter_settings",' + '"antenna_set": "ALL", "antenna_mask": [3, 2, 1], "filter": "filter_settings",' '"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.antenna_mask, [3, 2, 1]) self.assertEqual(sut.filter, "filter_settings") self.assertEqual(len(sut.SAPs), 1) sut = ObservationSettings.from_json( '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' - '"antenna_mask": [3, 2, 1], "filter": "filter_settings",' + '"antenna_set": "ALL", "antenna_mask": [3, 2, 1], "filter": "filter_settings",' '"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"} }' ) @@ -39,7 +40,7 @@ class TestObservationSettings(base.TestCase): sut = ObservationSettings.from_json( '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' - '"antenna_mask": [3, 2, 1], "filter": "filter_settings",' + '"antenna_set": "ALL", "antenna_mask": [3, 2, 1], "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}' ) @@ -48,28 +49,30 @@ class TestObservationSettings(base.TestCase): def test_from_json_type_missmatch(self, _): for json in [ - '{"observation_id": "3", "stop_time": "2012-04-23T18:25:43", "antenna_mask": [3, 2, 1], "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_mask": [3, 2, 1], "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_mask": ["3", 2, 1], "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_mask": "3", "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_mask": [3, 2, 1], "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_mask": [3, 2, 1], "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_mask": [3, 2, 1], "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_mask": [3, 2, 1], "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_mask": [3, 2, 1], "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_set": "ALL", "antenna_mask": [3, 2, 1], "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_set": "ALL", "antenna_mask": [3, 2, 1], "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_set": 4, "antenna_mask": [3, 2, 1], "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_set": "ALL", "antenna_mask": ["3", 2, 1], "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_set": "ALL", "antenna_mask": "3", "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_set": "ALL", "antenna_mask": [3, 2, 1], "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_set": "ALL", "antenna_mask": [3, 2, 1], "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_set": "ALL", "antenna_mask": [3, 2, 1], "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_set": "ALL", "antenna_mask": [3, 2, 1], "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_set": "ALL", "antenna_mask": [3, 2, 1], "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"}', ]: with self.assertRaises((ValidationError, ValueError)): ObservationSettings.from_json(json) def test_from_json_missing_fields(self, _): for json in [ - '{"stop_time": "2012-04-23T18:25:43", "antenna_mask": [3, 2, 1], "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, "antenna_mask": [3, 2, 1], "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", "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_mask": [], "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_mask": [3, 2, 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_mask": [3, 2, 1], "filter": "filter_settings","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_mask": [3, 2, 1], "filter": "filter_settings","SAPs": [],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', + '{"stop_time": "2012-04-23T18:25:43", "antenna_set": "ALL", "antenna_mask": [3, 2, 1], "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, "antenna_set": "ALL", "antenna_mask": [3, 2, 1], "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_mask": [3, 2, 1], "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_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_set": "ALL", "antenna_mask": [], "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_set": "ALL", "antenna_mask": [3, 2, 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_set": "ALL", "antenna_mask": [3, 2, 1], "filter": "filter_settings","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_set": "ALL", "antenna_mask": [3, 2, 1], "filter": "filter_settings","SAPs": [],"tile_beam": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}, "first_beamlet": 2}', ]: with self.assertRaises((ValidationError, ValueError)): ObservationSettings.from_json(json) @@ -78,6 +81,7 @@ class TestObservationSettings(base.TestCase): sut = ObservationSettings( 5, datetime.fromisoformat("2022-10-26T11:35:54.704150"), + "ALL", [3, 2, 1], "filter settings", [ @@ -88,7 +92,7 @@ class TestObservationSettings(base.TestCase): self.assertEqual( sut.to_json(), '{"observation_id": 5, "stop_time": "2022-10-26T11:35:54.704150", ' - '"antenna_mask": [3, 2, 1], "filter": "filter settings", "SAPs": ' + '"antenna_set": "ALL", "antenna_mask": [3, 2, 1], "filter": "filter settings", "SAPs": ' '[{"subbands": [3, 2], "pointing": {"angle1": 1.2, "angle2": 2.1, ' '"direction_type": "LMN"}}, {"subbands": [1], "pointing": {"angle1": 3.3, ' '"angle2": 4.4, "direction_type": "MOON"}}], "first_beamlet": 0}', @@ -100,7 +104,7 @@ class TestObservationSettings(base.TestCase): with self.assertRaises(RefResolutionError): ObservationSettings.from_json( '{"observation_id": 3, "stop_time": "2012-04-23T18:25:43", ' - '"antenna_mask": [3, 2, 1], "filter": "filter_settings",' + '"antenna_set": "ALL", "antenna_mask": [3, 2, 1], "filter": "filter_settings",' '"SAPs": [{"subbands": [3, 2, 1], "pointing": {"angle1":1.2, "angle2": 2.1, "direction_type":"LMN"}}]}' ) self.assertEqual(5, mock_get.call_count) diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py index c618e3c42705f2b86cb18ff29a7948bc5f096c58..231ef5187742f46d4b895586ba6622b00fe3b5ce 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py @@ -1,6 +1,9 @@ # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 +# invalid-name +# pylint: disable=C0103 + import logging import unittest from unittest import mock @@ -700,6 +703,16 @@ class TestAntennafieldDevice(device_base.DeviceTestCase): "Antenna_Field_Reference_ITRF": [3.0, 3.0, 3.0], "Antenna_Field_Reference_ETRS": [7.0, 7.0, 7.0], } + ANTENNA_PROPERTIES = { + "Antenna_Sets": ["INNER", "OUTER", "SPARSE_EVEN", "SPARSE_ODD", "ALL"], + "Antenna_Set_Masks": [ + "111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000", + "000000000000000000000000000000000000000000000000111111111111111111111111111111111111111111111111", + "101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010", + "010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101", + "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + ], + } def setUp(self): # DeviceTestCase setUp patches lofar_device DeviceProxy @@ -824,3 +837,25 @@ class TestAntennafieldDevice(device_base.DeviceTestCase): numpy.testing.assert_equal( m_proxy.return_value.write_attribute.call_args[0][1], data ) + + def test_read_Antenna_Mask_all_antennas(self): + """Verify if Antenna_Mask_R is correctly retrieved""" + with DeviceTestContext( + antennafield.AntennaField, + properties={**self.AT_PROPERTIES, **self.ANTENNA_PROPERTIES}, + process=True, + ) as proxy: + proxy.Antenna_Set_RW = "ALL" + expected = [True] * MAX_ANTENNA + numpy.testing.assert_equal(expected, proxy.Antenna_Mask_RW) + + def test_read_Antenna_Mask_half_antennas(self): + """Verify if Antenna_Mask_R is correctly retrieved""" + with DeviceTestContext( + antennafield.AntennaField, + properties={**self.AT_PROPERTIES, **self.ANTENNA_PROPERTIES}, + process=True, + ) as proxy: + proxy.Antenna_Set_RW = "INNER" + expected = [True] * int(MAX_ANTENNA / 2) + [False] * int(MAX_ANTENNA / 2) + numpy.testing.assert_equal(expected, proxy.Antenna_Mask_RW) diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py index b962f8ba707a5afde2f13586c471ca606840e710..10ebef47cd282df6a1c041f74a4bc9b1d4969670 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py @@ -8,6 +8,7 @@ class TestObservationBase: { "observation_id": 12345, "stop_time": "2106-02-07T00:00:00", + "antenna_set": "ALL", "antenna_mask": [0,1,2,9], "filter": "LBA_30_90", "SAPs": [{