diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 16240653ae827112f531a2df361948472a9ffc46..e88c625fd80ec7de203f736942946347bcd450f6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,15 +1,14 @@ default: - image: python:3.7 # minimum supported version + image: $CI_REGISTRY/lofar2.0/lofar-station-client/ci_python37:$CI_COMMIT_SHORT_SHA # minimum supported version # Make sure each step is executed in a virtual environment with some basic dependencies present before_script: - python --version # For debugging - - python -m pip install --upgrade pip - - pip install --upgrade tox cache: paths: - .cache/pip stages: + - image - lint - test - package @@ -20,6 +19,19 @@ stages: variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" +build_test_image_python37: + stage: image + image: docker + services: + - name: docker:dind + variables: + DOCKER_TLS_CERTDIR: "/certs" + before_script: + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + script: + - docker build -t $CI_REGISTRY/lofar2.0/lofar-station-client/ci_python37:$CI_COMMIT_SHORT_SHA -f docker/Dockerfile.ci_python37 docker + - docker push $CI_REGISTRY/lofar2.0/lofar-station-client/ci_python37:$CI_COMMIT_SHORT_SHA + run_black: stage: lint script: @@ -46,24 +58,37 @@ run_unit_tests_py37: - echo "run python3.7 unit tests /w coverage" - tox -e py37 -run_unit_tests_py38: - image: python:3.8 +.run_unit_tests_pyXX: + # installs the prerequisites explicitly, instead of piggy backing + # on the ci_python37 image. This allows us to use different base + # images with different python versions. stage: test + before_script: + - apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y libboost-python-dev libtango-dev # Needed to install pytango + - python -m pip install --upgrade pip + - pip install --upgrade tox + +run_unit_tests_py38: + extends: .run_unit_tests_pyXX + image: python:3.8-buster script: - echo "run python3.8 unit tests /w coverage" - tox -e py38 run_unit_tests_py39: - image: python:3.9 - stage: test + extends: .run_unit_tests_pyXX + image: python:3.9-bullseye script: - echo "run python3.9 unit tests /w coverage" - tox -e py39 run_unit_tests_py310: - image: python:3.10 - stage: test + extends: .run_unit_tests_pyXX + image: python:3.10-bullseye script: + # Debian Bullseye ships with libboost-python linked to Python 3.9. Use the one from Debian Sid instead. + - echo 'deb http://deb.debian.org/debian sid main' >> /etc/apt/sources.list + - apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y libboost-python1.74-dev - echo "run python3.10 unit tests /w coverage" - tox -e py310 diff --git a/README.md b/README.md index c6d9616b973d3f34fc5d8adf793d48a6d21331a4..32deb0e52f8e4f1770501e3bb6465af6cd68da12 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,32 @@ Client library for using Tango Station Control. Table of Contents: +- [Prerequisites](#prerequisites) - [Installation](#installation) - [Usage](#usage) - [Development](#development) - [Debug](#debug) - [Releasenotes](#releasenotes) +## Prerequisites + +This package uses [PyTango](https://pytango.readthedocs.io), which wraps the Tango C++ library. You will need to install the Tango C++ headers & library to allow `pip` to install PyTango as part of this package's requirements: + +Debian and Ubuntu provide these natively: + +```shell +apt-get install libtango-dev +``` + +Under Arch, install the [tango-cpp AUR package](https://aur.archlinux.org/packages/tango-cpp). For other distros and installation methods, see [Tango's Linux installation manual](https://tango-controls.readthedocs.io/en/latest/installation/tango-on-linux.html). + +You will also need the Boost Python C++ headers & libraries to compile PyTango. Under Debian/Ubuntu, obtain these through: + +```shell +apt-get install libboost-python-dev +``` + + ## Installation Wheel distributions are available from the [gitlab package registry](https://git.astron.nl/lofar2.0/lofar-station-client/-/packages/), @@ -86,6 +106,9 @@ tox -e debug tests.requests.test_prometheus ## Releasenotes +- 0.9 - Added `devices.LofarDeviceProxy` that transparently exposes >2D attributes +- 0.8 - Fixed XST packet parsing. +- 0.7. - Partial rework of DTS outside code, still many possible improvements. - 0.6. - Correctly transpose XST blocks in `XSTCollector`. - 0.5. - Swapped [x][y] for [y][x] dimensionality in `get_attribute_history` - 0.4. - Added collectors including `XSTCollector`, `BSTCollector` and `SSTCollecotr` diff --git a/VERSION b/VERSION index 5a2a5806df6e909afe3609b5706cb1012913ca0e..ac39a106c48515b621e90c028ed94c6f71bc03fa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6 +0.9.0 diff --git a/docker/Dockerfile.ci_python37 b/docker/Dockerfile.ci_python37 new file mode 100644 index 0000000000000000000000000000000000000000..1f4362fcff091ce1ba35661777eeb3e78c2a8b98 --- /dev/null +++ b/docker/Dockerfile.ci_python37 @@ -0,0 +1,9 @@ +FROM python:3.7-buster + +# Install PyTango dependencies +RUN apt-get update -y +RUN DEBIAN_FRONTEND=noninteractive apt-get install -y libboost-python-dev libtango-dev + +# Make sure we have the latest tooling for our tests +RUN python -m pip install --upgrade pip +RUN pip install --upgrade tox diff --git a/docs/source/source_documentation/lofar_station_client.rst b/docs/source/source_documentation/lofar_station_client.rst index ff79cba7ecc0bf053163dcb02b5a10274120c3e8..aa18120d5728a2511280c0bdb0b4e6a1f4224820 100644 --- a/docs/source/source_documentation/lofar_station_client.rst +++ b/docs/source/source_documentation/lofar_station_client.rst @@ -1,6 +1,18 @@ lofar\_station\_client package ============================== +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + lofar_station_client.dts + lofar_station_client.math + lofar_station_client.parsing + lofar_station_client.requests + lofar_station_client.statistics + Module contents --------------- diff --git a/lofar_station_client/RCUs.py b/lofar_station_client/RCUs.py deleted file mode 100644 index 2637079d1228c6da62fe47f36d3904f47344e179..0000000000000000000000000000000000000000 --- a/lofar_station_client/RCUs.py +++ /dev/null @@ -1,6 +0,0 @@ -RCU2L = [0, 1, 2, 3, 4, 5] -RCU2H = [8, 9] -# RCU2H = [8] -RCU2Hpwr = [8] # Should be even numbers -RCU2Hctl = [9] # Should be even numbers -# RCU2Hctl = [rcu + 1 for rcu in RCU2Hpwr] # This is then odd numbers diff --git a/lofar_station_client/devices.py b/lofar_station_client/devices.py new file mode 100644 index 0000000000000000000000000000000000000000..aac73f1b8f89f2c859c5f5fd42f9450b8116ad1f --- /dev/null +++ b/lofar_station_client/devices.py @@ -0,0 +1,77 @@ +""" Enhanced interfaces towards the station's Tango devices. """ + +import ast +from functools import lru_cache + +import numpy + +from tango import DeviceProxy +from tango import ExtractAs + + +class LofarDeviceProxy(DeviceProxy): + """A LOFAR-specific tango.DeviceProxy that provides + a richer experience.""" + + @lru_cache() + def get_attribute_config(self, name): + """Get cached attribute configurations, as they are not expected to change.""" + + return super().get_attribute_config(name) + + @lru_cache() + def get_attribute_shape(self, name): + """Get the shape of the requested attribute, as a tuple.""" + + config = self.get_attribute_config(name) + + if config.format and config.format[0] == "(": + # For >2D arrays, the "format" property describes actual + # the dimensions as a tuple (x, y, z, ...), + # so reshape the value accordingly. + shape = ast.literal_eval(config.format) + elif config.max_dim_y > 0: + # 2D array + shape = (config.max_dim_y, config.max_dim_x) + elif config.max_dim_x > 1: + # 1D array + shape = (config.max_dim_x,) + else: + # scalar + shape = () + + return shape + + def read_attribute(self, name, extract_as=ExtractAs.Numpy): + """Read an attribute from the server.""" + + attr = super().read_attribute(name, extract_as) + + # convert non-scalar values into their actual shape + shape = self.get_attribute_shape(name) + if shape != (): + attr.value = attr.value.reshape(shape) + + return attr + + def write_attribute(self, name, value): + """Write an attribute to the server.""" + + config = self.get_attribute_config(name) + shape = self.get_attribute_shape(name) + + # 2D arrays also represent arrays of higher dimensionality. reshape them + # to fit their original Tango shape before writing. + if shape != (): + value = numpy.array(value) + + if value.shape != shape: + raise ValueError( + f"Invalid shape. Given: {value.shape} Expected: {shape}" + ) + + if len(shape) > 2: + # >2D arrays collapse into 2D + value = value.reshape((config.max_dim_y, config.max_dim_x)) + + return super().write_attribute(name, value) diff --git a/lofar_station_client/dts/__init__.py b/lofar_station_client/dts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lofar_station_client/dts/bands.py b/lofar_station_client/dts/bands.py new file mode 100644 index 0000000000000000000000000000000000000000..2979f56c54df711d2b088cac09b62d1e74bca41d --- /dev/null +++ b/lofar_station_client/dts/bands.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Band methods and conversions to and from frequencies""" + +import copy +from typing import Any +from typing import Dict +from typing import List +from typing import Union +from typing import Tuple + +import numpy as np +from nptyping import NDArray +from nptyping import String +from nptyping import Float + +from lofar_station_client.dts import constants +from lofar_station_client.dts import index + + +def get_valid_band_names(): + """Return the valid band names.""" + + return copy.deepcopy(constants.BANDLIMS) + + +def get_band_name_and_subband_index_for_frequency( + freqs: Union[float, List[float]] +) -> Tuple[NDArray[Any, String], NDArray[Any, Float]]: + """Get band name and subband index for one or more frequencies. + + :param freqs: one or more frequencies in Hz + :return: list of band names (see get_valid_band_names) + sbi = one or more indices of the subbands for which the frequencies + should be returned + """ + # make sure freqs is a list + if not isinstance(freqs, list): + freqs = [freqs] + # get band name and subband index + freqs = np.array(freqs) + band_names = [] + for freq in freqs: + band_name = get_band_name_for_frequency(freq) + band_names.append(band_name) + band_names = np.array(band_names) + # get indices for band_name "LB" + sbis = np.round(freqs * constants.CST_N_SUB / constants.CST_FS) + # and update for the other bands: + for idx in np.where(np.array(band_names) == "HB1")[0]: + sbis[idx] = 2 * constants.CST_N_SUB - sbis[idx] + for idx in np.where(np.array(band_names) == "HB2")[0]: + sbis[idx] = sbis[idx] - 2 * constants.CST_N_SUB + return band_names, sbis + + +def get_band_name_for_frequency(freq: float, bandlims: Dict[str, float] = None) -> str: + """Get band name for frequency + + :param freq: one frequency in Hz + :param bandlims: dict with band names and their limits (default: LOFAR2 bands) + :return: name of analog band (see get_valid_band_names), + empty string ("") if unsuccessful + """ + if bandlims is None: + bandlims = constants.BANDLIMS + for band_name in bandlims: + if np.min(bandlims[band_name]) <= freq <= np.max(bandlims[band_name]): + return band_name + return "" + + +def get_frequency_for_band_name(band_name: str): + """Get all frequencies for given band name + + :param band_name: name of analog band (see get_valid_band_names) + :return: frequencies in Hz for the selected band + """ + + return get_frequency_for_band_name_and_subband_index(band_name, sbi=np.arange(512)) + + +def get_frequency_for_band_name_and_subband_index( + band_name: str, sbi: Union[float, List[float], NDArray[Any, Float]] +) -> Union[float, List[float], NDArray[Any, Float]]: + """Get frequency for band name and subband(s) + + Transparenently returns the same type as supplied for sbi parameter. + + :warning: Will return frequencies for non-existing subbands outside the + range of :py:attr:`~constants.CST_N_SUB` + + :param band_name: Name of analog band (see get_valid_band_names) + :param sbi: One or more indices of the subbands for which the frequencies + should be returned + :return: frequencies in Hz for the selected band and subband + """ + + # check input + valid_band_names = get_valid_band_names() + if band_name not in valid_band_names and band_name not in constants.LB_ALIASES: + raise ValueError( + f"Unknown band_name '{band_name}'. Valid band_names are: " + f"{valid_band_names}" + ) + + # generate frequencies + freqs = sbi * constants.CST_FS / constants.CST_N_SUB + if band_name == "HB1": + return 200e6 - freqs + if band_name == "HB2": + return 200e6 + freqs + # else: # LB + return freqs + + +# TODO(Bouwdewijn): dimensionality does not correspond to recv attributes +def get_band_names( + rcu_band_select_r, rcu_pcb_version_r, lb_tags: List = None, hb_tags: List = None +): + """Get the frequency band names per receiver, based on their band selection & RCU. + + PCB name is used to determine the band (Low Band "LB" or High Band "HB", + None otherwise). The band selection is added as a postfix. + RCU PCB version can also be used, but then the default tags will not be sufficient + + rcu_pcb_version_r can also hold IDs as returned by recv.RCU_PCB_ID_R + In that case, the lb_tags and hb_tags should be passed on by the user + + :param rcu_band_select_r: As returned by recv.RCU_band_select_R + :param rcu_pcb_version_r: As returned by recv.RCU_PCB_version_R + :param lb_tags: Substrings in RCU_PCB_version_R fields to indicate Low Band + Default: `["RCU2L"]` + :param hb_tags: Substrings in RCU_PCB_version_R fields to indicate High Band + Default: `["RCU2H"]` + :return: list of strings indicating the band name per receiver. + Returns `None` if no band name could be determined + + """ + + # if IDs are passed on, convert to list of strings + if isinstance(rcu_pcb_version_r, np.ndarray): + rcu_pcb_version_r = [str(x) for x in rcu_pcb_version_r] + # + if lb_tags is None: + lb_tags = ["RCU2L"] + if hb_tags is None: + hb_tags = ["RCU2H"] + + # This statement can never work if rcu_band_select_r is one dimensional + n_signal_indices = rcu_band_select_r.shape[0] * rcu_band_select_r.shape[1] + band_name_per_signal_index = [None] * n_signal_indices + + for rcu_index, _ in enumerate(rcu_band_select_r): + for rcu_input_index, _ in enumerate(rcu_band_select_r[rcu_index]): + signal_index = index.rcu_index_and_rcu_input_index_2_signal_index( + rcu_index, rcu_input_index + ) + + this_band = None + if _is_lowband(lb_tags, rcu_pcb_version_r[rcu_index]): + this_band = f"LB{rcu_band_select_r[rcu_index][rcu_input_index]:1}" + elif _is_highband(hb_tags, rcu_pcb_version_r[rcu_index]): + this_band = f"HB{rcu_band_select_r[rcu_index][rcu_input_index]:1}" + + band_name_per_signal_index[signal_index] = this_band + return band_name_per_signal_index + + +def _is_lowband(tags, indexes): + for tag in tags: + if tag not in indexes: + continue + return True + return False + + +def _is_highband(tags, indexes): + for tag in tags: + if tag not in indexes: + continue + return True + return False diff --git a/lofar_station_client/dts/common.py b/lofar_station_client/dts/common.py new file mode 100644 index 0000000000000000000000000000000000000000..8d2ecf62c87e345dd0238157a1ee5a13a66f99c2 --- /dev/null +++ b/lofar_station_client/dts/common.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Common methods shared across DTS not further classified""" + + +def devices_list_2_dict(devices_list, device_keys=None): + """Convert a list of devices to a dictionary with known keys + + Devices are selected based on substring presence in the devices name + + :param devices_list: list of Tango devices + :param device_keys: list of keys, should be a substring of the device names + :return: dict of devices with known keys + """ + if device_keys is None: + device_keys = [ + "boot", + "unb2", + "recv", + "sdp", + "sst", + "bst", + "xst", + "digitalbeam", + "tilebeam", + "beamlet", + "apsct", + "apspu", + ] + devices_dict = {} + if isinstance(devices_list, dict): + # if by accident devices_list is already a dict, + # then simply return the dict + return devices_list + for device in devices_list: + for device_key in device_keys: + if device_key in device.name().lower(): + devices_dict[device_key] = device + return devices_dict diff --git a/lofar_station_client/dts/constants.py b/lofar_station_client/dts/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..51e298587fe34926b6c4d854582db525ed6d2e24 --- /dev/null +++ b/lofar_station_client/dts/constants.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Constants used by DTS""" + +# pylint: disable=consider-using-f-string + +RCU2L = [0, 1, 2, 3, 4, 5] +RCU2H = [8, 9] +RCU2HPWR = [8] # Should be even numbers +# TODO(Corne): Why is this not an even number if it should be?! +RCU2HCTL = [9] # Should be even numbers + +CST_N_SUB = 512 # number of subbands as output of subband generation in firmware +CST_FS = 100e6 # sampling frequency in Hz +BANDLIMS = {"LB": [0, 100e6], "HB1": [100e6, 200e6], "HB2": [200e6, 300e6]} +LB_ALIASES = ["LB1", "LB2"] + +# define some constants for the setup: +N_FPGA = 4 # number of fpgas + +APS_LOCATION_LABELS = ["Slot%02d" % nr for nr in range(32)] + +# positions of RCU boards. (0,0) is bottom, left +APS_RCU_POS_X = ( + [11, 10, 9, 8, 7, 6] + + [11, 10, 9, 8, 7] * 2 + + [4, 3, 2, 1, 0] * 2 + + [5, 4, 3, 2, 1, 0] +) +APS_RCU_POS_Y = [2] * 6 + [1] * 5 + [0] * 5 + [2] * 5 + [1] * 5 + [0] * 6 +# positions of the other modules in the APS +APS_MOD_POS_X = [0, 1, 3, 4, 5] +APS_MOD_POS_Y = [0] * 5 + +# if the names are correctly provided by the RCU2s, +# then update plot_subband_statistics() (see annotation there) +# and remove these two lines: +# TODO(Corne): Improve mechanism for defining / undefining these variables +RCU2L_TAGS = ["8393812", "8416938", "8469028", "8386883", "8374523"] +RCU2H_TAGS = ["8514859", "8507272"] + +PLOT_DIR = "inspection_plots" +STATION_NAME = "DTS-Outside" + +RCU_INDICES = range(32) + +# RCU Mask: +RCU2L_MASK = [rcu_nr in RCU2L for rcu_nr in range(32)] + +# LBA Masks +LBA_MASK = [[True] * 3 if rcu_nr in RCU2L else [False] * 3 for rcu_nr in range(32)] + +# HBA masks +HBA_MASK = [[True] * 3 if rcui in RCU2H else [False] * 3 for rcui in RCU_INDICES] +HBA_PWR_MASK = [[True] * 3 if rcui in RCU2HPWR else [False] * 3 for rcui in RCU_INDICES] +HBA_CTL_MASK = [[True] * 3 if rcui in RCU2HCTL else [False] * 3 for rcui in RCU_INDICES] diff --git a/lofar_station_client/dts/index.py b/lofar_station_client/dts/index.py new file mode 100644 index 0000000000000000000000000000000000000000..ca81f4692672092c0de6e91f880cadd06c5dce9a --- /dev/null +++ b/lofar_station_client/dts/index.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Conversions between signal and node indexes""" + +import datetime +from typing import Tuple + +from deprecate import deprecated +import numpy as np + + +def signal_index_2_processing_node_index(signal_index) -> int: + """Convert signal index to processing node index. + + Can be used to determine on which processing node the signal is processed. + """ + return (signal_index % 192) // 12 + + +def signal_index_2_processing_node_input_index(signal_index) -> int: + """Convert signal index to processing node input index. + + Can be used to determine wich input to the processing node is used for the signal. + """ + return (signal_index % 192) % 12 + + +def signal_index_2_uniboard_index(signal_index) -> int: + """Convert signal index to UniBoard index. + + Can be used to determine on which UniBoard the signal is processed. + """ + return signal_index // 48 + + +def signal_index_2_rcu_index(signal_index) -> int: + """Convert signal index to RCU index. + + Can be used to determine which RCU is used to provide the signal. + """ + return (signal_index // 6) * 2 + signal_index % 2 + + +def signal_index_2_rcu_input_index(signal_index) -> int: + """Convert signal index to RCU input index. + + Can be used to determine which input to the RCU is used for the signal. + """ + return (signal_index // 2) % 3 + + +def signal_index_2_aps_index(signal_index) -> int: + """Convert signal index to APS index. + + Can be used to determine in which Antenna Processing Subrack this signal is + processed. + """ + return signal_index // 96 + + +def signal_input_2_rcu_index_and_rcu_input_index(signal_index) -> Tuple[int, int]: + """Given the gsi, get the RCU index and the RCU input index in one call.""" + + return ( + signal_index_2_rcu_index(signal_index), + signal_index_2_rcu_input_index(signal_index), + ) + + +# the inverse: +def rcu_index_2_signal_index(rcu_index): + """Convert RCU index to signal indices.""" + return rcu_index_and_rcu_input_index_2_signal_index(rcu_index) + + +def rcu_index_and_rcu_input_index_2_signal_index(rcu_index, rcu_input_index=None): + """Convert RCU index and RCU input index to signal index.""" + + if rcu_input_index is None: + rcu_input_index = np.array([0, 1, 2]) + + return rcu_index + rcu_input_index * 2 + (rcu_index // 2) * 4 + + +@deprecated(target=signal_index_2_rcu_input_index, deprecated_in="0.2", remove_in="0.5") +def gsi_2_rcu_input_index(gsi): + """Replaced by signal_index_2_rcu_input_index + + :deprecated: Please use signal_index_2_rcu_input_index. + """ + + return signal_index_2_rcu_input_index(gsi) + + +@deprecated(target=signal_index_2_rcu_index, deprecated_in="0.2", remove_in="0.5") +def gsi_2_rcu_index(gsi): + """Replaced by signal_index_2_rcu_index + + `lofar_station_control` + + :deprecated: Please use signal_index_2_rcu_index. + """ + + return signal_index_2_rcu_index(gsi) + + +@deprecated( + target=signal_input_2_rcu_index_and_rcu_input_index, + deprecated_in="0.2", + remove_in="0.5", +) +def gsi_2_rcu_index_and_rcu_input_index(gsi): + """Replaced by signal_input_2_rcu_index_and_rcu_input_index + + :deprecated: Please use signal_input_2_rcu_index_and_rcu_input_index. + """ + + return signal_input_2_rcu_index_and_rcu_input_index(gsi) + + +@deprecated(target=rcu_index_2_signal_index, deprecated_in="0.2", remove_in="0.5") +def rcu_index_2_gsi(rcu_index): + """Replaced by rcu_index_2_signal_index + + :deprecated: Please use rcu_index_2_signal_index. + """ + return rcu_index_2_signal_index(rcu_index) + + +@deprecated( + target=rcu_index_and_rcu_input_index_2_signal_index, + deprecated_in="0.2", + remove_in="0.5", +) +def rcu_index_and_rcu_input_index_2_gsi(rcu_index, rcu_input_index=None): + """Replaced by rcu_index_and_rcu_input_index_2_signal_index + + :deprecated: Please use rcu_index_and_rcu_input_index_2_signal_index. + """ + return rcu_index_and_rcu_input_index_2_signal_index(rcu_index, rcu_input_index) + + +def get_timestamp(format_specifier=None): + """Get the timestamp in standard format + + :param format_specifier: format specifier. iso format (default) + "filename": filename without spaces and special + characters, format: yyyymmddThhmmss + + :return: Current timestamp in requested format + """ + timestamp = datetime.datetime.isoformat(datetime.datetime.now()) + if format_specifier == "filename": + timestamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S") + return timestamp diff --git a/lofar_station_client/dts/outside.py b/lofar_station_client/dts/outside.py new file mode 100644 index 0000000000000000000000000000000000000000..fee31ac7902b701c0dce9448afc585eee0c18c7b --- /dev/null +++ b/lofar_station_client/dts/outside.py @@ -0,0 +1,720 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""DTS outside functionality such as restarting and configuring + +Functions to: +- restart the Dwingeloo Test Station (Outside) +- configure the Dwingeloo Test Station (Outside) to a defined default + +Boudewijn Hut +""" + +import time +import datetime +import os +import logging + +import numpy as np + +from lofar_station_client.dts import common +from lofar_station_client.dts import constants +from lofar_station_client.dts import plot + +folders = [constants.PLOT_DIR] +for folder in folders: + if not os.path.exists(folder): + os.makedirs(folder) + +logger = logging.getLogger() + + +def init_and_set_to_default_config(devices, skip_boot=False, silent=False): + """Restart system and set to default configuration + + :param devices: struct of software devices of m&c of the station name + + :return: True if an error ocurred, False otherwise + """ + + original_log_level = logger.getEffectiveLevel() + if silent: + logger.setLevel(logging.WARNING) + + found_error = False + # initialisation + if not skip_boot: + logger.info("Start initialisation") + found_error = found_error | initialise(devices) + else: + logger.info("SKIPPING INITIALISATION (skip_init was set to True)") + logger.info("Check initialisation") + found_error = found_error | check_initialise(devices) + # configuring + logger.info("Start setting to default configuration") + found_error = found_error | set_to_default_configuration(devices) + logger.info("Check setting to default configuration") + found_error = found_error | check_set_to_default_configuration( + devices, readonly=False + ) # readonly=False, only at first time after init + # plot statistics data + logger.info("Plot statistics data") + plot.plot_statistics(devices) + + if silent and original_log_level != logging.NOTSET: + logger.setLevel(original_log_level) + + return found_error + + +def initialise(devices, timeout=90): + """Initialise the system + + :param timeout: Timeout to flag an error in seconds + :return: true if error, false otherwise + """ + + devices = common.devices_list_2_dict(devices) + boot = devices["boot"] + recv = devices["recv"] + sst = devices["sst"] + unb2 = devices["unb2"] + + # initialise boot device + boot.put_property({"DeviceProxy_Time_Out": 60}) + boot.off() + boot.initialise() + boot.on() + + # increase timeouts for reboot + logger.debug("Time out settings prior to increase for reboot:") + logger.debug(recv.get_timeout_millis()) + logger.debug(unb2.get_timeout_millis()) + logger.debug(sst.get_timeout_millis()) + + recv.set_timeout_millis(30000) + unb2.set_timeout_millis(60000) + sst.set_timeout_millis(20000) + + unb2.put_property({"UNB2_On_Off_timeout": 20}) + recv.put_property({"RCU_On_Off_timeout": 60}) + + # reboot system + boot.reboot() + logger.info("Rebooting now") + time_start = time.time() + time_dt = time.time() - time_start + + # indicate progress + while boot.booting_R and (time_dt < timeout): + if time_dt % 5 < 1: # print every 5 seconds + logger.info("Initialisation at %f%%: %s", boot.progress_R, boot.status_R) + time.sleep(1) + time_dt = time.time() - time_start + logger.info("Initialisation took %d seconds", time_dt) + # check for errors + found_error = False + if boot.booting_R: + found_error = True + logger.warning( + "Warning! Initialisation still ongoing after timeout (%d sec)", timeout + ) + if boot.uninitialised_devices_R: + found_error = True + logger.error("Warning! Did not initialise %s.", boot.uninitialised_devices_R) + return found_error + + +def check_initialise(devices): + """Check the initialisation of the system by an independent monitoring point. + + Note that this method should be used after calling initialise() + + All checks that are directly related to the boot device are already + in the method initialise() + """ + + found_error = False + found_error = found_error | check_firmware_loaded(devices) + found_error = found_error | check_clock_locked(devices) + return found_error + + +def check_firmware_loaded(devices): + """Verify that the firmware has loaded in the fpgas + + :return: True if not loaded, False otherwise + """ + devices = common.devices_list_2_dict(devices) + sdp = devices["sdp"] + res = sdp.FPGA_boot_image_R + logger.debug("") + logger.debug("Checking sdp.FPGA_boot_image_R:") + logger.debug("Reply: %s", res) + if not np.array_equal(res[0 : constants.N_FPGA], np.ones(constants.N_FPGA)): + logger.debug("Firmware is not loaded!") + return True + logger.debug("Ok - Firmware loaded") + return False + + +def check_clock_locked(devices): + """Verify that the APSCT clock is locked + + :return: True if not loaded, False otherwise + """ + + devices = common.devices_list_2_dict(devices) + apsct = devices["apsct"] + res = apsct.APSCT_PLL_200MHz_error_R + + logger.debug("") + logger.debug("Checking apsct.APSCT_PLL_200MHz_error_R:") + logger.debug("Reply: %s", res) + if res: + logger.debug("APSCT clock not locked!") + return True + logger.debug("Ok - APSCT clock locked") + return False + + +def set_to_default_configuration(devices): + """Set the station to its default configuration + + :return: True if error found, False otherwise + """ + + found_error = False + logger.info("Start configuring Low Receivers") + found_error = found_error | set_rcul_to_default_config(devices) + logger.info("Start configuring High Receivers") + found_error = found_error | set_rcuh_to_default_config(devices) + logger.info("Start configuring Station Digital Processor") + found_error = found_error | set_sdp_to_default_config(devices) + logger.info("Start configuring Subband Statistics") + found_error = found_error | set_sdp_sst_to_default_config(devices) + logger.info("Start configuring Crosslet Statistics") + found_error = found_error | set_sdp_xst_to_default_config(devices) + logger.info("Start configuring Beamlet Statistics") + found_error = found_error | set_sdp_bst_to_default_config(devices) + return found_error + + +def set_rcul_to_default_config(devices): + """Set RCU Low to its default configuration""" + + devices = common.devices_list_2_dict(devices) + recv = devices["recv"] + + # Set RCUs Lows in default modes + + recv.set_defaults() + + # Set attenuator, antenna power etc. + recv.RCU_band_select_RW = set_rcu_ant_masked( + recv, constants.LBA_MASK, recv.RCU_band_select_R, [[1] * 3] * 32 + ) # 1 = 30-80 MHz + recv.RCU_PWR_ANT_on_RW = set_rcu_ant_masked( + recv, constants.LBA_MASK, recv.RCU_PWR_ANT_on_R, [[True] * 3] * 32 + ) # Off + recv.RCU_attenuator_dB_RW = set_rcu_ant_masked( + recv, constants.LBA_MASK, recv.RCU_attenuator_dB_R, [[0] * 3] * 32 + ) # 0dB attenuator + wait_rcu_receiver_busy(recv) + + # + found_error = False + return found_error + + +def set_rcuh_to_default_config(devices): + """Set RCU High to its default configuration""" + + devices = common.devices_list_2_dict(devices) + recv = devices["recv"] + + # Setup HBA RCUs and HBA Tiles + + # Set RCU in correct mode + rcu_modes = recv.RCU_band_select_R + rcu_modes[8] = [2, 2, 2] # 2 = 110-180 MHz, 1 = 170-230 MHz, 4 = 210-270 MHz ?? + rcu_modes[9] = [2, 2, 2] + recv.RCU_band_select_RW = set_rcu_ant_masked( + recv, constants.HBA_MASK, recv.RCU_band_select_R, rcu_modes + ) + wait_rcu_receiver_busy(recv) + + # Switch on the Antenna + recv.RCU_PWR_ANT_on_RW = set_rcu_ant_masked( + recv, constants.HBA_PWR_MASK, recv.RCU_PWR_ANT_on_R, [[False] * 3] * 32 + ) # Switch off + wait_rcu_receiver_busy(recv) + recv.HBAT_PWR_LNA_on_RW = set_rcu_hba_mask( + recv, constants.HBA_CTL_MASK, recv.HBAT_PWR_LNA_on_R, [[True] * 32] * 96 + ) # LNA default on + recv.HBAT_BF_delay_steps_RW = set_rcu_hba_mask( + recv, constants.HBA_CTL_MASK, recv.HBAT_BF_delay_steps_R, [[0] * 32] * 96 + ) # Default + recv.HBAT_PWR_on_RW = set_rcu_hba_mask( + recv, constants.HBA_CTL_MASK, recv.HBAT_PWR_on_R, [[True] * 32] * 96 + ) # Default + wait_rcu_receiver_busy(recv) + recv.RCU_PWR_ANT_on_RW = set_rcu_ant_masked( + recv, constants.HBA_PWR_MASK, recv.RCU_PWR_ANT_on_R, [[True] * 3] * 32 + ) # Switch on + wait_rcu_receiver_busy(recv) + # default in the tile: after power-on, zero delay + # delays should be set by the tile beam device + # recv.HBAT_BF_delay_steps_RW=[[1]*32]*32 + # wait_receiver_busy(recv) + # by default: equal delay settings for all elements --> Pointing to zenith + + # TODO(Boudewijn): read values and only update the ones that need to be changed. + # Could be a function that manages the masks. + logging.info(recv.RCU_PWR_ANT_on_RW) + logging.info("") + logging.info("done") + + found_error = False + return found_error + + +#### +# RCU specific functions and variables for DTS-Outside +#### + +# General RCU functions + + +def wait_rcu_receiver_busy(recv): + """Wait for the Receiver Translators busy monitoring point to return False""" + + while recv.RECVTR_translator_busy_R: + time.sleep(0.1) + + +def set_rcu_ant_masked(recv, mask, old_value, new_value): + """Set the antenna mask for the Receiver Translator""" + + recv.ANT_mask_RW = mask + + for idx, mask_elem in enumerate(mask): + # TODO(Boudewijn): Check that range(3) still is correct dimensionality + for rcu_input_index in range(3): + if mask_elem[rcu_input_index]: + old_value[idx][rcu_input_index] = new_value[idx][rcu_input_index] + return old_value + + +def set_rcu_hba_mask(recv, mask, old_value, new_value): + """Set the hba mask for the Receiver Translator + + :param mask: Takes flattened one dimensional array of values + :param old_value: Takes flattened one dimensional array of values + :param new_value: Takes flattened one dimensional array of values + """ + + recv.ANT_mask_RW = mask + for idx, mask_elem in enumerate(mask): + # TODO(Boudewijn): Check that range(3) still is correct dimensionality + for i in range(3): + if mask_elem[i]: + old_value[idx * 3 + i] = new_value[idx * 3 + i] + else: + old_value[idx * 3 + i] = old_value[idx * 3 + i] + return old_value + + +def set_sdp_to_default_config(devices): + """Set RCU High to its default configuration + + TODO(Corne): Update this docstring to reflect actual method functionality + """ + + devices = common.devices_list_2_dict(devices) + sdp = devices["sdp"] + # + # Set SDP in default modes + # + + logging.info("Start configuring SDP to default") + sdp.set_defaults() + # should be part of sdp.set_defaults: + next_ring = [False] * 16 + next_ring[3] = True + sdp.FPGA_ring_use_cable_to_previous_rn_RW = [True] + [False] * 15 + sdp.FPGA_ring_use_cable_to_next_rn_RW = next_ring + sdp.FPGA_ring_nof_nodes_RW = [constants.N_FPGA] * 16 + sdp.FPGA_ring_node_offset_RW = [0] * 16 + + # + found_error = False + return found_error + + +def set_sdp_sst_to_default_config(devices): + """Set SSTs to default""" + + devices = common.devices_list_2_dict(devices) + sst = devices["sst"] + + sst.set_defaults() + # prepare for subband stati + sst.FPGA_sst_offload_weighted_subbands_RW = [ + True + ] * 16 # should be in set_defaults() + + # + found_error = False + return found_error + + +def set_sdp_xst_to_default_config(devices): + """Set XSTs to default""" + + devices = common.devices_list_2_dict(devices) + xst = devices["xst"] + + # prepare for correlations + int_time = 1 # used for 'medium' measurement using statistics writer + subband_step_size = 7 # 0 is no stepping + n_xst_subbands = 7 + subband_select = [subband_step_size, 0, 1, 2, 3, 4, 5, 6] + + xst.set_defaults() + # should be part of set_defaults() + xst.FPGA_xst_processing_enable_RW = [False] * 16 + # this (line above) should be first, then configure and enable + xst.fpga_xst_ring_nof_transport_hops_RW = [3] * 16 # [((n_fgpa/2)+1)]*16 + + crosslets = [0] * 16 + for fpga_nr in range(constants.N_FPGA): + crosslets[fpga_nr] = n_xst_subbands + xst.FPGA_xst_offload_nof_crosslets_RW = crosslets + + xst.FPGA_xst_subband_select_RW = [subband_select] * 16 + xst.FPGA_xst_integration_interval_RW = [int_time] * 16 + + xst.FPGA_xst_processing_enable_RW = [True] * 16 + # + found_error = False + return found_error + + +def set_sdp_bst_to_default_config(devices): + """Set XSTs to default""" + + devices = common.devices_list_2_dict(devices) + beamlet = devices["beamlet"] + + # prepare for beamformer + beamlet.set_defaults() + + beamlet.FPGA_bf_ring_nof_transport_hops_RW = [[1]] * 3 + [[0]] * 13 + # line above should be part of set_defaults(), specifically for DTS-Outside + + beamletoutput_mask = [False] * 16 + beamletoutput_mask[3] = True + # beamlet.FPGA_beamlet_output_enable_RW=beamletoutput_mask + beamlet.FPGA_beamlet_output_hdr_eth_destination_mac_RW = ["40:a6:b7:2d:4f:68"] * 16 + beamlet.FPGA_beamlet_output_hdr_ip_destination_address_RW = ["192.168.0.249"] * 16 + + # define weights + # dim: FPGA, input, subband + weights_xx = beamlet.FPGA_bf_weights_xx_R.reshape([16, 6, 488]) + weights_yy = beamlet.FPGA_bf_weights_yy_R.reshape([16, 6, 488]) + # make a beam for the HBA + weights_xx[:][:] = 0 + weights_yy[:][:] = 0 + for signal_index in [0, 1]: # range(8*3,9*3+3): + unb2i = signal_index // 12 + signal_index_2 = signal_index // 2 + signal_index_2 = signal_index_2 % 6 + if signal_index % 2 == 0: + weights_xx[unb2i, signal_index_2] = 16384 + else: + weights_yy[unb2i, signal_index_2] = 16384 + + beamlet.FPGA_bf_weights_xx_RW = weights_xx.reshape([16, 6 * 488]) + beamlet.FPGA_bf_weights_yy_RW = weights_yy.reshape([16, 6 * 488]) + + blt = beamlet.FPGA_beamlet_subband_select_R.reshape([16, 12, 488]) + blt[:] = np.array(range(10, 10 + 488))[np.newaxis, np.newaxis, :] + beamlet.FPGA_beamlet_subband_select_RW = blt.reshape([16, 12 * 488]) + + # + found_error = False + return found_error + + +def enable_outputs( + devices, enable_sst=True, enable_xst=True, enable_bst=True, enable_beam_output=True +): + """Enable the station outputs after everything is set.""" + + devices = common.devices_list_2_dict(devices) + if enable_beam_output: + sdp = devices["sdp"] + sdp.FPGA_processing_enable_RW = [True] * 16 + if enable_sst: + sst = devices["sst"] + sst.FPGA_sst_offload_enable_RW = [True] * 16 + if enable_xst: + xst = devices["xst"] + xst.FPGA_xst_offload_enable_R = [True] * 16 + if enable_bst: + beamlet = devices["beamlet"] + beamlet.FPGA_beamlet_output_enable_RW = [i == 3 for i in range(16)] + + # + found_error = False + return found_error + + +def check_set_to_default_configuration(devices, readonly=True): + """Check the default configuration of the system to be set correctly.""" + + found_error = False + found_error = found_error | check_default_configuration_low_receivers(devices) + found_error = found_error | check_default_configuration_hbat( + devices, readonly=readonly + ) + found_error = found_error | check_rcu_fpga_interface(devices, si_to_check=range(18)) + found_error = found_error | check_set_sdp_xst_to_default_config( + devices, lane_mask=range(0, 6) + ) + + plot.print_fpga_input_statistics(devices) + + return found_error + + +def check_default_configuration_low_receivers(devices, mask=None): + """Check that the LNAs are on""" + + devices = common.devices_list_2_dict(devices) + recv = devices["recv"] + found_error = False + + if mask is None: + mask = constants.LBA_MASK + + res = recv.RCU_PWR_ANT_on_R + if len(mask) > 0: + res = res[mask] + logging.debug("") + logging.debug("Checking recv.RCU_PWR_ANT_on_R:") + logging.debug("Reply%s: %s}", " (masked)" if len(mask) > 0 else "", res) + if not res.all(): + logging.info("One or more LBAs are powered off!") + found_error = True + else: + logging.debug("Ok - Low Receivers operational") + return found_error + + +def check_default_configuration_hbat(devices, readonly=True): + """Check that the HBA Tiles are on""" + + devices = common.devices_list_2_dict(devices) + recv = devices["recv"] + hbat_si = [rcu * 3 + r_input for rcu in constants.RCU2HPWR for r_input in range(3)] + if not readonly: + recv.RECVTR_monitor_rate_RW = 1 # for this checking + time.sleep(3) + + found_error = False + found_error = found_error | check_rcu_vin(devices, si_mask=hbat_si) + found_error = found_error | check_hbat_vout(devices, si_mask=hbat_si) + found_error = found_error | check_hbat_iout(devices, si_mask=hbat_si) + + if not readonly: + recv.RECVTR_monitor_rate_RW = 10 + + # TODO(Boudewijn): - what is checked for here? + logging.info(recv.HBAT_PWR_LNA_on_R[8:10]) + logging.info(recv.ANT_mask_RW[8:10]) + + return found_error + + +def check_rcu_vin(devices, si_mask=None, valid_range=None): + """Verify HBA Tile is powered by comparing V_in against signal inputs thresholds. + + If no si_mask is given, all returned values are compared. + """ + + devices = common.devices_list_2_dict(devices) + recv = devices["recv"] + if si_mask is None: + si_mask = [] + if valid_range is None: + valid_range = [30, 50] + + res = recv.RCU_PWR_ANT_VIN_R + if len(si_mask) > 0: + res = [res[si // 3][si % 3] for si in si_mask] + logger.debug("") + logger.debug("Checking recv.RCU_PWR_ANT_VIN_R:") + logger.debug("Reply%s: %s", " (masked)" if len(si_mask) > 0 else "", res) + if not ( + np.all(valid_range[0] < np.array(res)) + and np.all(np.array(res) < valid_range[1]) + ): + logging.warning("No power at input RCU2!") + return True + + logging.debug("Ok - Power at input RCU2") + return False + + +def check_hbat_vout(devices, si_mask=None, valid_range=None): + """Verify HBA Tile is powered by comparing V_out against signal input thresholds + + If no si_mask is given, all returned values are compared. + """ + + # Vout should be 48 +- 1 V + devices = common.devices_list_2_dict(devices) + + recv = devices["recv"] + if si_mask is None: + si_mask = [] + if valid_range is None: + valid_range = [30, 50] + + res = recv.RCU_PWR_ANT_VOUT_R + if len(si_mask) > 0: + res = [res[si // 3][si % 3] for si in si_mask] + + logging.debug("") + logging.debug("Checking recv.RCU_PWR_ANT_VOUT_R:") + logging.debug("Reply%s: %s", " (masked)" if len(si_mask) > 0 else "", res) + + if not ( + np.all(valid_range[0] < np.array(res)) + and np.all(np.array(res) < valid_range[1]) + ): + logging.warning("Not all HBA Tiles powered on!") + return True + + logging.debug("Ok - HBA Tiles powered on") + return False + + +def check_hbat_iout(devices, si_mask=None, valid_range=None): + """Verify HBA Tile is powered by comparing I_out against signal input thresholds + + If no si_mask is given, all returned values are compared. + """ + + # Iout = 0.7 +- 0.1 Amp + devices = common.devices_list_2_dict(devices) + recv = devices["recv"] + if si_mask is None: + si_mask = [] + if valid_range is None: + valid_range = [0.6, 0.8] + + res = recv.RCU_PWR_ANT_IOUT_R + if len(si_mask) > 0: + res = [res[si // 3][si % 3] for si in si_mask] + logging.debug("") + logging.debug("Checking recv.RCU_PWR_ANT_IOUT_R:") + logging.debug("Reply%s: %s", " (masked)" if len(si_mask) > 0 else "", res) + if not ( + np.all(valid_range[0] < np.array(res)) + and np.all(np.array(res) < valid_range[1]) + ): + logging.warning("Not all HBA Tiles powered on (draw current)!") + return True + + logging.debug("Ok - HBA Tiles powered on (draw current)") + return False + + +def check_rcu_fpga_interface(devices, si_to_check=range(18)): + """Function to check the RCU - FPGA interface + + if error occurs, try to reboot by resetting Uniboard (switch) + if only one works than the APSCT board is not making contact with the backplane. + (Can take up to 3 times to get it right..) + 23/6/2022T15:42 errorcode 0x4h: xxxxx on all of them. + Error descriptions can be found here: + https://support.astron.nl/jira/browse/L2SDP-774 + """ + devices = common.devices_list_2_dict(devices) + sdp = devices["sdp"] + found_error = False + # TODO(Boudewijn): the number of calls can be reduced + for signal_index in si_to_check: + lock = sdp.FPGA_jesd204b_csr_dev_syncn_R[signal_index // 12][signal_index % 12] + if lock != 1: + found_error = True + print(f"Transceiver SI: {signal_index} is not locked {lock}") + err0 = sdp.FPGA_jesd204b_rx_err0_R[signal_index // 12][signal_index % 12] + if err0 != 0: + found_error = True + print(f"SI {signal_index} has JESD err0: 0x{err0:x}h") + err1 = sdp.FPGA_jesd204b_rx_err1_R[signal_index // 12][signal_index % 12] + if (err1 & 0x02FF) != 0: + found_error = True + print(f"SI {signal_index} has JESD err1: 0x{err1:x}h") + return found_error + + +def check_set_sdp_xst_to_default_config( + devices, lane_mask=None, time_1=None, time_dt=2.5 +): + """Check sdp/xst configuration by checking the returned timestamps for: + + 1. all being the same + 2. not epoch zero (at 1 Jan 1970 00:00:00) + 3. within 5 seconds from the local clock timestamp (now +/- 2.5 sec) + + :param time_1: number of seconds since the epoch to compare against + Default: None --> now() + :param time_dt: delta in seconds around time_1, in which the xst timestamps + should fall. + """ + + devices = common.devices_list_2_dict(devices) + xst = devices["xst"] + if lane_mask is None: + lane_mask = [] + if time_1 is None: + time_1 = datetime.datetime.now().timestamp() + res = xst.xst_timestamp_R + if len(lane_mask) > 0: # mask lanes + res = [res[lane] for lane in lane_mask] + logging.debug("") + logging.debug("Checking xst.xst_timestamp_R:") + logging.debug("Reply%s: %s", " (masked)" if len(lane_mask) > 0 else "", res) + logging.debug("Converted:") + logging.debug( + "\n".join([f"{time.asctime(time.gmtime(xst_time))}" for xst_time in res]) + ) + if not (res == res[0]).all(): + logging.warning("Not all xst timestamps are equal!") + return True + if res[0] == 0: + logging.warning("One or more xst timestamps are zero!") + return True + if abs(res[0] - time_1) > time_dt: + logging.warning( + "One or more xst timestamps are outside the specified time window!" + ) + return True + logging.debug("Ok - xst configured correctly") + return False diff --git a/lofar_station_client/dts/plot.py b/lofar_station_client/dts/plot.py new file mode 100644 index 0000000000000000000000000000000000000000..4f28156d5f0de4b72abeaf1fd72468ffe6d998d5 --- /dev/null +++ b/lofar_station_client/dts/plot.py @@ -0,0 +1,617 @@ +# -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Plotting functionality for DTS""" + +# pylint: disable=R0914,C0209,R0915,R0913 + +import logging + +import numpy as np +import matplotlib.pyplot as plt + +from lofar_station_client.dts import bands +from lofar_station_client.dts import constants +from lofar_station_client.dts import common +from lofar_station_client.dts import index as plib + +logger = logging.getLogger() + + +def print_fpga_input_statistics(devices): + """Print FPGA input statistics""" + devices = common.devices_list_2_dict(devices) + sdp = devices["sdp"] + + # get data from device + dc_offset = sdp.FPGA_signal_input_mean_R + signal_input_rms = sdp.FPGA_signal_input_rms_R + lock_to_adc = sdp.FPGA_jesd204b_csr_dev_syncn_R + err0 = sdp.FPGA_jesd204b_rx_err0_R + err1 = sdp.FPGA_jesd204b_rx_err1_R + count = sdp.FPGA_jesd204b_csr_rbd_count_R + signal_index = np.reshape(np.arange(count.shape[0] * count.shape[1]), count.shape) + # get some info on the hardware connections + pn_index = plib.signal_index_2_processing_node_index(signal_index) + pn_input_index = plib.signal_index_2_processing_node_input_index(signal_index) + rcu_index = plib.signal_index_2_rcu_index(signal_index) + rcu_input_index = plib.signal_index_2_rcu_input_index(signal_index) + + # print + print("+------------+-------------+----------------------------------------------+") + print("| Input | Processed by| Statistics of input |") + print("+---+---+----+------+------+---------+---------+-----+------+------+------+") + print("|RCU|RCU| SI | Node | Node | DC | RMS | Lock| Err0 | Err1 | Count|") + print("|idx|inp| | Index| Input| Offset | (LSB) | | (0x) | (0x) | (0x) |") + print("| |idx| | | Index| (LSB) | | | | | |") + print("+---+---+----+------+------+---------+---------+-----+------+------+------+") + pfmt = ( + "| %02d| %1d |%3d | %02d | %2d | %7.2f | %7.2f | %3s | %04x | %04x | %04x |" + ) + for rcui, rcuii, signali, pni, pnii, dco, rms, lock, err_0, err_1, cnt in zip( + rcu_index.flatten(), + rcu_input_index.flatten(), + signal_index.flatten(), + pn_index.flatten(), + pn_input_index.flatten(), + dc_offset.flatten(), + signal_input_rms.flatten(), + lock_to_adc.flatten(), + err0.flatten(), + err1.flatten(), + count.flatten(), + ): + print( + pfmt % (rcui, rcuii, signali, pni, pnii, dco, rms, lock, err_0, err_1, cnt) + ) + print("+---+---+----+------+------+---------+---------+-----+------+------+------+") + + +def report_setup_configuration(devices, save_fig=True): + """Read name and version data from the setup and print that as a report""" + devices = common.devices_list_2_dict(devices) + recv = devices["recv"] + apsct = devices["apsct"] + apspu = devices["apspu"] + unb2 = devices["unb2"] + sdp = devices["sdp"] + # Receiver / High Band Antenna Tile + + # APS Hardware + # APS / Receiver Unit2 side + rcu2_pcb_ids = recv.RCU_PCB_ID_R.tolist() + rcu2_pcb_nrs = list(recv.RCU_PCB_number_R) + rcu2_pcb_vrs = list(recv.RCU_PCB_version_R) + + # APS / UniBoard2 side + # TODO(Boudewijn): FIXME - shouldn't this be a list with multiple entries? + # See also LOFAR2-10774 (APSCT) + apsct_pcb_ids = [apsct.APSCT_PCB_ID_R] + apsct_pcb_nrs = [apsct.APSCT_PCB_number_R] + apsct_pcb_vrs = [apsct.APSCT_PCB_version_R] + # TODO(Boudewijn): FIXME - shoudln't this be a list with multiple entries? + # See also LOFAR2-11434 (APSPU) + apspu_pcb_ids = [apspu.APSPU_PCB_ID_R] + apspu_pcb_nrs = [apspu.APSPU_PCB_number_R] + apspu_pcb_vrs = [apspu.APSPU_PCB_version_R] + unb2_pcb_ids = unb2.UNB2_PCB_ID_R.tolist() + unb2_pcb_nrs = list(unb2.UNB2_PCB_number_R) + unb2_pcb_vrs = list(sdp.FPGA_hardware_version_R) + # TODO(Boudewijn): FIXME: + # FPGA_hardware_version_R returns hardware version info per FPGA. Hardware is shared + # per 4 fpgas, so why repeating values here? + unb2_pcb_vrs = [unb2_pcb_vrs[idx] for idx in [0, 4, 8, 12]] + + # TODO(Boudewijn): FIXME: + # fixing list lengths for a large station with multiple APS + # this makes sure that the list indexing below works + n_aps = 4 + + apsct_pcb_ids = apsct_pcb_ids + [0] * (n_aps - len(apsct_pcb_ids)) + apsct_pcb_nrs = apsct_pcb_nrs + [""] * (n_aps - len(apsct_pcb_nrs)) + apsct_pcb_vrs = apsct_pcb_vrs + [""] * (n_aps - len(apsct_pcb_vrs)) + + apspu_pcb_ids = apspu_pcb_ids + [0] * (n_aps * 2 - len(apspu_pcb_ids)) + apspu_pcb_nrs = apspu_pcb_nrs + [""] * (n_aps * 2 - len(apspu_pcb_nrs)) + apspu_pcb_vrs = apspu_pcb_vrs + [""] * (n_aps * 2 - len(apspu_pcb_vrs)) + + unb2_pcb_ids = unb2_pcb_ids + [0] * (n_aps * 2 - len(unb2_pcb_ids)) + unb2_pcb_nrs = unb2_pcb_nrs + [""] * (n_aps * 2 - len(unb2_pcb_nrs)) + unb2_pcb_vrs = unb2_pcb_vrs + [""] * (n_aps * 2 - len(unb2_pcb_vrs)) + + logging.debug("APSPU:") + logging.debug("apsct_pcb_ids: %s", apsct_pcb_ids) + logging.debug("apsct_pcb_nrs: %s", apsct_pcb_nrs) + logging.debug("apsct_pcb_vrs: %s", apsct_pcb_vrs) + logging.debug("APSPU:") + logging.debug("apspu_pcb_ids: %s", apspu_pcb_ids) + logging.debug("apspu_pcb_nrs: %s", apspu_pcb_nrs) + logging.debug("apspu_pcb_vrs: %s", apspu_pcb_vrs) + logging.debug("UNB2:") + logging.debug("unb2_pcb_ids: %s", unb2_pcb_ids) + logging.debug("unb2_pcb_nrs: %s", unb2_pcb_nrs) + logging.debug("unb2_pcb_vrs: %s", unb2_pcb_vrs) + + aps_pcb_ids = [ + apspu_pcb_ids[1], + unb2_pcb_ids[0], + apsct_pcb_ids[0], + apspu_pcb_ids[0], + unb2_pcb_ids[1], + ] + aps_pcb_nrs = [ + apspu_pcb_nrs[1], + unb2_pcb_nrs[0], + apsct_pcb_nrs[0], + apspu_pcb_nrs[0], + unb2_pcb_nrs[1], + ] + aps_pcb_vers = [ + apsct_pcb_vrs[1], + unb2_pcb_vrs[0], + apsct_pcb_vrs[0], + apsct_pcb_vrs[0], + unb2_pcb_vrs[1], + ] + + if not save_fig: + fig_filenames = [] + else: + fig_filenames = [ + f'{constants.PLOT_DIR}/{plib.get_timestamp("filename")}_aps_config.png', + f'{constants.PLOT_DIR}/{plib.get_timestamp("filename")}_aps_config.pdf', + ] + plot_aps_configuration( + rcu2_pcb_ids, + rcu2_pcb_nrs, + rcu2_pcb_vrs, + aps_pcb_ids, + aps_pcb_nrs, + aps_pcb_vers, + filenames=fig_filenames, + ) + + # Services + # Firmware images + firmware_versions = list(sdp.FPGA_firmware_version_R) + # make sure there are versions for at least n_aps subracks + firmware_versions = firmware_versions + [""] * (n_aps * 8 - len(firmware_versions)) + aps_services = {} + for apsi, fpgai in zip(sorted(list(range(n_aps)) * 8), range(n_aps * 8)): + aps_services["aps%i-fpga%02d" % (apsi, fpgai)] = firmware_versions[fpgai] + aps_services[f"aps{apsi}-unb2tr" % apsi] = "" # TODO(Boudewijn): + aps_services[f"aps{apsi}-recvtr" % apsi] = "" # TODO(Boudewijn): + aps_services[f"aps{apsi}-apscttr" % apsi] = "" # TODO(Boudewijn): + aps_services[f"aps{apsi}-apsputr" % apsi] = "" # TODO(Boudewijn): + # Translators + sdptr_software_versions = [sdp.TR_software_version_R] + sdptr_software_versions = sdptr_software_versions + [""] * ( + 2 - len(sdptr_software_versions) + ) + station_services = {} + station_services["sdptr0"] = sdptr_software_versions[0] + station_services["sdptr1"] = sdptr_software_versions[1] + station_services["ccdtr"] = "" # TODO(Boudewijn): currently missing + # station control tango devices + for dev in devices: + station_services[devices[dev].name().lower()] = devices[dev].version_R + if not save_fig: + figs = [] + else: + figs = [ + f"{constants.PLOT_DIR}/{plib.get_timestamp('filename')}" + "_station_services.png", + f"{constants.PLOT_DIR}/{plib.get_timestamp('filename')}" + "_station_services.pdf", + ] + plot_services_configuration(aps_services, station_services, filenames=figs) + + +def plot_aps_configuration( + rcu_pcb_identifiers, + rcu_pcb_numbers, + rcu_pcb_versions, + aps_pcb_ids, + aps_pcb_nrs, + aps_pcb_vers, + filenames=None, +): + """Plot the configuration of the Antenna Processing Subrack, both sides.""" + + fig, axs = plt.subplots(1, 2, figsize=(20, 8)) + + if filenames is None: + filenames = [ + f"{constants.PLOT_DIR}/aps_config.png", + f"{constants.PLOT_DIR}/aps_config.pdf", + ] + + plot_aps_rcu2_configuration( + axs[0], rcu_pcb_identifiers, rcu_pcb_numbers, rcu_pcb_versions + ) + plot_aps_unb2_configuration(axs[1], aps_pcb_ids, aps_pcb_nrs, aps_pcb_vers) + for filename in filenames: + fig.savefig(filename) + fig.show() + + +def plot_aps_rcu2_configuration( + plot_ax, + rcu_pcb_identifiers, + rcu_pcb_numbers, + rcu_pcb_versions, + rcu_locations=constants.APS_LOCATION_LABELS, + rcu_pos_x=None, + rcu_pos_y=None, +): + """Plot the configuration of the Antenna Processing Subrack, RCU2 side.""" + + if rcu_pos_x is None: + rcu_pos_x = constants.APS_RCU_POS_X + if rcu_pos_y is None: + rcu_pos_y = constants.APS_RCU_POS_Y + plot_ax.plot(0, 0) + plot_dx = plot_dy = 0.9 + plot_ax.set_xlim(-1 + plot_dx, np.max(rcu_pos_x) + 1) + plot_ax.set_ylim(-1 + plot_dy, np.max(rcu_pos_y) + 1) + + for plot_x, plot_y, loc, pcb_id, pcb_nr, pcb_v in zip( + rcu_pos_x, + rcu_pos_y, + rcu_locations, + rcu_pcb_identifiers, + rcu_pcb_numbers, + rcu_pcb_versions, + ): + plot_ax.plot( + [plot_x, plot_x + plot_dx, plot_x + plot_dx, plot_x, plot_x], + [plot_y, plot_y, plot_y + plot_dy, plot_y + plot_dy, plot_y], + color="black", + alpha=0.2, + ) + plot_ax.text(plot_x + plot_dx / 2, plot_y + plot_dy, loc, va="top", ha="center") + plot_ax.text( + plot_x + plot_dx / 2, + plot_y + plot_dy / 2, + "%s\n%s\n%s" % (pcb_v, pcb_id, pcb_nr), + va="center", + ha="center", + rotation=90, + ) + if pcb_id == 0 and pcb_v == "" and pcb_nr == "": + plot_ax.plot( + [plot_x, plot_x + plot_dx], + [plot_y, plot_y + plot_dy], + color="black", + alpha=0.2, + ) + plot_ax.plot( + [plot_x + plot_dx, plot_x], + [plot_y, plot_y + plot_dy], + color="black", + alpha=0.2, + ) + plot_ax.set_title("Configuration of RCU2s") + plot_ax.set_xticks([]) + plot_ax.set_yticks([]) + + +def plot_aps_unb2_configuration( + plot_ax, + aps_pcb_identifiers, + aps_pcb_numbers, + aps_pcb_versions, + mod_locations=constants.APS_LOCATION_LABELS, + mod_pos_x=None, + mod_pos_y=None, +): + """Plot the configuration of the Antenna Processing Subrack, UNB2 side.""" + + if mod_pos_x is None: + mod_pos_x = constants.APS_MOD_POS_X + if mod_pos_y is None: + mod_pos_y = constants.APS_MOD_POS_Y + plot_ax.plot(0, 0) + plot_dx = 0.9 + plot_dy = 3.9 + plot_ax.set_xlim(-1 + plot_dx, np.max(mod_pos_x) + 2) + plot_ax.set_ylim(-1 + plot_dx, np.max(mod_pos_y) + 4) + + for plot_x, plot_y, loc, pcb_id, pcb_nr, pcb_v in zip( + mod_pos_x, + mod_pos_y, + mod_locations, + aps_pcb_identifiers, + aps_pcb_numbers, + aps_pcb_versions, + ): + plot_ax.plot( + [plot_x, plot_x + plot_dx, plot_x + plot_dx, plot_x, plot_x], + [plot_y, plot_y, plot_y + plot_dy, plot_y + plot_dy, plot_y], + color="black", + alpha=0.2, + ) + plot_ax.text(plot_x + plot_dx / 2, plot_y + plot_dy, loc, va="top", ha="center") + plot_ax.text( + plot_x + plot_dx / 2, + plot_y + plot_dy / 2, + "%s\n%s\n%s" % (pcb_v, pcb_id, pcb_nr), + va="center", + ha="center", + rotation=90, + ) + if pcb_id == 0 and pcb_v == "" and pcb_nr == "": + plot_ax.plot( + [plot_x, plot_x + plot_dx], + [plot_y, plot_y + plot_dy], + color="black", + alpha=0.2, + ) + plot_ax.plot( + [plot_x + plot_dx, plot_x], + [plot_y, plot_y + plot_dy], + color="black", + alpha=0.2, + ) + plot_ax.set_title("Configuration of UniBoard2 side") + plot_ax.set_xticks([]) + plot_ax.set_yticks([]) + + +def plot_services_configuration( + aps_services, + station_services, + filenames=None, +): + """Plot the service version information in the setup.""" + + if filenames is None: + filenames = [ + f"{constants.PLOT_DIR}/station_services.png", + f"{constants.PLOT_DIR}/station_services.pdf", + ] + + fig, plot_ax = plt.subplots(1, 1, figsize=(16, 15)) + + aps_service_dx = 0.9 + aps_service_dy = 0.9 + first_aps_service = sorted(aps_services)[0][5:] + plot_y = plot_y0 = len(aps_services) / 2 + 2 + for aps_service in sorted(aps_services): + plot_x = 0 if (aps_service[3] in "01") else 1 + plot_y = ( + plot_y0 + if (aps_service[5:] == first_aps_service and aps_service[3] in "02") + else plot_y - 1 + ) + plot_y = ( + plot_y - 1 + if (aps_service[5:] == first_aps_service and aps_service[3] in "13") + else plot_y + ) + plot_ax.plot( + [plot_x, plot_x + aps_service_dx, plot_x + aps_service_dx, plot_x, plot_x], + [plot_y, plot_y, plot_y + aps_service_dy, plot_y + aps_service_dy, plot_y], + color="black", + alpha=0.2, + ) + plot_ax.text( + plot_x, + plot_y + aps_service_dy / 2, + aps_service, + va="center", + ha="left", + color="black", + ) + plot_ax.text( + plot_x + aps_service_dx, + plot_y + aps_service_dy / 2, + aps_services[aps_service], + va="center", + ha="right", + color="blue", + ) + if aps_services[aps_service] == "": + plot_ax.plot( + [plot_x, plot_x + aps_service_dx], + [plot_y, plot_y + aps_service_dy], + color="black", + alpha=0.2, + ) + plot_ax.plot( + [plot_x + aps_service_dx, plot_x], + [plot_y, plot_y + aps_service_dy], + color="black", + alpha=0.2, + ) + + stat_service_dx = 1.9 + stat_service_dy = 0.9 + plot_x = 0 + plot_y = 0 + for stat_service in sorted(station_services): + plot_ax.plot( + [ + plot_x, + plot_x + stat_service_dx, + plot_x + stat_service_dx, + plot_x, + plot_x, + ], + [ + plot_y, + plot_y, + plot_y + stat_service_dy, + plot_y + stat_service_dy, + plot_y, + ], + color="black", + alpha=0.2, + ) + plot_ax.text( + plot_x, + plot_y + stat_service_dy / 2, + stat_service, + va="center", + ha="left", + color="black", + ) + plot_ax.text( + plot_x + stat_service_dx, + plot_y + stat_service_dy / 2, + station_services[stat_service], + va="center", + ha="right", + color="blue", + ) + if station_services[stat_service] == "": + plot_ax.plot( + [plot_x, plot_x + stat_service_dx], + [plot_y, plot_y + stat_service_dy], + color="black", + alpha=0.2, + ) + plot_ax.plot( + [plot_x + stat_service_dx, plot_x], + [plot_y, plot_y + stat_service_dy], + color="black", + alpha=0.2, + ) + plot_y -= 1 + plot_ax.set_title("Services in setup") + plot_ax.set_xticks([]) + plot_ax.set_yticks([]) + + for filename in filenames: + fig.savefig(filename) + fig.show() + + +def plot_statistics(devices, save_fig=True): + """Plot the statistics: SST, XST and BST""" + + logging.info("Plot subband statistics") + if not save_fig: + figs = [] + else: + figs = [ + f"{constants.PLOT_DIR}/{plib.get_timestamp('filename')}" + "_subband_statistics.png", + f"{constants.PLOT_DIR}/{plib.get_timestamp('filename')}" + "_subband_statistics.pdf", + ] + plot_subband_statistics(devices, filenames=figs) + + # TODO(Boudewijn): + # if not silent: + # plib.log("Plot crosslet statistics") + # TODO(Boudewijn): + # if not silent: + # plib.log("Plot beamlet statistics") + + +def plot_subband_statistics( + devices, + xlim=None, + ylim=None, + filenames=None, +): + """Make the subband statistics plot""" + + if filenames is None: + filenames = [ + f"{constants.PLOT_DIR}/subband_statistics.png", + f"{constants.PLOT_DIR}/subband_statistics.pdf", + ] + + if xlim is None: + xlim = [-5, 305] + if ylim is None: + ylim = [-105, -30] + sst_data, band_names, frequency_axes = _get_subband_statistics(devices) + + fig, _ = plt.subplots(1, 1, figsize=(18, 6)) + for signal_index in range(len(sst_data)): + if band_names[signal_index] is None: + continue + freq = frequency_axes[band_names[signal_index]] + # plot data in dB full scale + plot_data = 10 * np.log10(sst_data[signal_index, :] + 1) - 128 - 6 * 4 + # since we're lacking antenna names, + # show the RCU input info as label + rcu_index, rcu_input_index = plib.signal_input_2_rcu_index_and_rcu_input_index( + signal_index + ) + label = "RCU%02d, Inp%d" % (rcu_index, rcu_input_index) + plt.plot(freq / 1e6, plot_data, label=label) + plt.grid() + plt.legend(bbox_to_anchor=(1, 1), loc="upper left") + plt.xlim(xlim) + plt.ylim(ylim) + plt.xlabel("Frequency (MHz)") + plt.ylabel("Power (dB full scale)") + plt.suptitle("Subband Statistics for all fully-connected receivers") + plt.title(f"{constants.STATION_NAME}; {plib.get_timestamp()}") + + for filename in filenames: + fig.savefig(filename) + fig.show() + + +def _get_subband_statistics(devices): + """Get subband statistics data from the setup, including proper axes and labels""" + + devices = common.devices_list_2_dict(devices) + recv = devices["recv"] + sst = devices["sst"] + + # get band names for the current configuration + cur_band_names = bands.get_band_names( + recv.RCU_band_select_R, + # TODO(Boudewijn): when correct LCNs are provided + # in "RCU_PCB_number_R", uncomment the + # following line: + # recv.RCU_PCB_number_R, + # and remove the following line: + recv.RCU_PCB_ID_R, + lb_tags=constants.RCU2L_TAGS, + hb_tags=constants.RCU2H_TAGS, + ) + + # replace the aliases of LB1 and LB2 into LB: (same frequency axis) + for alias in constants.LB_ALIASES: + while alias in cur_band_names: + cur_band_names[cur_band_names.index(alias)] = "LB" + # get frequency axes + + # TODO(Boudewijn): get frequency axes correct (indexing is incorrect/inconsequent) + key1 = "HB2" + key2 = "HB1" + tmp_key = "magic_band" + while key1 in cur_band_names: + cur_band_names[cur_band_names.index(key1)] = tmp_key + while key2 in cur_band_names: + cur_band_names[cur_band_names.index(key1)] = key1 + while tmp_key in cur_band_names: + cur_band_names[cur_band_names.index(tmp_key)] = key2 + + frequency_axes = {} + for band_name in bands.get_valid_band_names(): + if band_name in cur_band_names: + frequency_axes[band_name] = bands.get_frequency_for_band_name(band_name) + # get sst data + sst_data = sst.sst_r + # make sure header data is of correct size + cur_band_names = cur_band_names + [None] * (len(sst_data) - len(cur_band_names)) + return sst_data, cur_band_names, frequency_axes diff --git a/lofar_station_client/dts_outside.py b/lofar_station_client/dts_outside.py deleted file mode 100644 index 1fd015f5eba03d3d9331622fbf2185ae57d4152c..0000000000000000000000000000000000000000 --- a/lofar_station_client/dts_outside.py +++ /dev/null @@ -1,1420 +0,0 @@ -# dts_outside.py: Module with functions to restart dts outside and -# put it in a default configuration -# -# -*- coding: utf-8 -*- -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -""" module dts_outside - -This module contains functions to: -- restart the Dwingeloo Test Station (Outside) -- configure the Dwingeloo Test Station (Outside) to a defined default - -Boudewijn Hut -""" - -import sys -import time -import datetime -import os -import numpy as np -import matplotlib.pyplot as plt -import processing_lib as plib -import RCUs - - -# define some constants for the setup: -N_FPGA = 4 # number of fpgas - -APS_LOCATION_LABELS = [ - "Slot%02d" % nr for nr in range(32) # pylint: disable=consider-using-f-string -] -# positions of RCU boards. (0,0) is bottom, left -APS_RCU_POS_X = ( - [11, 10, 9, 8, 7, 6] - + [11, 10, 9, 8, 7] * 2 - + [4, 3, 2, 1, 0] * 2 - + [5, 4, 3, 2, 1, 0] -) -APS_RCU_POS_Y = [2] * 6 + [1] * 5 + [0] * 5 + [2] * 5 + [1] * 5 + [0] * 6 -# positions of the other modules in the APS -APS_MOD_POS_X = [0, 1, 3, 4, 5] -APS_MOD_POS_Y = [0] * 5 - -# if the names are correctly provided by the RCU2s, -# then update plot_subband_statistics() (see annotation there) -# and remove these two lines: -RCU2L_TAGS = ["8393812", "8416938", "8469028", "8386883", "8374523"] -RCU2H_TAGS = ["8514859", "8507272"] - -PLOT_DIR = "inspection_plots" -STATION_NAME = "DTS-Outside" - -# RCU Masks: -# LBA masks -RCU2L_MASK = [rcu_nr in RCUs.RCU2L for rcu_nr in range(32)] -LBA_MASK = [[True] * 3 if rcu_nr in RCUs.RCU2L else [False] * 3 for rcu_nr in range(32)] -# HBA masks -# signal inputs connected to HBAT power: -si_hbat_pwr = [signal_index * 3 + x for signal_index in RCUs.RCU2Hpwr for x in range(3)] -# signal inputs. HBAT control connect to first element: -si_nbat_ctl = [signal_index * 3 + x for signal_index in RCUs.RCU2Hctl for x in range(3)] -# -rcu_indices = range(32) -hba_rcu_mask_pwr = [rcui in RCUs.RCU2Hpwr for rcui in rcu_indices] -hba_rcu_mask_ctrl = [rcui in RCUs.RCU2Hctl for rcui in rcu_indices] -hba_PwrMask = [ - [True] * 3 if rcui in RCUs.RCU2Hpwr else [False] * 3 for rcui in rcu_indices -] -hba_CtlMask = [ - [True] * 3 if rcui in RCUs.RCU2Hctl else [False] * 3 for rcui in rcu_indices -] -hba_mask = [[True] * 3 if rcui in RCUs.RCU2H else [False] * 3 for rcui in rcu_indices] - -folders = [PLOT_DIR] -for folder in folders: - if not os.path.exists(folder): - os.makedirs(folder) - - -def init_and_set_to_default_config(devices, skip_boot=False, silent=False): - """ - Restart system and set to default configuration - - Input arguments: - devices = struct of software devices of m&c of the station - name: object - - Output arguments: - found_error = True if an error ocurred, False otherwise - """ - found_error = False - # initialisation - if not skip_boot: - if not silent: - plib.log("Start initialisation") - found_error = found_error | initialise(devices) - else: - plib.log("SKIPPING INITIALISATION (skip_init was set to True)") - if not silent: - plib.log("Check initialisation") - found_error = found_error | check_initialise(devices) - # configuring - if not silent: - plib.log("Start setting to default configuration") - found_error = found_error | set_to_default_configuration(devices, silent=silent) - if not silent: - plib.log("Check setting to default configuration") - found_error = found_error | check_set_to_default_configuration( - devices, readonly=False - ) # readonly=False, only at first time after init - # plot statistics data - if not silent: - plib.log("Plot statistics data") - plot_statistics(devices) - return found_error - - -def initialise(devices, timeout=90, debug_flag=False): - """ - Initialise the system - - Input arguments: - timeout = timeout to flag an error in seconds - - Output arguments: - found_error - """ - devices = devices_list_2_dict(devices) - boot = devices["boot"] - recv = devices["recv"] - sst = devices["sst"] - unb2 = devices["unb2"] - # initialise boot device - boot.put_property({"DeviceProxy_Time_Out": 60}) - boot.off() - boot.initialise() - boot.on() - # reboot system - # increase timeouts for that - if debug_flag: - print("Time out settings prior to increase for reboot:") - print(recv.get_timeout_millis()) - print(unb2.get_timeout_millis()) - print(sst.get_timeout_millis()) - recv.set_timeout_millis(30000) - unb2.set_timeout_millis(60000) - sst.set_timeout_millis(20000) - unb2.put_property({"UNB2_On_Off_timeout": 20}) - recv.put_property({"RCU_On_Off_timeout": 60}) - # and start reboot process - boot.reboot() - print("Rebooting now") - time_start = time.time() - time_dt = time.time() - time_start - # indicate progress - while boot.booting_R and (time_dt < timeout): - if time_dt % 5 < 1: # print every 5 seconds - print(f"Initialisation at {boot.progress_R}%: {boot.status_R}") - time.sleep(1) - time_dt = time.time() - time_start - print(f"Initialisation took {time_dt} seconds") - # check for errors - found_error = False - if boot.booting_R: - found_error = True - print(f"Warning! Initialisation still ongoing after timeout ({timeout} sec)") - if boot.uninitialised_devices_R: - found_error = True - print(f"Warning! Did not initialise {boot.uninitialised_devices_R}.") - return found_error - - -def check_initialise(devices): - """ - Check the initialisation of the system by an independent monitoring - point. - - Note that this method should be used after calling initialise() - - All checks that are directly related to the boot device are already - in the method initialise() - """ - found_error = False - found_error = found_error | check_firmware_loaded(devices) - found_error = found_error | check_clock_locked(devices) - return found_error - - -def check_firmware_loaded(devices, debug_flag=True): - """ - Verify that the firmware has loaded in the fpgas - Returns True if not loaded, False otherwise - """ - devices = devices_list_2_dict(devices) - sdp = devices["sdp"] - res = sdp.FPGA_boot_image_R - if debug_flag: - print("") - print("Checking sdp.FPGA_boot_image_R:") - print(f"Reply: {res}") - if not np.array_equal(res[0:N_FPGA], np.ones(N_FPGA)): - print("Firmware is not loaded!") - return True - if debug_flag: - print("Ok - Firmware loaded") - return False - - -def check_clock_locked(devices, debug_flag=True): - """ - Verify that the APSCT clock is locked - Returns True if not loaded, False otherwise - """ - devices = devices_list_2_dict(devices) - apsct = devices["apsct"] - res = apsct.APSCT_PLL_200MHz_error_R - if debug_flag: - print("") - print("Checking apsct.APSCT_PLL_200MHz_error_R:") - print(f"Reply: {res}") - if res: - print("APSCT clock not locked!") - return True - if debug_flag: - print("Ok - APSCT clock locked") - return False - - -def set_to_default_configuration(devices, silent=True): - """ - Set the station to its default configuration - Returns True if error found, False otherwise - """ - found_error = False - if not silent: - plib.log("Start configuring Low Receivers") - found_error = found_error | set_rcul_to_default_config(devices) - if not silent: - plib.log("Start configuring High Receivers") - found_error = found_error | set_rcuh_to_default_config(devices) - if not silent: - plib.log("Start configuring Station Digital Processor") - found_error = found_error | set_sdp_to_default_config(devices) - if not silent: - plib.log("Start configuring Subband Statistics") - found_error = found_error | set_sdp_sst_to_default_config(devices) - if not silent: - plib.log("Start configuring Crosslet Statistics") - found_error = found_error | set_sdp_xst_to_default_config(devices) - if not silent: - plib.log("Start configuring Beamlet Statistics") - found_error = found_error | set_sdp_bst_to_default_config(devices) - return found_error - - -def set_rcul_to_default_config(devices): - """ - Set RCU Low to its default configuration - """ - devices = devices_list_2_dict(devices) - recv = devices["recv"] - # - # Set RCUs Lows in default modes - # - - recv.set_defaults() - - # Set attenuator, antenna power etc. - recv.RCU_band_select_RW = set_ant_masked( - recv, LBA_MASK, recv.RCU_band_select_R, [[1] * 3] * 32 - ) # 1 = 30-80 MHz - recv.RCU_PWR_ANT_on_RW = set_ant_masked( - recv, LBA_MASK, recv.RCU_PWR_ANT_on_R, [[True] * 3] * 32 - ) # Off - recv.RCU_attenuator_dB_RW = set_ant_masked( - recv, LBA_MASK, recv.RCU_attenuator_dB_R, [[0] * 3] * 32 - ) # 0dB attenuator - wait_receiver_busy(recv) - - # - found_error = False - return found_error - - -def set_rcuh_to_default_config(devices): - """ - Set RCU High to its default configuration - """ - devices = devices_list_2_dict(devices) - recv = devices["recv"] - # - # Setup HBA RCUs and HBA Tiles - # - - # Set RCU in correct mode - rcu_modes = recv.RCU_band_select_R - rcu_modes[8] = [2, 2, 2] # 2 = 110-180 MHz, 1 = 170-230 MHz, 4 = 210-270 MHz ?? - rcu_modes[9] = [2, 2, 2] - recv.RCU_band_select_RW = set_ant_masked( - recv, hba_mask, recv.RCU_band_select_R, rcu_modes - ) - wait_receiver_busy(recv) - - # Switch on the Antenna - recv.RCU_PWR_ANT_on_RW = set_ant_masked( - recv, hba_PwrMask, recv.RCU_PWR_ANT_on_R, [[False] * 3] * 32 - ) # Switch off - wait_receiver_busy(recv) - recv.HBAT_PWR_LNA_on_RW = set_hba_mask( - recv, hba_CtlMask, recv.HBAT_PWR_LNA_on_R, [[True] * 32] * 96 - ) # LNA default on - recv.HBAT_BF_delay_steps_RW = set_hba_mask( - recv, hba_CtlMask, recv.HBAT_BF_delay_steps_R, [[0] * 32] * 96 - ) # Default - recv.HBAT_PWR_on_RW = set_hba_mask( - recv, hba_CtlMask, recv.HBAT_PWR_on_R, [[True] * 32] * 96 - ) # Default - wait_receiver_busy(recv) - recv.RCU_PWR_ANT_on_RW = set_ant_masked( - recv, hba_PwrMask, recv.RCU_PWR_ANT_on_R, [[True] * 3] * 32 - ) # Switch on - wait_receiver_busy(recv) - # default in the tile: after power-on, zero delay - # delays should be set by the tile beam device - # recv.HBAT_BF_delay_steps_RW=[[1]*32]*32 - # wait_receiver_busy(recv) - # by default: equal delay settings for all elements --> Pointing to zenith - - # TODO: read values and only update the ones that need to be changed. - # Could be a function that manages the masks. - print(recv.RCU_PWR_ANT_on_RW) - print() - print("done") - - # - found_error = False - return found_error - - -#### -# RCU specific functions and variables for DTS-Outside -#### - -# General RCU functions - - -def wait_receiver_busy(recv): - """ - Wait for the Receiver Translators busy monitoring point to return False - """ - time.sleep(0.5) - while recv.RECVTR_translator_busy_R: - time.sleep(0.1) - - -def set_ant_masked(recv, mask, old_value, new_value): - """ - Set the antenna mask for the Receiver Translator - """ - recv.ANT_mask_RW = mask - - for imask, maski in enumerate(mask): - for rcu_input_index in range(3): - if maski[rcu_input_index]: - old_value[imask][rcu_input_index] = new_value[imask][rcu_input_index] - # this used to be the following one liner, but that line was too long. - # Here is it, but cut in pieces: - # old_value[imask] = - # [new_value[imask][j] if maski[j] else old_value[imask][j] for j in range(3)] - return old_value - - -def set_hba_mask(recv, mask, old_value, new_value): - """ - Set the hba mask for the Receiver Translator - """ - # TODO: Is this function equal to set_ant_masked()? Can this definition be removed? - recv.ANT_mask_RW = mask - for imask, maski in enumerate(mask): - for i in range(3): - if maski[i]: - old_value[imask * 3 + i] = new_value[imask * 3 + i] - else: - old_value[imask * 3 + i] = old_value[imask * 3 + i] - return old_value - - -#### -# Till here RCU specific functions and variables for DTS-Outside -#### - - -def set_sdp_to_default_config(devices): - """ - Set RCU High to its default configuration - """ - devices = devices_list_2_dict(devices) - sdp = devices["sdp"] - # - # Set SDP in default modes - # - - plib.log("Start configuring SDP to default") - sdp.set_defaults() - # should be part of sdp.set_defaults: - next_ring = [False] * 16 - next_ring[3] = True - sdp.FPGA_ring_use_cable_to_previous_rn_RW = [True] + [False] * 15 - sdp.FPGA_ring_use_cable_to_next_rn_RW = next_ring - sdp.FPGA_ring_nof_nodes_RW = [N_FPGA] * 16 - sdp.FPGA_ring_node_offset_RW = [0] * 16 - - # - found_error = False - return found_error - - -def set_sdp_sst_to_default_config(devices): - """ - Set SSTs to default - """ - devices = devices_list_2_dict(devices) - sst = devices["sst"] - - sst.set_defaults() - # prepare for subband stati - sst.FPGA_sst_offload_weighted_subbands_RW = [ - True - ] * 16 # should be in set_defaults() - - # - found_error = False - return found_error - - -def set_sdp_xst_to_default_config(devices): - """ - Set XSTs to default - """ - devices = devices_list_2_dict(devices) - xst = devices["xst"] - - # prepare for correlations - int_time = 1 # used for 'medium' measurement using statistics writer - subband_step_size = 7 # 0 is no stepping - n_xst_subbands = 7 - subband_select = [subband_step_size, 0, 1, 2, 3, 4, 5, 6] - - xst.set_defaults() - # should be part of set_defaults() - xst.FPGA_xst_processing_enable_RW = [False] * 16 - # this (line above) should be first, then configure and enable - xst.fpga_xst_ring_nof_transport_hops_RW = [3] * 16 # [((n_fgpa/2)+1)]*16 - - crosslets = [0] * 16 - for fpga_nr in range(N_FPGA): - crosslets[fpga_nr] = n_xst_subbands - xst.FPGA_xst_offload_nof_crosslets_RW = crosslets - - xst.FPGA_xst_subband_select_RW = [subband_select] * 16 - xst.FPGA_xst_integration_interval_RW = [int_time] * 16 - - xst.FPGA_xst_processing_enable_RW = [True] * 16 - # - found_error = False - return found_error - - -def set_sdp_bst_to_default_config(devices): - """ - Set XSTs to default - """ - devices = devices_list_2_dict(devices) - beamlet = devices["beamlet"] - - # prepare for beamformer - beamlet.set_defaults() - - beamlet.FPGA_bf_ring_nof_transport_hops_RW = [[1]] * 3 + [[0]] * 13 - # line above should be part of set_defaults(), specifically for DTS-Outside - - beamletoutput_mask = [False] * 16 - beamletoutput_mask[3] = True - # beamlet.FPGA_beamlet_output_enable_RW=beamletoutput_mask - beamlet.FPGA_beamlet_output_hdr_eth_destination_mac_RW = ["40:a6:b7:2d:4f:68"] * 16 - beamlet.FPGA_beamlet_output_hdr_ip_destination_address_RW = ["192.168.0.249"] * 16 - - # define weights - # dim: FPGA, input, subband - weights_xx = beamlet.FPGA_bf_weights_xx_R.reshape([16, 6, 488]) - weights_yy = beamlet.FPGA_bf_weights_yy_R.reshape([16, 6, 488]) - # make a beam for the HBA - weights_xx[:][:] = 0 - weights_yy[:][:] = 0 - for signal_index in [0, 1]: # range(8*3,9*3+3): - unb2i = signal_index // 12 - signal_index_2 = signal_index // 2 - signal_index_2 = signal_index_2 % 6 - if signal_index % 2 == 0: - weights_xx[unb2i, signal_index_2] = 16384 - else: - weights_yy[unb2i, signal_index_2] = 16384 - - beamlet.FPGA_bf_weights_xx_RW = weights_xx.reshape([16, 6 * 488]) - beamlet.FPGA_bf_weights_yy_RW = weights_yy.reshape([16, 6 * 488]) - - blt = beamlet.FPGA_beamlet_subband_select_R.reshape([16, 12, 488]) - blt[:] = np.array(range(10, 10 + 488))[np.newaxis, np.newaxis, :] - beamlet.FPGA_beamlet_subband_select_RW = blt.reshape([16, 12 * 488]) - - # - found_error = False - return found_error - - -def enable_outputs( - devices, enable_sst=True, enable_xst=True, enable_bst=True, enable_beam_output=True -): - """ - Enable the station outputs after everything is set. - """ - devices = devices_list_2_dict(devices) - if enable_beam_output: - sdp = devices["sdp"] - sdp.FPGA_processing_enable_RW = [True] * 16 - if enable_sst: - sst = devices["sst"] - sst.FPGA_sst_offload_enable_RW = [True] * 16 - if enable_xst: - xst = devices["xst"] - xst.FPGA_xst_offload_enable_R = [True] * 16 - if enable_bst: - beamlet = devices["beamlet"] - beamlet.FPGA_beamlet_output_enable_RW = [i == 3 for i in range(16)] - - # - found_error = False - return found_error - - -def check_set_to_default_configuration(devices, readonly=True): - """ - Check the default configuration of the system to be set correctly. - """ - found_error = False - found_error = found_error | check_default_configuration_low_receivers(devices) - found_error = found_error | check_default_configuration_hbat( - devices, readonly=readonly - ) - found_error = found_error | check_rcu_fpga_interface(devices, si_to_check=range(18)) - found_error = found_error | check_set_sdp_xst_to_default_config( - devices, lane_mask=range(0, 6) - ) - - print_fpga_input_statistics(devices) - - return found_error - - -def check_default_configuration_low_receivers(devices, mask=None, debug_flag=True): - """ - Check that the LNAs are on - """ - devices = devices_list_2_dict(devices) - recv = devices["recv"] - found_error = False - - if mask is None: - mask = LBA_MASK - - res = recv.RCU_PWR_ANT_on_R - if len(mask) > 0: - res = res[mask] - if debug_flag: - print("") - print("Checking recv.RCU_PWR_ANT_on_R:") - print(f"Reply{' (masked)' if len(mask) > 0 else ''}: {res}") - if not (res).all(): - print("One or more LBAs are powered off!") - found_error = True - else: - if debug_flag: - print("Ok - Low Receivers operational") - return found_error - - -def check_default_configuration_hbat(devices, readonly=True): - """ - Check that the HBA Tiles are on - """ - devices = devices_list_2_dict(devices) - recv = devices["recv"] - HBATsi = [rcu * 3 + r_input for rcu in RCUs.RCU2Hpwr for r_input in range(3)] - if not readonly: - recv.RECVTR_monitor_rate_RW = 1 # for this checking - time.sleep(3) - - found_error = False - found_error = found_error | check_rcu_vin(devices, si_mask=HBATsi) - found_error = found_error | check_hbat_vout(devices, si_mask=HBATsi) - found_error = found_error | check_hbat_iout(devices, si_mask=HBATsi) - - if not readonly: - recv.RECVTR_monitor_rate_RW = 10 - - # TODO - what is checked for here? - print(recv.HBAT_PWR_LNA_on_R[8:10]) - print(recv.ANT_mask_RW[8:10]) - - return found_error - - -def check_rcu_vin(devices, si_mask=None, valid_range=None, debug_flag=True): - """ - Verify that the HBA Tile is powered on by comparing the V_in monitoring - point on the RCU against the thresholds for the given signal inputs. - - If no si_mask is given, all returned values are compared. - """ - devices = devices_list_2_dict(devices) - recv = devices["recv"] - if si_mask is None: - si_mask = [] - if valid_range is None: - valid_range = [30, 50] - - res = recv.RCU_PWR_ANT_VIN_R - if len(si_mask) > 0: - res = [res[si // 3][si % 3] for si in si_mask] - if debug_flag: - print("") - print("Checking recv.RCU_PWR_ANT_VIN_R:") - print(f"Reply{' (masked)' if len(si_mask) > 0 else ''}: {res}") - if not ( - np.all(valid_range[0] < np.array(res)) - and np.all(np.array(res) < valid_range[1]) - ): - print("No power at input RCU2!") - return True - if debug_flag: - print("Ok - Power at input RCU2") - return False - - -def check_hbat_vout(devices, si_mask=None, valid_range=None, debug_flag=True): - """ - Verify that the HBA Tile is powered on by comparing the V_out monitoring - point on the RCU against the thresholds for the given signal inputs. - - If no si_mask is given, all returned values are compared. - """ - # Vout should be 48 +- 1 V - devices = devices_list_2_dict(devices) - recv = devices["recv"] - if si_mask is None: - si_mask = [] - if valid_range is None: - valid_range = [30, 50] - - res = recv.RCU_PWR_ANT_VOUT_R - if len(si_mask) > 0: - res = [res[si // 3][si % 3] for si in si_mask] - if debug_flag: - print("") - print("Checking recv.RCU_PWR_ANT_VOUT_R:") - print(f"Reply{' (masked)' if len(si_mask) > 0 else ''}: {res}") - if not ( - np.all(valid_range[0] < np.array(res)) - and np.all(np.array(res) < valid_range[1]) - ): - print("Not all HBA Tiles powered on!") - return True - if debug_flag: - print("Ok - HBA Tiles powered on") - return False - - -def check_hbat_iout(devices, si_mask=None, valid_range=None, debug_flag=True): - """ - Verify that the HBA Tile is powered on by comparing the I_out monitoring - point on the RCU against the thresholds for the given signal inputs. - - If no si_mask is given, all returned values are compared. - """ - # Iout = 0.7 +- 0.1 Amp - devices = devices_list_2_dict(devices) - recv = devices["recv"] - if si_mask is None: - si_mask = [] - if valid_range is None: - valid_range = [0.6, 0.8] - - res = recv.RCU_PWR_ANT_IOUT_R - if len(si_mask) > 0: - res = [res[si // 3][si % 3] for si in si_mask] - if debug_flag: - print("") - print("Checking recv.RCU_PWR_ANT_IOUT_R:") - print(f"Reply{' (masked)' if len(si_mask) > 0 else ''}: {res}") - if not ( - np.all(valid_range[0] < np.array(res)) - and np.all(np.array(res) < valid_range[1]) - ): - print("Not all HBA Tiles powered on (draw current)!") - return True - if debug_flag: - print("Ok - HBA Tiles powered on (draw current)") - return False - - -def check_rcu_fpga_interface(devices, si_to_check=range(18)): - """ - Function to check the RCU - FPGA interface - - # if error occurs, try to reboot by resetting Uniboard (switch) - # if only one works than the APSCT board is not making contact with the backplane. - # (Can take up to 3 times to get it right..) - # 23/6/2022T15:42 errorcode 0x4h: xxxxx on all of them. - # Error descriptions can be found here: - # https://support.astron.nl/jira/browse/L2SDP-774 - """ - devices = devices_list_2_dict(devices) - sdp = devices["sdp"] - found_error = False - # TODO: the number of calls can be reduced - for signal_index in si_to_check: - lock = sdp.FPGA_jesd204b_csr_dev_syncn_R[signal_index // 12][signal_index % 12] - if lock != 1: - found_error = True - print(f"Transceiver SI: {signal_index} is not locked {lock}") - err0 = sdp.FPGA_jesd204b_rx_err0_R[signal_index // 12][signal_index % 12] - if err0 != 0: - found_error = True - print(f"SI {signal_index} has JESD err0: 0x{err0:x}h") - err1 = sdp.FPGA_jesd204b_rx_err1_R[signal_index // 12][signal_index % 12] - if (err1 & 0x02FF) != 0: - found_error = True - print(f"SI {signal_index} has JESD err1: 0x{err1:x}h") - return found_error - - -def check_set_sdp_xst_to_default_config( - devices, lane_mask=None, time_1=None, time_dt=2.5, debug_flag=True -): - """ - Check sdp/xst configuration by checking the returned timestamps for: - 1. all being the same - 2. not epoch zero (at 1 Jan 1970 00:00:00) - 3. within 5 seconds from the local clock timestamp (now +/- 2.5 sec) - - Input arguments: - time_1 = number of seconds since the epoch to compare against - Default: None --> now() - time_dt = delta in seconds around time_1, in which the xst timestamps should fall. - Default: 2.5 --> p/m 2.5 sec - """ - devices = devices_list_2_dict(devices) - xst = devices["xst"] - if lane_mask is None: - lane_mask = [] - if time_1 is None: - time_1 = datetime.datetime.now().timestamp() - res = xst.xst_timestamp_R - if len(lane_mask) > 0: # mask lanes - res = [res[lane] for lane in lane_mask] - if debug_flag: - print("") - print("Checking xst.xst_timestamp_R:") - print(f"Reply{' (masked)' if len(lane_mask) > 0 else ''}: {res}") - print("Converted:") - print("\n".join([f"{time.asctime(time.gmtime(xst_time))}" for xst_time in res])) - if not (res == res[0]).all(): - print("Not all xst timestamps are equal!") - return True - if res[0] == 0: - print("One or more xst timestamps are zero!") - return True - if abs(res[0] - time_1) > time_dt: - print("One or more xst timestamps are outside the specified time window!") - return True - if debug_flag: - print("Ok - xst configured correctly") - return False - - -def print_fpga_input_statistics(devices): - """ - Print FPGA input statistics - """ - devices = devices_list_2_dict(devices) - sdp = devices["sdp"] - - # get data from device - dc_offset = sdp.FPGA_signal_input_mean_R - signal_input_rms = sdp.FPGA_signal_input_rms_R - lock_to_adc = sdp.FPGA_jesd204b_csr_dev_syncn_R - err0 = sdp.FPGA_jesd204b_rx_err0_R - err1 = sdp.FPGA_jesd204b_rx_err1_R - count = sdp.FPGA_jesd204b_csr_rbd_count_R - signal_index = np.reshape(np.arange(count.shape[0] * count.shape[1]), count.shape) - # get some info on the hardware connections - pn_index = plib.signal_index_2_processing_node_index(signal_index) - pn_input_index = plib.signal_index_2_processing_node_input_index(signal_index) - rcu_index = plib.signal_index_2_rcu_index(signal_index) - rcu_input_index = plib.signal_index_2_rcu_input_index(signal_index) - # print - print("+------------+-------------+----------------------------------------------+") - print("| Input | Processed by| Statistics of input |") - print("+---+---+----+------+------+---------+---------+-----+------+------+------+") - print("|RCU|RCU| SI | Node | Node | DC | RMS | Lock| Err0 | Err1 | Count|") - print("|idx|inp| | Index| Input| Offset | (LSB) | | (0x) | (0x) | (0x) |") - print("| |idx| | | Index| (LSB) | | | | | |") - print("+---+---+----+------+------+---------+---------+-----+------+------+------+") - pfmt = "| %02d| %1d |%3d | %02d | %2d | %7.2f | %7.2f | %3s | %04x | %04x | %04x |" - for rcui, rcuii, signali, pni, pnii, dco, rms, lock, err_0, err_1, cnt in zip( - rcu_index.flatten(), - rcu_input_index.flatten(), - signal_index.flatten(), - pn_index.flatten(), - pn_input_index.flatten(), - dc_offset.flatten(), - signal_input_rms.flatten(), - lock_to_adc.flatten(), - err0.flatten(), - err1.flatten(), - count.flatten(), - ): - print( - pfmt % (rcui, rcuii, signali, pni, pnii, dco, rms, lock, err_0, err_1, cnt) - ) - print("+---+---+----+------+------+---------+---------+-----+------+------+------+") - - -def report_setup_configuration(devices, save_fig=True, debug_flag=False): - """ - Read name and version data from the setup and print that as a report - """ - devices = devices_list_2_dict(devices) - recv = devices["recv"] - apsct = devices["apsct"] - apspu = devices["apspu"] - unb2 = devices["unb2"] - sdp = devices["sdp"] - # Receiver / High Band Antenna Tile - - # APS Hardware - # APS / Receiver Unit2 side - rcu2_pcb_ids = recv.RCU_PCB_ID_R.tolist() - rcu2_pcb_nrs = list(recv.RCU_PCB_number_R) - rcu2_pcb_vrs = list(recv.RCU_PCB_version_R) - - # APS / UniBoard2 side - # TODO, FIXME - shouldn't this be a list with multiple entries? - # See also LOFAR2-10774 (APSCT) - apsct_pcb_ids = [apsct.APSCT_PCB_ID_R] - apsct_pcb_nrs = [apsct.APSCT_PCB_number_R] - apsct_pcb_vrs = [apsct.APSCT_PCB_version_R] - # TODO, FIXME - shoudln't this be a list with multiple entries? - # See also LOFAR2-11434 (APSPU) - apspu_pcb_ids = [apspu.APSPU_PCB_ID_R] - apspu_pcb_nrs = [apspu.APSPU_PCB_number_R] - apspu_pcb_vrs = [apspu.APSPU_PCB_version_R] - unb2_pcb_ids = unb2.UNB2_PCB_ID_R.tolist() - unb2_pcb_nrs = list(unb2.UNB2_PCB_number_R) - unb2_pcb_vrs = list(sdp.FPGA_hardware_version_R) - # TODO, FIXME: - # FPGA_hardware_version_R returns hardware version info per FPGA. Hardware is shared - # per 4 fpgas, so why repeating values here? - unb2_pcb_vrs = [unb2_pcb_vrs[idx] for idx in [0, 4, 8, 12]] - - # TODO, FIXME: - # fixing list lengths for a large station with multiple APS - # this makes sure that the list indexing below works - n_aps = 4 - apsct_pcb_ids = apsct_pcb_ids + [0] * (n_aps - len(apsct_pcb_ids)) - apsct_pcb_nrs = apsct_pcb_nrs + [""] * (n_aps - len(apsct_pcb_nrs)) - apsct_pcb_vrs = apsct_pcb_vrs + [""] * (n_aps - len(apsct_pcb_vrs)) - apspu_pcb_ids = apspu_pcb_ids + [0] * (n_aps * 2 - len(apspu_pcb_ids)) - apspu_pcb_nrs = apspu_pcb_nrs + [""] * (n_aps * 2 - len(apspu_pcb_nrs)) - apspu_pcb_vrs = apspu_pcb_vrs + [""] * (n_aps * 2 - len(apspu_pcb_vrs)) - unb2_pcb_ids = unb2_pcb_ids + [0] * (n_aps * 2 - len(unb2_pcb_ids)) - unb2_pcb_nrs = unb2_pcb_nrs + [""] * (n_aps * 2 - len(unb2_pcb_nrs)) - unb2_pcb_vrs = unb2_pcb_vrs + [""] * (n_aps * 2 - len(unb2_pcb_vrs)) - - if debug_flag: - print("APSPU:") - print(f"apsct_pcb_ids: {apsct_pcb_ids}") - print(f"apsct_pcb_nrs: {apsct_pcb_nrs}") - print(f"apsct_pcb_vrs: {apsct_pcb_vrs}") - print("APSPU:") - print(f"apspu_pcb_ids: {apspu_pcb_ids}") - print(f"apspu_pcb_nrs: {apspu_pcb_nrs}") - print(f"apspu_pcb_vrs: {apspu_pcb_vrs}") - print("UNB2:") - print(f"unb2_pcb_ids: {unb2_pcb_ids}") - print(f"unb2_pcb_nrs: {unb2_pcb_nrs}") - print(f"unb2_pcb_vrs: {unb2_pcb_vrs}") - - aps_pcb_ids = [ - apspu_pcb_ids[1], - unb2_pcb_ids[0], - apsct_pcb_ids[0], - apspu_pcb_ids[0], - unb2_pcb_ids[1], - ] - aps_pcb_nrs = [ - apspu_pcb_nrs[1], - unb2_pcb_nrs[0], - apsct_pcb_nrs[0], - apspu_pcb_nrs[0], - unb2_pcb_nrs[1], - ] - aps_pcb_vers = [ - apsct_pcb_vrs[1], - unb2_pcb_vrs[0], - apsct_pcb_vrs[0], - apsct_pcb_vrs[0], - unb2_pcb_vrs[1], - ] - - if not save_fig: - fig_filenames = [] - else: - fig_filenames = [ - f'{PLOT_DIR}/{plib.get_timestamp("filename")}_aps_config.png', - f'{PLOT_DIR}/{plib.get_timestamp("filename")}_aps_config.pdf', - ] - plot_aps_configuration( - rcu2_pcb_ids, - rcu2_pcb_nrs, - rcu2_pcb_vrs, - aps_pcb_ids, - aps_pcb_nrs, - aps_pcb_vers, - filenames=fig_filenames, - ) - - # Services - # Firmware images - firmware_versions = list(sdp.FPGA_firmware_version_R) - # make sure there are versions for at least n_aps subracks - firmware_versions = firmware_versions + [""] * (n_aps * 8 - len(firmware_versions)) - aps_services = {} - for apsi, fpgai in zip(sorted([i for i in range(n_aps)] * 8), range(n_aps * 8)): - aps_services["aps%i-fpga%02d" % (apsi, fpgai)] = firmware_versions[fpgai] - aps_services["aps%i-unb2tr" % apsi] = "" # TODO - aps_services["aps%i-recvtr" % apsi] = "" # TODO - aps_services["aps%i-apscttr" % apsi] = "" # TODO - aps_services["aps%i-apsputr" % apsi] = "" # TODO - # Translators - sdptr_software_versions = [sdp.TR_software_version_R] - sdptr_software_versions = sdptr_software_versions + [""] * ( - 2 - len(sdptr_software_versions) - ) - station_services = {} - station_services["sdptr0"] = sdptr_software_versions[0] - station_services["sdptr1"] = sdptr_software_versions[1] - station_services["ccdtr"] = "" # TODO, currently missing - # station control tango devices - for dev in devices: - station_services[devices[dev].name().lower()] = devices[dev].version_R - if not save_fig: - figs = [] - else: - figs = [ - f'{PLOT_DIR}/{plib.get_timestamp("filename")}_station_services.png', - f'{PLOT_DIR}/{plib.get_timestamp("filename")}_station_services.pdf', - ] - plot_services_configuration(aps_services, station_services, filenames=figs) - - -def plot_aps_configuration( - rcu_pcb_identifiers, - rcu_pcb_numbers, - rcu_pcb_versions, - aps_pcb_ids, - aps_pcb_nrs, - aps_pcb_vers, - filenames=[f"{PLOT_DIR}/aps_config.png", f"{PLOT_DIR}/aps_config.pdf"], -): - """ - Plot the configuration of the Antenna Processing Subrack, both sides. - """ - - fig, axs = plt.subplots(1, 2, figsize=(20, 8)) - - plot_aps_rcu2_configuration( - axs[0], rcu_pcb_identifiers, rcu_pcb_numbers, rcu_pcb_versions - ) - plot_aps_unb2_configuration(axs[1], aps_pcb_ids, aps_pcb_nrs, aps_pcb_vers) - for filename in filenames: - fig.savefig(filename) - fig.show() - - -def plot_aps_rcu2_configuration( - plot_ax, - rcu_pcb_identifiers, - rcu_pcb_numbers, - rcu_pcb_versions, - rcu_locations=APS_LOCATION_LABELS, - rcu_pos_x=None, - rcu_pos_y=None, -): - """ - Plot the configuration of the Antenna Processing Subrack, RCU2 side. - """ - if rcu_pos_x is None: - rcu_pos_x = APS_RCU_POS_X - if rcu_pos_y is None: - rcu_pos_y = APS_RCU_POS_Y - plot_ax.plot(0, 0) - plot_dx = plot_dy = 0.9 - plot_ax.set_xlim(-1 + plot_dx, np.max(rcu_pos_x) + 1) - plot_ax.set_ylim(-1 + plot_dy, np.max(rcu_pos_y) + 1) - for plot_x, plot_y, loc, pcb_id, pcb_nr, pcb_v in zip( - rcu_pos_x, - rcu_pos_y, - rcu_locations, - rcu_pcb_identifiers, - rcu_pcb_numbers, - rcu_pcb_versions, - ): - plot_ax.plot( - [plot_x, plot_x + plot_dx, plot_x + plot_dx, plot_x, plot_x], - [plot_y, plot_y, plot_y + plot_dy, plot_y + plot_dy, plot_y], - color="black", - alpha=0.2, - ) - plot_ax.text(plot_x + plot_dx / 2, plot_y + plot_dy, loc, va="top", ha="center") - plot_ax.text( - plot_x + plot_dx / 2, - plot_y + plot_dy / 2, - "%s\n%s\n%s" % (pcb_v, pcb_id, pcb_nr), - va="center", - ha="center", - rotation=90, - ) - if pcb_id == 0 and pcb_v == "" and pcb_nr == "": - plot_ax.plot( - [plot_x, plot_x + plot_dx], - [plot_y, plot_y + plot_dy], - color="black", - alpha=0.2, - ) - plot_ax.plot( - [plot_x + plot_dx, plot_x], - [plot_y, plot_y + plot_dy], - color="black", - alpha=0.2, - ) - plot_ax.set_title("Configuration of RCU2s") - plot_ax.set_xticks([]) - plot_ax.set_yticks([]) - - -def plot_aps_unb2_configuration( - plot_ax, - aps_pcb_identifiers, - aps_pcb_numbers, - aps_pcb_versions, - mod_locations=APS_LOCATION_LABELS, - mod_pos_x=None, - mod_pos_y=None, -): - """ - Plot the configuration of the Antenna Processing Subrack, UNB2 side. - """ - if mod_pos_x is None: - mod_pos_x = APS_MOD_POS_X - if mod_pos_y is None: - mod_pos_y = APS_MOD_POS_Y - plot_ax.plot(0, 0) - plot_dx = 0.9 - plot_dy = 3.9 - plot_ax.set_xlim(-1 + plot_dx, np.max(mod_pos_x) + 2) - plot_ax.set_ylim(-1 + plot_dx, np.max(mod_pos_y) + 4) - for plot_x, plot_y, loc, pcb_id, pcb_nr, pcb_v in zip( - mod_pos_x, - mod_pos_y, - mod_locations, - aps_pcb_identifiers, - aps_pcb_numbers, - aps_pcb_versions, - ): - plot_ax.plot( - [plot_x, plot_x + plot_dx, plot_x + plot_dx, plot_x, plot_x], - [plot_y, plot_y, plot_y + plot_dy, plot_y + plot_dy, plot_y], - color="black", - alpha=0.2, - ) - plot_ax.text(plot_x + plot_dx / 2, plot_y + plot_dy, loc, va="top", ha="center") - plot_ax.text( - plot_x + plot_dx / 2, - plot_y + plot_dy / 2, - "%s\n%s\n%s" % (pcb_v, pcb_id, pcb_nr), - va="center", - ha="center", - rotation=90, - ) - if pcb_id == 0 and pcb_v == "" and pcb_nr == "": - plot_ax.plot( - [plot_x, plot_x + plot_dx], - [plot_y, plot_y + plot_dy], - color="black", - alpha=0.2, - ) - plot_ax.plot( - [plot_x + plot_dx, plot_x], - [plot_y, plot_y + plot_dy], - color="black", - alpha=0.2, - ) - plot_ax.set_title("Configuration of UniBoard2 side") - plot_ax.set_xticks([]) - plot_ax.set_yticks([]) - - -def plot_services_configuration( - aps_services, - station_services, - filenames=[f"{PLOT_DIR}/station_services.png", f"{PLOT_DIR}/station_services.pdf"], -): - """ - Plot the service version information in the setup. - """ - - fig, plot_ax = plt.subplots(1, 1, figsize=(16, 15)) - - aps_service_dx = 0.9 - aps_service_dy = 0.9 - first_aps_service = sorted(aps_services)[0][5:] - plot_y = plot_y0 = len(aps_services) / 2 + 2 - for aps_service in sorted(aps_services): - plot_x = 0 if (aps_service[3] in "01") else 1 - plot_y = ( - plot_y0 - if (aps_service[5:] == first_aps_service and aps_service[3] in "02") - else plot_y - 1 - ) - plot_y = ( - plot_y - 1 - if (aps_service[5:] == first_aps_service and aps_service[3] in "13") - else plot_y - ) - plot_ax.plot( - [plot_x, plot_x + aps_service_dx, plot_x + aps_service_dx, plot_x, plot_x], - [plot_y, plot_y, plot_y + aps_service_dy, plot_y + aps_service_dy, plot_y], - color="black", - alpha=0.2, - ) - plot_ax.text( - plot_x, - plot_y + aps_service_dy / 2, - aps_service, - va="center", - ha="left", - color="black", - ) - plot_ax.text( - plot_x + aps_service_dx, - plot_y + aps_service_dy / 2, - aps_services[aps_service], - va="center", - ha="right", - color="blue", - ) - if aps_services[aps_service] == "": - plot_ax.plot( - [plot_x, plot_x + aps_service_dx], - [plot_y, plot_y + aps_service_dy], - color="black", - alpha=0.2, - ) - plot_ax.plot( - [plot_x + aps_service_dx, plot_x], - [plot_y, plot_y + aps_service_dy], - color="black", - alpha=0.2, - ) - - stat_service_dx = 1.9 - stat_service_dy = 0.9 - plot_x = 0 - plot_y = 0 - for stat_service in sorted(station_services): - plot_ax.plot( - [ - plot_x, - plot_x + stat_service_dx, - plot_x + stat_service_dx, - plot_x, - plot_x, - ], - [ - plot_y, - plot_y, - plot_y + stat_service_dy, - plot_y + stat_service_dy, - plot_y, - ], - color="black", - alpha=0.2, - ) - plot_ax.text( - plot_x, - plot_y + stat_service_dy / 2, - stat_service, - va="center", - ha="left", - color="black", - ) - plot_ax.text( - plot_x + stat_service_dx, - plot_y + stat_service_dy / 2, - station_services[stat_service], - va="center", - ha="right", - color="blue", - ) - if station_services[stat_service] == "": - plot_ax.plot( - [plot_x, plot_x + stat_service_dx], - [plot_y, plot_y + stat_service_dy], - color="black", - alpha=0.2, - ) - plot_ax.plot( - [plot_x + stat_service_dx, plot_x], - [plot_y, plot_y + stat_service_dy], - color="black", - alpha=0.2, - ) - plot_y -= 1 - plot_ax.set_title("Services in setup") - plot_ax.set_xticks([]) - plot_ax.set_yticks([]) - - for filename in filenames: - fig.savefig(filename) - fig.show() - - -def plot_statistics(devices, save_fig=True, silent=False): - """ - Plot the statistics: SST, XST and BST - """ - if not silent: - plib.log("Plot subband statistics") - if not save_fig: - figs = [] - else: - figs = [ - f'{PLOT_DIR}/{plib.get_timestamp("filename")}_subband_statistics.png', - f'{PLOT_DIR}/{plib.get_timestamp("filename")}_subband_statistics.pdf', - ] - plot_subband_statistics(devices, filenames=figs) - # TODO: - # if not silent: - # plib.log("Plot crosslet statistics") - # TODO: - # if not silent: - # plib.log("Plot beamlet statistics") - - -def plot_subband_statistics( - devices, - xlim=None, - ylim=[-105, -30], - filenames=[ - f"{PLOT_DIR}/subband_statistics.png", - f"{PLOT_DIR}/subband_statistics.pdf", - ], -): - """ - Make the subband statistics plot - """ - if xlim is None: - xlim = [-5, 305] - if ylim is None: - ylim = [-105, -30] - sst_data, band_names, frequency_axes = get_subband_statistics(devices) - - fig, axs = plt.subplots(1, 1, figsize=(18, 6)) - for signal_index in range(len(sst_data)): - if band_names[signal_index] is None: - continue - freq = frequency_axes[band_names[signal_index]] - # plot data in dB full scale - plot_data = 10 * np.log10(sst_data[signal_index, :] + 1) - 128 - 6 * 4 - # since we're lacking antenna names, - # show the RCU input info as label - rcu_index, rcu_input_index = plib.signal_input_2_rcu_index_and_rcu_input_index( - signal_index - ) - label = "RCU%02d, Inp%d" % (rcu_index, rcu_input_index) - plt.plot(freq / 1e6, plot_data, label=label) - plt.grid() - plt.legend(bbox_to_anchor=(1, 1), loc="upper left") - plt.xlim(xlim) - plt.ylim(ylim) - plt.xlabel("Frequency (MHz)") - plt.ylabel("Power (dB full scale)") - plt.suptitle("Subband Statistics for all fully-connected receivers") - plt.title(f"{STATION_NAME}; {plib.get_timestamp()}") - - for filename in filenames: - fig.savefig(filename) - fig.show() - - -def get_subband_statistics(devices): - """ - Get subband statistics data from the setup, including proper axes and labels - """ - devices = devices_list_2_dict(devices) - recv = devices["recv"] - sst = devices["sst"] - - # get band names for the current configuration - cur_band_names = plib.get_band_names( - recv.RCU_band_select_R, - # TODO - when correct LCNs are provided - # in "RCU_PCB_number_R", uncomment the - # following line: - # recv.RCU_PCB_number_R, - # and remove the following line: - recv.RCU_PCB_ID_R, - lb_tags=RCU2L_TAGS, - hb_tags=RCU2H_TAGS, - ) - - # replace the aliases of LB1 and LB2 into LB: (same frequency axis) - for alias in plib.LB_ALIASES: - while alias in cur_band_names: - cur_band_names[cur_band_names.index(alias)] = "LB" - # get frequency axes - - # TODO - get frequency axes correct (indexing is incorrect/inconsequent) - key1 = "HB2" - key2 = "HB1" - tmp_key = "magic_band" - while key1 in cur_band_names: - cur_band_names[cur_band_names.index(key1)] = tmp_key - while key2 in cur_band_names: - cur_band_names[cur_band_names.index(key1)] = key1 - while tmp_key in cur_band_names: - cur_band_names[cur_band_names.index(tmp_key)] = key2 - - frequency_axes = {} - for band_name in plib.get_valid_band_names(): - if band_name in cur_band_names: - frequency_axes[band_name] = plib.get_frequency_for_band_name(band_name) - # get sst data - sst_data = sst.sst_r - # make sure header data is of correct size - cur_band_names = cur_band_names + [None] * (len(sst_data) - len(cur_band_names)) - return sst_data, cur_band_names, frequency_axes - - -def devices_list_2_dict(devices_list, device_keys=None): - """ - Convert a list of devices to a dictionary with known keys for internal - use. - - Devices are selected based on substring presence in the devices name - - Input arguments: - devices_list = list of Tango devices - device_keys = list of keys, should be a substring of the device names - - Output arguments: - devices_dict = dict of devices with known keys - """ - if device_keys is None: - device_keys = [ - "boot", - "unb2", - "recv", - "sdp", - "sst", - "bst", - "xst", - "digitalbeam", - "tilebeam", - "beamlet", - "apsct", - "apspu", - ] - devices_dict = {} - if isinstance(devices_list, dict): - # if by accident devices_list is already a dict, - # then simply return the dict - return devices_list - for device in devices_list: - for device_key in device_keys: - if device_key in device.name().lower(): - devices_dict[device_key] = device - return devices_dict - - -def main(): - """Main""" - return False - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/lofar_station_client/processing_lib.py b/lofar_station_client/processing_lib.py deleted file mode 100644 index 285654b9d10ef1bc025293f3a7ee2ea05cced9c5..0000000000000000000000000000000000000000 --- a/lofar_station_client/processing_lib.py +++ /dev/null @@ -1,467 +0,0 @@ -# processing_lib.py: Module with generic functions for processing -# LOFAR2 station data -# -# -*- coding: utf-8 -*- -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -""" module processing_lib - -This module contains generic functions for processing LOFAR2 station data. - -""" - -import sys - -# import h5py -# from os import listdir -# from os.path import isfile, join -import datetime -import numpy as np - -# from mpl_toolkits.mplot3d import axes3d -# import matplotlib.pyplot as plt - - -CST_N_SUB = 512 # number of subbands as output of subband generation in firmware -CST_FS = 100e6 # sampling frequency in Hz -BANDLIMS = {"LB": [0, 100e6], "HB1": [100e6, 200e6], "HB2": [200e6, 300e6]} -LB_ALIASES = ["LB1", "LB2"] - - -def signal_index_2_processing_node_index(signal_index): - """ - Convert signal index to processing node index. - Can be used to determine on which processing node the signal is processed. - """ - return (signal_index % 192) // 12 - - -def signal_index_2_processing_node_input_index(signal_index): - """ - Convert signal index to processing node input index. - Can be used to determine wich input to the processing node is used for the signal. - """ - return (signal_index % 192) % 12 - - -def signal_index_2_uniboard_index(signal_index): - """ - Convert signal index to UniBoard index. - Can be used to determine on which UniBoard the signal is processed. - """ - return signal_index // 48 - - -def signal_index_2_rcu_index(signal_index): - """ - Convert signal index to RCU index. - Can be used to determine which RCU is used to provide the signal. - """ - return (signal_index // 6) * 2 + signal_index % 2 - - -def signal_index_2_rcu_input_index(signal_index): - """ - Convert signal index to RCU input index. - Can be used to determine which input to the RCU is used for the signal. - """ - return (signal_index // 2) % 3 - - -def signal_index_2_aps_index(signal_index): - """ - Convert signal index to APS index. - Can be used to determine in which Antenna Processing Subrack this signal is - processed. - """ - return signal_index // 96 - - -def signal_input_2_rcu_index_and_rcu_input_index(signal_index): - """ - Given the gsi, get the RCU index and the RCU input index in one call. - """ - return ( - signal_index_2_rcu_index(signal_index), - signal_index_2_rcu_input_index(signal_index), - ) - - -# the inverse: -def rcu_index_2_signal_index(rcu_index): - """ - Convert RCU index to signal indices. - """ - return rcu_index_and_rcu_input_index_2_signal_index(rcu_index) - - -def rcu_index_and_rcu_input_index_2_signal_index( - rcu_index, rcu_input_index=np.array([0, 1, 2]) -): - """ - Convert RCU index and RCU input index to signal index. - """ - return rcu_index + rcu_input_index * 2 + (rcu_index // 2) * 4 - - -# backward compatible methods: -def gsi_2_rcu_input_index(gsi): - """Deprecated. Please use signal_index_2_rcu_input_index.""" - print("Warning! Do not use gsi_2_rcu_input_index().") - print("Use signal_index_2_rcu_input_index() instead") - return signal_index_2_rcu_input_index(gsi) - - -def gsi_2_rcu_index(gsi): - """Deprecated. Please use signal_index_2_rcu_index.""" - print("Warning! Do not use gsi_2_rcu_index().") - print("Use signal_index_2_rcu_index() instead") - return signal_index_2_rcu_index(gsi) - - -def gsi_2_rcu_index_and_rcu_input_index(gsi): - """Deprecated. Please use signal_input_2_rcu_index_and_rcu_input_index.""" - print("Warning! Do not use gsi_2_rcu_index_and_rcu_input_index().") - print("Use signal_input_2_rcu_index_and_rcu_input_index() instead") - return signal_input_2_rcu_index_and_rcu_input_index(gsi) - - -def rcu_index_2_gsi(rcu_index): - """Deprecated. Please use rcu_index_2_signal_index.""" - print("Warning! Do not use rcu_index_2_gsi().") - print("Use rcu_index_2_signal_index() instead") - return rcu_index_2_signal_index(rcu_index) - - -def rcu_index_and_rcu_input_index_2_gsi(rcu_index, rcu_input_index=np.array([0, 1, 2])): - """Deprecated. Please use rcu_index_and_rcu_input_index_2_signal_index.""" - print("Warning! Do not use rcu_index_and_rcu_input_index_2_gsi().") - print("Use rcu_index_and_rcu_input_index_2_signal_index() instead") - return rcu_index_and_rcu_input_index_2_signal_index(rcu_index, rcu_input_index) - - -def test_rcu_index_2_signal_index(): - """ - Test code for rcu_index_2_signal_index - """ - test_input = [] - test_output = [] - # test 1 - test_input.append(np.array([0])) - test_output.append(np.array([0, 2, 4])) - # test 2 - test_input.append(np.array([1])) - test_output.append(np.array([1, 3, 5])) - for input_rcu_index, expected_output in zip(test_input, test_output): - actual_output = rcu_index_2_signal_index(input_rcu_index) - if not np.all(expected_output == actual_output): - print(expected_output) - print(actual_output) - raise Exception("Tests fails!") - - -def test_rcu_index_and_rcu_input_index_2_signal_index(): - """ - Test code for rcu_index_and_rcu_input_index_2_signal_index - """ - test_input = [] - test_output = [] - # test 1 - test_input.append([np.array([0]), np.array([0])]) - test_output.append(np.array([0])) - # test 2 - test_input.append([np.array([0]), np.array([0, 1, 2])]) - test_output.append(np.array([0, 2, 4])) - for inputs, expected_output in zip(test_input, test_output): - input_rcu_index = inputs[0] - input_rcu_input_index = inputs[1] - actual_output = rcu_index_and_rcu_input_index_2_signal_index( - input_rcu_index, input_rcu_input_index - ) - if not np.all(expected_output == actual_output): - print(expected_output) - print(actual_output) - raise Exception("Tests fails!") - - -def test_signal_input_2_rcu_index_and_rcu_input_index(): - """ - Test code for signal_input_2_rcu_index_and_rcu_input_index - """ - test_input = [] - test_output = [] - # test 1 - test_input.append(np.array([0])) - test_output.append((np.array([0]), np.array([0]))) - # test 2 - test_input.append(np.array([1])) - test_output.append((np.array([1]), np.array([0]))) - for input_signal_index, expected_output in zip(test_input, test_output): - actual_output = signal_input_2_rcu_index_and_rcu_input_index(input_signal_index) - print(actual_output) - print(expected_output) - if not np.all(expected_output == actual_output): - print(expected_output) - print(actual_output) - raise Exception("Tests fails!") - - -def get_timestamp(format_specifier=None): - """ - Get the timestamp in standard format - - Input argument: - format_specifier = format specifier. - iso format (default) - "filename": filename without spaces and special characters, - format: yyyymmddThhmmss - - Output argument: - timestamp = timestamp in correct format - """ - timestamp = datetime.datetime.isoformat(datetime.datetime.now()) - if format_specifier == "filename": - timestamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S") - return timestamp - - -def log(string): - """ - Standard printing of a log line, including a timestamp. - """ - print(f"{get_timestamp()} : {string}") - - -def get_valid_band_names(): - """ - Return the valid band names. - """ - band_names = [] - for band_name in BANDLIMS: - band_names.append(band_name) - return band_names - - -def get_band_name_and_subband_index_for_frequency(freqs): - """ - Get band name and subband index for one or more frequencies. - - Input arguments: - freqs = one or more frequencies in Hz - - Output arguments: - band_names = list of band names (see get_valid_band_names) - sbi = one or more indices of the subbands for which the frequencies - should be returned - """ - # make sure freqs is a list - if not isinstance(freqs, list): - freqs = [freqs] - # get band name and subband index - freqs = np.array(freqs) - band_names = [] - for freq in freqs: - band_name = get_band_name_for_frequency(freq) - band_names.append(band_name) - # get indices for band_name "LB" - sbis = np.round(freqs * CST_N_SUB / CST_FS) - # and update for the other bands: - for idx in np.where(np.array(band_names) == "HB1")[0]: - sbis[idx] = 2 * CST_N_SUB - sbis[idx] - for idx in np.where(np.array(band_names) == "HB2")[0]: - sbis[idx] = sbis[idx] - 2 * CST_N_SUB - return band_names, sbis - - -def get_band_name_for_frequency(freq, bandlims=None): - """ - Get band name for frequency - - Input arguments: - freq = one frequency in Hz, float - bandlims = dict with band names and their limits (default: LOFAR2 bands) - - Output arguments: - band_name = name of analog band (see get_valid_band_names), - empty string ("") if unsuccessful - """ - if bandlims is None: - bandlims = BANDLIMS - for band_name in bandlims: - if np.min(bandlims[band_name]) <= freq <= np.max(bandlims[band_name]): - return band_name - return "" - - -def get_frequency_for_band_name(band_name): - """ - Get all frequencies for given band name - - Input arguments: - band_name = name of analog band (see get_valid_band_names) - - Output arguments: - freqs = frequencies in Hz for the selected band - """ - return get_frequency_for_band_name_and_subband_index(band_name, sbi=np.arange(512)) - - -def get_frequency_for_band_name_and_subband_index(band_name, sbi): - """ - Get frequency for band name and subband(s) - - Input arguments: - band_name = name of analog band (see get_valid_band_names) - sbi = one or more indices of the subbands for which the frequencies - should be returned - - Output arguments: - freqs = frequencies in Hz for the selected band and subband - """ - # check input - valid_band_names = get_valid_band_names() - if band_name not in valid_band_names and band_name not in LB_ALIASES: - raise Exception( - f"Unknown band_name '{band_name}'. Valid band_names are: {valid_band_names}" - ) - # generate frequencies - freqs = sbi * CST_FS / CST_N_SUB - if band_name == "HB1": - return 200e6 - freqs - if band_name == "HB2": - return 200e6 + freqs - # else: # LB - return freqs - - -def get_band_names(rcu_band_select_r, rcu_pcb_version_r, lb_tags=None, hb_tags=None): - """ - Get the frequency band names per receiver, based on their band selection - and RCU PCB name. - - PCB name is used to determine the band (Low Band "LB" or High Band "HB", - None otherwise). The band selection is added as a postfix. - RCU PCB version can also be used, but then the default tags will not be sufficient - - Input Arguments: - rcu_band_select_r = as returned by recv.RCU_band_select_R - rcu_pcb_version_r = as returned by recv.RCU_PCB_version_R - lb_tags = substrings in RCU_PCB_version_R fields to indicate Low Band - Default: RCU2L - hb_tags = substrings in RCU_PCB_version_R fields to indicate High Band - Default: RCU2H - - Note: - rcu_pcb_version_r can also hold IDs as returned by recv.RCU_PCB_ID_R - In that case, the lb_tags and hb_tags should be passed on by the user - - Output Arguments: - band_name = list of strings indicating the band name per receiver. - containts None if no band name could be determined - - """ - # if IDs are passed on, convert to list of strings - if isinstance(rcu_pcb_version_r, np.ndarray): - rcu_pcb_version_r = [str(x) for x in rcu_pcb_version_r] - # - if lb_tags is None: - lb_tags = ["RCU2L"] - if hb_tags is None: - hb_tags = ["RCU2H"] - - n_signal_indices = rcu_band_select_r.shape[0] * rcu_band_select_r.shape[1] - band_name_per_signal_index = [None] * n_signal_indices - - for rcu_index in range(len(rcu_band_select_r)): - for rcu_input_index in range(len(rcu_band_select_r[rcu_index])): - signal_index = rcu_index_and_rcu_input_index_2_signal_index( - rcu_index, rcu_input_index - ) - this_band = None - for tag in lb_tags: - if tag in rcu_pcb_version_r[rcu_index]: - this_band = f"LB{rcu_band_select_r[rcu_index][rcu_input_index]:1}" - break - if this_band is None: # continue with high band - for tag in hb_tags: - if tag in rcu_pcb_version_r[rcu_index]: - this_band = ( - f"HB{rcu_band_select_r[rcu_index][rcu_input_index]:1}" - ) - break - band_name_per_signal_index[signal_index] = this_band - return band_name_per_signal_index - - -# some basic test methods, for internal use: -def test_get_band_name_and_subband_index_for_frequency(): - """ - Test code for get_band_name_and_subband_index_for_frequency - """ - input_frequencies = [138.1e6, 137.9e6, 0.1, 0, 195312.5, 234e6] - expected_band_names = ["HB1", "HB1", "LB", "LB", "LB", "HB2"] - expected_subband_index = np.array([317.0, 318.0, 0.0, 0.0, 1.0, 174.0]) - ( - actual_band_names, - actual_subband_index, - ) = get_band_name_and_subband_index_for_frequency(input_frequencies) - if expected_band_names != actual_band_names: - print(expected_band_names) - print(actual_band_names) - raise Exception("Tests fails!") - if not (expected_subband_index == actual_subband_index).all(): - print(expected_subband_index) - print(actual_subband_index) - raise Exception("Tests fails!") - - -def test_get_frequency_for_band_name_and_subband_index(): - """ - Test code for get_frequency_for_band_name_and_subband_index - """ - for input_band_name, input_subband_index, expected_frequencies in zip( - ["HB1", "HB1", "LB", "LB", "LB", "HB2", "LB1"], - [195.0, 194.0, 0.0, 0.0, 1.0, 174.0, 1.0], - [161914062.5, 162109375.0, 0.0, 0.0, 195312.5, 233984375, 195312.5], - ): - actual_frequencies = get_frequency_for_band_name_and_subband_index( - input_band_name, input_subband_index - ) - if not expected_frequencies == actual_frequencies: - print(expected_frequencies) - print(actual_frequencies) - raise Exception("Tests fails!") - - -def test(): - """ - Method to call the defined test methods. - """ - test_rcu_index_2_signal_index() - test_rcu_index_and_rcu_input_index_2_signal_index() - test_signal_input_2_rcu_index_and_rcu_input_index() - test_get_band_name_and_subband_index_for_frequency() - test_get_frequency_for_band_name_and_subband_index() - - -def main(): - """ - Main function - """ - return False - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/lofar_station_client/requests/prometheus.py b/lofar_station_client/requests/prometheus.py index 0c61ace42a391e9895164e545e661f675aa61169..801cad2f8f14f189ecfafa1611280b3e3a6764df 100644 --- a/lofar_station_client/requests/prometheus.py +++ b/lofar_station_client/requests/prometheus.py @@ -42,6 +42,7 @@ class PrometheusRequests: DEFAULT_HOST = "http://prometheus:9090" DEFAULT_DURATION = 3600 # in seconds DEFAULT_STEP_SIZE = 60 # in seconds + DEFAULT_TIMEOUT = 60.0 # in seconds @staticmethod def get_attribute_history( @@ -98,7 +99,8 @@ class PrometheusRequests: # Perform the actual get request http_result = requests.get( - f"{host}{path}{query}&start={start}&end={end}&step={step}" + f"{host}{path}{query}&start={start}&end={end}&step={step}", + timeout=PrometheusRequests.DEFAULT_TIMEOUT, ) # Check status code for any errors, only OK 200 accepted diff --git a/requirements.txt b/requirements.txt index 2c7f9268c3114449507a67ed7db4d9206b86a7ef..5e2e3478a9c1d974fc92a40993834cc03b7fd5c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,8 @@ -# TODO(Corne): Uncomment below when L2SS-832 is fixed -# tangostationcontrol@git+https://git.astron.nl/lofar2.0/tango.git#egg=tangostationcontrol&subdirectory=tangostationcontrol - +# Order does matter requests>=2.0 # Apache 2 -numpy>=1.21.0 # BSD \ No newline at end of file +numpy>=1.21.0 # BSD +nptyping>=2.3.0 # MIT +matplotlib>=3.5.0 # PSF +pyDeprecate>=0.3.0 # MIT +# commented out until https://gitlab.com/tango-controls/pytango/-/issues/468 is resolved +# PyTango>=9.3.5 # GNU LGPL v3 diff --git a/test-requirements.txt b/test-requirements.txt index 80999ddacb2ce4ee1e95e89428be850bd76bd63d..12f8572083f614e07782b856455a54982f3eff6e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,8 @@ -black -build -flake8 -pylint +black>=22.0.0 # MIT +build>=0.8.0 # MIT +flake8>=5.0.0 # MIT +pylint>=2.15.0 # GPLv2 +autopep8>=1.7.0 # MIT coverage!=4.4,>=4.0 # Apache-2.0 stestr>=2.0.0 # Apache-2.0 testtools>=2.2.0 # MIT diff --git a/tests/dts/__init__.py b/tests/dts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/dts/test_bands.py b/tests/dts/test_bands.py new file mode 100644 index 0000000000000000000000000000000000000000..ed5b8ce14712c93db00e047c63a018a403e98f73 --- /dev/null +++ b/tests/dts/test_bands.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import numpy as np + +from lofar_station_client.dts import bands + +from tests import base + + +class DTSBandsTest(base.TestCase): + def test_get_band_name_and_subband_index_for_frequency_single(self): + """Test automatic single value to list conversion in sbi_for_freq""" + + result = bands.get_band_name_and_subband_index_for_frequency(138.1e6) + + self.assertEqual(result[0], "HB1") + self.assertEqual(result[1], 317.0) + + # Access ndarray element directly + self.assertEqual(result[0][0], "HB1") + self.assertEqual(result[1][0], 317.0) + + def test_get_band_name_for_frequency(self): + result = bands.get_band_name_for_frequency(138.1e6) + self.assertEqual("HB1", result) + + def test_get_band_name_for_frequency_not_exist(self): + result = bands.get_band_name_for_frequency(998.1e6) + self.assertEqual("", result) + + def test_get_band_name_for_frequency_custom_bandlims(self): + t_bandlims = {"LB": [0, 900e6], "HB1": [900e6, 1000e6], "HB2": [1000e6, 1300e6]} + result = bands.get_band_name_for_frequency(998.1e6, t_bandlims) + self.assertEqual("HB1", result) + + def test_get_band_name_and_subband_index_for_frequency(self): + """Test code for get_band_name_and_subband_index_for_frequency""" + + input_frequencies = [138.1e6, 137.9e6, 0.1, 0, 195312.5, 234e6] + expected_band_names = ["HB1", "HB1", "LB", "LB", "LB", "HB2"] + expected_subband_index = np.array([317.0, 318.0, 0.0, 0.0, 1.0, 174.0]) + ( + actual_band_names, + actual_subband_index, + ) = bands.get_band_name_and_subband_index_for_frequency(input_frequencies) + + np.testing.assert_array_equal(expected_band_names, actual_band_names) + np.testing.assert_array_equal(expected_subband_index, actual_subband_index) + + def test_get_frequency_for_band_name_and_subband_index(self): + """Test code for get_frequency_for_band_name_and_subband_index""" + + for input_band_name, input_subband_index, expected_frequencies in zip( + ["HB1", "HB1", "LB", "LB", "LB", "HB2", "LB1"], + [195, 194, 0, 0, 1, 174, 1], + [161914062.5, 162109375.0, 0.0, 0.0, 195312.5, 233984375, 195312.5], + ): + actual_frequencies = bands.get_frequency_for_band_name_and_subband_index( + input_band_name, input_subband_index + ) + + self.assertEqual(expected_frequencies, actual_frequencies) + + def test_get_frequency_for_band_name_and_subband_index_except(self): + """Test retrieving frequency for non-existing band raises exception""" + + self.assertRaises( + ValueError, + bands.get_frequency_for_band_name_and_subband_index, + "not-exists", + 0, + ) + + def test_get_frequency_for_band_name_hb1(self): + """HB1 band should return frequencies in descending order""" + frequencies = bands.get_frequency_for_band_name("HB1") + + previous_freq = frequencies[0] + for x in frequencies[1:]: + self.assertLess(x, previous_freq) + + def test_get_frequency_for_band_name_hb2(self): + """HB2 band should return frequencies in ascending order""" + frequencies = bands.get_frequency_for_band_name("HB2") + + previous_freq = frequencies[0] + for x in frequencies[1:]: + self.assertGreater(x, previous_freq) + + def test_get_frequency_for_band_name_lb(self): + """All bands except HB1 should return frequencies in ascending order""" + frequencies = bands.get_frequency_for_band_name("LB1") + + previous_freq = frequencies[0] + for x in frequencies[1:]: + self.assertGreater(x, previous_freq) + + def test_get_band_names(self): + pass diff --git a/tests/dts/test_index.py b/tests/dts/test_index.py new file mode 100644 index 0000000000000000000000000000000000000000..3c07caf8a46a7bc882e66af39dca4be8cdd06102 --- /dev/null +++ b/tests/dts/test_index.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import numpy as np + +from lofar_station_client.dts import index + +from tests import base + + +class DTSIndexTest(base.TestCase): + @staticmethod + def test_rcu_index_2_signal_index(): + """Test code for rcu_index_2_signal_index""" + + test_input = [] + test_output = [] + + # test 1 + test_input.append(np.array([0])) + test_output.append(np.array([0, 2, 4])) + + # test 2 + test_input.append(np.array([1])) + test_output.append(np.array([1, 3, 5])) + + for input_rcu_index, expected_output in zip(test_input, test_output): + actual_output = index.rcu_index_2_signal_index(input_rcu_index) + np.testing.assert_array_equal(expected_output, actual_output) + + @staticmethod + def test_rcu_index_and_rcu_input_index_2_signal_index(): + """Test code for rcu_index_and_rcu_input_index_2_signal_index""" + + test_input = [] + test_output = [] + + # test 1 + test_input.append([np.array([0]), np.array([0])]) + test_output.append(np.array([0])) + + # test 2 + test_input.append([np.array([0]), np.array([0, 1, 2])]) + test_output.append(np.array([0, 2, 4])) + + for inputs, expected_output in zip(test_input, test_output): + input_rcu_index = inputs[0] + input_rcu_input_index = inputs[1] + actual_output = index.rcu_index_and_rcu_input_index_2_signal_index( + input_rcu_index, input_rcu_input_index + ) + np.testing.assert_array_equal(expected_output, actual_output) + + @staticmethod + def test_signal_input_2_rcu_index_and_rcu_input_index(): + """Test code for signal_input_2_rcu_index_and_rcu_input_index""" + + test_input = [] + test_output = [] + + # test 1 + test_input.append(np.array([0])) + test_output.append((np.array([0]), np.array([0]))) + + # test 2 + test_input.append(np.array([1])) + test_output.append((np.array([1]), np.array([0]))) + + for input_signal_index, expected_output in zip(test_input, test_output): + actual_output = index.signal_input_2_rcu_index_and_rcu_input_index( + input_signal_index + ) + np.testing.assert_array_equal(expected_output, actual_output) diff --git a/tests/dts/test_outside.py b/tests/dts/test_outside.py new file mode 100644 index 0000000000000000000000000000000000000000..05cae1a19a1327fb56f122591679091afa5d5ecb --- /dev/null +++ b/tests/dts/test_outside.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import numpy as np + +import copy + +from unittest import mock + +from lofar_station_client.dts import outside + +from tests import base + + +class DTSOutsideTest(base.TestCase): + def test_set_rcu_ant_masked_update_all(self): + """Test masked mixing of new and old values, update all""" + + t_recv = mock.Mock() + + mask = [[True] * 3] * 12 + old = [[0] * 3] * 12 + new = [[1] * 3] * 12 + + result = outside.set_rcu_ant_masked(t_recv, mask, copy.deepcopy(old), new) + np.testing.assert_array_equal(result, new) + + def test_set_rcu_ant_masked_update_none(self): + """Test masked mixing of new and old values, update none""" + + t_recv = mock.Mock() + + mask = [[False] * 3] * 12 + old = [[0] * 3] * 12 + new = [[1] * 3] * 12 + + result = outside.set_rcu_ant_masked(t_recv, mask, copy.deepcopy(old), new) + np.testing.assert_array_equal(result, old) + + def test_set_rcu_hba_mask_update_all(self): + """Test masked mixing of new and old values, update all""" + t_recv = mock.Mock() + + mask = [[True] * 3] * 12 + old = np.array([[0] * 3] * 12).flatten() + new = np.array([[1] * 3] * 12).flatten() + + result = outside.set_rcu_hba_mask(t_recv, mask, copy.deepcopy(old), new) + np.testing.assert_array_equal(result, new) + + def test_set_rcu_hba_mask_update_none(self): + """Test masked mixing of new and old values, update none""" + t_recv = mock.Mock() + + mask = [[False] * 3] * 12 + old = np.array([[0] * 3] * 12).flatten() + new = np.array([[1] * 3] * 12).flatten() + + result = outside.set_rcu_hba_mask(t_recv, mask, copy.deepcopy(old), new) + np.testing.assert_array_equal(result, old) diff --git a/tests/statistics/test_collector.py b/tests/statistics/test_collector.py index 8ca45f9cc8583387318dc950a5df319f63640717..bc5a147b11a0835eeff7eda2a0dd87eefa455843 100644 --- a/tests/statistics/test_collector.py +++ b/tests/statistics/test_collector.py @@ -218,7 +218,12 @@ class TestXSTCollector(base.TestCase): f"but was written to the XST matrix.", ) - self.assertEqual(correct_nr_values, actual_nr_values, "Mismatch between number of values in the packet and in the resulting matrix") + self.assertEqual( + correct_nr_values, + actual_nr_values, + "Mismatch between number of values in the packet and in the resulting " + "matrix", + ) def test_multiple_subbands(self): collector = XSTCollector() diff --git a/tests/test_devices.py b/tests/test_devices.py new file mode 100644 index 0000000000000000000000000000000000000000..26701397a49ef7c5d38f07142a6366fe37c6f707 --- /dev/null +++ b/tests/test_devices.py @@ -0,0 +1,136 @@ +import numpy + +from tests import base + +from lofar_station_client.devices import LofarDeviceProxy + +from tango.test_context import MultiDeviceTestContext +from tango.server import Device, attribute, AttrWriteType + + +class MyDevice(Device): + A = attribute( + dtype=((bool,),), + max_dim_x=4, + max_dim_y=6, + format="(2,3,4)", + access=AttrWriteType.READ_WRITE, + ) + + scalar = attribute(dtype=bool, access=AttrWriteType.READ_WRITE) + + spectrum = attribute(dtype=(bool,), max_dim_x=2, access=AttrWriteType.READ_WRITE) + + image = attribute( + dtype=((bool,),), max_dim_x=2, max_dim_y=3, access=AttrWriteType.READ_WRITE + ) + + def init_device(self): + self.value_A = numpy.zeros((6, 4), dtype=bool) + self.value_scalar = False + self.value_spectrum = [False, False] + self.value_image = [[False, False]] * 3 + + def read_scalar(self): + return self.value_scalar + + def write_scalar(self, value): + self.value_scalar = value + + def read_spectrum(self): + return self.value_spectrum + + def write_spectrum(self, value): + self.value_spectrum = value + + def read_image(self): + return self.value_image + + def write_image(self, value): + self.value_image = value + + def read_A(self): + return self.value_A + + def write_A(self, value): + self.value_A = value + + +class LofarDeviceProxyTest(base.TestCase): + TEST_DEVICE_INFO = [ + { + "class": MyDevice, + "devices": [{"name": "STAT/MyDevice/1", "properties": {}, "memorized": {}}], + } + ] + + @classmethod + def setUpClass(cls): + # setting up the TestContext takes ~1 second, so do it only once + cls.context = MultiDeviceTestContext( + cls.TEST_DEVICE_INFO, + process=True, + ) + + cls.context.start() + cls.proxy = LofarDeviceProxy(cls.context.get_device_access("STAT/MyDevice/1")) + + @classmethod + def tearDownClass(cls): + # In Python3.8+, we can use addClassCleanup instead + cls.context.stop() + + def test_read_scalar(self): + value = self.proxy.scalar + + self.assertEqual(bool, type(value)) + + def test_write_scalar(self): + self.proxy.scalar = True + + def test_read_spectrum(self): + value = self.proxy.spectrum + + self.assertEqual((2,), value.shape) + self.assertEqual(numpy.bool_, type(value[0])) + + def test_write_spectrum(self): + self.proxy.spectrum = [True, False] + + def test_read_image(self): + value = self.proxy.image + + self.assertEqual((3, 2), value.shape) + self.assertEqual(numpy.bool_, type(value[0, 0])) + + def test_write_image(self): + self.proxy.image = [[True, False]] * 3 + + def test_write_3D_attribute_lists(self): + self.proxy.A = [ + [True, True, True, True], + [True, True, True, True], + [True, True, True, True], + ], [ + [False, False, False, False], + [False, False, False, False], + [False, False, False, False], + ] + + def test_write_3D_attribute_numpy(self): + self.proxy.A = numpy.zeros((2, 3, 4), dtype=bool) + + def test_write_3D_attribute_numpy_with_too_few_dimensions(self): + # write a 2D shape + with self.assertRaises(ValueError): + self.proxy.A = numpy.zeros((2, 12), dtype=bool) + + def test_write_3D_attribute_numpy_with_too_many_dimension(self): + # write a 4D shape + with self.assertRaises(ValueError): + self.proxy.A = numpy.zeros((2, 3, 2, 2), dtype=bool) + + def test_read_3D_attribute(self): + value = self.proxy.A + + self.assertEqual((2, 3, 4), value.shape) diff --git a/tox.ini b/tox.ini index 6610b9523416f658c7a4d60c2ab37bfd22be470c..a358069440d42dfbfe9790a122a5cca328be7acf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] # Generative environment list to test all supported Python versions -envlist = py3{7,8,9,10},black,pep8,pylint +envlist = black,pep8,pylint,py3{7,8,9,10},docs minversion = 3.18.0 # Source distributions are explicitly build using tox -e build skipsdist = True @@ -14,6 +14,10 @@ setenv = deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt +commands_pre = + # required until https://gitlab.com/tango-controls/pytango/-/issues/468 is resolved + pip install 'numpy>=1.21.0' + pip install --no-cache 'PyTango>=9.3.5' commands = {envpython} --version stestr run {posargs}