diff --git a/.gitattributes b/.gitattributes index 929b6bb4f5a33f37a97181698c5a0626021018f0..fd70048c7ad908b94cd835cee34f0f16eadd84a4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,6 @@ *.h5 binary *.jpg binary *.bin binary + +# casacore measures tables +table.* binary diff --git a/CDB/stations/DTS_ConfigDb.json b/CDB/stations/DTS_ConfigDb.json index 741d9dc910e6ff35d04e993dab21dbd3eb08cc01..bd797a7eabcfff3050d9650f334edb11c3999376 100644 --- a/CDB/stations/DTS_ConfigDb.json +++ b/CDB/stations/DTS_ConfigDb.json @@ -64,6 +64,207 @@ ], "OPC_Time_Out": [ "5.0" + ], + "HBAT_reference_ETRS": [ + "3839371.416", "430339.901", "5057958.886", + "3839368.919", "430335.979", "5057961.1", + "3839365.645", "430339.299", "5057963.288", + "3839368.142", "430343.221", "5057961.074", + "3839374.094", "430299.513", "5057960.017", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0", + "0", "0", "0" + ], + "HBAT_PQR_rotation_angle_deg": [ + "45.73", + "45.73", + "45.73", + "45.73", + "54.40", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0", + "0" + ], + "HBAT_PQR_to_ETRS_rotation_matrix": [ + "-0.11660087", "-0.79095632", "0.60065992", + " 0.99317077", "-0.09529842", "0.06730545", + " 0.00400627", " 0.60440575", "0.79666658" ] } } diff --git a/CDB/stations/dummy_positions_ConfigDb.json b/CDB/stations/dummy_positions_ConfigDb.json index 5f998a8102a8ceaad66b7a7a46ed293aa2223b67..1608917b748c4d7466f726153610be09981c7510 100644 --- a/CDB/stations/dummy_positions_ConfigDb.json +++ b/CDB/stations/dummy_positions_ConfigDb.json @@ -103,23 +103,108 @@ "3826577.066", "461022.948", "5064892.786", "3826577.066", "461022.948", "5064892.786" ], - "HBAT_antenna_itrf_offsets": [ - "-1.847", "-1.180", " 1.493", - "-1.581", " 0.003", " 1.186", - "-1.315", " 1.185", " 0.880", - "-1.049", " 2.367", " 0.573", - "-0.882", "-1.575", " 0.804", - "-0.616", "-0.393", " 0.498", - "-0.350", " 0.789", " 0.191", - "-0.083", " 1.971", "-0.116", - " 0.083", "-1.971", " 0.116", - " 0.350", "-0.789", "-0.191", - " 0.616", " 0.393", "-0.498", - " 0.882", " 1.575", "-0.804", - " 1.049", "-2.367", "-0.573", - " 1.315", "-1.185", "-0.880", - " 1.581", "-0.003", "-1.186", - " 1.847", " 1.180", "-1.493" + "HBAT_PQR_rotation_angles_deg": [ + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24", + "24" + ], + "HBAT_PQR_to_ETRS_rotation_matrix": [ + "-0.1195951054", "-0.7919544517", "0.5987530018", + " 0.9928227484", "-0.0954186800", "0.0720990002", + " 0.0000330969", " 0.6030782884", "0.7976820024" ] } } diff --git a/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.dat b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.dat new file mode 100644 index 0000000000000000000000000000000000000000..1f7869a8f1af1f6d4089128eeed130c78d5c6ae2 Binary files /dev/null and b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.dat differ diff --git a/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0 b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0 new file mode 100644 index 0000000000000000000000000000000000000000..5c14b2d3c72812c815005ec6fbd10110c7ceab70 Binary files /dev/null and b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0 differ diff --git a/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0i b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0i new file mode 100644 index 0000000000000000000000000000000000000000..d50b1bdd28ae76847df63f335bcbdd1bd1e7cc9f Binary files /dev/null and b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.f0i differ diff --git a/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.info b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.info new file mode 100644 index 0000000000000000000000000000000000000000..d1fa70b4e608c03fc4f6da5a213c36d568f9fbc4 Binary files /dev/null and b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.info differ diff --git a/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.lock b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.lock new file mode 100644 index 0000000000000000000000000000000000000000..dab1a78a5dec98a99cad64cd140e1308abf761a8 Binary files /dev/null and b/docker-compose/lofar-device-base/WSRT_Measures_stub/ephemerides/DE200/table.lock differ diff --git a/tangostationcontrol/docs/source/devices/recv.rst b/tangostationcontrol/docs/source/devices/recv.rst index 847f8bb51f7f52850e66b0d25c01269b9961e726..00bbd89ef2e748b120e34143c8e6cc4cb5763887 100644 --- a/tangostationcontrol/docs/source/devices/recv.rst +++ b/tangostationcontrol/docs/source/devices/recv.rst @@ -13,3 +13,28 @@ The ``recv == DeviceProxy("STAT/RECV/1")`` device controls the RCUs, the LBA ant Typically, ``N_RCUs == 32``, and ``N_antennas_per_RCU == 3``. +Error information +--------------------- + +These attributes summarise the basic state of the device. Any elements which are not present in ``FPGA_mask_RW`` will be ignored and thus not report errors: + +:RCU_error_R: Whether the RCUs appear usable. + + :type: ``bool[N_RCUs]`` + +:ANT_error_R: Whether the antennas appear usable. + + :type: ``bool[N_RCUs][N_antennas_per_RCU]`` + +:RCU_IOUT_error_R: Whether there are alarms on any of the amplitudes in the measured currents. + + :type: ``bool[N_RCUs]`` + +:RCU_VOUT_error_R: Whether there are alarms on any of the voltages in the measured currents. + + :type: ``bool[N_RCUs]`` + +:RCU_TEMP_error_R: Whether there are alarms on any of the temperatures. NB: These values are also exposed for unused RCUs (the ``RCU_mask_RW`` is ignored). + + :type: ``bool[N_RCUs]`` + diff --git a/tangostationcontrol/docs/source/devices/sdp.rst b/tangostationcontrol/docs/source/devices/sdp.rst index 2ca1ea6fa95e7295a21041d12242e15cec3b8001..6386dd851b178ce44b0b1b53968ba0f219fe5140 100644 --- a/tangostationcontrol/docs/source/devices/sdp.rst +++ b/tangostationcontrol/docs/source/devices/sdp.rst @@ -30,16 +30,29 @@ The following points are significant for the operations of this device: Data-quality information --------------------------- -The following fields describe the data quality: +The following fields describe the data quality (see also :doc:`../signal_chain`): :FPGA_signal_input_mean_R: Mean value of the last second of input (in ADC quantisation units). Should be close to 0. :type: ``double[N_fpgas][N_ants_per_fpga]`` -:FPGA_signal_input_rms_R: Root means square value of the last second of input (in ADC quantisation units). ``rms^2 = mean^2 + std^2``. Values above 2048 indicate strong RFI. +:FPGA_signal_input_rms_R: Root means square value of the last second of input (in ADC quantisation units). ``rms^2 = mean^2 + std^2``. Values above 2048 indicate strong RFI. Values of 0 indicate a lack of signal input. :type: ``double[N_fpgas][N_ants_per_fpga]`` +Error information +--------------------- + +These attributes summarise the basic state of the device. Any elements which are not present in ``FPGA_mask_RW`` will be ignored and thus not report errors: + +:FPGA_error_R: Whether the FPGAs appear usable. + + :type: ``bool[N_fpgas]`` + +:FPGA_procesing_error_R: Whether the FPGAs are processing their input from the RCUs. NB: This will also raise an error if the Waveform Generator is enabled. + + :type: ``bool[N_fpgas]`` + Version Information --------------------- diff --git a/tangostationcontrol/docs/source/devices/using.rst b/tangostationcontrol/docs/source/devices/using.rst index 7aef380071836e7cd6ece9d7202f04658e68a99a..825ee74d83b5a7ad8a8e18c8813ee1ace81f5459 100644 --- a/tangostationcontrol/docs/source/devices/using.rst +++ b/tangostationcontrol/docs/source/devices/using.rst @@ -59,13 +59,12 @@ typically involves the following sequence of commands:: # turn the device off completely first. device.off() - # setup any connections and threads - device.initialise() + # turn on the device and fully reinitialise it + # alternatively, device.warm_boot() can be used, + # in which case no hardware is reinitialised. + device.boot() - # turn on the device - device.on() - -Of course, the device could go into ``FAULT`` again, even during the ``initialise()`` command, for example because the hardware it manages is unreachable. To debug the fault condition, check the :doc:`../interfaces/logs` of the device in question. +Of course, the device could go into ``FAULT`` again, even during the ``boot()`` command, for example because the hardware it manages is unreachable. To debug the fault condition, check the :doc:`../interfaces/logs` of the device in question. Initialise hardware ```````````````````` @@ -74,6 +73,10 @@ Most devices provide the following commands, in order to configure the hardware :initialise(): Initialise the device (connect to the hardware). Moves from ``OFF`` to ``STANDBY``. +:set_translator_defaults(): Select the hardware to configure and monitor. + +:prepare_hardware(): For devices that control hardware, this command prepares the hardware to accept commands (f.e. load firmware). + :set_defaults(): Upload default attribute settings from the TangoDB to the hardware. :initialise_hardware(): For devices that control hardware, this command runs the hardware initialisation procedure. @@ -145,4 +148,3 @@ For example, the ``RCU_mask_RW`` array is the RCU mask in the ``recv`` device. I # <--- only LED0 on RCU3 is now on # recv.RCU_LED0_R should show this, # if you have the RCU hardware installed. - diff --git a/tangostationcontrol/requirements.txt b/tangostationcontrol/requirements.txt index f8bad33a66897ce01bfb5084430a9e7af8ce621b..a93c35d3d5a643afaf0d78f53cdd2c36be65f8f2 100644 --- a/tangostationcontrol/requirements.txt +++ b/tangostationcontrol/requirements.txt @@ -12,3 +12,4 @@ psutil >= 5.8.0 # BSD docker >= 5.0.3 # Apache 2 python-logstash-async >= 2.3.0 # MIT python-casacore >= 3.3.1 # GPL2 +etrs-itrs@git+https://github.com/brentjens/etrs-itrs # license pending diff --git a/tangostationcontrol/tangostationcontrol/beam/delays.py b/tangostationcontrol/tangostationcontrol/beam/delays.py index 771fad6c90a59ebe002867fedfc55446ead51000..ff81bd485fb66d8a043922a9a65443b3c080ff81 100644 --- a/tangostationcontrol/tangostationcontrol/beam/delays.py +++ b/tangostationcontrol/tangostationcontrol/beam/delays.py @@ -50,9 +50,21 @@ class delay_calculator: return numpy.dot(reference_direction_vector, relative_itrf) / speed_of_light + def is_valid_direction(self, direction): + try: + _ = self.measure.direction(*direction) + except RuntimeError as e: + return False + + return True + def convert(self, direction, antenna_itrf: list([float])): - # obtain the direction vector for a specific pointing - pointing = self.measure.direction(*direction) + try: + # obtain the direction vector for a specific pointing + pointing = self.measure.direction(*direction) + except RuntimeError as e: + raise ValueError(f"Could not convert direction {direction} into a pointing") from e + reference_dir_vector = self.get_direction_vector(pointing) # # compute the delays for an antennas w.r.t. the reference position diff --git a/tangostationcontrol/tangostationcontrol/beam/geo.py b/tangostationcontrol/tangostationcontrol/beam/geo.py new file mode 100644 index 0000000000000000000000000000000000000000..033a6c4e4293573d05cb3fa09bcab5071c88a97b --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/beam/geo.py @@ -0,0 +1,33 @@ +import etrsitrs +import numpy + +""" + LOFAR station positions are measured in ETRS89, which are the coordinates of the position as it would be in 1989. + + These coordinates are carthesian (X, Y, Z), with (0, 0, 0) being the center of the Earth. + + The ETRS89 positions differ from the current due to tectonic movements. Periodically, these differences are modelled + as position offsets and velocities. For example, ITRF2005 represents these offsets updated to 2005. We can + furthermore extrapolate these models to later dates, such as 2015.5. + + By periodically extrapolating to later dates, or by using later models, we can obtain more precise positions of our + antennas without having to remeasure them. + + The ETRSitrs package does all the transformation calculations for us. +""" + +def ETRS_to_ITRF(ETRS_coordinates: numpy.array, ITRF_reference_frame: str = "ITRF2005", ITRF_reference_epoch: float = 2015.5) -> numpy.array: + """ Convert an array of coordinate triples from ETRS to ITRF, in the given reference frame and epoch. """ + + # fetch converter + ETRS_to_ITRF_fn = etrsitrs.convert_fn("ETRF2000", ITRF_reference_frame, ITRF_reference_epoch) + + if ETRS_coordinates.ndim == 1: + # convert a single coordinate triple + ITRF_coordinates = ETRS_to_ITRF_fn(ETRS_coordinates) + else: + # convert each coordinate triple + ITRF_coordinates = numpy.apply_along_axis(ETRS_to_ITRF_fn, 1, ETRS_coordinates) + + # return computed ITRF coordinates + return ITRF_coordinates diff --git a/tangostationcontrol/tangostationcontrol/beam/hba_tile.py b/tangostationcontrol/tangostationcontrol/beam/hba_tile.py new file mode 100644 index 0000000000000000000000000000000000000000..cede7be17c1c12773931e19ec81fd7d1a666b72b --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/beam/hba_tile.py @@ -0,0 +1,68 @@ +import numpy +from math import sin, cos + +class HBATAntennaOffsets(object): + """ + This class helps calculate the absolute offsets of the antennas within a tile, + based on their relative orientation to ETRS. + + These offsets are Known in LOFAR1 as "iHBADeltas". + + Within LOFAR, we use a relative "PQR" coordinate system for each station: + + * The origin is the same as the ETRS reference position of the station, + * "Q" points to true North as seen from CS002LBA, the center of LOFAR, + * "P" points (roughly) east, + * "R" points (roughly) up, + * All antennas are positioned in the same plane (R=0 with an error < 3cm). + + With the PQR->ETRS rotation matrix (or "ROTATION_MATRIX" in LOFAR1), + we can thus convert from PQR to (relative) ETRS coordinates. + + Below, the rotation of the HBA tiles is provided with respect to this PQR frame. + Along with the PQR->ETRS rotation matrix, this allows us to calculate the + ` relative positions of each antenna element within a tile in ETRS space. + + The relative ITRF positions are subsequently equal to the relative ETRS positions. + + For reference, see: + https://git.astron.nl/RD/lofar-referentie-vlak/-/blob/master/data/dts/dts.ipynb + https://git.astron.nl/ro/lofar/-/blob/master/MAC/Deployment/data/Coordinates/calc_hba_deltas.py + https://github.com/brentjens/lofar-antenna-positions/blob/master/lofarantpos/db.py#L208 + """ + + """ Model of the HBAT1 tile, as offsets of each antenna with respect to the reference center, in metres. """ + HBAT1_BASE_ANTENNA_OFFSETS = numpy.array( + [[-1.5, +1.5, 0.0], [-0.5, +1.5, 0.0], [+0.5, +1.5, 0.0], [+1.5, +1.5, 0.0], + [-1.5, +0.5, 0.0], [-0.5, +0.5, 0.0], [+0.5, +0.5, 0.0], [+1.5, +0.5, 0.0], + [-1.5, -0.5, 0.0], [-0.5, -0.5, 0.0], [+0.5, -0.5, 0.0], [+1.5, -0.5, 0.0], + [-1.5, -1.5, 0.0], [-0.5, -1.5, 0.0], [+0.5, -1.5, 0.0], [+1.5, -1.5, 0.0]]) * 1.25 + + @staticmethod + def rotation_matrix(rad: float) -> numpy.array: + """ Return a rotation matrix for coordinates for a given number of radians. """ + + rotation_matrix = numpy.array( + [[ cos(rad), sin(rad), 0], + [-sin(rad), cos(rad), 0], + [ 0, 0, 1]]) + + return rotation_matrix + + @staticmethod + def ITRF_offsets(base_antenna_offsets: numpy.array, PQR_rotation: float, PQR_to_ETRS_rotation_matrix: numpy.array) -> numpy.array: + """ Return the antenna offsets in ITRF, given: + + :param: base_antenna_offsets: antenna offsets within an unrotated tile (16x3). + :param: PQR_rotation: rotation of the tile(s) in PQR space (radians). + :param: PQR_to_ETRS_rotation_matrix: rotation matrix for PQR -> ETRS conversion (3x3). + """ + + # Offsets in PQR are derived by rotating the base tile by the specified number of radians + PQR_offsets = numpy.inner(base_antenna_offsets, HBATAntennaOffsets.rotation_matrix(PQR_rotation)) + + # The PQR->ETRS mapping is a rotation as well + ETRS_offsets = numpy.inner(PQR_offsets, PQR_to_ETRS_rotation_matrix) + + # The ITRF offsets are the same as the ETRS offsets + return ETRS_offsets diff --git a/tangostationcontrol/tangostationcontrol/beam/test_delays.py b/tangostationcontrol/tangostationcontrol/beam/test_delays.py deleted file mode 100644 index 1668345163c4f3bff244ede6ff63cf2c2786f9b1..0000000000000000000000000000000000000000 --- a/tangostationcontrol/tangostationcontrol/beam/test_delays.py +++ /dev/null @@ -1,65 +0,0 @@ -from delays import * - -if __name__ == '__main__': - # # create a frame tied to the reference position - reference_itrf = [3826577.066, 461022.948, 5064892.786] # CS002LBA, in ITRF2005 epoch 2012.5 - d = delay_calculator(reference_itrf) - - # # set the timestamp to solve for - timestamp = datetime.datetime(2021,1,1,0,0,5) - d.set_measure_time(timestamp) - - # compute the delays for an antennas w.r.t. the reference position - antenna_itrf = [[3826923.546, 460915.441, 5064643.489]] # CS001LBA, in ITRF2005 epoch 2012.5 - - # # obtain the direction vector for a specific pointing - direction = "J2000","0deg","0deg" - - # calculate the delays based on the set reference position, the set time and now the set direction and antenna positions. - delays = d.convert(direction, antenna_itrf) - - # print the delays - # pprint.pprint(delays) - - - #test changing the time - - print(f"Changing timestamp test\nBase parametres: Direction: {direction}, position: {antenna_itrf}") - for i in range(10): - # # set the timestamp to solve for - timestamp = datetime.datetime(2021,1,1,0,i,5) - d.set_measure_time(timestamp) - - delays = d.convert(direction, antenna_itrf) - - # print the delays - print(f"Timestamp: {timestamp}: {delays}") - - - # reset time - timestamp = datetime.datetime(2021, 1, 1, 0, 0, 5) - d.set_measure_time(timestamp) - - - #test changing the antenna position - print(f"Changing Antenna position test.\nBase parametres: Time: {timestamp} Direction: {direction}") - for i in range(10): - antenna_itrf = [[3826577.066 + i, 461022.948, 5064892.786]] # CS002LBA, in ITRF2005 epoch 2012.5 - - delays = d.convert(direction, antenna_itrf) - - # print the delays - print(f"Antenna position: {antenna_itrf}: {delays}") - - # test changing the direction - - antenna_itrf = [[3826923.546, 460915.441, 5064643.489]] # CS001LBA, in ITRF2005 epoch 2012.5 - print(f"Changing direction test.\nBase parametres: Time: {timestamp} , position: {antenna_itrf}") - - for i in range(10): - direction = "J2000", f"{i}deg", "0deg" - - delays = d.convert(direction, antenna_itrf) - - # print the delays - print(f"Direction: {direction}: {delays}") diff --git a/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py b/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py index b8e829ede0b5857cc5cc6c4da5d6052118cf57b7..7f9c8d07fb3e5ceca540c2a03294dc9a92450d66 100644 --- a/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py +++ b/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py @@ -1,8 +1,8 @@ from tango.server import attribute -from tango import AttrWriteType, DevState +from tango import AttrWriteType import numpy -from tangostationcontrol.devices.device_decorators import only_in_states, fault_on_error +from tangostationcontrol.devices.device_decorators import fault_on_error import logging logger = logging.getLogger() @@ -72,7 +72,6 @@ class attribute_wrapper(attribute): if access == AttrWriteType.READ_WRITE: """ if the attribute is of READ_WRITE type, assign the write function to it""" - @only_in_states([DevState.STANDBY, DevState.ON, DevState.ALARM], log=False) @fault_on_error() def write_func_wrapper(device, value): """ @@ -86,7 +85,6 @@ class attribute_wrapper(attribute): """ Assign the read function to the attribute""" - @only_in_states([DevState.STANDBY, DevState.ON, DevState.ALARM], log=False) @fault_on_error() def read_func_wrapper(device): """ @@ -97,9 +95,13 @@ class attribute_wrapper(attribute): self.fget = read_func_wrapper - super().__init__(dtype=datatype, max_dim_y=max_dim_y, max_dim_x=max_dim_x, access=access, **kwargs) - - return + # "fisallowed" is called to ask us whether an attribute can be accessed. If not, the attribute won't be accessed, + # and the cache not updated. This forces Tango to also force a read the moment an attribute does become accessible. + # The provided function will be used with the call signature "(device: Device, req_type: AttReqType) -> bool". + # + # NOTE: fisallowed=<callable> does not work: https://gitlab.com/tango-controls/pytango/-/issues/435 + # So we have to use fisallowed=<str> here, which causes the function device.<str> to be called. + super().__init__(dtype=datatype, max_dim_y=max_dim_y, max_dim_x=max_dim_x, access=access, fisallowed="is_attribute_wrapper_allowed", **kwargs) def initial_value(self): """ diff --git a/tangostationcontrol/tangostationcontrol/clients/statistics_client.py b/tangostationcontrol/tangostationcontrol/clients/statistics_client.py index 16d46bc71bbeba33c66aa8aa590f301cd0de0fa8..08d2889d0b61c5bd9c9ce5468e35ac6bf82fdffb 100644 --- a/tangostationcontrol/tangostationcontrol/clients/statistics_client.py +++ b/tangostationcontrol/tangostationcontrol/clients/statistics_client.py @@ -97,7 +97,14 @@ class StatisticsClient(AsyncCommClient): # redirect to right object. this works as long as the parameter names are unique among them. if annotation["type"] == "statistics": def read_function(): - return self.collector.parameters[parameter] + if annotation.get("reshape", False): + # force array into the shape of the attribute + if attribute.dim_y > 1: + return self.collector.parameters[parameter].reshape(attribute.dim_y, attribute.dim_x) + else: + return self.collector.parameters[parameter].reshape(attribute.dim_x) + else: + return self.collector.parameters[parameter] elif annotation["type"] == "udp": def read_function(): return self.udp.parameters[parameter] diff --git a/tangostationcontrol/tangostationcontrol/devices/apsct.py b/tangostationcontrol/tangostationcontrol/devices/apsct.py index 20b161e97b16f99d352340246770d5db9f99f3e3..6a3ef5ff8a609eecbc86242d745c7fcb97f61b1a 100644 --- a/tangostationcontrol/tangostationcontrol/devices/apsct.py +++ b/tangostationcontrol/tangostationcontrol/devices/apsct.py @@ -79,12 +79,12 @@ class APSCT(opcua_device): def read_APSCT_error_R(self): return ((self.proxy.APSCTTR_I2C_error_R > 0) - | self.alarm_val("APSCT_PCB_ID_R") - | ~self.proxy.APSCT_INPUT_10MHz_good_R - | (~self.proxy.APSCT_INPUT_PPS_good_R & ~self.proxy.ASPCT_PPS_ignore_R) - | (~self.proxy.APSCT_PLL_160MHz_locked_R & ~self.proxy.APSCT_PLL_200MHz_locked_R) - | (self.proxy.APSCT_PLL_200MHz_locked_R & self.proxy.APSCT_PLL_200MHz_error_R) - | (self.proxy.APSCT_PLL_160MHz_locked_R & self.proxy.APSCT_PLL_160MHz_error_R) + or self.alarm_val("APSCT_PCB_ID_R") + or (not self.proxy.APSCT_INPUT_10MHz_good_R) + or (not self.proxy.APSCT_INPUT_PPS_good_R and not self.proxy.APSCT_PPS_ignore_R) + or (not self.proxy.APSCT_PLL_160MHz_locked_R and not self.proxy.APSCT_PLL_200MHz_locked_R) + or (self.proxy.APSCT_PLL_200MHz_locked_R and self.proxy.APSCT_PLL_200MHz_error_R) + or (self.proxy.APSCT_PLL_160MHz_locked_R and self.proxy.APSCT_PLL_160MHz_error_R) ) APSCT_TEMP_error_R = attribute(dtype=bool) @@ -95,13 +95,13 @@ class APSCT(opcua_device): def read_APSCT_VOUT_error_R(self): return ( self.alarm_val("APSCT_PWR_PPSDIST_3V3_R") - | self.alarm_val("APSCT_PWR_CLKDIST1_3V3_R") - | self.alarm_val("APSCT_PWR_CLKDIST2_3V3_R") - | self.alarm_val("APSCT_PWR_CTRL_3V3_R") - | self.alarm_val("APSCT_PWR_INPUT_3V3_R") - | (self.proxy.APSCT_PWR_PLL_160MHz_on_R & self.alarm_val("APSCT_PWR_PLL_160MHz_3V3_R")) - | (self.proxy.APSCT_PWR_PLL_200MHz_on_R & self.alarm_val("APSCT_PWR_PLL_200MHz_3V3_R")) - | ~self.proxy.APSCT_PWR_on_R + or self.alarm_val("APSCT_PWR_CLKDIST1_3V3_R") + or self.alarm_val("APSCT_PWR_CLKDIST2_3V3_R") + or self.alarm_val("APSCT_PWR_CTRL_3V3_R") + or self.alarm_val("APSCT_PWR_INPUT_3V3_R") + or (self.proxy.APSCT_PWR_PLL_160MHz_on_R and self.alarm_val("APSCT_PWR_PLL_160MHz_3V3_R")) + or (self.proxy.APSCT_PWR_PLL_200MHz_on_R and self.alarm_val("APSCT_PWR_PLL_200MHz_3V3_R")) + or (not self.proxy.APSCT_PWR_on_R) ) # -------- diff --git a/tangostationcontrol/tangostationcontrol/devices/apspu.py b/tangostationcontrol/tangostationcontrol/devices/apspu.py index d014a0eae151d89e77c71a9f0048dfb5e01bcd6b..6be213696c16efc69ca8f20319efc03cfc0f4fe7 100644 --- a/tangostationcontrol/tangostationcontrol/devices/apspu.py +++ b/tangostationcontrol/tangostationcontrol/devices/apspu.py @@ -67,10 +67,10 @@ class APSPU(opcua_device): def read_APSPU_error_R(self): return ((self.proxy.APSPUTR_I2C_error_R > 0) - | self.alarm_val("APSPU_PCB_ID_R") - | self.alarm_val("APSPU_FAN1_RPM_R") - | self.alarm_val("APSPU_FAN2_RPM_R") - | self.alarm_val("APSPU_FAN3_RPM_R")) + or self.alarm_val("APSPU_PCB_ID_R") + or self.alarm_val("APSPU_FAN1_RPM_R") + or self.alarm_val("APSPU_FAN2_RPM_R") + or self.alarm_val("APSPU_FAN3_RPM_R")) APSPU_IOUT_error_R = attribute(dtype=bool) APSPU_TEMP_error_R = attribute(dtype=bool) @@ -78,20 +78,20 @@ class APSPU(opcua_device): def read_APSPU_IOUT_error_R(self): return ( self.alarm_val("APSPU_LBA_IOUT_R") - | self.alarm_val("APSPU_RCU2A_IOUT_R") - | self.alarm_val("APSPU_RCU2D_IOUT_R") + or self.alarm_val("APSPU_RCU2A_IOUT_R") + or self.alarm_val("APSPU_RCU2D_IOUT_R") ) def read_APSPU_TEMP_error_R(self): return ( self.alarm_val("APSPU_LBA_TEMP_R") - | self.alarm_val("APSPU_RCU2A_TEMP_R") - | self.alarm_val("APSPU_RCU2D_TEMP_R") + or self.alarm_val("APSPU_RCU2A_TEMP_R") + or self.alarm_val("APSPU_RCU2D_TEMP_R") ) def read_APSPU_VOUT_error_R(self): return ( self.alarm_val("APSPU_LBA_VOUT_R") - | self.alarm_val("APSPU_RCU2A_VOUT_R") - | self.alarm_val("APSPU_RCU2D_VOUT_R") + or self.alarm_val("APSPU_RCU2A_VOUT_R") + or self.alarm_val("APSPU_RCU2D_VOUT_R") ) # -------- diff --git a/tangostationcontrol/tangostationcontrol/devices/beam.py b/tangostationcontrol/tangostationcontrol/devices/beam.py index ac6accea05a7131ff58f0c48daf880267fdb098d..1f24e804abd1469c11caf86506ac95fecb88a0ac 100644 --- a/tangostationcontrol/tangostationcontrol/devices/beam.py +++ b/tangostationcontrol/tangostationcontrol/devices/beam.py @@ -11,8 +11,9 @@ import numpy import datetime from json import loads -from tango.server import attribute, command -from tango import AttrWriteType, DebugIt, DevState, DeviceProxy, DevVarStringArray, DevVarDoubleArray, DevString +from tango.server import attribute, command, device_property +from tango import AttrWriteType, DebugIt, DevState, DeviceProxy, DevVarStringArray, DevVarDoubleArray, DevString, DevSource +from threading import Thread, Lock, Condition # Additional import from tangostationcontrol.common.entrypoint import entry @@ -25,30 +26,56 @@ from tangostationcontrol.devices.device_decorators import * import logging logger = logging.getLogger() -__all__ = ["Beam", "main"] +__all__ = ["Beam", "main", "BeamTracker"] + @device_logging_to_python() class Beam(lofar_device): - _hbat_pointing_direction = numpy.zeros((96,3), dtype=numpy.str) - _hbat_pointing_timestamp = numpy.zeros(96, dtype=numpy.double) - # ----------------- # Device Properties # ----------------- + HBAT_beam_tracking_interval = device_property( + dtype='DevFloat', + doc='HBAT beam weights updating interval time [seconds]', + mandatory=False, + default_value = 10.0 + ) + + HBAT_beam_tracking_preparation_period = device_property( + dtype='DevFloat', + doc='Preparation time [seconds] needed before starting update operation', + mandatory=False, + default_value = 0.25 + ) + # ---------- # Attributes # ---------- HBAT_pointing_direction_R = attribute(access=AttrWriteType.READ, dtype=((numpy.str,),), max_dim_x=3, max_dim_y=96, - fget=lambda self: self._hbat_pointing_direction) + fget=lambda self: self._hbat_pointing_direction_r) + + HBAT_pointing_direction_RW = attribute(access=AttrWriteType.READ_WRITE, + dtype=((numpy.str,),), max_dim_x=3, max_dim_y=96, + fget=lambda self: self._hbat_pointing_direction_rw) HBAT_pointing_timestamp_R = attribute(access=AttrWriteType.READ, dtype=(numpy.double,), max_dim_x=96, - fget=lambda self: self._hbat_pointing_timestamp) + fget=lambda self: self._hbat_pointing_timestamp_r) + + HBAT_tracking_enabled_R = attribute(access=AttrWriteType.READ, + doc="Whether the HBAT tile beam is updated periodically", + dtype=numpy.bool, + fget=lambda self: self.HBAT_beam_tracker.is_alive()) + + HBAT_tracking_enabled_RW = attribute(access=AttrWriteType.READ_WRITE, + doc="Whether the HBAT tile beam should be updated periodically", + dtype=numpy.bool, + fget=lambda self: self._hbat_tracking_enabled_rw) # Directory where the casacore measures that we use, reside. We configure ~/.casarc to # use the symlink /opt/IERS/current, which we switch to the actual set of files to use. @@ -64,24 +91,72 @@ class Beam(lofar_device): def configure_for_initialise(self): super().configure_for_initialise() + # Initialise pointing array data and attribute + self._hbat_pointing_timestamp_r = numpy.zeros(96, dtype=numpy.double) + self._hbat_pointing_direction_r = numpy.zeros((96,3), dtype="<U32") + self._hbat_pointing_direction_rw = numpy.array([["AZELGEO","0deg","90deg"]] * 96, dtype="<U32") + + # Initialise tracking control + self._hbat_tracking_enabled_rw = True + # Set a reference of RECV device self.recv_proxy = DeviceProxy("STAT/RECV/1") + self.recv_proxy.set_source(DevSource.DEV) # Retrieve positions from RECV device HBAT_reference_itrf = self.recv_proxy.HBAT_reference_itrf_R - HBAT_antenna_itrf_offsets = self.recv_proxy.HBAT_antenna_itrf_offsets_R + HBAT_antenna_itrf_offsets = self.recv_proxy.HBAT_antenna_itrf_offsets_R.reshape(96,16,3) # a delay calculator for each tile self.HBAT_delay_calculators = [delay_calculator(reference_itrf) for reference_itrf in HBAT_reference_itrf] # absolute positions of each antenna element - self.HBAT_antenna_positions = [reference_itrf + HBAT_antenna_itrf_offsets for reference_itrf in HBAT_reference_itrf] + self.HBAT_antenna_positions = [HBAT_reference_itrf[tile] + HBAT_antenna_itrf_offsets[tile] for tile in range(96)] + + # Create a thread object to update HBAT beam weights + self.HBAT_beam_tracker = BeamTracker(self) + + @log_exceptions() + def configure_for_on(self): + super().configure_for_on() + + # Start beam tracking thread + if self._hbat_tracking_enabled_rw: + self.HBAT_beam_tracker.start() + + @log_exceptions() + def configure_for_off(self): + # Stop thread object + self.HBAT_beam_tracker.stop() + + super().configure_for_off() # -------- # internal functions # -------- - def _HBAT_delays(self, pointing_direction: numpy.array, timestamp: datetime.datetime = None): + def write_HBAT_pointing_direction_RW(self, value): + """ Setter method for attribute HBAT_pointing_direction_RW """ + # verify whether values are valid + for tile in range(96): + if not self.HBAT_delay_calculators[tile].is_valid_direction(value[tile]): + raise ValueError(f"Invalid direction: {value[tile]}") + + self._hbat_pointing_direction_rw = value + + # force update across tiles if pointing changes + self.HBAT_beam_tracker.force_update() + logger.info("Pointing direction update requested") + + def write_HBAT_tracking_enabled_RW(self, value): + self._hbat_tracking_enabled_rw = value + + if value: + self.HBAT_beam_tracker.start() + else: + self.HBAT_beam_tracker.stop() + + def _HBAT_delays(self, pointing_direction: numpy.array, timestamp: datetime.datetime = datetime.datetime.now()): """ Calculate the delays (in seconds) based on the pointing list and the timestamp """ @@ -120,17 +195,18 @@ class Beam(lofar_device): delays = delays.flatten() HBAT_bf_delay_steps = self.recv_proxy.calculate_HBAT_bf_delay_steps(delays) HBAT_bf_delay_steps = numpy.array(HBAT_bf_delay_steps, dtype=numpy.int64).reshape(96,32) - + # Write weights to RECV self.recv_proxy.HBAT_BF_delay_steps_RW = HBAT_bf_delay_steps # Record where we now point to, now that we've updated the weights. # Only the entries within the mask have been updated - mask = self.recv_proxy.Ant_mask_RW.flatten() + mask = self.recv_proxy.ANT_mask_RW.flatten() for rcu in range(96): if mask[rcu]: - self._hbat_pointing_direction[rcu] = pointing_direction[rcu] - self._hbat_pointing_timestamp[rcu] = timestamp.timestamp() + self._hbat_pointing_direction_r[rcu] = pointing_direction[rcu] + self._hbat_pointing_timestamp_r[rcu] = timestamp.timestamp() + logger.info("Pointing direction updated") # -------- # Commands @@ -190,6 +266,7 @@ class Beam(lofar_device): @command(dtype_in=DevVarStringArray) @DebugIt() + @log_exceptions() @only_in_states([DevState.ON]) def HBAT_set_pointing(self, pointing_direction: list, timestamp: datetime.datetime = None): """ @@ -236,3 +313,104 @@ class Beam(lofar_device): def main(**kwargs): """Main function of the ObservationControl module.""" return entry(Beam, **kwargs) + +# ---------- +# Beam Tracker +# ---------- +class BeamTracker(): + + DISCONNECT_TIMEOUT = 3.0 + + """ Object that encapsulates a Thread, resposible for beam tracking operations """ + def __init__(self, device: lofar_device): + self.thread = None + self.device = device + + # Condition to trigger a forced update or early abort + self.update_lock = Lock() + self.update_condition = Condition(self.update_lock) + + # Whether the pointing has to be forced updated + self.stale_pointing = True + + def start(self): + """ Starts the Beam Tracking thread """ + if self.thread: + # already started + return + + self.done = False + self.thread = Thread(target=self._update_HBAT_pointing_direction, name=f"BeamTracker of {self.device.get_name()}") + self.thread.start() + + logger.info("BeamTracking thread started") + + def is_alive(self): + """ Returns True just before the Thread run() method starts until just after the Thread run() method terminates. """ + return self.thread and self.thread.is_alive() + + def force_update(self): + """ Force the pointing to be updated. """ + + self.stale_pointing = True + self.unlock_thread() + + def unlock_thread(self): + # inform the thread to stop waiting + with self.update_lock: + self.update_condition.notify() + + def stop(self): + """ Stops the Beam Tracking loop """ + + if not self.thread: + return + + logger.info("BeamTracking thread stopping") + + self.done = True + self.force_update() + + # wait for thread to finish + self.thread.join(self.DISCONNECT_TIMEOUT) + + if self.is_alive(): + logger.error("BeamTracking Thread did not properly terminate") + + self.thread = None + + logger.info("BeamTracking thread stopped") + + def _get_sleep_time(self): + """ Computes the sleep time (in seconds) that needs to be waited for the next beam tracking update """ + now = datetime.datetime.now().timestamp() + + # Computes the left seconds before the next update + next_update_in = self.device.HBAT_beam_tracking_interval - (now % self.device.HBAT_beam_tracking_interval) + + # Computes the needed sleep time before the next update + sleep_time = next_update_in - self.device.HBAT_beam_tracking_preparation_period + # If sleep time is negative, add the tracking interval for the next update + if sleep_time < 0: + return sleep_time + self.device.HBAT_beam_tracking_interval + else: + return sleep_time + + # @fault_on_error routes errors here. we forward them to our device + def Fault(self, msg): + self.device.Fault(msg) + + @log_exceptions() + @fault_on_error() + def _update_HBAT_pointing_direction(self): + """ Updates the beam weights using a fixed interval of time """ + + # Check if flag beamtracking is true + with self.update_lock: + while not self.done: + self.stale_pointing = False + self.device._HBAT_set_pointing(self.device._hbat_pointing_direction_rw, datetime.datetime.now()) + + # sleep until the next update, or when interrupted (this releases the lock, allowing for notification) + # note that we need wait_for as conditions can be triggered multiple times in succession + self.update_condition.wait_for(lambda: self.done or self.stale_pointing, self._get_sleep_time()) diff --git a/tangostationcontrol/tangostationcontrol/devices/boot.py b/tangostationcontrol/tangostationcontrol/devices/boot.py index 7dc95e7480231237a6c45ba349797a01e2c10056..0bee84c2edacafae8e387a5c2309efe2f35bcd5e 100644 --- a/tangostationcontrol/tangostationcontrol/devices/boot.py +++ b/tangostationcontrol/tangostationcontrol/devices/boot.py @@ -260,10 +260,16 @@ class Boot(lofar_device): super().init_device() # always turn on automatically, so the user doesn't have to boot the boot device - # note: we overloaded our own boot() to boot the station, so explicitly call our own initialisation self.Initialise() self.On() + # Override to avoid having this called during Initialise, which we call + # from init_device. The original clear_poll_cache requires a functioning + # self.proxy, which is not yet the case during init_device. The proxy + # needs a running server_loop, which is started after init_device. + def clear_poll_cache(self): + pass + @log_exceptions() def configure_for_off(self): """ user code here. is called when the state is set to OFF """ diff --git a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py b/tangostationcontrol/tangostationcontrol/devices/lofar_device.py index a53fdfcfd8b61167789a4ed2a05728379d026e61..6943f86d4bde44010662cbcea45cb350ca271cea 100644 --- a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py +++ b/tangostationcontrol/tangostationcontrol/devices/lofar_device.py @@ -62,7 +62,10 @@ class lofar_device(Device, metaclass=DeviceMeta): version_R = attribute(dtype=str, access=AttrWriteType.READ, fget=lambda self: get_version()) - # list of property names too be set first by set_defaults + # list of translator property names to be set by set_translator_defaults + translator_default_settings = [] + + # list of hardware property names to be set first by set_defaults first_default_settings = [] @classmethod @@ -75,6 +78,31 @@ class lofar_device(Device, metaclass=DeviceMeta): self.value_dict = {i: i.initial_value() for i in self.attr_list()} + def is_attribute_wrapper_allowed(self, req_type): + """ Returns whether an attribute wrapped by the attribute_wrapper be accessed. """ + + return self.get_state() in [DevState.STANDBY, DevState.ON, DevState.ALARM] + + def clear_poll_cache(self): + """ Remove all attributes from the poll cache, to remove stale entries + once we know the values can be read. """ + + # we use proxy.attribute_query_list(), as proxy.get_attribute_list() will throw if we + # call it too soon when starting everything (database, device) from scratch. + attr_names = [config.name for config in self.proxy.attribute_list_query()] + + for attr_name in attr_names: + if self.proxy.is_attribute_polled(attr_name): + # save poll period + poll_period = self.proxy.get_attribute_poll_period(attr_name) + + try: + # stop polling to remove cache entry + self.proxy.stop_poll_attribute(attr_name) + finally: + # start polling again + self.proxy.poll_attribute(attr_name, poll_period) + @log_exceptions() def init_device(self): """ Instantiates the device in the OFF state. """ @@ -131,10 +159,12 @@ class lofar_device(Device, metaclass=DeviceMeta): # reload our class & device properties from the Tango database self.get_device_properties() + self.properties_changed() self.configure_for_initialise() - self.properties_changed() + # any values read so far are stale. clear the polling cache. + self.clear_poll_cache() self.set_state(DevState.STANDBY) self.set_status("Device is in the STANDBY state.") @@ -224,6 +254,27 @@ class lofar_device(Device, metaclass=DeviceMeta): """Method always executed before any TANGO command is executed.""" pass + def _set_defaults(self, attribute_names: list): + """ Set hardware points to their default value. + + attribute_names: The names of the attributes to set to their default value. + + A hardware point XXX is set to the value of the object member named XXX_default, if it exists. + XXX_default can be f.e. a constant, or a device_property. + """ + + # set them all + for name in attribute_names: + try: + default_value = getattr(self, f"{name}_default") + + # set the attribute to the configured default + logger.debug(f"Setting attribute {name} to {default_value}") + self.proxy.write_attribute(name, default_value) + except Exception as e: + # log which attribute we're addressing + raise Exception(f"Cannot assign default to attribute {name}") from e + def properties_changed(self): pass @@ -240,7 +291,7 @@ class lofar_device(Device, metaclass=DeviceMeta): The points are set in the following order: 1) The python class property 'first_default_settings' is read, as an array of strings denoting property names. Each property is set in that order. - 2) Any remaining default properties are set. + 2) Any remaining default properties are set, except the translators (those in 'translator_default_settings'). """ # collect all attributes for which defaults are provided @@ -248,22 +299,33 @@ class lofar_device(Device, metaclass=DeviceMeta): # collect all attribute members if isinstance(getattr(self, name), Attribute) # with a default set - and hasattr(self, f"{name}_default")] + and hasattr(self, f"{name}_default") + and name not in self.translator_default_settings] # determine the order: first do the ones mentioned in default_settings_order attributes_to_set = self.first_default_settings + [name for name in attributes_with_defaults if name not in self.first_default_settings] - # set them all - for name in attributes_to_set: - try: - default_value = getattr(self, f"{name}_default") + # set them + self._set_defaults(attributes_to_set) - # set the attribute to the configured default - logger.debug(f"Setting attribute {name} to {default_value}") - self.proxy.write_attribute(name, default_value) - except Exception as e: - # log which attribute we're addressing - raise Exception(f"Cannot assign default to attribute {name}") from e + + @only_in_states([DevState.STANDBY, DevState.INIT, DevState.ON]) + @fault_on_error() + @command() + def set_translator_defaults(self): + """ Initialise the translator translators to their configured settings. """ + + # This is just the command version of _set_translator_defaults(). + self._set_translator_defaults() + + @only_in_states([DevState.STANDBY, DevState.INIT, DevState.ON]) + @fault_on_error() + @command() + def prepare_hardware(self): + """ Load firmware required before configuring anything. """ + + # This is just the command version of _prepare_hardware(). + self._prepare_hardware() @only_in_states([DevState.STANDBY, DevState.INIT, DevState.ON]) @fault_on_error() @@ -278,7 +340,12 @@ class lofar_device(Device, metaclass=DeviceMeta): # setup connections self.Initialise() + self.set_translator_defaults() + if initialise_hardware: + # prepare hardware to accept settings + self.prepare_hardware() + # initialise settings self.set_defaults() @@ -288,7 +355,6 @@ class lofar_device(Device, metaclass=DeviceMeta): # make device available self.On() - @only_in_states([DevState.OFF]) @command() def boot(self): @@ -299,6 +365,15 @@ class lofar_device(Device, metaclass=DeviceMeta): def warm_boot(self): self._boot(initialise_hardware=False) + def _set_translator_defaults(self): + """ Initialise any translators to their default settings. """ + + self._set_defaults(self.translator_default_settings) + + def _prepare_hardware(self): + """ Override this method to load any firmware before configuring the hardware. """ + pass + def _initialise_hardware(self): """ Override this method to initialise any hardware after configuring it. """ pass @@ -309,13 +384,21 @@ class lofar_device(Device, metaclass=DeviceMeta): Raises an Exception if it has not after the timeout. + value: The value that needs to be matched, or a function + that needs to evaluate to True given the attribute. timeout: time until an Exception is raised, in seconds. pollperiod: how often to check the attribute, in seconds. """ + if type(value) == type(lambda x: True): + # evaluate function + is_correct = value + else: + # compare to value + is_correct = lambda x: x == value # Poll every half a second for _ in range(math.ceil(timeout/pollperiod)): - if getattr(self.proxy, attr_name) != value: + if is_correct(getattr(self.proxy, attr_name)): return time.sleep(pollperiod) @@ -335,7 +418,7 @@ class lofar_device(Device, metaclass=DeviceMeta): # fetch attribute value as an array value = self.proxy.read_attribute(attr_name).value if is_scalar: - value = numpy.array(value) + value = numpy.array(value) # this stays a scalar in numpy # construct alarm state, in the same shape as the attribute alarm_state = numpy.zeros(value.shape, dtype=bool) @@ -346,9 +429,9 @@ class lofar_device(Device, metaclass=DeviceMeta): if alarms.min_alarm != 'Not specified': alarm_state |= value <= value.dtype.type(alarms.min_alarm) - # return alarm state, as the same type as the attribute + # return alarm state, with the same dimensions as the attribute if is_scalar: - return alarm_state[0] + return alarm_state.item() else: return alarm_state diff --git a/tangostationcontrol/tangostationcontrol/devices/recv.py b/tangostationcontrol/tangostationcontrol/devices/recv.py index f2acb0794d294eece593ad247b5b15b19c0ee4ad..78dbece6bd2be4d56bd877e77b2ba2067c0570c3 100644 --- a/tangostationcontrol/tangostationcontrol/devices/recv.py +++ b/tangostationcontrol/tangostationcontrol/devices/recv.py @@ -17,9 +17,13 @@ from tango import DebugIt from tango.server import command from tango.server import device_property, attribute from tango import AttrWriteType, DevState, DevVarFloatArray + import numpy +from math import pi # Additional import +from tangostationcontrol.beam.hba_tile import HBATAntennaOffsets +from tangostationcontrol.beam.geo import ETRS_to_ITRF from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python from tangostationcontrol.clients.attribute_wrapper import attribute_wrapper @@ -37,6 +41,9 @@ class RECV(opcua_device): # ----------------- # Device Properties # ----------------- + + # ----- Default settings + ANT_mask_RW_default = device_property( dtype='DevVarBooleanArray', mandatory=False, @@ -67,6 +74,13 @@ class RECV(opcua_device): default_value=1 ) + translator_default_settings = [ + 'ANT_mask_RW', + 'RCU_mask_RW' + ] + + # ----- Calibration values + HBAT_bf_delay_step_delays = device_property( dtype="DevVarFloatArray", mandatory=False, @@ -80,40 +94,64 @@ class RECV(opcua_device): 14.9781E-9, 15.5063E-9 ],dtype=numpy.float64) ) - - HBAT_reference_itrf = device_property( + + HBAT_signal_input_delays = device_property( dtype='DevVarFloatArray', - mandatory=False + mandatory=False, + default_value = numpy.zeros((96,32), dtype=numpy.float64) ) - HBAT_antenna_itrf_offsets = device_property( + HBAT_base_antenna_offsets = device_property( + doc="Offsets of the antennas in a HBAT, with respect to its reference center (16x3).", dtype='DevVarFloatArray', - mandatory=False + mandatory=False, + default_value = HBATAntennaOffsets.HBAT1_BASE_ANTENNA_OFFSETS.flatten() ) - HBAT_signal_input_delays = device_property( + # ----- Position information + + HBAT_reference_ITRF = device_property( + doc="ITRF position (XYZ) of each HBAT (leave empty to auto-derive from ETRS)", dtype='DevVarFloatArray', - mandatory=False, - default_value = numpy.zeros((96,32), dtype=numpy.float64) + mandatory=False + ) + + HBAT_reference_ETRS = device_property( + doc="ETRS position (XYZ) of each HBAT", + dtype='DevVarFloatArray', + mandatory=False ) ITRF_Reference_Frame = device_property( + doc="Reference frame in which the ITRF coordinates are provided, or are to be computed from ETRS89", dtype='DevString', mandatory=False, default_value = "ITRF2005" ) ITRF_Reference_Epoch = device_property( + doc="Reference epoch in which the ITRF coordinates are provided, or are to be extrapolated from ETRS89", dtype='DevFloat', mandatory=False, default_value = 2015.5 ) - first_default_settings = [ - # set the masks first, as those filter any subsequent settings - 'ANT_mask_RW', - 'RCU_mask_RW' - ] + HBAT_PQR_rotation_angles_deg = device_property( + doc='Rotation of each tile in the PQ plane ("horizontal") in degrees.', + dtype='DevVarFloatArray', + mandatory=False, + default_value = [0.0] * 96 + ) + + HBAT_PQR_to_ETRS_rotation_matrix = device_property( + doc="Field-specific rotation matrix to convert PQR offsets to ETRS/ITRF offsets.", + dtype='DevVarFloatArray', + mandatory=False, + default_value = numpy.array([ # PQR->ETRS rotation matrix for the core stations + [-0.1195951054, -0.7919544517, 0.5987530018], + [ 0.9928227484, -0.0954186800, 0.0720990002], + [ 0.0000330969, 0.6030782884, 0.7976820024]]).flatten() + ) # ---------- # Attributes @@ -162,13 +200,38 @@ class RECV(opcua_device): RECVTR_monitor_rate_RW = attribute_wrapper(comms_annotation=["RECVTR_monitor_rate_RW" ],datatype=numpy.int64 , access=AttrWriteType.READ_WRITE) RECVTR_translator_busy_R = attribute_wrapper(comms_annotation=["RECVTR_translator_busy_R" ],datatype=numpy.bool_ ) - HBAT_antenna_itrf_offsets_R = attribute(access=AttrWriteType.READ, - dtype=((numpy.float,),), max_dim_x=3, max_dim_y=16, - fget=lambda self: numpy.array(self.HBAT_antenna_itrf_offsets).reshape(16,3)) + # ----- Position information + + HBAT_antenna_ITRF_offsets_R = attribute(access=AttrWriteType.READ, + doc='Offsets of the antennas within a tile, in ITRF ("iHBADeltas"). True shape: 96x16x3.', + dtype=((numpy.float,),), max_dim_x=48, max_dim_y=96) + + HBAT_reference_ITRF_R = attribute(access=AttrWriteType.READ, + doc='Absolute reference position of each tile, in ITRF', + dtype=((numpy.float,),), max_dim_x=3, max_dim_y=96) + + def read_HBAT_antenna_ITRF_offsets_R(self): + base_antenna_offsets = numpy.array(self.HBAT_base_antenna_offsets).reshape(16,3) + PQR_to_ETRS_rotation_matrix = numpy.array(self.HBAT_PQR_to_ETRS_rotation_matrix).reshape(3,3) - HBAT_reference_itrf_R = attribute(access=AttrWriteType.READ, - dtype=((numpy.float,),), max_dim_x=3, max_dim_y=96, - fget=lambda self: numpy.array(self.HBAT_reference_itrf).reshape(96,3)) + # each tile has its own rotation angle, resulting in different offsets per tile + all_offsets = numpy.array( + [HBATAntennaOffsets.ITRF_offsets( + base_antenna_offsets, + self.HBAT_PQR_rotation_angles_deg[tile] * pi / 180, + PQR_to_ETRS_rotation_matrix) + for tile in range(96)]) + + return all_offsets.reshape(96,48) + + def read_HBAT_reference_ITRF_R(self): + # provide ITRF coordinates if they were configured + if self.HBAT_reference_ITRF: + return numpy.array(self.HBAT_reference_ITRF).reshape(96,3) + + # calculate them from ETRS coordinates if not, using the configured ITRF reference + ETRS_coordinates = numpy.array(self.HBAT_reference_ETRS).reshape(96,3) + return ETRS_to_ITRF(ETRS_coordinates, self.ITRF_Reference_Frame, self.ITRF_Reference_Epoch) # ---------- # Summarising Attributes @@ -197,9 +260,9 @@ class RECV(opcua_device): RECV_VOUT_error_R = attribute(dtype=(bool,), max_dim_x=32) def read_RECV_IOUT_error_R(self): - return self.proxy.ANT_mask_RW & ( + return (self.proxy.ANT_mask_RW & ( self.alarm_val("RCU_PWR_ANT_IOUT_R") - ).any(axis=1) + )).any(axis=1) def read_RECV_TEMP_error_R(self): # Don't apply the mask here --- we always want to know if things get too hot! @@ -211,13 +274,13 @@ class RECV(opcua_device): return (self.proxy.ANT_mask_RW & ( self.alarm_val("RCU_PWR_ANT_VIN_R") | self.alarm_val("RCU_PWR_ANT_VOUT_R") - ).any(axis=1) | (self.proxy.RCU_mask_RW & ( + )).any(axis=1) | (self.proxy.RCU_mask_RW & ( self.alarm_val("RCU_PWR_1V8_R") | self.alarm_val("RCU_PWR_2V5_R") | self.alarm_val("RCU_PWR_3V3_R") | ~self.proxy.RCU_PWR_DIGITAL_on_R | ~self.proxy.RCU_PWR_good_R - ))) + )) # -------- # overloaded functions diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py index 93e9077f07580f79a3f06e89d81a4e5fe42ae7c8..a483aec43eacf571a6344ee1b1823cf62af96a92 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py @@ -89,8 +89,7 @@ class SDP(opcua_device): default_value=[[8192] * 12 * 512] * 16 ) - first_default_settings = [ - # set the masks first, as those filter any subsequent settings + translator_default_settings = [ 'TR_fpga_mask_RW' ] @@ -109,8 +108,8 @@ class SDP(opcua_device): FPGA_beamlet_output_scale_R = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_scale_R"], datatype=numpy.uint32, dims=(16,)) FPGA_beamlet_output_scale_RW = attribute_wrapper(comms_annotation=["FPGA_beamlet_output_scale_RW"], datatype=numpy.uint32, dims=(16,), access=AttrWriteType.READ_WRITE) FPGA_firmware_version_R = attribute_wrapper(comms_annotation=["FPGA_firmware_version_R"], datatype=numpy.str, dims=(16,)) - FPGA_reboot_R = attribute_wrapper(comms_annotation=["FPGA_reboot_R"], datatype=numpy.uint32, dims=(16,), doc="Active FPGA image (0=factory, 1=user)") - FPGA_reboot_RW = attribute_wrapper(comms_annotation=["FPGA_reboot_R"], datatype=numpy.uint32, dims=(16,), access=AttrWriteType.READ_WRITE) + FPGA_boot_image_R = attribute_wrapper(comms_annotation=["FPGA_boot_image_R"], datatype=numpy.uint32, dims=(16,), doc="Active FPGA image (0=factory, 1=user)") + FPGA_boot_image_RW = attribute_wrapper(comms_annotation=["FPGA_boot_image_RW"], datatype=numpy.uint32, dims=(16,), access=AttrWriteType.READ_WRITE) FPGA_global_node_index_R = attribute_wrapper(comms_annotation=["FPGA_global_node_index_R"], datatype=numpy.uint32, dims=(16,)) FPGA_hardware_version_R = attribute_wrapper(comms_annotation=["FPGA_hardware_version_R"], datatype=numpy.str, dims=(16,)) FPGA_processing_enable_R = attribute_wrapper(comms_annotation=["FPGA_processing_enable_R"], datatype=numpy.bool_, dims=(16,)) @@ -130,8 +129,6 @@ class SDP(opcua_device): FPGA_subband_weights_R = attribute_wrapper(comms_annotation=["FPGA_subband_weights_R"], datatype=numpy.uint32, dims=(12 * 512, 16)) FPGA_subband_weights_RW = attribute_wrapper(comms_annotation=["FPGA_subband_weights_RW"], datatype=numpy.uint32, dims=(12 * 512, 16), access=AttrWriteType.READ_WRITE) FPGA_temp_R = attribute_wrapper(comms_annotation=["FPGA_temp_R"], datatype=numpy.float_, dims=(16,)) - FPGA_weights_R = attribute_wrapper(comms_annotation=["FPGA_weights_R"], datatype=numpy.int16, dims=(12 * 488 * 2, 16)) - FPGA_weights_RW = attribute_wrapper(comms_annotation=["FPGA_weights_RW"], datatype=numpy.int16, dims=(12 * 488 * 2, 16), access=AttrWriteType.READ_WRITE) FPGA_wg_amplitude_R = attribute_wrapper(comms_annotation=["FPGA_wg_amplitude_R"], datatype=numpy.float_, dims=(12, 16)) FPGA_wg_amplitude_RW = attribute_wrapper(comms_annotation=["FPGA_wg_amplitude_RW"], datatype=numpy.float_, dims=(12, 16), access=AttrWriteType.READ_WRITE) FPGA_wg_enable_R = attribute_wrapper(comms_annotation=["FPGA_wg_enable_R"], datatype=numpy.bool_, dims=(12, 16)) @@ -212,25 +209,39 @@ class SDP(opcua_device): # ---------- FPGA_error_R = attribute(dtype=(bool,), max_dim_x=16) FPGA_processing_error_R = attribute(dtype=(bool,), max_dim_x=16) + FPGA_input_error_R = attribute(dtype=(bool,), max_dim_x=16) def read_FPGA_error_R(self): return self.proxy.TR_fpga_mask_RW & ( self.proxy.TR_fpga_communication_error_R - | self.proxy.FPGA_firmware_version_R != "" + | (self.proxy.FPGA_firmware_version_R != "") | (self.proxy.FPGA_jesd204b_csr_dev_syncn_R == 0).any(axis=1) ) def read_FPGA_processing_error_R(self): return self.proxy.TR_fpga_mask_RW & ( ~self.proxy.FPGA_processing_enable_R - | (self.proxy.FPGA_reboot_R == 0) - | ~self.proxy.FPGA_wg_enable_R.any(axis=1) + | (self.proxy.FPGA_boot_image_R == 0) + ) + + def read_FPGA_input_error_R(self): + return self.proxy.TR_fpga_mask_RW & ( + self.proxy.FPGA_wg_enable_R.any(axis=1) + | (self.proxy.FPGA_signal_input_rms_R == 0).any(axis=1) ) # -------- # overloaded functions # -------- + def _prepare_hardware(self): + # FPGAs need the correct firmware loaded + self.FPGA_boot_image_RW = [1] * self.N_pn + + # wait for the firmware to be loaded (ignoring masked out elements) + mask = self.proxy.TR_fpga_mask_RW + self.wait_attribute("FPGA_boot_image_R", lambda attr: ((attr == 1) | ~mask).all(), 10) + # -------- # Commands # -------- diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py index 0e96366bcaa2758921ef4431a82e01ac653d19ad..338c5506458f0f63bf65fd33dd34f4d3e6c4122e 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py @@ -13,7 +13,7 @@ # PyTango imports from tango.server import device_property -from tango import DeviceProxy +from tango import DeviceProxy, DevSource # Additional import import asyncio @@ -126,6 +126,7 @@ class Statistics(opcua_device): # proxy the SDP device in case we need the FPGA mask self.sdp_proxy = DeviceProxy("STAT/SDP/1") + self.sdp_proxy.set_source(DevSource.DEV) async def _connect_statistics(self): # map an access helper class diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics_collector.py b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics_collector.py index 5c00c90b1dff70e5aa281ee84093454c44b4ef96..8c88a3a638eafdd152e0ee39be5db3dfc8e1063f 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics_collector.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics_collector.py @@ -2,6 +2,7 @@ from queue import Queue from threading import Thread import logging import numpy +import datetime from .statistics_packet import SSTPacket, XSTPacket from tangostationcontrol.common.baselines import nr_baselines, baseline_index, baseline_from_index @@ -99,7 +100,21 @@ class SSTCollector(StatisticsCollector): self.parameters["subbands_calibrated"][input_index] = fields.subband_calibrated_flag class XSTCollector(StatisticsCollector): - """ Class to process XST statistics packets. """ + """ Class to process XST statistics packets. + + XSTs are received for up to MAX_PARALLEL_SUBBANDS simultaneously, and only the values of the last + MAX_PARALLEL_SUBBANDS are kept. Raw data are collected for each subband in parameters["xst_blocks"], + and overwritten if newer (younger) data is received for the same subband. As such, the data represent + a rolling view on the XSTs. + + The xst_values() function is a user-friendly way to read the xst_blocks. + + The hardware can be configured to emit different and/or fewer subbands, causing some of the XSTs + to become stale. It is therefor advised to inspect parameters["xst_timestamps"] as well. + """ + + # Maximum number of subbands for which we collect XSTs simultaneously + MAX_PARALLEL_SUBBANDS = 8 # Maximum number of antenna inputs we support (used to determine array sizes) MAX_INPUTS = 192 @@ -130,16 +145,33 @@ class XSTCollector(StatisticsCollector): "nof_payload_errors": numpy.zeros((self.MAX_FPGAS,), dtype=numpy.uint64), # Last value array we've constructed out of the packets - "xst_blocks": numpy.zeros((self.MAX_BLOCKS, self.BLOCK_LENGTH * self.BLOCK_LENGTH * self.VALUES_PER_COMPLEX), dtype=numpy.int64), + "xst_blocks": numpy.zeros((self.MAX_PARALLEL_SUBBANDS, self.MAX_BLOCKS, self.BLOCK_LENGTH * self.BLOCK_LENGTH * self.VALUES_PER_COMPLEX), dtype=numpy.int64), # Whether the values are actually conjugated and transposed - "xst_conjugated": numpy.zeros((self.MAX_BLOCKS,), dtype=numpy.bool_), - "xst_timestamps": numpy.zeros((self.MAX_BLOCKS,), dtype=numpy.float64), - "xst_subbands": numpy.zeros((self.MAX_BLOCKS,), dtype=numpy.uint16), - "integration_intervals": numpy.zeros((self.MAX_BLOCKS,), dtype=numpy.float32), + "xst_conjugated": numpy.zeros((self.MAX_PARALLEL_SUBBANDS, self.MAX_BLOCKS,), dtype=numpy.bool_), + # When the youngest data for each subband was received + "xst_timestamps": numpy.zeros((self.MAX_PARALLEL_SUBBANDS,), dtype=numpy.float64), + "xst_subbands": numpy.zeros((self.MAX_PARALLEL_SUBBANDS,), dtype=numpy.uint16), + "xst_integration_intervals": numpy.zeros((self.MAX_PARALLEL_SUBBANDS,), dtype=numpy.float32), }) return defaults + def select_subband_slot(self, subband): + """ Return which subband slot (0..MAX_PARALLEL_SUBBANDS) to use when confronted with a new subband. + Keep recording the same subband if we're already tracking it, but allocate or replace a slot if not. """ + + indices = numpy.where(self.parameters["xst_subbands"] == subband)[0] + + if len(indices) > 0: + # subband already being recorded, use same spot + return indices[0] + else: + # a new subband, kick out the oldest + oldest_timestamp = self.parameters["xst_timestamps"].min() + + # prefer the first one in case of multiple minima + return numpy.where(self.parameters["xst_timestamps"] == oldest_timestamp)[0][0] + def parse_packet(self, packet): fields = XSTPacket(packet) @@ -172,6 +204,19 @@ class XSTCollector(StatisticsCollector): else: conjugated = False + # we keep track of multiple subbands. select slot for this one + subband_slot = self.select_subband_slot(fields.subband_index) + + assert 0 <= subband_slot < self.MAX_PARALLEL_SUBBANDS, f"Selected slot {subband_slot}, but only have room for {self.MAX_PARALLEL_SUBBANDS}. Existing slots are {self.parameters['xst_subbands']}, processing subband {fields.subband_index}." + + # log if we're replacing a subband we were once recording + previous_subband_in_slot = self.parameters["xst_subbands"][subband_slot] + if previous_subband_in_slot != fields.subband_index: + previous_subband_timestamp = datetime.datetime.fromtimestamp(self.parameters["xst_timestamps"][subband_slot]) + + if previous_subband_timestamp.timestamp() > 0: + logger.info(f"Stopped recording XSTs for subband {previous_subband_in_slot}. Last data for this subband was received at {previous_subband_timestamp}.") + # the payload contains complex values for the block of baselines of size BLOCK_LENGTH x BLOCK_LENGTH # starting at baseline first_baseline. # @@ -185,36 +230,40 @@ class XSTCollector(StatisticsCollector): # process the packet self.parameters["nof_valid_payloads"][fields.gn_index] += numpy.uint64(1) - self.parameters["xst_blocks"][block_index][:fields.nof_statistics_per_packet] = fields.payload - self.parameters["xst_timestamps"][block_index] = numpy.float64(fields.timestamp().timestamp()) - self.parameters["xst_conjugated"][block_index] = conjugated - self.parameters["xst_subbands"][block_index] = numpy.uint16(fields.subband_index) - self.parameters["integration_intervals"][block_index] = fields.integration_interval() + self.parameters["xst_blocks"][subband_slot, block_index, :fields.nof_statistics_per_packet] = fields.payload + self.parameters["xst_timestamps"][subband_slot] = numpy.float64(fields.timestamp().timestamp()) + self.parameters["xst_conjugated"][subband_slot, block_index] = conjugated + self.parameters["xst_subbands"][subband_slot] = numpy.uint16(fields.subband_index) + self.parameters["xst_integration_intervals"][subband_slot] = fields.integration_interval() - def xst_values(self): - """ xst_blocks, but as a matrix[MAX_INPUTS][MAX_INPUTS] of complex values. """ + def xst_values(self, subband_indices=range(MAX_PARALLEL_SUBBANDS)): + """ xst_blocks, but as a matrix[len(subband_indices)][MAX_INPUTS][MAX_INPUTS] of complex values. + + The subband indices must be in [0..MAX_PARALLEL_SUBBANDS). By default, all recorded XSTs are returned. + """ - matrix = numpy.zeros((self.MAX_INPUTS, self.MAX_INPUTS), dtype=numpy.complex64) + matrix = numpy.zeros((len(subband_indices), self.MAX_INPUTS, self.MAX_INPUTS), dtype=numpy.complex64) xst_blocks = self.parameters["xst_blocks"] xst_conjugated = self.parameters["xst_conjugated"] - for block_index in range(self.MAX_BLOCKS): - # convert real/imag int to complex float values. this works as real/imag come in pairs - block = xst_blocks[block_index].astype(numpy.float32).view(numpy.complex64) + for matrix_idx, subband_index in enumerate(subband_indices): + for block_index in range(self.MAX_BLOCKS): + # convert real/imag int to complex float values. this works as real/imag come in pairs + block = xst_blocks[subband_index][block_index].astype(numpy.float32).view(numpy.complex64) - if xst_conjugated[block_index]: - # block is conjugated and transposed. process. - block = block.conjugate().transpose() + if xst_conjugated[subband_index][block_index]: + # block is conjugated and transposed. process. + block = block.conjugate().transpose() - # reshape into [a][b] - block = block.reshape(self.BLOCK_LENGTH, self.BLOCK_LENGTH) + # reshape into [a][b] + block = block.reshape(self.BLOCK_LENGTH, self.BLOCK_LENGTH) - # compute destination in matrix - first_baseline = baseline_from_index(block_index) - first_baseline = (first_baseline[0] * self.BLOCK_LENGTH, first_baseline[1] * self.BLOCK_LENGTH) + # compute destination in matrix + first_baseline = baseline_from_index(block_index) + first_baseline = (first_baseline[0] * self.BLOCK_LENGTH, first_baseline[1] * self.BLOCK_LENGTH) - # copy block into matrix - matrix[first_baseline[0]:first_baseline[0]+self.BLOCK_LENGTH, first_baseline[1]:first_baseline[1]+self.BLOCK_LENGTH] = block + # copy block into matrix + matrix[matrix_idx][first_baseline[0]:first_baseline[0]+self.BLOCK_LENGTH, first_baseline[1]:first_baseline[1]+self.BLOCK_LENGTH] = block return matrix diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py b/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py index 89da8dddcf19dc9195db6bc6c88c61475c61c2f6..8be9cdb483ef4f21818791009a19e8fbc2b91cb9 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py @@ -116,33 +116,86 @@ class XST(Statistics): # number of packets with invalid payloads nof_payload_errors_R = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "nof_payload_errors"}, dims=(XSTCollector.MAX_FPGAS,), datatype=numpy.uint64) # latest XSTs - xst_blocks_R = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_blocks"}, dims=(XSTCollector.BLOCK_LENGTH * XSTCollector.BLOCK_LENGTH * XSTCollector.VALUES_PER_COMPLEX, XSTCollector.MAX_BLOCKS), datatype=numpy.int64) + xst_blocks_R = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_blocks", "reshape": True}, dims=(XSTCollector.MAX_BLOCKS * XSTCollector.BLOCK_LENGTH * XSTCollector.BLOCK_LENGTH * XSTCollector.VALUES_PER_COMPLEX, XSTCollector.MAX_PARALLEL_SUBBANDS), datatype=numpy.int64) # whether the values in the block are conjugated and transposed - xst_conjugated_R = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_conjugated"}, dims=(XSTCollector.MAX_BLOCKS,), datatype=numpy.bool_) - # reported timestamp for each row in the latest XSTs - xst_timestamp_R = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_timestamps"}, dims=(XSTCollector.MAX_BLOCKS,), datatype=numpy.uint64) + xst_conjugated_R = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_conjugated", "reshape": True}, dims=(XSTCollector.MAX_BLOCKS, XSTCollector.MAX_PARALLEL_SUBBANDS), datatype=numpy.bool_) + # reported timestamp for each subband in the latest XSTs + xst_timestamp_R = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_timestamps"}, dims=(XSTCollector.MAX_PARALLEL_SUBBANDS,), datatype=numpy.uint64) # which subband the XSTs describe - xst_subbands_R = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_subbands"}, dims=(XSTCollector.MAX_BLOCKS,), datatype=numpy.uint16) - # integration interval for each row in the latest XSTs - integration_interval_R = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "integration_intervals"}, dims=(XSTCollector.MAX_BLOCKS,), datatype=numpy.float32) + xst_subbands_R = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_subbands"}, dims=(XSTCollector.MAX_PARALLEL_SUBBANDS,), datatype=numpy.uint16) + # integration interval for each subband in the latest XSTs + xst_integration_interval_R = attribute_wrapper(comms_id=StatisticsClient, comms_annotation={"type": "statistics", "parameter": "xst_integration_intervals"}, dims=(XSTCollector.MAX_PARALLEL_SUBBANDS,), datatype=numpy.float32) - # xst_R, but as a matrix of input x input - xst_real_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),)) - xst_imag_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),)) - xst_power_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),)) - xst_phase_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),)) + # xst_R, but as a matrix of subband x (input x input) + xst_real_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_PARALLEL_SUBBANDS, dtype=((numpy.float32,),)) + xst_imag_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_PARALLEL_SUBBANDS, dtype=((numpy.float32,),)) + xst_power_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_PARALLEL_SUBBANDS, dtype=((numpy.float32,),)) + xst_phase_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_PARALLEL_SUBBANDS, dtype=((numpy.float32,),)) def read_xst_real_R(self): - return numpy.real(self.statistics_client.collector.xst_values()) + return numpy.real(self.statistics_client.collector.xst_values()).reshape(XSTCollector.MAX_PARALLEL_SUBBANDS, XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS) def read_xst_imag_R(self): - return numpy.imag(self.statistics_client.collector.xst_values()) + return numpy.imag(self.statistics_client.collector.xst_values()).reshape(XSTCollector.MAX_PARALLEL_SUBBANDS, XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS) def read_xst_power_R(self): - return numpy.abs(self.statistics_client.collector.xst_values()) + return numpy.abs(self.statistics_client.collector.xst_values()).reshape(XSTCollector.MAX_PARALLEL_SUBBANDS, XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS) def read_xst_phase_R(self): - return numpy.angle(self.statistics_client.collector.xst_values()) + return numpy.angle(self.statistics_client.collector.xst_values()).reshape(XSTCollector.MAX_PARALLEL_SUBBANDS, XSTCollector.MAX_INPUTS * XSTCollector.MAX_INPUTS) + + # xst_R, but as a matrix of input x input, for each specific subband index + xst_0_real_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(0)) + xst_0_imag_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(0)) + xst_0_power_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(0)) + xst_0_phase_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(0)) + + xst_1_real_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(1)) + xst_1_imag_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(1)) + xst_1_power_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(1)) + xst_1_phase_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(1)) + + xst_2_real_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(2)) + xst_2_imag_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(2)) + xst_2_power_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(2)) + xst_2_phase_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(2)) + + xst_3_real_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(3)) + xst_3_imag_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(3)) + xst_3_power_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(3)) + xst_3_phase_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(3)) + + xst_4_real_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(4)) + xst_4_imag_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(4)) + xst_4_power_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(4)) + xst_4_phase_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(4)) + + xst_5_real_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(5)) + xst_5_imag_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(5)) + xst_5_power_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(5)) + xst_5_phase_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(5)) + + xst_6_real_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(6)) + xst_6_imag_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(6)) + xst_6_power_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(6)) + xst_6_phase_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(6)) + + xst_7_real_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_real_R(7)) + xst_7_imag_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_imag_R(7)) + xst_7_power_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_power_R(7)) + xst_7_phase_R = attribute(max_dim_x=XSTCollector.MAX_INPUTS, max_dim_y=XSTCollector.MAX_INPUTS, dtype=((numpy.float32,),), fget = lambda self: self.read_xst_N_phase_R(7)) + + def read_xst_N_real_R(self, subband_idx): + return numpy.real(self.statistics_client.collector.xst_values([subband_idx])[0]) + + def read_xst_N_imag_R(self, subband_idx): + return numpy.imag(self.statistics_client.collector.xst_values([subband_idx])[0]) + + def read_xst_N_power_R(self, subband_idx): + return numpy.abs(self.statistics_client.collector.xst_values([subband_idx])[0]) + + def read_xst_N_phase_R(self, subband_idx): + return numpy.angle(self.statistics_client.collector.xst_values([subband_idx])[0]) # ---------- # Summarising Attributes diff --git a/tangostationcontrol/tangostationcontrol/devices/unb2.py b/tangostationcontrol/tangostationcontrol/devices/unb2.py index a49b120bc6f3502b7d836b86ca68e8ec0b2df9b8..0b2d5c48256405a83b6de67e6e39103f76d044bd 100644 --- a/tangostationcontrol/tangostationcontrol/devices/unb2.py +++ b/tangostationcontrol/tangostationcontrol/devices/unb2.py @@ -59,8 +59,7 @@ class UNB2(opcua_device): default_value=[True] * 2 ) - first_default_settings = [ - # set the masks first, as those filter any subsequent settings + translator_default_settings = [ 'UNB2_mask_RW' ] diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py index db9f51eb7b82d081c5b83ebe04dc998a8e5f1506..7a2609e811432a8c772a239186e16f2f442ba7a3 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py @@ -61,9 +61,30 @@ class AbstractTestBases: self.self.assertListEqual([], self.proxy.opcua_missing_attributes_R) def test_device_on(self): - """Test if we can transition to on""" + """Test if we can transition off -> standby -> on""" self.proxy.initialise() self.proxy.on() self.assertEqual(DevState.ON, self.proxy.state()) + + def test_device_warm_boot(self): + """Test if we can transition off -> on using a warm boot""" + + self.proxy.warm_boot() + + self.assertEqual(DevState.ON, self.proxy.state()) + + def test_device_read_all_attributes(self): + """Test if we can read all of the exposed attributes in the ON state. + + This test covers the reading logic of all attributes. """ + + self.proxy.initialise() + self.proxy.on() + + for attribute_name in self.proxy.get_attribute_list(): + try: + _ = self.proxy.read_attribute(attribute_name).value + except Exception as e: + raise AssertionError(f"Could not read attribute {attribute_name} from device {self.name}") from e diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beam.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beam.py index 21c93eab4f434350fa091523a421837513aadf9c..381678118b123793767aff753e4461b7916fe5bd 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beam.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_beam.py @@ -12,7 +12,6 @@ import numpy import datetime import json -from tango import DevState from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy from .base import AbstractTestBases @@ -36,11 +35,8 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase): # setup RECV recv_proxy = TestDeviceProxy("STAT/RECV/1") recv_proxy.off() - recv_proxy.initialise() - self.assertEqual(DevState.STANDBY, recv_proxy.state()) + recv_proxy.warm_boot() recv_proxy.set_defaults() - recv_proxy.on() - self.assertEqual(DevState.ON, recv_proxy.state()) return recv_proxy def test_HBAT_delays_dims(self): @@ -48,12 +44,7 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase): self.setup_recv_proxy() # setup BEAM - self.proxy.init() - self.proxy.Initialise() - self.assertEqual(DevState.STANDBY, self.proxy.state()) - self.proxy.set_defaults() - self.proxy.on() - self.assertEqual(DevState.ON, self.proxy.state()) + self.proxy.warm_boot() # verify HBAT_delays method returns the correct dimensions HBAT_delays = self.proxy.HBAT_delays(self.pointing_direction) @@ -64,12 +55,8 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase): recv_proxy = self.setup_recv_proxy() # setup BEAM - self.proxy.init() - self.proxy.Initialise() - self.assertEqual(DevState.STANDBY, self.proxy.state()) - self.proxy.set_defaults() - self.proxy.on() - self.assertEqual(DevState.ON, self.proxy.state()) + self.proxy.warm_boot() + self.proxy.HBAT_tracking_enabled_RW = False # Verify attribute is present (all zeros if never used before) HBAT_delays_r1 = numpy.array(recv_proxy.read_attribute('HBAT_BF_delay_steps_RW').value) @@ -89,8 +76,8 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase): # setup RECV as well recv_proxy = self.setup_recv_proxy() - self.proxy.initialise() - self.proxy.on() + self.proxy.warm_boot() + self.proxy.HBAT_tracking_enabled_RW = False # Point to Zenith self.proxy.HBAT_set_pointing(numpy.array([["AZELGEO","0deg","90deg"]] * 96).flatten()) @@ -101,12 +88,39 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase): numpy.testing.assert_equal(calculated_HBAT_delay_steps, expected_HBAT_delay_steps) + def test_pointing_across_horizon(self): + # setup RECV as well + recv_proxy = self.setup_recv_proxy() + + self.proxy.warm_boot() + self.proxy.HBAT_tracking_enabled_RW = False + + # point at north on the horizon + self.proxy.HBAT_set_pointing(["AZELGEO","0deg","0deg"] * 96) + + # obtain delays of the X polarisation of all the elements of the first tile + north_beam_delay_steps = recv_proxy.HBAT_BF_delay_steps_RW[0].reshape(2,4,4)[0] + + # delays must differ under rotation, or our test will give a false positive + self.assertNotEqual(north_beam_delay_steps.tolist(), numpy.rot90(north_beam_delay_steps).tolist()) + + for angle in (90,180,270): + # point at angle degrees (90=E, 180=S, 270=W) + self.proxy.HBAT_set_pointing(["AZELGEO",f"{angle}deg","0deg"] * 96) + + # obtain delays of the X polarisation of all the elements of the first tile + angled_beam_delay_steps = recv_proxy.HBAT_BF_delay_steps_RW[0].reshape(2,4,4)[0] + + expected_delay_steps = numpy.rot90(north_beam_delay_steps, k=-(angle/90)) + + self.assertListEqual(expected_delay_steps.tolist(), angled_beam_delay_steps.tolist(), msg=f"angle={angle}") + def test_delays_same_as_LOFAR_ref_pointing(self): # setup RECV as well recv_proxy = self.setup_recv_proxy() - self.proxy.initialise() - self.proxy.on() + self.proxy.warm_boot() + self.proxy.HBAT_tracking_enabled_RW = False # Point to LOFAR 1 ref pointing (0.929342, 0.952579, J2000) pointings = numpy.array([["J2000", "0.929342rad", "0.952579rad"]] * 96).flatten() @@ -131,3 +145,20 @@ class TestDeviceBeam(AbstractTestBases.TestDeviceBase): expected_HBAT_delay_steps = numpy.array([24, 25, 27, 29, 17, 18, 20, 21, 10, 11, 13, 14, 3, 4, 5, 7] * 2, dtype=numpy.int64) numpy.testing.assert_equal(calculated_HBAT_delay_steps[0], expected_HBAT_delay_steps) numpy.testing.assert_equal(calculated_HBAT_delay_steps[48], expected_HBAT_delay_steps) + + def test_beam_tracking(self): + # setup RECV as well + recv_proxy = self.setup_recv_proxy() + + self.proxy.warm_boot() + + # check if we're really tracking + self.assertTrue(self.proxy.HBAT_tracking_enabled_R) + + # point somewhere + new_pointings = [("J2000",f"{tile}deg","0deg") for tile in range(96)] + self.proxy.HBAT_pointing_direction_RW = new_pointings + + # check pointing + self.assertListEqual(new_pointings, list(self.proxy.HBAT_pointing_direction_R)) + diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sdp.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sdp.py index 3a075e697c473c9800257b4796f21f7b79e5e430..570d8ce6cee1bc2f1ab833c9d93acdd11cc270f7 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sdp.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sdp.py @@ -19,7 +19,6 @@ class TestDeviceSDP(AbstractTestBases.TestDeviceBase): def test_device_sdp_read_attribute(self): """Test if we can read an attribute obtained over OPC-UA""" - self.proxy.initialise() - self.proxy.on() + self.proxy.warm_boot() self.assertListEqual([True]*16, list(self.proxy.TR_fpga_communication_error_R)) diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sst.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sst.py index 60675e121364b52fb692b2f8461001bfdc78b50a..2398206083533a5fcc40051b6a80e42923a38347 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sst.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_sst.py @@ -12,6 +12,7 @@ import sys import time from tango._tango import DevState +from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy from .base import AbstractTestBases @@ -22,14 +23,24 @@ class TestDeviceSST(AbstractTestBases.TestDeviceBase): """Intentionally recreate the device object in each test""" super().setUp("STAT/SST/1") + def test_device_read_all_attributes(self): + # We need to connect to SDP first to read some of our attributes + self.sdp_proxy = self.setup_sdp() + + super().test_device_read_all_attributes() + + def setup_sdp(self): + # setup SDP, on which this device depends + sdp_proxy = TestDeviceProxy("STAT/SDP/1") + sdp_proxy.off() + sdp_proxy.warm_boot() + sdp_proxy.set_defaults() + return sdp_proxy + def test_device_sst_send_udp(self): port_property = {"Statistics_Client_TCP_Port": "4998"} self.proxy.put_property(port_property) - self.proxy.initialise() - - self.assertEqual(DevState.STANDBY, self.proxy.state()) - - self.proxy.on() + self.proxy.warm_boot() self.assertEqual(DevState.ON, self.proxy.state()) @@ -44,13 +55,7 @@ class TestDeviceSST(AbstractTestBases.TestDeviceBase): def test_device_sst_connect_tcp_receive(self): port_property = {"Statistics_Client_TCP_Port": "5101"} self.proxy.put_property(port_property) - self.proxy.initialise() - - self.assertEqual(DevState.STANDBY, self.proxy.state()) - - self.proxy.on() - - self.assertEqual(DevState.ON, self.proxy.state()) + self.proxy.warm_boot() time.sleep(2) diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_xst.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_xst.py new file mode 100644 index 0000000000000000000000000000000000000000..071108ee57e025c30fc423d7ce2312b66d5012a7 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_xst.py @@ -0,0 +1,32 @@ + +# -*- coding: utf-8 -*- +# +# This file is part of the LOFAR 2.0 Station Software +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. +from .base import AbstractTestBases + +from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy + +class TestDeviceSST(AbstractTestBases.TestDeviceBase): + + def setUp(self): + """Intentionally recreate the device object in each test""" + super().setUp("STAT/XST/1") + + def test_device_read_all_attributes(self): + # We need to connect to SDP first to read some of our attributes + self.sdp_proxy = self.setup_sdp() + + super().test_device_read_all_attributes() + + def setup_sdp(self): + # setup SDP, on which this device depends + sdp_proxy = TestDeviceProxy("STAT/SDP/1") + sdp_proxy.off() + sdp_proxy.warm_boot() + sdp_proxy.set_defaults() + return sdp_proxy diff --git a/tangostationcontrol/tangostationcontrol/integration_test/device_proxy.py b/tangostationcontrol/tangostationcontrol/integration_test/device_proxy.py index 25c92411ecaababad20007868cfd19bdc3e9e18a..93f84dc98a789c2e0fa5451529728dbed2e73f79 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/device_proxy.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/device_proxy.py @@ -1,7 +1,7 @@ import logging import time -from tango import DeviceProxy +from tango import DeviceProxy, DevSource logger = logging.getLogger() @@ -10,8 +10,18 @@ class TestDeviceProxy(DeviceProxy): def __init__(self, *args, **kwargs): super(TestDeviceProxy, self).__init__(*args, **kwargs) + + # extend timeout for running commands self.set_timeout_millis(10000) + # When requesting attribute values and states, + # always get values from the device, never the polling + # cache. This makes sure updates are immediately reflected + # in this proxy, which is essential for fast-paced tests. + # + # See also https://www.tango-controls.org/community/forum/c/development/python/attribute-direct-reading-from-device-when-polling-is-turned-on/ + self.set_source(DevSource.DEV) + @staticmethod def test_device_turn_off(endpoint): d = TestDeviceProxy(endpoint) diff --git a/tangostationcontrol/tangostationcontrol/integration_test/devices/test_lofar_device.py b/tangostationcontrol/tangostationcontrol/integration_test/devices/test_lofar_device.py new file mode 100644 index 0000000000000000000000000000000000000000..d5a935eb537a9031ee67b2252ba1b1cfd1eb6687 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/integration_test/devices/test_lofar_device.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the LOFAR 2.0 Station Software +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +import time +from tango import DevState +from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy + +from tangostationcontrol.integration_test import base + +class TestProxyAttributeAccess(base.IntegrationTestCase): + """ Test whether DeviceProxy's can always access attributes immediately after turning them on. """ + + # We use RECV as our victim. Any device would do. + device_name = "STAT/RECV/1" + + # an attribute to access + attribute_name = "ANT_mask_RW" + + def setUp(self): + self.proxy = TestDeviceProxy(self.device_name) + self.proxy.off() + + def poll_attribute(self): + # make sure the attribute is polled, so we force proxies to access the poll first + if self.proxy.is_attribute_polled(self.attribute_name): + self.proxy.stop_poll_attribute(self.attribute_name) + self.proxy.poll_attribute(self.attribute_name, 1000) + + def dont_poll_attribute(self): + # make sure the attribute is NOT polled, so we force proxies to access the device + if self.proxy.is_attribute_polled(self.attribute_name): + self.proxy.stop_poll_attribute(self.attribute_name) + + def read_attribute(self): + # turn on the device + self.proxy.initialise() + self.assertEqual(DevState.STANDBY, self.proxy.state()) + + # read the attribute -- shouldn't throw + _ = self.proxy.read_attribute(self.attribute_name) + + def test_fast_setup_polled_attribute(self): + """ Setup a device as fast as possible and access its attributes immediately. """ + + self.poll_attribute() + self.read_attribute() + + + def test_slow_setup_polled_attribute(self): + """ Have the device be off for a while, allowing Tango to poll. Then, + Setup a device as fast as possible and access its attributes immediately. """ + + self.poll_attribute() + time.sleep(3) # allow Tango to poll the attribute in OFF state + self.read_attribute() + + + def test_fast_setup_nonpolled_attribute(self): + """ Setup a device as fast as possible and access its attributes immediately. """ + self.dont_poll_attribute() + self.read_attribute() + + + def test_slow_setup_nonpolled_attribute(self): + """ Have the device be off for a while, allowing Tango to poll. Then, + Setup a device as fast as possible and access its attributes immediately. """ + self.dont_poll_attribute() + time.sleep(3) # allow Tango to poll the attribute in OFF state + self.read_attribute() + diff --git a/tangostationcontrol/tangostationcontrol/statistics_writer/hdf5_writer.py b/tangostationcontrol/tangostationcontrol/statistics_writer/hdf5_writer.py index 30710a871e157909aa9e4d42169ac686fc23e889..d2bdbad1c02eae894ec100b63477a92ce1e84994 100644 --- a/tangostationcontrol/tangostationcontrol/statistics_writer/hdf5_writer.py +++ b/tangostationcontrol/tangostationcontrol/statistics_writer/hdf5_writer.py @@ -7,6 +7,7 @@ import h5py import numpy import logging +from abc import ABC, abstractmethod # import statistics classes with workaround import sys @@ -17,9 +18,10 @@ import tangostationcontrol.devices.sdp.statistics_collector as statistics_collec logger = logging.getLogger("statistics_writer") -__all__ = ["hdf5_writer"] +__all__ = ["hdf5_writer", "parallel_xst_hdf5_writer", "xst_hdf5_writer", "sst_hdf5_writer"] -class hdf5_writer: + +class hdf5_writer(ABC): SST_MODE = "SST" XST_MODE = "XST" @@ -39,18 +41,22 @@ class hdf5_writer: self.statistics_header = None # file handing - self.file_location = file_location + self.file_location = file_location or '.' self.decimation_factor = decimation_factor self.new_file_time_interval = timedelta(seconds=new_file_time_interval) self.last_file_time = datetime.min.replace(tzinfo=pytz.UTC) self.file = None # parameters that are configured depending on the mode the statistics writer is in (SST,XST,BST) - self.decoder = None - self.collector = None - self.store_function = None self.mode = statistics_mode.upper() - self.config_mode() + + @abstractmethod + def decoder(self): + pass + + @abstractmethod + def new_collector(self): + pass def next_packet(self, packet): """ @@ -123,7 +129,7 @@ class hdf5_writer: self.start_new_hdf5(timestamp) # create a new and empty current_matrix - self.current_matrix = self.collector() + self.current_matrix = self.new_collector() self.statistics_header = None def write_matrix(self): @@ -136,7 +142,7 @@ class hdf5_writer: current_group = self.file.create_group("{}_{}".format(self.mode, self.current_timestamp.isoformat(timespec="milliseconds"))) # store the statistics values for the current group - self.store_function(current_group) + self.write_values_matrix(current_group) # might be optional, but they're easy to add. current_group.create_dataset(name="nof_payload_errors", data=self.current_matrix.parameters["nof_payload_errors"]) @@ -145,6 +151,10 @@ class hdf5_writer: # get the statistics header header = self.statistics_header + if not header: + # edge case: no valid packet received at all + return + # can't store datetime objects, convert to string instead header["timestamp"] = header["timestamp"].isoformat(timespec="milliseconds") @@ -156,17 +166,13 @@ class hdf5_writer: else: current_group.attrs[k] = v - def write_sst_matrix(self, current_group): - # store the SST values - current_group.create_dataset(name="values", data=self.current_matrix.parameters["sst_values"].astype(numpy.float32), compression="gzip") - - def write_xst_matrix(self, current_group): - # requires a function call to transform the xst_blocks in to the right structure - current_group.create_dataset(name="values", data=self.current_matrix.xst_values().astype(numpy.cfloat), compression="gzip") - - def write_bst_matrix(self, current_group): - raise NotImplementedError("BST values not implemented") + @abstractmethod + def write_values_matrix(self, current_group): + pass + def next_filename(self, timestamp, suffix=".h5"): + time_str = str(timestamp.strftime("%Y-%m-%d-%H-%M-%S")) + return f"{self.file_location}/{self.mode}_{time_str}{suffix}" def process_packet(self, packet): """ @@ -186,44 +192,17 @@ class hdf5_writer: except Exception as e: logger.exception(f"Error while attempting to close hdf5 file to disk. file {self.file} likely empty, please verify integrity.") - current_time = str(timestamp.strftime("%Y-%m-%d-%H-%M-%S")) - logger.info(f"creating new file: {self.file_location}/{self.mode}_{current_time}.h5") + filename = self.next_filename(timestamp) + logger.info(f"creating new file: {filename}") try: - self.file = h5py.File(f"{self.file_location}/{self.mode}_{current_time}.h5", 'w') + self.file = h5py.File(filename, 'w') except Exception as e: logger.exception(f"Error while creating new file") raise e self.last_file_time = timestamp - def config_mode(self): - logger.debug(f"attempting to configure {self.mode} mode") - - """ - Configures the object for the correct statistics type to be used. - decoder: the class to decode a single packet - collector: the class to collect statistics packets - store_function: the function to write the mode specific data to file - """ - - if self.mode == self.SST_MODE: - self.decoder = SSTPacket - self.collector = statistics_collector.SSTCollector - self.store_function = self.write_sst_matrix - - elif self.mode == self.XST_MODE: - self.decoder = XSTPacket - self.collector = statistics_collector.XSTCollector - self.store_function = self.write_xst_matrix - - elif self.mode == self.BST_MODE: - self.store_function = self.write_bst_matrix - raise NotImplementedError("BST collector has not yet been implemented") - - else: - raise ValueError("invalid statistics mode specified '{}', please use 'SST', 'XST' or 'BST' ".format(self.mode)) - def close_writer(self): """ Function that can be used to stop the writer without data loss. @@ -240,3 +219,79 @@ class hdf5_writer: self.file.close() logger.debug(f"{filename} closed") logger.debug(f"Received a total of {self.statistics_counter} statistics while running. With {int(self.statistics_counter/self.decimation_factor)} written to disk ") + + +class sst_hdf5_writer(hdf5_writer): + def __init__(self, new_file_time_interval, file_location, decimation_factor): + super().__init__(new_file_time_interval, file_location, hdf5_writer.SST_MODE, decimation_factor) + + def decoder(self, packet): + return SSTPacket(packet) + + def new_collector(self): + return statistics_collector.SSTCollector() + + def write_values_matrix(self, current_group): + # store the SST values + current_group.create_dataset(name="values", data=self.current_matrix.parameters["sst_values"].astype(numpy.float32), compression="gzip") + + +class xst_hdf5_writer(hdf5_writer): + def __init__(self, new_file_time_interval, file_location, decimation_factor, subband_index): + super().__init__(new_file_time_interval, file_location, hdf5_writer.XST_MODE, decimation_factor) + self.subband_index = subband_index + + def decoder(self, packet): + return XSTPacket(packet) + + def new_collector(self): + return statistics_collector.XSTCollector() + + def next_filename(self, timestamp): + time_str = str(timestamp.strftime("%Y-%m-%d-%H-%M-%S")) + return f"{self.file_location}/{self.mode}_SB{self.subband_index}_{time_str}.h5" + + def write_values_matrix(self, current_group): + # requires a function call to transform the xst_blocks in to the right structure + current_group.create_dataset(name="values", data=self.current_matrix.xst_values([self.subband_index])[0].astype(numpy.cfloat), compression="gzip") + + +class parallel_xst_hdf5_writer: + """ Writes multiple subbands in parallel. Each subband gets written to its own HDF5 file(s). """ + + def __init__(self, new_file_time_interval, file_location, decimation_factor): + # maintain a dedicated hdf5_writer per subband + self.writers = {} + + # function to create a new writer, to avoid having to store + # all the init parameters just for this purpose. + # + def new_writer(subband): + # Since we use a dedicated writer per subband, the data will end + # up at subband_index == 0 in each of them. + return xst_hdf5_writer( + new_file_time_interval, + file_location, + decimation_factor, + 0) + + self.new_writer = new_writer + + def next_packet(self, packet): + # decode to get subband of this packet + fields = XSTPacket(packet) + subband = fields.subband_index + + # make sure there is a writer for it + if subband not in self.writers: + self.writers[subband] = self.new_writer(subband) + + # demux packet to the correct writer + self.writers[subband].next_packet(packet) + + def close_writer(self): + for writer in self.writers.values(): + writer.close_writer() + + self.writers = {} + diff --git a/tangostationcontrol/tangostationcontrol/statistics_writer/statistics_writer.py b/tangostationcontrol/tangostationcontrol/statistics_writer/statistics_writer.py index 1a1ecb671159e1b3ca143ecbf860000d6cdbe0c5..52747fff71d436d62bcbe40844aa0a3a45a2ab25 100644 --- a/tangostationcontrol/tangostationcontrol/statistics_writer/statistics_writer.py +++ b/tangostationcontrol/tangostationcontrol/statistics_writer/statistics_writer.py @@ -3,7 +3,7 @@ import time import sys from tangostationcontrol.statistics_writer.receiver import tcp_receiver, file_receiver -from tangostationcontrol.statistics_writer.hdf5_writer import hdf5_writer +from tangostationcontrol.statistics_writer.hdf5_writer import sst_hdf5_writer, parallel_xst_hdf5_writer import logging logging.basicConfig(level=logging.INFO, format = '%(asctime)s:%(levelname)s: %(message)s') @@ -13,13 +13,13 @@ def main(): parser = argparse.ArgumentParser( description='Converts a stream of statistics packets into HDF5 files.') parser.add_argument( - '-a', '--host', type=str, required=True, help='the host to connect to') + '-a', '--host', type=str, required=False, help='the host to connect to') parser.add_argument( '-p', '--port', type=int, default=0, help='the port to connect to, or 0 to use default port for the ' 'selected mode (default: %(default)s)') parser.add_argument( - '-f', '--file', type=str, required=True, help='the file to read from') + '-f', '--file', type=str, required=False, help='the file to read from') parser.add_argument( '-m', '--mode', type=str, choices=['SST', 'XST', 'BST'], default='SST', help='sets the statistics type to be decoded options (default: ' @@ -57,6 +57,9 @@ def main(): debug = args.debug reconnect = args.reconnect + if not filename and not host: + raise ValueError("Supply either a filename (--file) or a hostname (--host)") + if decimation < 1: raise ValueError("Please use an integer --Decimation value 1 or higher to only store one every n statistics' ") @@ -78,7 +81,16 @@ def main(): sys.exit(1) # create the writer - writer = hdf5_writer(new_file_time_interval=interval, file_location=output_dir, statistics_mode=mode, decimation_factor=decimation) + if mode == "XST": + writer = parallel_xst_hdf5_writer(new_file_time_interval=interval, file_location=output_dir, decimation_factor=decimation) + elif mode == "SST": + writer = sst_hdf5_writer(new_file_time_interval=interval, file_location=output_dir, decimation_factor=decimation) + elif mode == "BST": + logger.fatal(f"BST mode not supported") + sys.exit(1) + else: + logger.fatal(f"Invalid mode: {mode}") + sys.exit(1) # start looping try: diff --git a/tangostationcontrol/tangostationcontrol/test/beam/__init__.py b/tangostationcontrol/tangostationcontrol/test/beam/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py b/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py new file mode 100644 index 0000000000000000000000000000000000000000..624491c454f6d1b6795295b66eee072f45774928 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py @@ -0,0 +1,103 @@ +import datetime + +from tangostationcontrol.beam.delays import * +from tangostationcontrol.test import base + + + + + +class TestDelays(base.TestCase): + def test_init(self): + """ + Fail condition is simply the object creation failing + """ + + reference_itrf = [3826577.066, 461022.948, 5064892.786] # CS002LBA, in ITRF2005 epoch 2012.5 + d = delay_calculator(reference_itrf) + + self.assertIsNotNone(d) + + def test_sun(self): + # # create a frame tied to the reference position + reference_itrf = [3826577.066, 461022.948, 5064892.786] + d = delay_calculator(reference_itrf) + + for i in range(24): + + # set the time to the day of the winter solstice 2021 (21 december 16:58) as this is the time with the least change in sunlight + timestamp = datetime.datetime(2021, 12, 21, i, 58, 0) + d.set_measure_time(timestamp) + + + # point to the sun + direction = "SUN", "0deg", "0deg" + + # calculate the delays based on the set reference position, the set time and now the set direction and antenna positions. + pointing = d.measure.direction(*direction) + direction = d.get_direction_vector(pointing) + + """ + direction[2] is the z-coordinate of ITRF, which points to the north pole. + This direction is constant when pointing to the sun, as the earth rotates around its axis, + but changes slowly due to the earths rotation around the sun. + The summer and winter solstices are when these values are at their peaks and the changes are the smallest. + This test takes the value at the winter solstice and checks whether the measured values are near enough to that. + """ + + # Measured manually at the winter solstice. Using datetime.datetime(2021, 12, 21, 16, 58, 0) + z_at_solstice = -0.3977784695213487 + z_direction = direction[2] + + self.assertAlmostEqual(z_at_solstice, z_direction, 4) + + def test_identical_location(self): + # # create a frame tied to the reference position + reference_itrf = [3826577.066, 461022.948, 5064892.786] # CS002LBA, in ITRF2005 epoch 2012.5 + d = delay_calculator(reference_itrf) + + # set the antenna position identical to the reference position + antenna_itrf = [[reference_itrf[0], reference_itrf[1], reference_itrf[2]]] # CS001LBA, in ITRF2005 epoch 2012.5 + + # # set the timestamp to solve for + timestamp = datetime.datetime(2000, 1, 1, 0, 0, 0) + d.set_measure_time(timestamp) + + # compute the delays for an antennas w.r.t. the reference position + + # # obtain the direction vector for a specific pointing + direction = "J2000", "0deg", "0deg" + + # calculate the delays based on the set reference position, the set time and now the set direction and antenna positions. + delays = d.convert(direction, antenna_itrf) + + self.assertListEqual(delays, [0.0], msg=f"delays = {delays}") + + def test_light_second_delay(self): + """ + This test measures the delay between 2 positions 1 light second apart. + """ + + # # create a frame tied to the reference position + reference_itrf = [3826577.066, 461022.948, 5064892.786] # CS002LBA, in ITRF2005 epoch 2012.5 + d = delay_calculator(reference_itrf) + + # set the antenna position identical to the reference position + speed_of_light = 299792458.0 + antenna_itrf = [[reference_itrf[0], reference_itrf[1] - speed_of_light, reference_itrf[2]]] # CS001LBA, in ITRF2005 epoch 2012.5 + + # # set the timestamp to solve for + timestamp = datetime.datetime(2000, 1, 1, 0, 0, 0) + d.set_measure_time(timestamp) + + # compute the delays for an antennas w.r.t. the reference position + + # # obtain the direction vector for a specific pointing + direction = "J2000", "0deg", "0deg" + + # calculate the delays based on the set reference position, the set time and now the set direction and antenna positions. + delays = d.convert(direction, antenna_itrf) + + + self.assertTrue(0.98 <= delays[0] <= 1.02, f"delays[0] = {delays[0]}") + diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_geo.py b/tangostationcontrol/tangostationcontrol/test/beam/test_geo.py new file mode 100644 index 0000000000000000000000000000000000000000..858b3f32e954d19271f8a0dc6fc3cba7b92f47e2 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/test/beam/test_geo.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the LOFAR 2.0 Station Software +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +from tangostationcontrol.beam.geo import ETRS_to_ITRF + +from tangostationcontrol.test import base + +import numpy.testing + +class TestETRS_to_ITRF(base.TestCase): + def test_convert_single_coordinate(self): + """ Convert a single coordinate. """ + ETRS_coords = numpy.array([1.0, 1.0, 1.0]) + ITRF_coords = ETRS_to_ITRF(ETRS_coords, "ITRF2005", 2015.5) + + self.assertEqual(ETRS_coords.shape, ITRF_coords.shape) + + def test_convert_array(self): + """ Convert an array of coordinates. """ + ETRS_coords = numpy.array([ [1.0, 1.0, 1.0], [2.0, 2.0, 2.0] ]) + ITRF_coords = ETRS_to_ITRF(ETRS_coords, "ITRF2005", 2015.5) + + self.assertEqual(ETRS_coords.shape, ITRF_coords.shape) + + def test_verify_CS001_LBA(self): + """ Verify if the calculated CS001LBA phase center matches those calculated in LOFAR1. """ + + # See CLBA in MAC/Deployment/data/Coordinates/ETRF_FILES/CS001/CS001-antenna-positions-ETRS.csv + CS001_LBA_ETRS = [3826923.942, 460915.117, 5064643.229] + + # Convert to ITRF + CS001_LBA_ITRF = ETRS_to_ITRF(numpy.array(CS001_LBA_ETRS), "ITRF2005", 2015.5) + + # verify against LOFAR1 (MAC/Deployment/data/StaticMetaData/AntennaFields/CS001-AntennaField.conf) + LOFAR1_CS001_LBA_ITRF = [3826923.50275, 460915.488115, 5064643.517] + + numpy.testing.assert_almost_equal(CS001_LBA_ITRF, LOFAR1_CS001_LBA_ITRF, decimal=1.5) diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py b/tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py new file mode 100644 index 0000000000000000000000000000000000000000..d698264f845cde35c5af63612e040832120e2455 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the LOFAR 2.0 Station Software +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +from tangostationcontrol.beam.hba_tile import HBATAntennaOffsets + +from tangostationcontrol.test import base + +from math import pi +import numpy.testing + +class TestHBATAntennaOffsets(base.TestCase): + def test_verify_CS001_HBA0(self): + """ Verify if the calculated HBAT Antenna Offsets match those calculated in LOFAR1. """ + + CS001_HBA0_rotation_angle_deg = 24 + CS001_PQR_to_ETRS_rotation_matrix = numpy.array([ + [-0.1195951054, -0.7919544517, 0.5987530018], + [ 0.9928227484, -0.0954186800, 0.0720990002], + [ 0.0000330969, 0.6030782884, 0.7976820024]]) + + # recalculate the ITRF offsets + ITRF_offsets = HBATAntennaOffsets.ITRF_offsets( + HBATAntennaOffsets.HBAT1_BASE_ANTENNA_OFFSETS, + CS001_HBA0_rotation_angle_deg * pi / 180, + CS001_PQR_to_ETRS_rotation_matrix) + + # verify against LOFAR1 (MAC/Deployment/data/StaticMetaData/iHBADeltas/CS001-iHBADeltas.conf) + LOFAR1_CS001_HBA0_ITRF_offsets = numpy.array([ + [-1.847, -1.180, 1.493], + [-1.581, 0.003, 1.186], + [-1.315, 1.185, 0.880], + [-1.049, 2.367, 0.573], + [-0.882, -1.575, 0.804], + [-0.616, -0.393, 0.498], + [-0.350, 0.789, 0.191], + [-0.083, 1.971, -0.116], + [ 0.083, -1.971, 0.116], + [ 0.350, -0.789, -0.191], + [ 0.616, 0.393, -0.498], + [ 0.882, 1.575, -0.804], + [ 1.049, -2.367, -0.573], + [ 1.315, -1.185, -0.880], + [ 1.581, -0.003, -1.186], + [ 1.847, 1.180, -1.493]]) + + numpy.testing.assert_almost_equal(ITRF_offsets, LOFAR1_CS001_HBA0_ITRF_offsets, decimal=3) diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py b/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py index 651fbabe8a47061811fcfa67f68978ff527cec72..bffc23aaa24b7ed0269dffa4fec3afb5363b7ba9 100644 --- a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py +++ b/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py @@ -7,7 +7,7 @@ """ # External imports -from tango import DevState +from tango import DevState, DevFailed # Internal imports from tangostationcontrol.test.clients.test_client import test_client @@ -609,3 +609,29 @@ class TestAttributeTypes(base.TestCase): self.readback_test( attribute_type_test['image'], attribute_type_test['type'], 'image') + +class TestAttributeAccess(base.TestCase): + def setUp(self): + # Avoid the device trying to access itself as a client + self.deviceproxy_patch = mock.patch.object(tangostationcontrol.devices.lofar_device,'DeviceProxy') + self.deviceproxy_patch.start() + self.addCleanup(self.deviceproxy_patch.stop) + + class float32_scalar_device(lofar_device): + scalar_R = attribute_wrapper(comms_annotation="float32_scalar_R", datatype=numpy.float32) + scalar_RW = attribute_wrapper(comms_annotation="float32_scalar_RW", datatype=numpy.float32, access=AttrWriteType.READ_WRITE) + + def configure_for_initialise(self): + dev_init(self) + self.set_state(DevState.STANDBY) + + def test_cannot_access_before_initialise(self): + with DeviceTestContext(self.float32_scalar_device, process=True) as proxy: + with self.assertRaises(DevFailed): + _ = proxy.scalar_R + + def test_can_access_after_initialise(self): + with DeviceTestContext(self.float32_scalar_device, process=True) as proxy: + proxy.initialise() + + _ = proxy.scalar_R diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py b/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py index c071dddfe8570f987079807fc76c7ef530132069..48ce76bf15d0c445b58320d223285153b7d57259 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py @@ -31,11 +31,13 @@ class TestBeamDevice(base.TestCase): def test_get_pointing_directions(self): """Verify can read pointings attribute and length matches without err""" with DeviceTestContext(beam.Beam, process=True, timeout=10) as proxy: + proxy.initialise() self.assertEqual(96, len(proxy.read_attribute( "HBAT_pointing_direction_R").value)) def test_get_pointing_timestamps(self): """Verify can read timestamps attribute and length matches without err""" with DeviceTestContext(beam.Beam, process=True, timeout=10) as proxy: + proxy.initialise() self.assertEqual(96, len(proxy.read_attribute( "HBAT_pointing_timestamp_R").value)) diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_collector.py b/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_collector.py index 4b58141c06d9b09d68dba295b007b41081ca3618..7113ee837631789e99218f3356a602637ccac116 100644 --- a/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_collector.py +++ b/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_collector.py @@ -3,6 +3,45 @@ from tangostationcontrol.devices.sdp.statistics_packet import XSTPacket from tangostationcontrol.test import base +class TestSelectSubbandSlot(base.TestCase): + def test_first_entry(self): + collector = XSTCollector() + + # on start, any subband should map on the first entry + self.assertEqual(0, collector.select_subband_slot(102)) + + def test_subsequent_entries(self): + collector = XSTCollector() + + # assign some subbands + collector.parameters["xst_subbands"][0] = 102 + collector.parameters["xst_subbands"][2] = 103 + collector.parameters["xst_subbands"][3] = 104 + + # give them non-zero timestamps to make them newer than the other entries + collector.parameters["xst_timestamps"][0] = 1 + collector.parameters["xst_timestamps"][2] = 1 + collector.parameters["xst_timestamps"][3] = 1 + + # these should be reported back when looking them up again + self.assertEqual(0, collector.select_subband_slot(102)) + self.assertEqual(2, collector.select_subband_slot(103)) + self.assertEqual(3, collector.select_subband_slot(104)) + + # a place for another subband should be the lowest + self.assertEqual(1, collector.select_subband_slot(101)) + + def test_spilling(self): + collector = XSTCollector() + + # assign all subbands, in decreasing age + for n in range(XSTCollector.MAX_PARALLEL_SUBBANDS): + collector.parameters["xst_subbands"][n] = 100 + n + collector.parameters["xst_timestamps"][n] = 100 - n + + # check where a new subband replaces the oldest + self.assertEqual(XSTCollector.MAX_PARALLEL_SUBBANDS - 1, collector.select_subband_slot(200)) + class TestXSTCollector(base.TestCase): def test_valid_packet(self): collector = XSTCollector() @@ -17,6 +56,9 @@ class TestXSTCollector(base.TestCase): # baseline indeed should be (12,0) self.assertEqual((12,0), fields.first_baseline) + # subband should indeed be 102 + self.assertEqual(102, fields.subband_index) + # this should not throw collector.process_packet(packet) @@ -27,8 +69,10 @@ class TestXSTCollector(base.TestCase): self.assertEqual(1, collector.parameters["nof_valid_payloads"][fpga_index]) self.assertEqual(0, collector.parameters["nof_payload_errors"][fpga_index]) + self.assertListEqual([102,0,0,0,0,0,0,0], list(collector.parameters["xst_subbands"])) + # check whether the data ended up in the right block, and the rest is still zero - xst_values = collector.xst_values() + xst_values = collector.xst_values()[0] for baseline_a in range(collector.MAX_INPUTS): for baseline_b in range(collector.MAX_INPUTS): @@ -67,7 +111,7 @@ class TestXSTCollector(base.TestCase): self.assertEqual(0, collector.parameters["nof_invalid_packets"]) # check whether the data ended up in the right block, and the rest is still zero - xst_values = collector.xst_values() + xst_values = collector.xst_values()[0] for baseline_a in range(collector.MAX_INPUTS): for baseline_b in range(collector.MAX_INPUTS): @@ -84,6 +128,48 @@ class TestXSTCollector(base.TestCase): else: self.assertEqual(0+0j, xst_values[baseline_a][baseline_b], msg=f'element [{baseline_a}][{baseline_b}] was not in packet, but was written to the XST matrix.') + def test_multiple_subbands(self): + collector = XSTCollector() + + # a valid packet as obtained from SDP, with 64-bit BE 1+1j as payload at (12,0) + packet_subband_102 = b'X\x05\x00\x00\x00\x00\x00\x00\x10\x08\x00\x02\xfa\xef\x00f\x0c\x00\x0c\x08\x01 \x14\x00\x00\x01!\xd9&z\x1b\xb3' + 288 * b'\x00\x00\x00\x00\x00\x00\x00\x01' + packet_subband_103 = b'X\x05\x00\x00\x00\x00\x00\x00\x10\x08\x00\x02\xfa\xef\x00g\x0c\x00\x0c\x08\x01 \x14\x00\x00\x01!\xd9&z\x1b\xb3' + 288 * b'\x00\x00\x00\x00\x00\x00\x00\x02' + + # make sure the subband_indices are indeed what we claim they are + fields = XSTPacket(packet_subband_102) + self.assertEqual(102, fields.subband_index) + + fields = XSTPacket(packet_subband_103) + self.assertEqual(103, fields.subband_index) + + # process our packets + collector.process_packet(packet_subband_102) + collector.process_packet(packet_subband_103) + + # counters should now be updated + self.assertListEqual([102,103,0,0,0,0,0,0], list(collector.parameters["xst_subbands"])) + + # check whether the data ended up in the right block, and the rest is still zero + xst_values = collector.xst_values() + + for subband_idx in range(collector.MAX_PARALLEL_SUBBANDS): + for baseline_a in range(collector.MAX_INPUTS): + for baseline_b in range(collector.MAX_INPUTS): + if baseline_b > baseline_a: + # only scan top-left triangle + continue + + baseline_a_was_in_packet = (fields.first_baseline[0] <= baseline_a < fields.first_baseline[0] + fields.nof_signal_inputs) + baseline_b_was_in_packet = (fields.first_baseline[1] <= baseline_b < fields.first_baseline[1] + fields.nof_signal_inputs) + + if baseline_a_was_in_packet and baseline_b_was_in_packet and subband_idx == 0: + self.assertEqual(1+1j, xst_values[subband_idx][baseline_a][baseline_b], msg=f'element [{baseline_a}][{baseline_b}] did not end up in XST matrix.') + elif baseline_a_was_in_packet and baseline_b_was_in_packet and subband_idx == 1: + self.assertEqual(2+2j, xst_values[subband_idx][baseline_a][baseline_b], msg=f'element [{baseline_a}][{baseline_b}] did not end up in XST matrix.') + else: + self.assertEqual(0+0j, xst_values[subband_idx][baseline_a][baseline_b], msg=f'element [{baseline_a}][{baseline_b}] was not in packet, but was written to the XST matrix.') + + def test_invalid_packet(self): collector = XSTCollector() diff --git a/tangostationcontrol/tangostationcontrol/test/test_statistics_writer.py b/tangostationcontrol/tangostationcontrol/test/test_statistics_writer.py new file mode 100644 index 0000000000000000000000000000000000000000..4b10230c8fca8c0773d085c6e42719b74dd7b2c3 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/test/test_statistics_writer.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the LOFAR 2.0 Station Software +# +# +# +# Distributed under the terms of the APACHE license. +# See LICENSE.txt for more info. + +from tangostationcontrol.test import base +from tangostationcontrol.statistics_writer import statistics_writer +import sys +from os.path import dirname +from tempfile import TemporaryDirectory +from unittest import mock + +class TestStatisticsWriter(base.TestCase): + def test_sst(self): + with TemporaryDirectory() as tmpdir: + new_sys_argv = [sys.argv[0], "--mode", "SST", "--file", dirname(__file__) + "/SDP_SST_statistics_packets.bin", "--output_dir", tmpdir] + with mock.patch.object(statistics_writer.sys, 'argv', new_sys_argv): + with self.assertRaises(SystemExit): + statistics_writer.main() + + def test_xst(self): + with TemporaryDirectory() as tmpdir: + new_sys_argv = [sys.argv[0], "--mode", "XST", "--file", dirname(__file__) + "/SDP_XST_statistics_packets.bin", "--output_dir", tmpdir] + with mock.patch.object(statistics_writer.sys, 'argv', new_sys_argv): + with self.assertRaises(SystemExit): + statistics_writer.main()