From a0daceeb65fcbf0d412c8413726953c9e36a19b1 Mon Sep 17 00:00:00 2001 From: Hannes Feldt <feldt@astron.nl> Date: Mon, 17 Apr 2023 09:29:53 +0000 Subject: [PATCH] L2SS-1083 As a maintainer, I want to apply available HDF5-formatted calibration tables --- CDB/LOFAR_ConfigDb.json | 734 +++++++++--------- Makefile | 11 + docker-compose/.env | 3 + docker-compose/Makefile | 41 +- docker-compose/calibration-tables/Dockerfile | 2 - docker-compose/calibration-tables/README.md | 3 - docker-compose/device-antennafield.yml | 3 - docker-compose/device-calibration.yml | 58 ++ docker-compose/lofar-device-base/Dockerfile | 1 - docker-compose/object-storage.yml | 48 ++ .../CalTable-DevStation-HBA-150MHz.h5 | Bin 872448 -> 874584 bytes .../CalTable-DevStation-HBA-200MHz.h5 | Bin 872448 -> 874584 bytes .../CalTable-DevStation-HBA-250MHz.h5 | Bin 872448 -> 874584 bytes .../CalTable-DevStation-LBA-50MHz.h5 | Bin 1740824 -> 1740888 bytes sbin/run_integration_test.sh | 9 +- sbin/tag_and_push_docker_image.sh | 2 +- tangostationcontrol/requirements.txt | 1 + tangostationcontrol/setup.cfg | 1 + .../tangostationcontrol/common/__init__.py | 8 +- .../tangostationcontrol/common/calibration.py | 254 ++++-- .../common/calibration_table.py | 24 - .../tangostationcontrol/devices/__init__.py | 4 +- .../devices/antennafield.py | 396 +--------- .../tangostationcontrol/devices/boot.py | 28 +- .../devices/calibration.py | 212 +++++ .../devices/test_device_antennafield.py | 166 +--- .../devices/test_device_calibration.py | 229 ++++++ .../devices/test_device_digitalbeam.py | 107 ++- .../devices/test_device_observation.py | 7 +- .../test/common/test_calibration.py | 118 ++- 30 files changed, 1459 insertions(+), 1011 deletions(-) create mode 100644 Makefile delete mode 100644 docker-compose/calibration-tables/Dockerfile delete mode 100644 docker-compose/calibration-tables/README.md create mode 100644 docker-compose/device-calibration.yml create mode 100644 docker-compose/object-storage.yml rename docker-compose/{calibration-tables/caltables => object-storage/caltables/DevStation}/CalTable-DevStation-HBA-150MHz.h5 (94%) rename docker-compose/{calibration-tables/caltables => object-storage/caltables/DevStation}/CalTable-DevStation-HBA-200MHz.h5 (92%) rename docker-compose/{calibration-tables/caltables => object-storage/caltables/DevStation}/CalTable-DevStation-HBA-250MHz.h5 (94%) rename docker-compose/{calibration-tables/caltables => object-storage/caltables/DevStation}/CalTable-DevStation-LBA-50MHz.h5 (94%) delete mode 100644 tangostationcontrol/tangostationcontrol/common/calibration_table.py create mode 100644 tangostationcontrol/tangostationcontrol/devices/calibration.py create mode 100644 tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_calibration.py diff --git a/CDB/LOFAR_ConfigDb.json b/CDB/LOFAR_ConfigDb.json index 8aa444d30..73fb4d573 100644 --- a/CDB/LOFAR_ConfigDb.json +++ b/CDB/LOFAR_ConfigDb.json @@ -1,392 +1,426 @@ { - "servers": { + "servers": { + "StationManager": { + "STAT": { "StationManager": { - "STAT": { - "StationManager": { - "STAT/StationManager/1": { - "properties": { - "Station_Name": [ - "DevStation" - ], - "Station_Number":[ - "999" - ] - } - } - } + "STAT/StationManager/1": { + "properties": { + "Station_Name": [ + "DevStation" + ], + "Station_Number": [ + "999" + ] } - }, + } + } + } + }, + "Docker": { + "STAT": { "Docker": { - "STAT": { - "Docker": { - "STAT/Docker/1": {} - } - } - }, + "STAT/Docker/1": {} + } + } + }, + "Calibration": { + "STAT": { + "Calibration": { + "STAT/Calibration/1": {} + } + } + }, + "Configuration": { + "STAT": { "Configuration": { - "STAT": { - "Configuration": { - "STAT/Configuration/1": {} - } - } - }, + "STAT/Configuration/1": {} + } + } + }, + "Observation": { + "STAT": { "Observation": { - "STAT": { - "Observation": { - } - } - }, + } + } + }, + "ObservationControl": { + "STAT": { "ObservationControl": { - "STAT": { - "ObservationControl": { - "STAT/ObservationControl/1": {} - } - } - }, + "STAT/ObservationControl/1": {} + } + } + }, + "AntennaField": { + "STAT": { "AntennaField": { - "STAT": { - "AntennaField": { - "STAT/AntennaField/HBA": { - "properties": { - "RECV_devices": ["STAT/RECV/1"] - } - } - } + "STAT/AntennaField/HBA": { + "properties": { + "RECV_devices": [ + "STAT/RECV/1" + ] } - }, + } + } + } + }, + "PSOC": { + "STAT": { "PSOC": { - "STAT": { - "PSOC": { - "STAT/PSOC/1": { - "properties": { - "SNMP_host": ["127.0.0.1"], - "SNMP_community": ["public"], - "SNMP_mib_dir": ["devices/mibs/PowerNet-MIB.mib"], - "SNMP_timeout": ["10.0"], - "SNMP_version": ["1"], - "PSOC_sockets": [ - "socket_1", - "socket_2", - "socket_3", - "socket_4", - "socket_5", - "socket_6", - "socket_7", - "socket_8" - ] - } - } - } + "STAT/PSOC/1": { + "properties": { + "SNMP_host": [ + "127.0.0.1" + ], + "SNMP_community": [ + "public" + ], + "SNMP_mib_dir": [ + "devices/mibs/PowerNet-MIB.mib" + ], + "SNMP_timeout": [ + "10.0" + ], + "SNMP_version": [ + "1" + ], + "PSOC_sockets": [ + "socket_1", + "socket_2", + "socket_3", + "socket_4", + "socket_5", + "socket_6", + "socket_7", + "socket_8" + ] } - }, + } + } + } + }, + "PCON": { + "STAT": { "PCON": { - "STAT": { - "PCON": { - "STAT/PCON/1": { - "properties": { - "SNMP_host": ["127.0.0.1"], - "SNMP_community": ["public"], - "SNMP_mib_dir": ["devices/mibs/ACC-MIB.mib"], - "SNMP_timeout": ["10.0"], - "SNMP_version": ["1"] - } - } - } + "STAT/PCON/1": { + "properties": { + "SNMP_host": [ + "127.0.0.1" + ], + "SNMP_community": [ + "public" + ], + "SNMP_mib_dir": [ + "devices/mibs/ACC-MIB.mib" + ], + "SNMP_timeout": [ + "10.0" + ], + "SNMP_version": [ + "1" + ] } - }, + } + } + } + }, + "TemperatureManager": { + "STAT": { "TemperatureManager": { - "STAT": { - "TemperatureManager": { - "STAT/TemperatureManager/1": { - "properties": { - "Alarm_Error_List": [ - "APSCT, APSCT_TEMP_error_R", - "APSPU, APSPU_TEMP_error_R", - "UNB2, UNB2_TEMP_error_R", - "RECV, RECV_TEMP_error_R" - ], - "Shutdown_Device_List":[ - "STAT/SDP/1", "STAT/UNB2/1", "STAT/RECV/1", "STAT/APSCT/1", "STAT/CCD/1", "STAT/APSPU/1" - ] - } - } - } + "STAT/TemperatureManager/1": { + "properties": { + "Alarm_Error_List": [ + "APSCT, APSCT_TEMP_error_R", + "APSPU, APSPU_TEMP_error_R", + "UNB2, UNB2_TEMP_error_R", + "RECV, RECV_TEMP_error_R" + ], + "Shutdown_Device_List": [ + "STAT/SDP/1", + "STAT/UNB2/1", + "STAT/RECV/1", + "STAT/APSCT/1", + "STAT/CCD/1", + "STAT/APSPU/1" + ] } - }, + } + } + } + }, + "TileBeam": { + "STAT": { "TileBeam": { - "STAT": { - "TileBeam": { - "STAT/TileBeam/HBA": { - } - } - } - }, + "STAT/TileBeam/HBA": { + } + } + } + }, + "Beamlet": { + "STAT": { "Beamlet": { - "STAT": { - "Beamlet": { - "STAT/Beamlet/1": { - "properties": { - "FPGA_beamlet_output_hdr_eth_source_mac_RW_default": [ - "00:22:86:08:00:00", - "00:22:86:08:00:01", - "00:22:86:08:00:02", - "00:22:86:08:00:03", - "00:22:86:08:01:00", - "00:22:86:08:01:01", - "00:22:86:08:01:02", - "00:22:86:08:01:03", - "00:22:86:08:02:00", - "00:22:86:08:02:01", - "00:22:86:08:02:02", - "00:22:86:08:02:03", - "00:22:86:08:03:00", - "00:22:86:08:03:01", - "00:22:86:08:03:02", - "00:22:86:08:03:03" - ], - "FPGA_beamlet_output_hdr_ip_source_address_RW_default": [ - "192.168.0.1", - "192.168.0.2", - "192.168.0.3", - "192.168.0.4", - "192.168.1.1", - "192.168.1.2", - "192.168.1.3", - "192.168.1.4", - "192.168.2.1", - "192.168.2.2", - "192.168.2.3", - "192.168.2.4", - "192.168.3.1", - "192.168.3.2", - "192.168.3.3", - "192.168.3.4" - ], - "FPGA_beamlet_output_hdr_udp_source_port_RW_default": [ - "53248", - "53249", - "53250", - "53251", - "53252", - "53253", - "53254", - "53255", - "53256", - "53257", - "53258", - "53259", - "53260", - "53261", - "53262", - "53263" - ], - "FPGA_beamlet_output_hdr_udp_destination_port_RW_default": [ - "10001", - "10001", - "10001", - "10001", - "10001", - "10001", - "10001", - "10001", - "10001", - "10001", - "10001", - "10001", - "10001", - "10001", - "10001", - "10001" - ] - } - } - } + "STAT/Beamlet/1": { + "properties": { + "FPGA_beamlet_output_hdr_eth_source_mac_RW_default": [ + "00:22:86:08:00:00", + "00:22:86:08:00:01", + "00:22:86:08:00:02", + "00:22:86:08:00:03", + "00:22:86:08:01:00", + "00:22:86:08:01:01", + "00:22:86:08:01:02", + "00:22:86:08:01:03", + "00:22:86:08:02:00", + "00:22:86:08:02:01", + "00:22:86:08:02:02", + "00:22:86:08:02:03", + "00:22:86:08:03:00", + "00:22:86:08:03:01", + "00:22:86:08:03:02", + "00:22:86:08:03:03" + ], + "FPGA_beamlet_output_hdr_ip_source_address_RW_default": [ + "192.168.0.1", + "192.168.0.2", + "192.168.0.3", + "192.168.0.4", + "192.168.1.1", + "192.168.1.2", + "192.168.1.3", + "192.168.1.4", + "192.168.2.1", + "192.168.2.2", + "192.168.2.3", + "192.168.2.4", + "192.168.3.1", + "192.168.3.2", + "192.168.3.3", + "192.168.3.4" + ], + "FPGA_beamlet_output_hdr_udp_source_port_RW_default": [ + "53248", + "53249", + "53250", + "53251", + "53252", + "53253", + "53254", + "53255", + "53256", + "53257", + "53258", + "53259", + "53260", + "53261", + "53262", + "53263" + ], + "FPGA_beamlet_output_hdr_udp_destination_port_RW_default": [ + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001" + ] } - }, + } + } + } + }, + "DigitalBeam": { + "STAT": { "DigitalBeam": { - "STAT": { - "DigitalBeam": { - "STAT/DigitalBeam/HBA": { - "properties": { - "Beam_tracking_interval": [ - "1.0" - ], - "Beam_tracking_preparation_period": [ - "0.5" - ] - } - } - } + "STAT/DigitalBeam/HBA": { + "properties": { + "Beam_tracking_interval": [ + "1.0" + ], + "Beam_tracking_preparation_period": [ + "0.5" + ] } - }, + } + } + } + }, + "Boot": { + "STAT": { "Boot": { - "STAT": { - "Boot": { - "STAT/Boot/1": {} - } - } - }, + "STAT/Boot/1": {} + } + } + }, + "APSCT": { + "STAT": { "APSCT": { - "STAT": { - "APSCT": { - "STAT/APSCT/1": { - "properties": { - } - } - } + "STAT/APSCT/1": { + "properties": { } - }, + } + } + } + }, + "CCD": { + "STAT": { "CCD": { - "STAT": { - "CCD": { - "STAT/CCD/1": { - "properties": { - } - } - } + "STAT/CCD/1": { + "properties": { } - }, + } + } + } + }, + "APSPU": { + "STAT": { "APSPU": { - "STAT": { - "APSPU": { - "STAT/APSPU/1": { - "properties": { - } - } - } + "STAT/APSPU/1": { + "properties": { } - }, + } + } + } + }, + "RECV": { + "STAT": { "RECV": { - "STAT": { - "RECV": { - "STAT/RECV/1": { - "properties": { - } - } - } + "STAT/RECV/1": { + "properties": { } - }, + } + } + } + }, + "SDP": { + "STAT": { "SDP": { - "STAT": { - "SDP": { - "STAT/SDP/1": { - "properties": { - } - } - } + "STAT/SDP/1": { + "properties": { } - }, + } + } + } + }, + "BST": { + "STAT": { "BST": { - "STAT": { - "BST": { - "STAT/BST/1": { - "properties": { - "Statistics_Client_UDP_Port": [ - "5003" - ], - "Statistics_Client_TCP_Port": [ - "5103" - ], - "FPGA_bst_offload_hdr_udp_destination_port_RW_default": [ - "5003", - "5003", - "5003", - "5003", - "5003", - "5003", - "5003", - "5003", - "5003", - "5003", - "5003", - "5003", - "5003", - "5003", - "5003", - "5003" - ] - } - } - } + "STAT/BST/1": { + "properties": { + "Statistics_Client_UDP_Port": [ + "5003" + ], + "Statistics_Client_TCP_Port": [ + "5103" + ], + "FPGA_bst_offload_hdr_udp_destination_port_RW_default": [ + "5003", + "5003", + "5003", + "5003", + "5003", + "5003", + "5003", + "5003", + "5003", + "5003", + "5003", + "5003", + "5003", + "5003", + "5003", + "5003" + ] } - }, + } + } + } + }, + "SST": { + "STAT": { "SST": { - "STAT": { - "SST": { - "STAT/SST/1": { - "properties": { - "Statistics_Client_UDP_Port": [ - "5001" - ], - "Statistics_Client_TCP_Port": [ - "5101" - ], - "FPGA_sst_offload_hdr_udp_destination_port_RW_default": [ - "5001", - "5001", - "5001", - "5001", - "5001", - "5001", - "5001", - "5001", - "5001", - "5001", - "5001", - "5001", - "5001", - "5001", - "5001", - "5001" - ] - } - } - } + "STAT/SST/1": { + "properties": { + "Statistics_Client_UDP_Port": [ + "5001" + ], + "Statistics_Client_TCP_Port": [ + "5101" + ], + "FPGA_sst_offload_hdr_udp_destination_port_RW_default": [ + "5001", + "5001", + "5001", + "5001", + "5001", + "5001", + "5001", + "5001", + "5001", + "5001", + "5001", + "5001", + "5001", + "5001", + "5001", + "5001" + ] } - }, + } + } + } + }, + "XST": { + "STAT": { "XST": { - "STAT": { - "XST": { - "STAT/XST/1": { - "properties": { - "Statistics_Client_UDP_Port": [ - "5002" - ], - "Statistics_Client_TCP_Port": [ - "5102" - ], - "FPGA_xst_offload_hdr_udp_destination_port_RW_default": [ - "5002", - "5002", - "5002", - "5002", - "5002", - "5002", - "5002", - "5002", - "5002", - "5002", - "5002", - "5002", - "5002", - "5002", - "5002", - "5002" - ] - } - } - } + "STAT/XST/1": { + "properties": { + "Statistics_Client_UDP_Port": [ + "5002" + ], + "Statistics_Client_TCP_Port": [ + "5102" + ], + "FPGA_xst_offload_hdr_udp_destination_port_RW_default": [ + "5002", + "5002", + "5002", + "5002", + "5002", + "5002", + "5002", + "5002", + "5002", + "5002", + "5002", + "5002", + "5002", + "5002", + "5002", + "5002" + ] } - }, + } + } + } + }, + "UNB2": { + "STAT": { "UNB2": { - "STAT": { - "UNB2": { - "STAT/UNB2/1": { - "properties": { - } - } - } + "STAT/UNB2/1": { + "properties": { } + } } + } } + } } diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..992ce93ad --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +# forward everything to docker-compose subdirectory +$(firstword $(MAKECMDGOALS)): + $(MAKE) -C docker-compose $(MAKECMDGOALS) + +docker-compose: + $(MAKE) -C $@ $(MAKECMDGOALS) + +.PHONY: docker-compose tox + +%:: + cd . diff --git a/docker-compose/.env b/docker-compose/.env index 7de2eff68..f410ee249 100644 --- a/docker-compose/.env +++ b/docker-compose/.env @@ -16,4 +16,7 @@ TANGO_STARTER_VERSION=2021-05-28 MYSQL_ROOT_PASSWORD=secret MYSQL_PASSWORD=tango +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin + TEST_MODULE=default diff --git a/docker-compose/Makefile b/docker-compose/Makefile index 34decf3d5..8a691278e 100644 --- a/docker-compose/Makefile +++ b/docker-compose/Makefile @@ -5,6 +5,11 @@ MAKEPATH := $(abspath $(lastword $(MAKEFILE_LIST))) BASEDIR := $(notdir $(patsubst %/,%,$(dir $(MAKEPATH)))) +ifeq (, $(shell which docker-compose)) + DOCKER_COMPOSE = docker compose +endif +DOCKER_COMPOSE ?= docker-compose + DOCKER_COMPOSE_ENV_FILE := $(abspath .env) COMPOSE_FILES := $(wildcard *.yml) COMPOSE_FILE_ARGS := --env-file $(DOCKER_COMPOSE_ENV_FILE) $(foreach yml,$(COMPOSE_FILES),-f $(yml)) @@ -164,33 +169,33 @@ DOCKER_COMPOSE_ARGS := DISPLAY=$(DISPLAY) \ .DEFAULT_GOAL := help pull: ## pull the images from the Docker hub - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) pull + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) pull base: context ## Build base lofar device image - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) build --progress=plain lofar-device-base + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) build --progress=plain lofar-device-base base-nocache: context ## Rebuild base lofar device image - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) build --no-cache --progress=plain lofar-device-base + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) build --no-cache --progress=plain lofar-device-base build: base ## build images # docker-compose does not support build dependencies, so manage those here - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) build --parallel --progress=plain $(SERVICE) + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) build --parallel --progress=plain $(SERVICE) build-nocache: base-nocache ## rebuild images from scratch # docker-compose does not support build dependencies, so manage those here - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) build --no-cache --progress=plain $(SERVICE) + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) build --no-cache --progress=plain $(SERVICE) up: base minimal ## start the base TANGO system and prepare requested services - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) up --no-start --no-recreate $(SERVICE) + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) up --no-start --no-recreate $(SERVICE) run: base minimal ## run a service using arguments and delete it afterwards - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) run -T --no-deps --rm $(SERVICE) $(SERVICE_ARGS) + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) run -T --no-deps --rm $(SERVICE) $(SERVICE_ARGS) integration: minimal ## run a service using arguments and delete it afterwards - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) run -T --no-deps --rm integration-test $(INTEGRATION_ARGS) + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) run -T --no-deps --rm integration-test $(INTEGRATION_ARGS) down: ## stop all services and tear down the system - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) down + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) down ifneq ($(NETWORK_MODE),host) docker network inspect $(NETWORK_MODE) &> /dev/null && ([ $$? -eq 0 ] && docker network rm $(NETWORK_MODE)) || true docker network inspect 9000-$(NETWORK_MODE) &> /dev/null && ([ $$? -eq 0 ] && docker network rm 9000-$(NETWORK_MODE)) || true @@ -202,7 +207,7 @@ ifneq ($(NETWORK_MODE),host) docker network inspect 9000-$(NETWORK_MODE) &> /dev/null || ([ $$? -ne 0 ] && docker network create 9000-$(NETWORK_MODE) -o com.docker.network.driver.mtu=9000) endif - $(DOCKER_COMPOSE_ARGS) docker-compose -f tango.yml -f networks.yml up --no-recreate -d + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) -f tango.yml -f networks.yml up --no-recreate -d context: ## Move the necessary files to create minimal docker context @mkdir -p tmp @@ -216,24 +221,24 @@ bootstrap: pull build # first start, initialise from scratch start: up ## start a service (usage: make start <servicename>) if [ $(UNAME_S) = Linux ]; then touch ~/.Xauthority; chmod a+r ~/.Xauthority; fi - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) start $(SERVICE) + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) start $(SERVICE) stop: ## stop a service (usage: make stop <servicename>) - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) stop $(SERVICE) - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) rm -f $(SERVICE) + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) stop $(SERVICE) + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) rm -f $(SERVICE) restart: ## restart a service (usage: make restart <servicename>) make stop $(SERVICE) # cannot use dependencies, as that would allow start and stop to run in parallel.. make start $(SERVICE) attach: ## attach a service to an existing Tango network - $(DOCKER_COMPOSE_ARGS) docker-compose $(ATTACH_COMPOSE_FILE_ARGS) up --no-recreate -d $(SERVICE) + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(ATTACH_COMPOSE_FILE_ARGS) up --no-recreate -d $(SERVICE) TIME := 0 await: ## Await every container with total max timeout of 300, do not reset timeout time=$(TIME); \ for i in $(SERVICE); do \ - current_service=$$($(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) ps -q $${i}); \ + current_service=$$($(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) ps -q $${i}); \ if [ -z "$${current_service}" ]; then \ continue; \ fi; \ @@ -254,14 +259,14 @@ await: ## Await every container with total max timeout of 300, do not reset tim done status: ## show the container status - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) ps + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) ps images: ## show the container images - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) images + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) images clean: down ## clear all TANGO database entries, and all containers docker volume rm $(BASEDIR)_tangodb - $(DOCKER_COMPOSE_ARGS) docker-compose $(COMPOSE_FILE_ARGS) rm -f + $(DOCKER_COMPOSE_ARGS) $(DOCKER_COMPOSE) $(COMPOSE_FILE_ARGS) rm -f help: ## show this help. @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/docker-compose/calibration-tables/Dockerfile b/docker-compose/calibration-tables/Dockerfile deleted file mode 100644 index e7ce3d858..000000000 --- a/docker-compose/calibration-tables/Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM nginx -COPY caltables /usr/share/nginx/html diff --git a/docker-compose/calibration-tables/README.md b/docker-compose/calibration-tables/README.md deleted file mode 100644 index 10261e80c..000000000 --- a/docker-compose/calibration-tables/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Calibration Tables - -Service to serve HDF5 calibration table files over HTTP. Used by antennafield device. diff --git a/docker-compose/device-antennafield.yml b/docker-compose/device-antennafield.yml index 69ccb78c0..5c38de7f3 100644 --- a/docker-compose/device-antennafield.yml +++ b/docker-compose/device-antennafield.yml @@ -34,9 +34,6 @@ services: - "host.docker.internal:host-gateway" volumes: - ..:/opt/lofar/tango:rw - - type: bind - source: ./calibration-tables/caltables - target: /opt/calibration-tables environment: - TANGO_HOST=${TANGO_HOST} - TANGO_ZMQ_EVENT_PORT=5815 diff --git a/docker-compose/device-calibration.yml b/docker-compose/device-calibration.yml new file mode 100644 index 000000000..2be7c73bd --- /dev/null +++ b/docker-compose/device-calibration.yml @@ -0,0 +1,58 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 +# +# Docker compose file that launches an interactive iTango session. +# +# Connect to the interactive session with 'docker attach itango'. +# Disconnect with the Docker deattach sequence: <CTRL>+<P> <CTRL>+<Q> +# +# Defines: +# - itango: iTango interactive session +# +# Requires: +# - lofar-device-base.yml +# +version: '2.1' + +services: + device-calibration: + image: lofar-device-base + container_name: device-calibration + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "10" + networks: + - control + ports: + - "5724:5724" # unique port for this DS + - "5824:5824" # ZeroMQ event port + - "5924:5924" # ZeroMQ heartbeat port + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ..:/opt/lofar/tango:rw + environment: + - TANGO_HOST=${TANGO_HOST} + - TANGO_ZMQ_EVENT_PORT=5824 + - TANGO_ZMQ_HEARTBEAT_PORT=5924 + - MINIO_ROOT_USER=${MINIO_ROOT_USER} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} + healthcheck: + test: l2ss-health STAT/Calibration/1 + interval: 1m + timeout: 30s + retries: 3 + start_period: 30s + working_dir: /opt/lofar/tango + entrypoint: + - bin/start-ds.sh + # configure CORBA to _listen_ on 0:port, but tell others we're _reachable_ through ${HOSTNAME}:port, since CORBA + # can't know about our Docker port forwarding + - l2ss-calibration Calibration STAT -v -ORBendPoint giop:tcp:0:5724 -ORBendPointPublish giop:tcp:${HOSTNAME}:5724 + restart: unless-stopped + stop_signal: SIGINT # request a graceful shutdown of Tango + stop_grace_period: 2s + depends_on: + - object-storage diff --git a/docker-compose/lofar-device-base/Dockerfile b/docker-compose/lofar-device-base/Dockerfile index 42254ec6f..7b49a6cfc 100644 --- a/docker-compose/lofar-device-base/Dockerfile +++ b/docker-compose/lofar-device-base/Dockerfile @@ -27,4 +27,3 @@ COPY lofar-device-base/casarc /home/tango/.casarc ENV TANGO_LOG_PATH=/var/log/tango RUN sudo mkdir -p /var/log/tango && sudo chmod a+rwx /var/log/tango -RUN sudo mkdir -p /opt/calibration-tables && sudo chmod a+rwx /opt/calibration-tables diff --git a/docker-compose/object-storage.yml b/docker-compose/object-storage.yml new file mode 100644 index 000000000..5ba417b41 --- /dev/null +++ b/docker-compose/object-storage.yml @@ -0,0 +1,48 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 +# +# Docker compose file that launches Minio as a S3 compatible object storage. A UI is available via http +# +# Connect by surfing to http://localhost:9001/ +# View logs through 'docker logs -f -t object-storage' + +version: '2.1' + +services: + object-storage: + image: minio/minio + container_name: object-storage + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "10" + networks: + - control + volumes: + - object-storage:/data:rw + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} + ports: + - "9000:9000" + - "9001:9001" + command: server --console-address ":9001" /data + restart: unless-stopped + + init-object-storage: + image: minio/mc + networks: + - control + depends_on: + - object-storage + volumes: + - ..:/opt/lofar/tango:rw + entrypoint: '' + command: > + sh -c "mc alias set object-storage http://object-storage:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD + mc mb --with-versioning object-storage/caltables + mc cp --recursive /opt/lofar/tango/docker-compose/object-storage/caltables/ object-storage/caltables/" + +volumes: + object-storage: diff --git a/docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-150MHz.h5 b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-150MHz.h5 similarity index 94% rename from docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-150MHz.h5 rename to docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-150MHz.h5 index f2beb4b078b240d5dc6d52804999707d98b5524e..0cb1af7b0cc5b0517a00de2b41a294643138aed7 100644 GIT binary patch delta 6758 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 delta 6586 zcmcby!L(t4=>!cXhJcA$77CmUj0_A65)2R^z#sr-C@_O5h6JX`zr@w*E1>cT5K#t& zPZ0SAD8J$i3z)S4OfoPqXt04<JHR9ZgMt$~n56(F85kaDGl5wGAiEhD7!;Vez<dKJ zzv2#r?*ZXUF)�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 diff --git a/docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-200MHz.h5 b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-200MHz.h5 similarity index 92% rename from docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-200MHz.h5 rename to docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-200MHz.h5 index d560a6f71dd2c935bd993ad947503e864e129bc6..8af326794446c05ce57a1b548146346dc3b8fae3 100644 GIT binary patch delta 6761 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 delta 6583 zcmcby!L(t4=>!cXhJcA$77CmUj0_A65)2R^z#sr-C@_O5h6JX`zr@w*E1>cT5K#t& zPZ0SAD8J$i3z)S4OfoPqXt04<JHR9ZgMt$~n56(F85kaDGl5wGAiEhD7!;Vez<dKJ zzv2#r?*ZXUF)�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 diff --git a/docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-250MHz.h5 b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-250MHz.h5 similarity index 94% rename from docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-250MHz.h5 rename to docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-250MHz.h5 index 868cf35e8677d1070f048e4b6150d1a41291033e..af5980a9fbabf0f5aa9ef3cd55e89fea3cbeea2f 100644 GIT binary patch delta 6739 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 E0Nd5lu>b%7 delta 6429 zcmcby!L(t4=>!cXhJcA$77CmUj0_A65)2R^z#sr-C@_O5h6JX`zr@w*E1>cT5K#t& zPZ0SAD8J$i3z)S4OfoPqXt04<JHR9ZgMt$~n56(F85kaDGl5wGAiEhD7!;Vez<dKJ zzv2#r?*ZXUF)�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 diff --git a/docker-compose/calibration-tables/caltables/CalTable-DevStation-LBA-50MHz.h5 b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-LBA-50MHz.h5 similarity index 94% rename from docker-compose/calibration-tables/caltables/CalTable-DevStation-LBA-50MHz.h5 rename to docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-LBA-50MHz.h5 index 6637b2019a02e3d02335b5393b908bc20146fa44..baef10d555c1b0101146cca088bcac9c37365d67 100644 GIT binary patch delta 13082 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 delta 12777 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 diff --git a/sbin/run_integration_test.sh b/sbin/run_integration_test.sh index 2a814ffaa..9267f4692 100755 --- a/sbin/run_integration_test.sh +++ b/sbin/run_integration_test.sh @@ -1,6 +1,8 @@ #!/bin/bash -e -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 +# # Usage function explains how parameters are parsed function usage { @@ -74,7 +76,7 @@ echo '/usr/local/bin/wait-for-it.sh ${TANGO_HOST} --strict --timeout=300 -- true # Devices list is used to explitly word split when supplied to commands, must # disable shellcheck SC2086 for each case. -DEVICES=(device-station-manager device-boot device-apsct device-ccd device-apspu device-sdp device-recv device-bst device-sst device-unb2 device-xst device-beamlet device-digitalbeam device-tilebeam device-psoc device-pcon device-antennafield device-temperature-manager device-observation device-observation-control device-configuration) +DEVICES=(device-station-manager device-boot device-apsct device-ccd device-apspu device-sdp device-recv device-bst device-sst device-unb2 device-xst device-beamlet device-digitalbeam device-tilebeam device-psoc device-pcon device-antennafield device-temperature-manager device-observation device-observation-control device-configuration device-calibration) SIMULATORS=(sdptr-sim recv-sim unb2-sim apsct-sim apspu-sim ccd-sim) @@ -87,11 +89,12 @@ make build logstash integration-test http-json-schemas # Start and stop sequence make stop http-json-schemas +make stop object-storage init-object-storage make stop "${DEVICES[@]}" "${SIMULATORS[@]}" make stop device-docker # this one does not test well in docker-in-docker make stop logstash -make start logstash http-json-schemas +make start logstash http-json-schemas object-storage init-object-storage # Update the dsconfig # Do not remove `bash`, otherwise statement ignored by gitlab ci shell! diff --git a/sbin/tag_and_push_docker_image.sh b/sbin/tag_and_push_docker_image.sh index 4ae48590b..7d398b075 100755 --- a/sbin/tag_and_push_docker_image.sh +++ b/sbin/tag_and_push_docker_image.sh @@ -1,5 +1,5 @@ #!/bin/bash -e -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 function usage { diff --git a/tangostationcontrol/requirements.txt b/tangostationcontrol/requirements.txt index 7c6af9b85..86f4505eb 100644 --- a/tangostationcontrol/requirements.txt +++ b/tangostationcontrol/requirements.txt @@ -17,3 +17,4 @@ python-casacore >= 3.3.1 # LGPLv3 etrs-itrs@git+https://github.com/brentjens/etrs-itrs # Apache 2 lofarantpos >= 0.5.0 # Apache 2 python-geohash >= 0.8.5 # Apache 2 / MIT +minio >= 7.1.14 # Apache 2 diff --git a/tangostationcontrol/setup.cfg b/tangostationcontrol/setup.cfg index 5ccd4a8ac..2b87dbffc 100644 --- a/tangostationcontrol/setup.cfg +++ b/tangostationcontrol/setup.cfg @@ -54,6 +54,7 @@ console_scripts = l2ss-xst = tangostationcontrol.devices.sdp.xst:main l2ss-temperaturemanager = tangostationcontrol.devices.temperature_manager:main l2ss-configuration = tangostationcontrol.devices.configuration:main + l2ss-calibration = tangostationcontrol.devices.calibration:main # The following entry points should eventually be removed / replaced l2ss-hardware-device-template = tangostationcontrol.examples.HW_device_template:main diff --git a/tangostationcontrol/tangostationcontrol/common/__init__.py b/tangostationcontrol/tangostationcontrol/common/__init__.py index a212ea51d..bbdd80eaa 100644 --- a/tangostationcontrol/tangostationcontrol/common/__init__.py +++ b/tangostationcontrol/tangostationcontrol/common/__init__.py @@ -1,8 +1,6 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 from .observation_controller import ObservationController -__all__ = [ - "ObservationController", -] +__all__ = ["ObservationController"] diff --git a/tangostationcontrol/tangostationcontrol/common/calibration.py b/tangostationcontrol/tangostationcontrol/common/calibration.py index 970781c70..e949179e7 100644 --- a/tangostationcontrol/tangostationcontrol/common/calibration.py +++ b/tangostationcontrol/tangostationcontrol/common/calibration.py @@ -1,17 +1,159 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 +import logging +import os +import tempfile +from typing import Dict +from urllib.parse import urlparse import numpy -from lofar_station_client.file_access import read_hdf5 +from lofar_station_client.file_access import member, attribute, read_hdf5 +from minio import Minio +from tango import DeviceProxy -from tangostationcontrol.common.calibration_table import ( - CalibrationTable as Hdf5CalibrationTable, -) from tangostationcontrol.common.constants import ( - N_pol, N_subbands, + N_pol, + N_pn, + S_pn, ) +logger = logging.getLogger() + + +class CalibrationData: + x: numpy.ndarray = member() + y: numpy.ndarray = member() + + +class CalibrationTable: + observation_station: str = attribute() + observation_station_version: str = attribute() + observation_mode: str = attribute() + observation_source: str = attribute() + observation_date: str = attribute() + calibration_version: int = attribute() + calibration_name: str = attribute() + calibration_date: str = attribute() + antennas: Dict[str, CalibrationData] = member() + + +class CalibrationManager: + @property + def url(self): + return self._url + + @url.setter + def url(self, new_url): + self._url = new_url + + def __init__(self, url: str, station_name: str): + self._url = url + self._station_name = station_name + self._tmp_dir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True) + self.bucket_name = "caltabules" + self.prefix = self._station_name + self._init_minio() + self.sync_calibration_tables() + + def _init_minio(self): + result = urlparse(self._url) + bucket_name, prefix, *_ = result.path[1:].split("/", 1) + [""] + if len(prefix) > 0: + self.prefix = "/".join([prefix.rstrip("/"), self._station_name]) + if len(bucket_name) > 0: + self.bucket_name = bucket_name + + self._storage = Minio( + result.netloc, + access_key=os.getenv("MINIO_ROOT_USER"), + secret_key=os.getenv("MINIO_ROOT_PASSWORD"), + secure=result.scheme == "https", + ) + + def sync_calibration_tables(self): + logger.debug( + f"Sync calibration tables from bucket {self.bucket_name} " + f"with prefix {self.prefix}/" + ) + objects = self._storage.list_objects(self.bucket_name, prefix=f"{self.prefix}/") + for obj in objects: + filename = os.path.basename(obj.object_name) + self._storage.fget_object( + self.bucket_name, + obj.object_name, + os.path.join(self._tmp_dir.name, filename), + ) + + @staticmethod + def _band_to_reference_frequency(is_hba, rcu_band): + if is_hba: + match rcu_band: + case 1: + return "200" + case 2: + return "150" + case 4: + return "250" + return "50" + + def calibrate_subband_weights(self, antenna_field: DeviceProxy, sdp: DeviceProxy): + # ----------------------------------------------------------- + # Compute calibration of subband weights for the remaining + # delay and loss corrections. + # ----------------------------------------------------------- + + # Mapping [antenna] -> [fpga][input] + antenna_to_sdp_mapping = antenna_field.Antenna_to_SDP_Mapping_R + + # read-modify-write on [fpga][(input, polarisation)] + fpga_subband_weights = sdp.FPGA_subband_weights_RW.reshape( + (N_pn, S_pn, N_subbands) + ) + + antennafield_name = antenna_field.name().split("/")[2] + rcu_bands = antenna_field.RCU_band_select_RW + antenna_names = antenna_field.Antenna_Names_R + + is_hba = antenna_field.Antenna_Type_R != "LBA" + + for antenna_nr, rcu_band in enumerate(rcu_bands): + fpga_nr, input_nr = antenna_to_sdp_mapping[antenna_nr] + + if input_nr == -1: + # skip unconnected antennas + continue + + calibration_filename = os.path.join( + self._tmp_dir.name, + f"CalTable-{self._station_name}-{antennafield_name}" + f"-{self._band_to_reference_frequency(is_hba, rcu_band)}MHz.h5", + ) + with read_hdf5(calibration_filename, CalibrationTable) as table: + # Retrieve data and convert them in the correct Tango attr shape + if ( + table.observation_station.casefold() + != self._station_name.casefold() + ): + logger.error( + f"Expected calibration table for {self._station_name}, " + f"but got {table.observation_station}" + ) + + # set weights + fpga_subband_weights[fpga_nr, input_nr * N_pol + 0] = table.antennas[ + antenna_names[antenna_nr] + ].x + + fpga_subband_weights[fpga_nr, input_nr * N_pol + 1] = table.antennas[ + antenna_names[antenna_nr] + ].y + + # TODO(L2SS-1312): This should use atomic_read_modify_write + sdp.FPGA_subband_weights_RW = fpga_subband_weights.reshape( + N_pn, S_pn * N_subbands + ) + def delay_compensation(delays_seconds: numpy.ndarray, clock: int): """Return the delay compensation required to line up @@ -48,7 +190,7 @@ def delay_compensation(delays_seconds: numpy.ndarray, clock: int): signal_delays_subsample_seconds = delays_seconds - signal_delays_samples / clock input_delays_subsample_seconds = -signal_delays_subsample_seconds - return (input_delays_samples, input_delays_subsample_seconds) + return input_delays_samples, input_delays_subsample_seconds def dB_to_factor(dB: numpy.ndarray) -> numpy.ndarray: @@ -56,6 +198,63 @@ def dB_to_factor(dB: numpy.ndarray) -> numpy.ndarray: return 10 ** (dB / 10) +# SDP input delay calibration +def calibrate_input_samples_delay(antenna_field: DeviceProxy, sdp: DeviceProxy): + # Mapping [antenna] -> [fpga][input] + antenna_to_sdp_mapping = antenna_field.Antenna_to_SDP_Mapping_R + + # ----------------------------------------------------------- + # Set coarse delay compensation by delaying the samples. + # ----------------------------------------------------------- + + # The delay to apply, in samples [antenna] + # Correct for signal delays in the cables + signal_delay_seconds = antenna_field.Antenna_Cables_Delay_R + + # compute the required compensation + clock = sdp.clock_RW + input_samples_delay, _ = delay_compensation(signal_delay_seconds, clock) + + # read-modify-write on [fpga][(input, polarisation)] + fpga_signal_input_samples_delay = sdp.FPGA_signal_input_samples_delay_RW + + for antenna_nr, (fpga_nr, input_nr) in enumerate(antenna_to_sdp_mapping): + if input_nr == -1: + # skip unconnected antennas + continue + + # set for X polarisation + fpga_signal_input_samples_delay[ + fpga_nr, input_nr * N_pol + 0 + ] = input_samples_delay[antenna_nr] + # set for Y polarisation + fpga_signal_input_samples_delay[ + fpga_nr, input_nr * N_pol + 1 + ] = input_samples_delay[antenna_nr] + + # TODO(L2SS-1312): This should use atomic_read_modify_write + sdp.FPGA_signal_input_samples_delay_RW = fpga_signal_input_samples_delay + + +# RCU calibration + + +def calibrate_RCU_attenuator_dB(antenna_field: DeviceProxy): + # ----------------------------------------------------------- + # Set signal-input attenuation to compensate for + # differences in cable length. + # ----------------------------------------------------------- + + # Correct for signal loss in the cables + signal_delay_loss = ( + antenna_field.Antenna_Cables_Loss_R - antenna_field.Field_Attenuation_R + ) + + # return coarse attenuation to apply + rcu_attenuator_db, _ = loss_compensation(signal_delay_loss) + antenna_field.RCU_attenuator_dB_RW = rcu_attenuator_db + + def loss_compensation(losses_dB: numpy.ndarray): """Return the attenuation required to line up signals that are dampened by "lossed_dB" decibel. @@ -89,42 +288,3 @@ def loss_compensation(losses_dB: numpy.ndarray): input_attenuation_remainder_factor = dB_to_factor(input_attenuation_remainder_dB) return (input_attenuation_integer_dB, input_attenuation_remainder_factor) - - -class CalibrationTable: - """A class to represent calibration tables, and to retrieve calibration data""" - - def __init__(self, filename: str): - # open the file and fill the calibration with actual data - with read_hdf5(filename, Hdf5CalibrationTable) as table: - self.table = table - - @staticmethod - def complex_to_float(caltable: numpy.ndarray) -> numpy.ndarray: - """Numpy array conversion from - (antenna_list, N_pol, N_subbands),dtype=complex128 to - (antenna_list, N_pol, N_subbands, VALUES_PER_COMPLEX),dtype=float64 - """ - caltable_float = caltable.view(dtype=numpy.float64) - return caltable_float.reshape(caltable.shape + (2,)) - - def get_antenna_data(self, antenna_list: list) -> numpy.ndarray: - """ - Returns a multi-dimensional array of the calibration data. - The shape is (antenna_list, N_pol, N_subbands), type is numpy.complex128 - """ - data = [] - - for antenna in antenna_list: - if antenna in self.table._data_reader.data.keys(): - # load the calibration for this antenna - x_data = self.table._data_reader.data[antenna]["x"] - y_data = self.table._data_reader.data[antenna]["y"] - x = numpy.array(x_data, dtype=numpy.complex128) - y = numpy.array(y_data, dtype=numpy.complex128) - pol = numpy.array([x, y]) - data.append(pol) - else: - # append data with default 1.0 value - data.append([[1.0] * N_subbands] * N_pol) - return numpy.array(data, dtype=numpy.complex128) diff --git a/tangostationcontrol/tangostationcontrol/common/calibration_table.py b/tangostationcontrol/tangostationcontrol/common/calibration_table.py deleted file mode 100644 index 19d4f809c..000000000 --- a/tangostationcontrol/tangostationcontrol/common/calibration_table.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 - -from typing import Dict - -from lofar_station_client.file_access import member, attribute -from numpy import ndarray - - -class CalibrationData: - x: ndarray = member() - y: ndarray = member() - - -class CalibrationTable: - observation_station: str = attribute() - observation_station_version: str = attribute() - observation_mode: str = attribute() - observation_source: str = attribute() - observation_date: str = attribute() - calibration_version: int = attribute() - calibration_name: str = attribute() - calibration_date: str = attribute() - antennas: Dict[str, CalibrationData] = member() diff --git a/tangostationcontrol/tangostationcontrol/devices/__init__.py b/tangostationcontrol/tangostationcontrol/devices/__init__.py index 68ddd5cdc..c92b61544 100644 --- a/tangostationcontrol/tangostationcontrol/devices/__init__.py +++ b/tangostationcontrol/tangostationcontrol/devices/__init__.py @@ -1,2 +1,2 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 diff --git a/tangostationcontrol/tangostationcontrol/devices/antennafield.py b/tangostationcontrol/tangostationcontrol/devices/antennafield.py index d2a2baa77..0a2213a68 100644 --- a/tangostationcontrol/tangostationcontrol/devices/antennafield.py +++ b/tangostationcontrol/tangostationcontrol/devices/antennafield.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 """ AntennaField Device Server for LOFAR2.0 @@ -28,8 +28,6 @@ from tangostationcontrol.beam.geo import GEO_to_GEOHASH from tangostationcontrol.beam.geo import ITRF_to_GEO from tangostationcontrol.beam.hba_tile import HBATAntennaOffsets from tangostationcontrol.common.cables import cable_types -from tangostationcontrol.common.calibration import delay_compensation -from tangostationcontrol.common.calibration import loss_compensation from tangostationcontrol.common.constants import ( N_elements, MAX_ANTENNA, @@ -39,14 +37,10 @@ from tangostationcontrol.common.constants import ( N_rcu, N_rcu_inp, N_pn, - S_pn, A_pn, - N_subbands, - VALUES_PER_COMPLEX, ) from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.frequency_bands import bands, Band -from tangostationcontrol.common.calibration import CalibrationTable from tangostationcontrol.common.lofar_logging import ( device_logging_to_python, log_exceptions, @@ -55,13 +49,8 @@ from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES from tangostationcontrol.common.type_checking import type_not_sequence from tangostationcontrol.devices.device_decorators import fault_on_error, only_in_states from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice -from tangostationcontrol.devices.sdp.common import ( - real_imag_to_weights, -) -from tangostationcontrol.devices.sdp.sdp import SDP logger = logging.getLogger() -CALIBRATION_ROOT_DIR = "/opt/calibration-tables" __all__ = ["AntennaField", "AntennaToRecvMapper", "AntennaToSdpMapper", "main"] @@ -299,7 +288,7 @@ class AntennaField(LOFARDevice): ) SDP_device = device_property( - dtype=str, + dtype="DevString", doc="Which SDP device is processing this AntennaField.", mandatory=False, default_value="STAT/SDP/1", @@ -373,6 +362,26 @@ class AntennaField(LOFARDevice): max_dim_y=MAX_ANTENNA, ) + @attribute( + access=AttrWriteType.READ, + dtype=str, + ) + def SDP_device_R(self): + return self.SDP_device + + @attribute(access=AttrWriteType.READ, dtype="DevFloat") + def Field_Attenuation_R(self): + return self.Field_Attenuation + + @attribute( + access=AttrWriteType.READ, + dtype=((numpy.int32,),), + max_dim_x=N_pol, + max_dim_y=MAX_ANTENNA, + ) + def Control_to_RECV_mapping_R(self): + return numpy.array(self.Control_to_RECV_mapping).reshape(-1, 2) + Frequency_Band_RW = attribute( doc="The selected frequency band of each antenna.", dtype=(str,), @@ -402,65 +411,6 @@ class AntennaField(LOFARDevice): unit="dB", ) - # ----- Calibration information - - Calibration_SDP_Signal_Input_Samples_Delay_R = attribute( - doc="Number of samples that each antenna signal should be delayed to line " - "up. To be applied on sdp.FPGA_signal_input_samples_delay_RW.", - dtype=(numpy.uint32,), - max_dim_x=MAX_ANTENNA, - unit="samples", - ) - Calibration_RCU_Attenuation_dB_R = attribute( - doc="Amount of dB with which each antenna signal must be adjusted to line " - "up. To be applied on recv.RCU_attenuator_dB_RW.", - dtype=(numpy.uint32,), - max_dim_x=MAX_ANTENNA, - unit="dB", - ) - Calibration_SDP_Fine_Calibration_Default_R = attribute( - doc="Computed calibration values for the fine calibration of each " - "antenna. Each antenna is represented by a (delay, phase_offset, " - "amplitude_scaling) triplet.", - dtype=((numpy.float64,),), - max_dim_y=MAX_ANTENNA * N_pol, - max_dim_x=3, - ) - Calibration_SDP_Subband_Weights_R = attribute( - doc="Calibration values for the rows in sdp.FPGA_subband_weights_RW " - "relevant for our antennas. Each subband of each polarisation of " - "each antenna is represented by a real_imag number (real, imag). " - "Returns the measured values from " - "Calibration_SDP_Subband_Weights_XXXMHz if available, and values " - "computed from Calibration_SDP_Fine_Calibration_Default_R otherwise.", - dtype=((numpy.float64,),), - max_dim_y=MAX_ANTENNA * N_pol, - max_dim_x=N_subbands * VALUES_PER_COMPLEX, - ) - - # calibration loading - - Calibration_SDP_Subband_Weights_50MHz_R = attribute( - dtype=((numpy.float64,),), - max_dim_y=MAX_ANTENNA, - max_dim_x=(N_pol * N_subbands * VALUES_PER_COMPLEX), - ) - Calibration_SDP_Subband_Weights_150MHz_R = attribute( - dtype=((numpy.float64,),), - max_dim_y=MAX_ANTENNA, - max_dim_x=(N_pol * N_subbands * VALUES_PER_COMPLEX), - ) - Calibration_SDP_Subband_Weights_200MHz_R = attribute( - dtype=((numpy.float64,),), - max_dim_y=MAX_ANTENNA, - max_dim_x=(N_pol * N_subbands * VALUES_PER_COMPLEX), - ) - Calibration_SDP_Subband_Weights_250MHz_R = attribute( - dtype=((numpy.float64,),), - max_dim_y=MAX_ANTENNA, - max_dim_x=(N_pol * N_subbands * VALUES_PER_COMPLEX), - ) - # ----- Quality and usage information Antenna_Quality_R = attribute( @@ -804,7 +754,7 @@ class AntennaField(LOFARDevice): ): # NB: "value[0] in bands" holds at this point raise ValueError( f"All frequency bands must use the same clock. \ - These do not: {val} and {value[0,0]}." + These do not: {val} and {value[0, 0]}." ) # apply settings on RECV @@ -837,63 +787,6 @@ class AntennaField(LOFARDevice): sdp_nyquist_zone, self.sdp_proxy, "nyquist_zone_RW", None, numpy.uint32 ) - # frequencies changed, so we need to recalibrate - self.calibrate_recv() - self.calibrate_sdp() - - def _create_calibration_table_filename(self, reference_frequency: str) -> str: - """Create a valid calibration table filename given a mode""" - antennafield_name = self.get_name().split("/")[2] - return ( - f"{CALIBRATION_ROOT_DIR}/" - + f"CalTable-{self.station}-{antennafield_name}-{reference_frequency}MHz.h5" - ) - - def _get_calibration_table(self, reference_frequency: str) -> numpy.ndarray: - """Returns a calibration table of shape - (MAX_ANTENNA, N_pol, N_subbands, VALUES_PER_COMPLEX)""" - filename = self._create_calibration_table_filename(reference_frequency) - # Initialise table from relative HDF5 file - try: - calibration_data = CalibrationTable(filename) - # Retrieve data and convert them in the correct Tango attr shape - caltable = CalibrationTable.complex_to_float( - calibration_data.get_antenna_data(self.Antenna_Names) - ) - except FileNotFoundError: - caltable = numpy.full( - ( - self.read_attribute("nr_antennas_R"), - N_pol * N_subbands * VALUES_PER_COMPLEX, - ), - 1.0, - ) - return caltable - - def read_Calibration_SDP_Subband_Weights_50MHz_R(self): - subband_weights = self._get_calibration_table(reference_frequency="50") - return numpy.reshape( - subband_weights, (self.read_attribute("nr_antennas_R"), -1) - ) - - def read_Calibration_SDP_Subband_Weights_150MHz_R(self): - subband_weights = self._get_calibration_table(reference_frequency="150") - return numpy.reshape( - subband_weights, (self.read_attribute("nr_antennas_R"), -1) - ) - - def read_Calibration_SDP_Subband_Weights_200MHz_R(self): - subband_weights = self._get_calibration_table(reference_frequency="200") - return numpy.reshape( - subband_weights, (self.read_attribute("nr_antennas_R"), -1) - ) - - def read_Calibration_SDP_Subband_Weights_250MHz_R(self): - subband_weights = self._get_calibration_table(reference_frequency="250") - return numpy.reshape( - subband_weights, (self.read_attribute("nr_antennas_R"), -1) - ) - def read_Antenna_Cables_R(self): return self.Antenna_Cables @@ -921,150 +814,6 @@ class AntennaField(LOFARDevice): ] ) - def read_Calibration_SDP_Signal_Input_Samples_Delay_R(self): - # Correct for signal delays in the cables - signal_delay_seconds = self.read_attribute("Antenna_Cables_Delay_R") - - # compute the required compensation - clock = self.sdp_proxy.clock_RW - input_delay_samples, _ = delay_compensation(signal_delay_seconds, clock) - - # return the delay to apply (in samples) - return input_delay_samples - - def read_Calibration_SDP_Fine_Calibration_Default_R(self): - def repeat_per_pol(arr): - # repeat values twice, and restore the shape (with the inner dimension - # being twice the size now) - return numpy.dstack((arr, arr)).reshape( - arr.shape[0] * N_pol, *arr.shape[1:] - ) - - # ----- Delay - - # correct for signal delays in the cables (equal for both polarisations) - signal_delay_seconds = repeat_per_pol( - self.read_attribute("Antenna_Cables_Delay_R") - ) - - # compute the required compensation - clock = self.sdp_proxy.clock_RW - _, input_delay_subsample_seconds = delay_compensation( - signal_delay_seconds, clock - ) - - # ----- Phase offsets - - # we don't have any - phase_offsets = repeat_per_pol( - numpy.zeros((self.read_attribute("nr_antennas_R"),), dtype=numpy.float64) - ) - - # ----- Amplitude - - # correct for signal loss in the cables - signal_delay_loss = repeat_per_pol( - self.read_attribute("Antenna_Cables_Loss_R") - self.Field_Attenuation - ) - - # return fine scaling to apply - _, input_attenuation_remaining_factor = loss_compensation(signal_delay_loss) - - # Return as (delay, phase_offset, amplitude) triplet per polarisation - return numpy.stack( - ( - input_delay_subsample_seconds, - phase_offsets, - input_attenuation_remaining_factor, - ), - axis=1, - ) - - def _rcu_band_to_calibration_table(self) -> dict: - """ - Returns the SDP subband weights to apply per RCU band. - """ - nr_antennas = self.read_attribute("nr_antennas_R") - - # construct selector for the right calibration table - if self.Antenna_Type == "LBA": - rcu_band_to_caltable = { - 1: self.read_attribute( - "Calibration_SDP_Subband_Weights_50MHz_R" - ).tolist(), - 2: self.read_attribute( - "Calibration_SDP_Subband_Weights_50MHz_R" - ).tolist(), - } - else: # HBA - rcu_band_to_caltable = { - 2: self.read_attribute( - "Calibration_SDP_Subband_Weights_150MHz_R" - ).tolist(), - 1: self.read_attribute( - "Calibration_SDP_Subband_Weights_200MHz_R" - ).tolist(), - 4: self.read_attribute( - "Calibration_SDP_Subband_Weights_250MHz_R" - ).tolist(), - } - - # reshape them into their actual form - for band, caltable in rcu_band_to_caltable.items(): - rcu_band_to_caltable[band] = numpy.array(caltable).reshape( - nr_antennas, N_pol, N_subbands, VALUES_PER_COMPLEX - ) - - return rcu_band_to_caltable - - def read_Calibration_SDP_Subband_Weights_R(self): - # obtain the calibration tables and the RCU bands they depend on - rcu_bands = self.read_attribute("RCU_band_select_RW") - rcu_band_to_caltable = self._rcu_band_to_calibration_table() - - # antenna mapping onto RECV - control_to_recv_mapping = numpy.array(self.Control_to_RECV_mapping).reshape( - -1, 2 - ) - recvs = control_to_recv_mapping[:, 0] # first column is RECV device number - - # antenna mapping onto SDP - antenna_to_sdp_mapping = self.read_attribute("Antenna_to_SDP_Mapping_R") - - # construct the subband weights based on the rcu_band of each antenna, - # combining the relevant tables. - nr_antennas = self.read_attribute("nr_antennas_R") - subband_weights = numpy.zeros( - (nr_antennas, N_pol, N_subbands, VALUES_PER_COMPLEX), dtype=numpy.float64 - ) - for antenna_nr, rcu_band in enumerate(rcu_bands): - # Skip antennas not connected to RECV. These do not have a valid RCU band - # selected. - if recvs[antenna_nr] == 0: - continue - - # Skip antennas not connected to SDP. They must retain a weight of 0. - if antenna_to_sdp_mapping[antenna_nr, 1] == -1: - continue - - subband_weights[antenna_nr, :, :, :] = rcu_band_to_caltable[rcu_band][ - antenna_nr, :, :, : - ] - - return subband_weights.reshape( - nr_antennas * N_pol, N_subbands * VALUES_PER_COMPLEX - ) - - def read_Calibration_RCU_Attenuation_dB_R(self): - # Correct for signal loss in the cables - signal_delay_loss = ( - self.read_attribute("Antenna_Cables_Loss_R") - self.Field_Attenuation - ) - - # return coarse attenuation to apply - input_attenuation_integer_db, _ = loss_compensation(signal_delay_loss) - return input_attenuation_integer_db - def read_Antenna_Use_R(self): return self.Antenna_Use @@ -1322,105 +1071,6 @@ class AntennaField(LOFARDevice): antenna_type, self.sdp_proxy, "antenna_type_RW", None, str ) - @command() - def calibrate_recv(self): - """Calibrate RECV for our antennas. - - Run whenever the following changes: - sdp.clock_RW - antennafield.RCU_band_select_RW - """ - - # ----------------------------------------------------------- - # Set signal-input attenuation to compensate for - # differences in cable length. - # ----------------------------------------------------------- - - rcu_attenuator_db = self.read_attribute("Calibration_RCU_Attenuation_dB_R") - self.proxy.write_attribute("RCU_attenuator_dB_RW", rcu_attenuator_db) - - @command() - def calibrate_sdp(self): - """Calibrate SDP for our antennas. - - Run whenever the following changes: - sdp.clock_RW - antennafield.RCU_band_select_RW - """ - - # Mapping [antenna] -> [fpga][input] - antenna_to_sdp_mapping = self.read_attribute("Antenna_to_SDP_Mapping_R") - - # ----------------------------------------------------------- - # Set coarse delay compensation by delaying the samples. - # ----------------------------------------------------------- - - # The delay to apply, in samples [antenna] - input_samples_delay = self.read_attribute( - "Calibration_SDP_Signal_Input_Samples_Delay_R" - ) - - # read-modify-write on [fpga][(input, polarisation)] - fpga_signal_input_samples_delay = numpy.full((N_pn, A_pn * N_pol), None) - - for antenna_nr, (fpga_nr, input_nr) in enumerate(antenna_to_sdp_mapping): - if input_nr == -1: - # skip unconnected antennas - continue - - # set for X polarisation - fpga_signal_input_samples_delay[ - fpga_nr, input_nr * N_pol + 0 - ] = input_samples_delay[antenna_nr] - # set for Y polarisation - fpga_signal_input_samples_delay[ - fpga_nr, input_nr * N_pol + 1 - ] = input_samples_delay[antenna_nr] - - self.atomic_read_modify_write_attribute( - fpga_signal_input_samples_delay, - self.sdp_proxy, - "FPGA_signal_input_samples_delay_RW", - None, - numpy.uint32, - ) - - # ----------------------------------------------------------- - # Compute calibration of subband weights for the remaining - # delay and loss corrections. - # ----------------------------------------------------------- - - # obtain caltable - caltable = self.read_attribute("Calibration_SDP_Subband_Weights_R") - - # read-modify-write on [fpga][(input, polarisation)] - fpga_subband_weights = numpy.full((N_pn, S_pn, N_subbands), None) - - for antenna_nr, (fpga_nr, input_nr) in enumerate(antenna_to_sdp_mapping): - if input_nr == -1: - # skip unconnected antennas - continue - - # set weights - fpga_subband_weights[ - fpga_nr, input_nr * N_pol + 0, : - ] = real_imag_to_weights( - caltable[antenna_nr * N_pol + 0, :], SDP.SUBBAND_UNIT_WEIGHT - ) - fpga_subband_weights[ - fpga_nr, input_nr * N_pol + 1, : - ] = real_imag_to_weights( - caltable[antenna_nr * N_pol + 1, :], SDP.SUBBAND_UNIT_WEIGHT - ) - - self.atomic_read_modify_write_attribute( - fpga_subband_weights.reshape(N_pn, S_pn * N_subbands), - self.sdp_proxy, - "FPGA_subband_weights_RW", - None, - numpy.uint32, - ) - @command(dtype_in=DevVarFloatArray, dtype_out=DevVarLongArray) def calculate_HBAT_bf_delay_steps(self, delays: numpy.ndarray): num_tiles = self.read_nr_antennas_R() diff --git a/tangostationcontrol/tangostationcontrol/devices/boot.py b/tangostationcontrol/tangostationcontrol/devices/boot.py index 6bc05990b..5e4d79d4f 100644 --- a/tangostationcontrol/tangostationcontrol/devices/boot.py +++ b/tangostationcontrol/tangostationcontrol/devices/boot.py @@ -1,5 +1,5 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 """ Boot Device Server for LOFAR2.0 @@ -17,6 +17,7 @@ from tango import AttrWriteType, DeviceProxy, DevState, DevSource from tango import DebugIt from tango.server import command from tango.server import device_property, attribute + from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import ( device_logging_to_python, @@ -148,13 +149,16 @@ class DevicesInitialiser(object): # reset initialisation parameters self.progress = 0 + logger.debug(self.devices.keys()) # restart devices in order for num_restarted_devices, device in enumerate(self.devices.keys(), 1): # allow resuming by skipping already initialised devices if self.device_initialised[device]: + logger.debug(f"{device} already initialised") continue if self.is_available(device): + logger.debug(f"{device} available") if ( self.reboot or self.devices[device].state() not in OPERATIONAL_STATES @@ -246,16 +250,22 @@ class Boot(LOFARDevice): dtype="DevVarStringArray", mandatory=False, default_value=[ - "STAT/Docker/1", # Docker controls the device containers, so it goes before anything else - "STAT/Configuration/1", # Configuration device loads and update station configuration - "STAT/PSOC/1", # PSOC boot early to detect power delivery failure as fast as possible - "STAT/PCON/1", # PCON boot early because it is responsible for power delivery. - "STAT/APSPU/1", # APS Power Units control other hardware we want to initialise + "STAT/Docker/1", + # Docker controls the device containers, so it goes before anything else + "STAT/Configuration/1", + # Configuration device loads and update station configuration + "STAT/PSOC/1", + # PSOC boot early to detect power delivery failure as fast as possible + "STAT/PCON/1", + # PCON boot early because it is responsible for power delivery. + "STAT/APSPU/1", + # APS Power Units control other hardware we want to initialise "STAT/APSCT/1", "STAT/CCD/1", "STAT/RECV/1", # RCUs are input for SDP, so initialise them first "STAT/UNB2/1", # Uniboards host SDP, so initialise them first - "STAT/SDP/1", # SDP controls the mask for SST/XST/BST/Beamlet, so initialise it first + "STAT/SDP/1", + # SDP controls the mask for SST/XST/BST/Beamlet, so initialise it first "STAT/BST/1", "STAT/SST/1", "STAT/XST/1", @@ -267,6 +277,8 @@ class Boot(LOFARDevice): "STAT/DigitalBeam/HBA", # Accessed SDP and Beamlet # "STAT/DigitalBeam/LBA", # Accessed SDP and Beamlet "STAT/TemperatureManager/1", + "STAT/Calibration/1", + # Calibration device loads and update station calibration ], ) diff --git a/tangostationcontrol/tangostationcontrol/devices/calibration.py b/tangostationcontrol/tangostationcontrol/devices/calibration.py new file mode 100644 index 000000000..d5894f836 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/devices/calibration.py @@ -0,0 +1,212 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 +import logging + +from tango import DeviceProxy, EventType, Database, AttributeProxy +from tango.server import device_property, command, attribute + +from tangostationcontrol.common.calibration import ( + CalibrationManager, + calibrate_RCU_attenuator_dB, + calibrate_input_samples_delay, +) +from tangostationcontrol.common.entrypoint import entry +from tangostationcontrol.common.lofar_logging import ( + device_logging_to_python, + log_exceptions, +) +from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES +from tangostationcontrol.devices.antennafield import AntennaField +from tangostationcontrol.devices.device_decorators import only_in_states +from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice +from tangostationcontrol.devices.sdp.sdp import SDP + +logger = logging.getLogger() +__all__ = ["Calibration", "main"] + + +@device_logging_to_python() +class Calibration(LOFARDevice): + """Manages the calibration of antenna field, SDP and RECV devices.""" + + def __init__(self, cl, name): + super().__init__(cl, name) + self._calibration_manager: CalibrationManager = None + self.event_subscriptions: list = [] + self.sdp_proxies: dict = {} + self.ant_proxies: dict = {} + self.station_name: AttributeProxy = None + + @log_exceptions() + def _frequency_band_changed_event(self, event): + """Trigger on external changes in frequency settings.""" + + if event.err: + # little we can do here. note that errors are also + # thrown if the device we subscribed to is offline + return + + logger.info( + f"Received attribute change event from {event.device}: " + f"{event.attr_value.name} := {event.attr_value.value}" + ) + + if self.dev_state() not in DEFAULT_COMMAND_STATES: + logger.warning("Device not active. Ignore freq band changed event") + return + + # frequencies changed, so we need to recalibrate + self.calibrate_recv(event.device) + self.calibrate_sdp(event.device) + + @log_exceptions() + def _clock_changed_event(self, event): + """Trigger on external changes in frequency settings.""" + if event.err: + # little we can do here. note that errors are also + # thrown if the device we subscribed to is offline + return + + logger.info( + f"Received attribute change event from {event.device}: " + f"{event.attr_value.name} := {event.attr_value.value}" + ) + + if self.dev_state() not in DEFAULT_COMMAND_STATES: + logger.warning("Device not active. Ignore clock changed event") + return + + for k, ant in self.ant_proxies.items(): + if ant.SDP_device_R.casefold() == str(event.device).casefold(): + logger.info(f"Re-calibrate antenna field {k}") + self.calibrate_sdp(k) + self.calibrate_recv(k) + + Calibration_Table_Base_URL = device_property( + doc="Base URL of the calibration tables", + dtype="DevString", + mandatory=False, + update_db=True, + default_value="http://object-storage:9000/caltables", + ) + + @attribute(dtype=(str,), max_dim_x=20) + def AntennaFields_Monitored_R(self): + return list(self.ant_proxies.keys()) + + @attribute(dtype=(str,), max_dim_x=20) + def SDPs_Monitored_R(self): + return list(self.sdp_proxies.keys()) + + @command + def download_calibration_tables(self): + self._calibration_manager.sync_calibration_tables() + for ant in self.ant_proxies.keys(): + self.calibrate_recv(ant) + self.calibrate_sdp(ant) + + @command(dtype_in=str) + @only_in_states(DEFAULT_COMMAND_STATES) + def calibrate_recv(self, device: str): + """Calibrate RECV for our antennas. + + Run whenever the following changes: + sdp.clock_RW + antennafield.RCU_band_select_RW + """ + + # ----------------------------------------------------------- + # Set signal-input attenuation to compensate for + # differences in cable length. + # ----------------------------------------------------------- + + calibrate_RCU_attenuator_dB(self.ant_proxies[device]) + + @command(dtype_in=str) + @only_in_states(DEFAULT_COMMAND_STATES) + def calibrate_sdp(self, device: str): + """Calibrate SDP for our antennas. + + Run whenever the following changes: + sdp.clock_RW + antennafield.RCU_band_select_RW + """ + + ant_proxy = self.ant_proxies[device] + sdp_device = str(ant_proxy.SDP_device_R) + sdp_proxy = self.sdp_proxies[sdp_device] + + calibrate_input_samples_delay(ant_proxy, sdp_proxy) + + self._calibration_manager.calibrate_subband_weights(ant_proxy, sdp_proxy) + + # -------- + # Overloaded functions + # -------- + + def configure_for_initialise(self): + super().configure_for_initialise() + + self.station_name = AttributeProxy("STAT/StationManager/1/station_name_R") + self._calibration_manager = CalibrationManager( + self.Calibration_Table_Base_URL, self.station_name.read().value + ) + + db = Database() + devices = db.get_device_exported_for_class(AntennaField.__name__) + for d in devices: + logger.debug("found antenna field device " + str(d)) + self.ant_proxies = {d: DeviceProxy(d) for d in devices} + + devices = db.get_device_exported_for_class(SDP.__name__) + for d in devices: + logger.debug("found SDP device " + str(d)) + self.sdp_proxies = {d: DeviceProxy(d) for d in devices} + + # subscribe to events to notice setting changes in SDP that determine the + # input frequency + for prx in self.ant_proxies.values(): + self.event_subscriptions.append( + ( + prx, + prx.subscribe_event( + "Frequency_Band_RW", + EventType.CHANGE_EVENT, + self._frequency_band_changed_event, + stateless=True, + ), + ) + ) + for prx in self.sdp_proxies.values(): + self.event_subscriptions.append( + ( + prx, + prx.subscribe_event( + "clock_RW", + EventType.CHANGE_EVENT, + self._clock_changed_event, + stateless=True, + ), + ) + ) + + def configure_for_off(self): + super().configure_for_off() + + # unsubscribe from all events + subscriptions = self.event_subscriptions + self.event_subscriptions = [] + for prx, s in subscriptions: + prx.unsubscribe_event(s) + + +# ---------- +# Run server +# ---------- +def main(**kwargs): + """Main function of the Calibration module.""" + return entry(Calibration, **kwargs) + + +if __name__ == "__main__": + main() diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py index fafcc3308..d9f95b6a2 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_antennafield.py @@ -1,9 +1,11 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 -import numpy import time + +import numpy from tango import DevState + from tangostationcontrol.common.constants import ( N_elements, MAX_ANTENNA, @@ -14,68 +16,14 @@ from tangostationcontrol.common.constants import ( CLK_200_MHZ, N_pn, S_pn, - N_subbands, - VALUES_PER_COMPLEX, ) from tangostationcontrol.common.frequency_bands import bands from tangostationcontrol.devices.antennafield import AntennaQuality, AntennaUse from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy - from .base import AbstractTestBases class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): - HBA_ANTENNA_NAMES = [ - "H0", - "H1", - "H2", - "H3", - "H4", - "H5", - "H6", - "H7", - "H8", - "H9", - "H10", - "H11", - "H12", - "H13", - "H14", - "H15", - "H16", - "H17", - "H18", - "H19", - "H20", - "H21", - "H22", - "H23", - "H24", - "H25", - "H26", - "H27", - "H28", - "H29", - "H30", - "H31", - "H32", - "H33", - "H34", - "H35", - "H36", - "H37", - "H38", - "H39", - "H40", - "H41", - "H42", - "H43", - "H44", - "H45", - "H46", - "H47", - ] - def setUp(self): self.stationmanager_proxy = self.setup_stationmanager_proxy() super().setUp("STAT/AntennaField/HBA") @@ -452,107 +400,3 @@ class TestAntennaFieldDevice(AbstractTestBases.TestDeviceBase): antennafield_proxy.read_attribute("Frequency_Band_RW").value[:2], err_msg=f"{band.name}", ) - - def test_calibrate_recv(self): - calibration_properties = { - "Antenna_Type": ["LBA"], - "Antenna_Cables": ["50m", "80m"] * (DEFAULT_N_HBA_TILES // 2), - "Control_to_RECV_mapping": - # [1, 0, 1, 1, 1, 2, 1, x ... 1, 47] - numpy.array([[1, x] for x in range(0, DEFAULT_N_HBA_TILES)]).flatten(), - } - - antennafield_proxy = self.proxy - antennafield_proxy.off() - antennafield_proxy.put_property(calibration_properties) - antennafield_proxy.boot() - - # calibrate - antennafield_proxy.calibrate_recv() - - # check the results - rcu_attenuator_db = antennafield_proxy.RCU_attenuator_dB_RW - - # values should be the same for the same cable length - self.assertEqual( - 1, - len(set(rcu_attenuator_db[0::2])), - msg=f"rcu_attenuator_db={rcu_attenuator_db}", - ) - self.assertEqual( - 1, - len(set(rcu_attenuator_db[1::2])), - msg=f"rcu_attenuator_db={rcu_attenuator_db}", - ) - # value should be larger for the shorter cable, as those signals need damping - self.assertGreater(rcu_attenuator_db[0], rcu_attenuator_db[1]) - # longest cable should require no damping - self.assertEqual(0, rcu_attenuator_db[1]) - - def test_calibrate_sdp(self): - calibration_properties = { - "Antenna_Type": ["HBA"], - "Antenna_Names": self.HBA_ANTENNA_NAMES, - "Antenna_Cables": ["50m", "80m"] * (DEFAULT_N_HBA_TILES // 2), - "Antenna_to_SDP_Mapping": [0, 1, 0, 0] - + [-1, -1] * (DEFAULT_N_HBA_TILES - 2), - "Control_to_RECV_mapping": - # [1, 0, 1, 1, 1, 2, 1, x ... 1, 47] - numpy.array([[1, x] for x in range(0, DEFAULT_N_HBA_TILES)]).flatten(), - } - - antennafield_proxy = self.proxy - antennafield_proxy.off() - antennafield_proxy.put_property(calibration_properties) - antennafield_proxy.boot() - - # calibrate - antennafield_proxy.calibrate_sdp() - - # check the results - # antenna #0 is on FPGA 0, input 2 and 3, - # antenna #1 is on FPGA 0, input 0 and 1 - signal_input_samples_delay = self.sdp_proxy.FPGA_signal_input_samples_delay_RW - - # delays should be equal for both polarisations - self.assertEqual( - signal_input_samples_delay[0, 0], signal_input_samples_delay[0, 1] - ) - self.assertEqual( - signal_input_samples_delay[0, 2], signal_input_samples_delay[0, 3] - ) - - # antenna #0 is shorter, so should have a greater delay - self.assertGreater( - signal_input_samples_delay[0, 2], - signal_input_samples_delay[0, 0], - msg=f"{signal_input_samples_delay}", - ) - # antenna #1 is longest, so should have delay 0 - self.assertEqual(0, signal_input_samples_delay[0, 0]) - - def test_calibration_table(self): - """Test whether calibration table are correctly retrieved and reshaped""" - calibration_properties = { - "Antenna_Type": ["HBA"], - "Antenna_Names": self.HBA_ANTENNA_NAMES, - "Control_to_RECV_mapping": [1, 1, 1, 0] - + [-1, -1] * (DEFAULT_N_HBA_TILES - 2), - "Antenna_to_SDP_Mapping": [0, 1, 0, 0] - + [-1, -1] * (DEFAULT_N_HBA_TILES - 2), - } - - antennafield_proxy = self.proxy - antennafield_proxy.off() - antennafield_proxy.put_property(calibration_properties) - antennafield_proxy.warm_boot() - - calibration_table = antennafield_proxy.Calibration_SDP_Subband_Weights_250MHz_R - - # test whether the shape is correct - shape = calibration_table.shape - self.assertEqual( - shape, - (antennafield_proxy.nr_antennas_R, N_pol * N_subbands * VALUES_PER_COMPLEX), - f"Wrong shape, got {shape}", - ) diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_calibration.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_calibration.py new file mode 100644 index 000000000..875775647 --- /dev/null +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_calibration.py @@ -0,0 +1,229 @@ +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 + +import numpy +from tango import DevState + +from tangostationcontrol.common.constants import ( + N_rcu, + N_rcu_inp, + DEFAULT_N_HBA_TILES, + CLK_200_MHZ, +) +from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy +from .base import AbstractTestBases + + +class TestCalibrationDevice(AbstractTestBases.TestDeviceBase): + HBA_ANTENNA_NAMES = [ + "H0", + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12", + "H13", + "H14", + "H15", + "H16", + "H17", + "H18", + "H19", + "H20", + "H21", + "H22", + "H23", + "H24", + "H25", + "H26", + "H27", + "H28", + "H29", + "H30", + "H31", + "H32", + "H33", + "H34", + "H35", + "H36", + "H37", + "H38", + "H39", + "H40", + "H41", + "H42", + "H43", + "H44", + "H45", + "H46", + "H47", + ] + + def setUp(self): + self.stationmanager_proxy = self.setup_stationmanager_proxy() + super().setUp("STAT/Calibration/1") + self.antennafield_proxy = self.setup_proxy("STAT/AntennaField/HBA") + self.antennafield_proxy.put_property( + { + "RECV_devices": ["STAT/RECV/1"], + "Power_to_RECV_mapping": [1, 1, 1, 0] + + [-1] * ((DEFAULT_N_HBA_TILES * 2) - 4), + } + ) + self.recv_proxy = self.setup_recv_proxy() + self.sdp_proxy = self.setup_sdp_proxy() + + self.addCleanup(self.shutdown_recv) + self.addCleanup(self.shutdown_sdp) + + # configure the frequencies, which allows access + # to the calibration attributes and commands + self.sdp_proxy.clock_RW = CLK_200_MHZ + self.recv_proxy.RCU_band_select_RW = [[1] * N_rcu_inp] * N_rcu + + def restore_antennafield(self): + self.proxy.put_property( + { + "RECV_devices": ["STAT/RECV/1"], + "Power_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES, + "Control_to_RECV_mapping": [-1, -1] * DEFAULT_N_HBA_TILES, + } + ) + + @staticmethod + def shutdown_recv(): + recv_proxy = TestDeviceProxy("STAT/RECV/1") + recv_proxy.off() + + @staticmethod + def shutdown_sdp(): + sdp_proxy = TestDeviceProxy("STAT/SDP/1") + sdp_proxy.off() + + @staticmethod + def shutdown(device: str): + def off(): + proxy = TestDeviceProxy(device) + proxy.off() + + return off + + def setup_recv_proxy(self): + # setup RECV + recv_proxy = TestDeviceProxy("STAT/RECV/1") + recv_proxy.off() + recv_proxy.warm_boot() + recv_proxy.set_defaults() + return recv_proxy + + def setup_sdp_proxy(self): + # setup SDP + sdp_proxy = TestDeviceProxy("STAT/SDP/1") + sdp_proxy.off() + sdp_proxy.warm_boot() + return sdp_proxy + + def setup_proxy(self, dev: str): + # setup SDP + proxy = TestDeviceProxy(dev) + proxy.off() + proxy.warm_boot() + self.addCleanup(self.shutdown(dev)) + return proxy + + def setup_stationmanager_proxy(self): + """Setup StationManager""" + stationmanager_proxy = TestDeviceProxy("STAT/StationManager/1") + stationmanager_proxy.off() + stationmanager_proxy.boot() + self.assertEqual(stationmanager_proxy.state(), DevState.ON) + return stationmanager_proxy + + def test_calibrate_recv(self): + calibration_properties = { + "Antenna_Type": ["LBA"], + "Antenna_Cables": ["50m", "80m"] * (DEFAULT_N_HBA_TILES // 2), + "Control_to_RECV_mapping": + # [1, 0, 1, 1, 1, 2, 1, x ... 1, 47] + numpy.array([[1, x] for x in range(0, DEFAULT_N_HBA_TILES)]).flatten(), + } + + self.antennafield_proxy = self.setup_proxy("STAT/AntennaField/HBA") + self.antennafield_proxy.off() + self.antennafield_proxy.put_property(calibration_properties) + self.antennafield_proxy.boot() + + self.proxy.boot() + + # calibrate + self.proxy.calibrate_recv("STAT/AntennaField/HBA") + + # check the results + rcu_attenuator_db = self.antennafield_proxy.RCU_attenuator_dB_RW + + # values should be the same for the same cable length + self.assertEqual( + 1, + len(set(rcu_attenuator_db[0::2])), + msg=f"rcu_attenuator_db={rcu_attenuator_db}", + ) + self.assertEqual( + 1, + len(set(rcu_attenuator_db[1::2])), + msg=f"rcu_attenuator_db={rcu_attenuator_db}", + ) + # value should be larger for the shorter cable, as those signals need damping + self.assertGreater(rcu_attenuator_db[0], rcu_attenuator_db[1]) + # longest cable should require no damping + self.assertEqual(0, rcu_attenuator_db[1]) + + def test_calibrate_sdp(self): + calibration_properties = { + "Antenna_Type": ["HBA"], + "Antenna_Names": self.HBA_ANTENNA_NAMES, + "Antenna_Cables": ["50m", "80m"] * (DEFAULT_N_HBA_TILES // 2), + "Antenna_to_SDP_Mapping": [0, 1, 0, 0] + + [-1, -1] * (DEFAULT_N_HBA_TILES - 2), + "Control_to_RECV_mapping": + # [1, 0, 1, 1, 1, 2, 1, x ... 1, 47] + numpy.array([[1, x] for x in range(0, DEFAULT_N_HBA_TILES)]).flatten(), + } + + self.antennafield_proxy = self.setup_proxy("STAT/AntennaField/HBA") + self.antennafield_proxy.off() + self.antennafield_proxy.put_property(calibration_properties) + self.antennafield_proxy.boot() + + self.proxy.boot() + + # calibrate + self.proxy.calibrate_sdp("STAT/AntennaField/HBA") + + # check the results + # antenna #0 is on FPGA 0, input 2 and 3, + # antenna #1 is on FPGA 0, input 0 and 1 + signal_input_samples_delay = self.sdp_proxy.FPGA_signal_input_samples_delay_RW + + # delays should be equal for both polarisations + self.assertEqual( + signal_input_samples_delay[0, 0], signal_input_samples_delay[0, 1] + ) + self.assertEqual( + signal_input_samples_delay[0, 2], signal_input_samples_delay[0, 3] + ) + + # antenna #0 is shorter, so should have a greater delay + self.assertGreater( + signal_input_samples_delay[0, 2], + signal_input_samples_delay[0, 0], + msg=f"{signal_input_samples_delay}", + ) + # antenna #1 is longest, so should have delay 0 + self.assertEqual(0, signal_input_samples_delay[0, 0]) diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py index ed7fd6982..8225f89c2 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py @@ -1,8 +1,9 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 -import time import logging +import time + import numpy import timeout_decorator @@ -16,7 +17,6 @@ from tangostationcontrol.common.constants import ( ) from tangostationcontrol.devices.antennafield import AntennaQuality, AntennaUse from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy - from .base import AbstractTestBases logger = logging.getLogger() @@ -88,6 +88,105 @@ class TestDeviceDigitalBeam(AbstractTestBases.TestDeviceBase): "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(), "Antenna_Quality": antenna_qualities, "Antenna_Use": antenna_use, + "Antenna_Cables": ["50m", "80m"] * (DEFAULT_N_HBA_TILES // 2), + "Antenna_to_SDP_Mapping": [ + "0", + "0", + "0", + "1", + "0", + "2", + "0", + "3", + "0", + "4", + "0", + "5", + "1", + "0", + "1", + "1", + "1", + "2", + "1", + "3", + "1", + "4", + "1", + "5", + "2", + "0", + "2", + "1", + "2", + "2", + "2", + "3", + "2", + "4", + "2", + "5", + "3", + "0", + "3", + "1", + "3", + "2", + "3", + "3", + "3", + "4", + "3", + "5", + "4", + "0", + "4", + "1", + "4", + "2", + "4", + "3", + "4", + "4", + "4", + "5", + "5", + "0", + "5", + "1", + "5", + "2", + "5", + "3", + "5", + "4", + "5", + "5", + "6", + "0", + "6", + "1", + "6", + "2", + "6", + "3", + "6", + "4", + "6", + "5", + "7", + "0", + "7", + "1", + "7", + "2", + "7", + "3", + "7", + "4", + "7", + "5", + ], } ) antennafield_proxy.off() diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py index 00257ead7..d16455a35 100644 --- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py +++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/test_device_observation.py @@ -1,11 +1,12 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 from datetime import datetime from json import loads import numpy from tango import DevState, DevFailed + from tangostationcontrol.common.constants import ( N_beamlets_ctrl, MAX_ANTENNA, @@ -14,7 +15,6 @@ from tangostationcontrol.common.constants import ( from tangostationcontrol.devices.antennafield import AntennaQuality, AntennaUse from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy from tangostationcontrol.test.devices.test_observation_base import TestObservationBase - from .base import AbstractTestBases @@ -152,6 +152,7 @@ class TestDeviceObservation(AbstractTestBases.TestDeviceBase): antenna_use = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA) antennafield_proxy.put_property( { + "Antenna_Type": ["LBA"], "RECV_devices": ["STAT/RECV/1"], "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(), "Antenna_to_SDP_Mapping": self.ANTENNA_TO_SDP_MAPPING, diff --git a/tangostationcontrol/test/common/test_calibration.py b/tangostationcontrol/test/common/test_calibration.py index 44b87271f..9a71bdd13 100644 --- a/tangostationcontrol/test/common/test_calibration.py +++ b/tangostationcontrol/test/common/test_calibration.py @@ -1,17 +1,129 @@ -# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy) -# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy) +# SPDX-License-Identifier: Apache-2.0 +import os +from os import path +from unittest.mock import patch, Mock, call, PropertyMock import numpy +from numpy.testing import assert_array_equal from tangostationcontrol.common.calibration import ( delay_compensation, loss_compensation, dB_to_factor, + CalibrationManager, + CalibrationTable, ) - +from tangostationcontrol.common.constants import S_pn, N_subbands, N_pn +from tangostationcontrol.devices.sdp.sdp import SDP from test import base +class MockMinio: + def __init__(self, **kwargs): + self.args = kwargs + + +@patch("tangostationcontrol.common.calibration.Minio") +@patch.dict( + os.environ, + {"MINIO_ROOT_USER": "my_user", "MINIO_ROOT_PASSWORD": "my_passwd"}, + clear=True, +) +class TestCalibrationManager(base.TestCase): + def test_sync_calibration_tables(self, minio): + minio.return_value.list_objects.return_value = [ + Mock(object_name="/unittest-station/file1.h5"), + Mock(object_name="/unittest-station/file2.h5"), + Mock(object_name="/unittest-station/file3.h5"), + ] + sut = CalibrationManager( + "http://server:1234/test_bucket/test_prefix", "unittest-station" + ) + minio.has_call_with( + "server:1234", access_key="my_user", secret_key="my_passwd", secure=False + ) + minio.return_value.list_objects.has_call_with( + "test_bucket", prefix="test_prefix/unittest-station/" + ) + minio.return_value.fget_object.assert_has_calls( + [ + call( + "test_bucket", + "/unittest-station/file1.h5", + path.join(sut._tmp_dir.name, "file1.h5"), + ), + call( + "test_bucket", + "/unittest-station/file2.h5", + path.join(sut._tmp_dir.name, "file2.h5"), + ), + call( + "test_bucket", + "/unittest-station/file3.h5", + path.join(sut._tmp_dir.name, "file3.h5"), + ), + ] + ) + + @patch("tangostationcontrol.common.calibration.read_hdf5") + def test_calibrate_subband_weights(self, hdf_reader, _): + antenna_field_mock = Mock( + Antenna_to_SDP_Mapping_R=numpy.array( + [[1, 1], [1, 2]], dtype=numpy.int32 + ).reshape(-1, 2), + Antenna_Names_R=[f"T{n + 1}" for n in range(2)], + Antenna_Type_R="HBA", + RCU_band_select_RW=numpy.array([1, 2]), + **{"name.return_value": "Stat/AntennaField/TEST"}, + ) + subband_weights = numpy.array( + [[SDP.SUBBAND_UNIT_WEIGHT] * S_pn * N_subbands] * N_pn + ) + + def subband_weights_side_effect(new_value=None): + nonlocal subband_weights + if new_value is not None: + subband_weights = new_value + return subband_weights + + sdp_mock = Mock() + subband_property_mock = PropertyMock(side_effect=subband_weights_side_effect) + type(sdp_mock).FPGA_subband_weights_RW = subband_property_mock + caltable_mock = Mock( + observation_station="unittest-station", + antennas={ + "T1": Mock(x=numpy.arange(0, 512), y=numpy.arange(512, 1024)), + "T2": Mock(x=numpy.arange(1024, 1536), y=numpy.arange(1536, 2048)), + }, + ) + hdf_reader.return_value.__enter__.return_value = caltable_mock + + sut = CalibrationManager("http://server:1234", "unittest-station") + sut.calibrate_subband_weights(antenna_field_mock, sdp_mock) + + hdf_reader.assert_has_calls( + [ + call( + f"{sut._tmp_dir.name}/CalTable-unittest-station-TEST-200MHz.h5", + CalibrationTable, + ), + call().__enter__(), + call().__exit__(None, None, None), + call( + f"{sut._tmp_dir.name}/CalTable-unittest-station-TEST-150MHz.h5", + CalibrationTable, + ), + call().__enter__(), + call().__exit__(None, None, None), + ] + ) + assert_array_equal(subband_weights[1, 1024:1536], numpy.arange(0, 512)) + assert_array_equal(subband_weights[1, 1536:2048], numpy.arange(512, 1024)) + assert_array_equal(subband_weights[1, 2048:2560], numpy.arange(1024, 1536)) + assert_array_equal(subband_weights[1, 2560:3072], numpy.arange(1536, 2048)) + + class TestCalibration(base.TestCase): def test_dB_to_factor(self): # Throw some known values at it -- GitLab