diff --git a/CDB/LOFAR_ConfigDb.json b/CDB/LOFAR_ConfigDb.json index 8aa444d302dc352578fa57c2aff9c0c5688289df..73fb4d5731ec78b84d094939e5db94b301c56f04 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 0000000000000000000000000000000000000000..992ce93adc8faea96632dd15043d3dc1b7337697 --- /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 7de2eff68b7741790da2a3cabe1a98139b20202f..f410ee249419741fa9bc99eb5a67e2cb0717d5e4 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 34decf3d511dc1ee8d49c383d0821d6473499630..8a691278ef38891feec7569a8d37b313b6c758fc 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 e7ce3d858a362c221401627b722ffef2e5fd9abe..0000000000000000000000000000000000000000 --- 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 10261e80cbacf9081284a2ca4e950d3300a8b639..0000000000000000000000000000000000000000 --- 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 69ccb78c0f84b967ba0b0dc952e6d433621739fc..5c38de7f3326b7f8a3069766394f4f890d2170f3 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 0000000000000000000000000000000000000000..2be7c73bdcfdba4560f7ab7f169d8b9dccc110ca --- /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 42254ec6f31dfc7e4844826e208556a45edba233..7b49a6cfcd2db4b786d7f5698267a672c6790a01 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 0000000000000000000000000000000000000000..5ba417b415080e287500149896f727216a66b95c --- /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 Binary files a/docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-150MHz.h5 and b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-150MHz.h5 differ 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 Binary files a/docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-200MHz.h5 and b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-200MHz.h5 differ 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 Binary files a/docker-compose/calibration-tables/caltables/CalTable-DevStation-HBA-250MHz.h5 and b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-HBA-250MHz.h5 differ 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 Binary files a/docker-compose/calibration-tables/caltables/CalTable-DevStation-LBA-50MHz.h5 and b/docker-compose/object-storage/caltables/DevStation/CalTable-DevStation-LBA-50MHz.h5 differ diff --git a/sbin/run_integration_test.sh b/sbin/run_integration_test.sh index 2a814ffaaeafaa7da9d39cf84c7f496e0a2f7ca0..9267f4692b12e174d7b5b9e631304e8a8030d727 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 4ae48590ba149268bc3670999bdbc1cd00ded147..7d398b0757c60494b10b1581922d0c17d1eaf4e3 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 7c6af9b857826f93a7240f428a412609565f9fc1..86f4505ebed4e03c60bbc2fb6f317f30bf6f3edf 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 5ccd4a8ac711b068f986fd11c257ab5c7eb29380..2b87dbffc9d46a051aa3245e1561f535732c0342 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 a212ea51d7847a8f4e0340d00932f723c51f2857..bbdd80eaa2ac676116d0ae5a10856f970e3378b0 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 970781c70a8ba0e5470725fe6bce5bf498babec7..e949179e7396a134f9d59608774db011ff31543c 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 19d4f809c32291502c50a4274b96fbe61f0dd0fa..0000000000000000000000000000000000000000 --- 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 68ddd5cdc3efaa38e853aef337c08beb99c50c4c..c92b615444d854a6e87370b16cf733a5859a07e7 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 d2a2baa771839b71e47710891357a224274ed718..0a2213a6880fcdd519a41c256d134470ade1e26c 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 6bc05990be595c2d3dcd3f63d1500d80a4136bf2..5e4d79d4fcbec0c8dedd5a93ce0f4dd34aca89d3 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 0000000000000000000000000000000000000000..d5894f836baa37dbeaf0411a8cb6edcb673e234f --- /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 fafcc330804084ab4d187d1c3aef8f8428a1c468..d9f95b6a2b7cf2c3fd8af230a932e3494004ef3f 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 0000000000000000000000000000000000000000..8757756475fd811231781fe8c32452c9f9252065 --- /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 ed7fd69821bdbd9d54edca82821f7a08a5f1d9e2..8225f89c246a7b797df076c80ebd2f8483c2580f 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 00257ead770400ae9e0d09fe27b4e5401d53243b..d16455a3558400dd1d774f8c7bf1bf3cfe160b05 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 44b87271f5ff535e2fc17b747d446d7be727f86a..9a71bdd131b55eb146b6043eec9ea82750dd1fda 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