diff --git a/tangostationcontrol/docs/source/calibration.rst b/tangostationcontrol/docs/source/calibration.rst index b38b38d5c6fb5ccb3341d7ab60bfbc40ca9a7e42..84ca470e73be81a439492afb6d01ef675c195baa 100644 --- a/tangostationcontrol/docs/source/calibration.rst +++ b/tangostationcontrol/docs/source/calibration.rst @@ -122,34 +122,44 @@ The ``AntennaField`` device provides the following attributes that provide the r :type: ``uint32[N_ant]`` -:Calibration_SDP_Subband_Weights_Default_R: The computed remaining delay & loss compensation, to be applied as subband weights in SDP. Each antenna is represented by a triplet (delay_seconds, phase_offset, amplitude_scaling) that can be used to compute the complex correction factor for each subband. +:Calibration_SDP_Fine_Calibration_Default_R: The computed remaining delay & loss compensation, to be applied as subband weights in SDP. Each antenna is represented by a triplet (delay_seconds, phase_offset, amplitude_scaling) that can be used to compute the complex correction factor for each subband. :type: ``float[N_ant][3]`` +:Calibration_SDP_Subband_Weights_Default_R: The computed remaining delay & loss compensation, per subband, as (real, imag) pairs representing complex weights. This array represents ``Calibration_SDP_Fine_Calibration_Default_R`` transformed into complex weights for every subband. + + :type: ``float[N_ant][N_subbands][2]`` + :Calibration_SDP_Subband_Weights_R: The remaining delay & loss compensation to be applied. Returns rows from the preconfigured calibration table if available, falling back to rows from the computed ``Calibration_SDP_Subband_Weights_Default_R`` table otherwise. - :type: ``float[N_ant][3]`` + :type: ``float[N_ant][N_subbands][2]`` The preconfigured calibration tables are described below. Calibration configuration ````````````````````````````` +The software allows a generic additional calibration to tune all inputs further with the following property: + +:Field_Attenuation: Attenuation value to apply on all inputs in this field (in dB). + + :type: ``float`` + The software models cannot account for all required corrections. We assume that the software correctly computes the coarse compensation, but that additional tuning of the fine compensations is needed. Actual measurements must be used to derive the precise correction factors for each antenna. The ``AntennaField`` device allows the following properties to be used to replace the computed ``Calibration_SDP_Subband_Weights_Default_R`` table for the fine correction factors: -:Calibration_SDP_Subband_Weights_50MHz: The measured correction factors for each input, at the 50 MHz reference frequency. Each antenna is represented by a triplet (delay_seconds, phase_offset, amplitude_scaling). +:Calibration_SDP_Subband_Weights_50MHz: The measured correction factors for each input, at the 50 MHz reference frequency. Each antenna is by a weight for every subband, where each weight is a (real, imag) pair. - :type: ``float[N_ant][3]`` + :type: ``float[N_ant][N_subbands][2]`` -:Calibration_SDP_Subband_Weights_150MHz: The measured correction factors for each input, at the 150 MHz reference frequency. Each antenna is represented by a triplet (delay_seconds, phase_offset, amplitude_scaling). - :type: ``float[N_ant][3]`` +:Calibration_SDP_Subband_Weights_150MHz: The measured correction factors for each input, at the 150 MHz reference frequency. Each antenna is by a weight for every subband, where each weight is a (real, imag) pair. -:Calibration_SDP_Subband_Weights_200MHz: The measured correction factors for each input, at the 200 MHz reference frequency. Each antenna is represented by a triplet (delay_seconds, phase_offset, amplitude_scaling). + :type: ``float[N_ant][N_subbands][2]`` - :type: ``float[N_ant][3]`` +:Calibration_SDP_Subband_Weights_200MHz: The measured correction factors for each input, at the 200 MHz reference frequency. Each antenna is by a weight for every subband, where each weight is a (real, imag) pair. -:Calibration_SDP_Subband_Weights_250MHz: The measured correction factors for each input, at the 250 MHz reference frequency. Each antenna is represented by a triplet (delay_seconds, phase_offset, amplitude_scaling). + :type: ``float[N_ant][N_subbands][2]`` - :type: ``float[N_ant][3]`` +:Calibration_SDP_Subband_Weights_250MHz: The measured correction factors for each input, at the 250 MHz reference frequency. Each antenna is by a weight for every subband, where each weight is a (real, imag) pair. + :type: ``float[N_ant][N_subbands][2]`` diff --git a/tangostationcontrol/tangostationcontrol/devices/antennafield.py b/tangostationcontrol/tangostationcontrol/devices/antennafield.py index 1822181ca9b2342dacb29d7456a0acbdfd094cdf..0363b5af38f6706a4a3334ed12b2a50eebbea1b0 100644 --- a/tangostationcontrol/tangostationcontrol/devices/antennafield.py +++ b/tangostationcontrol/tangostationcontrol/devices/antennafield.py @@ -12,18 +12,20 @@ import numpy from typing import List # PyTango imports -from tango import DeviceProxy, DevSource, AttrWriteType, DevVarFloatArray, DevVarLongArray +from tango import DeviceProxy, DevSource, AttrWriteType, DevVarFloatArray, DevVarLongArray, DebugIt from tango.server import device_property, attribute, command # Additional import from tangostationcontrol.common.type_checking import type_not_sequence from tangostationcontrol.common.entrypoint import entry from tangostationcontrol.common.lofar_logging import device_logging_to_python, log_exceptions -from tangostationcontrol.common.constants import N_elements, MAX_ANTENNA, N_pol, N_xyz, N_latlong, N_rcu, N_rcu_inp, N_pn, S_pn, N_subbands +from tangostationcontrol.common.constants import N_elements, MAX_ANTENNA, N_pol, N_xyz, N_latlong, N_rcu, N_rcu_inp, N_pn, S_pn, N_subbands, VALUES_PER_COMPLEX from tangostationcontrol.common.cables import cable_types from tangostationcontrol.common.calibration import delay_compensation, loss_compensation +from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES from tangostationcontrol.devices.lofar_device import lofar_device -from tangostationcontrol.devices.device_decorators import fault_on_error +from tangostationcontrol.devices.device_decorators import fault_on_error, only_in_states +from tangostationcontrol.devices.sdp.common import subband_frequency, real_imag_to_weights from tangostationcontrol.devices.sdp.sdp import SDP from tangostationcontrol.beam.geo import ETRS_to_ITRF, ITRF_to_GEO, GEO_to_GEOHASH from tangostationcontrol.beam.hba_tile import HBATAntennaOffsets @@ -142,26 +144,33 @@ class AntennaField(lofar_device): default_value = numpy.array(["0m"] * MAX_ANTENNA) ) + Field_Attenuation = device_property( + doc=f"Attenuation value to apply on all inputs.", + dtype='DevFloat', + mandatory=False, + default_value = 0.0 + ) + Calibration_SDP_Subband_Weights_50MHz = device_property( - doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 50 MHz. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet.", + doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 50 MHz. Each antenna is represented by a (real, imag) pair for every subband.", dtype='DevVarFloatArray', mandatory=False ) Calibration_SDP_Subband_Weights_150MHz = device_property( - doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 150 MHz. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet.", + doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 150 MHz. Each antenna is represented by a (real, imag) pair for every subband.", dtype='DevVarFloatArray', mandatory=False ) Calibration_SDP_Subband_Weights_200MHz = device_property( - doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 200 MHz. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet.", + doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 200 MHz. Each antenna is represented by a (real, imag) pair for every subband.", dtype='DevVarFloatArray', mandatory=False ) Calibration_SDP_Subband_Weights_250MHz = device_property( - doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 250 MHz. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet.", + doc=f"Measured calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna, at 250 MHz. Each antenna is represented by a (real, imag) pair for every subband.", dtype='DevVarFloatArray', mandatory=False ) @@ -292,12 +301,15 @@ class AntennaField(lofar_device): dtype=(numpy.uint32,), max_dim_x=MAX_ANTENNA, unit="samples") Calibration_RCU_Attenuation_dB_R = attribute(doc=f"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_Subband_Weights_Default_R = attribute( - doc=f"Computed calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet.", + Calibration_SDP_Fine_Calibration_Default_R = attribute( + doc=f"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, max_dim_x=3) + Calibration_SDP_Subband_Weights_Default_R = attribute( + doc=f"Calibration values for the rows in sdp.FPGA_subband_weights_RW relevant for our antennas, as computed. Each subband of each antenna is represented by a real_imag number (real, imag).", + dtype=((numpy.float64,),), max_dim_y=MAX_ANTENNA, max_dim_x=N_subbands * VALUES_PER_COMPLEX) Calibration_SDP_Subband_Weights_R = attribute( - doc=f"Calibration values for the sdp.FPGA_subband_weights_RW columns of each antenna. Each antenna is represented by a (delay, phase_offset, amplitude_scaling) triplet. Returns the measured values from Calibration_SDP_Subband_Weights_XXXMHz if available, and values from Calibration_SDP_Subband_Weights_Default_R otherwise.", - dtype=((numpy.float64,),), max_dim_y=MAX_ANTENNA, max_dim_x=3) + doc=f"Calibration values for the rows in sdp.FPGA_subband_weights_RW relevant for our antennas. Each subband 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, max_dim_x=N_subbands * VALUES_PER_COMPLEX) # ----- Quality and usage information @@ -413,7 +425,7 @@ class AntennaField(lofar_device): # return the delay to apply (in samples) return input_delay_samples - def read_Calibration_SDP_Subband_Weights_Default_R(self): + def read_Calibration_SDP_Fine_Calibration_Default_R(self): # ----- Delay # correct for signal delays in the cables @@ -431,7 +443,7 @@ class AntennaField(lofar_device): # ----- Amplitude # correct for signal loss in the cables - signal_delay_loss = self.read_attribute("Antenna_Cables_Loss_R") + signal_delay_loss = self.read_attribute("Antenna_Cables_Loss_R") - self.Field_Attenuation # return fine scaling to apply _, input_attenuation_remaining_factor = loss_compensation(signal_delay_loss) @@ -439,7 +451,44 @@ class AntennaField(lofar_device): # Return as (delay, phase_offset, amplitude) triplet per antenna return numpy.stack((input_delay_subsample_seconds, phase_offsets, input_attenuation_remaining_factor), axis=1) - def read_Calibration_SDP_Subband_Weights_R(self): + def read_Calibration_SDP_Subband_Weights_Default_R(self): + delay_phase_amplitude = self.read_attribute("Calibration_SDP_Fine_Calibration_Default_R") + + clock = self.sdp_proxy.clock_RW + nyquist_zone = self.sdp_proxy.nyquist_zone_R # NB: for all inputs, unmapped, not just ours + nr_antennas = self.read_attribute("nr_antennas_R") + antenna_to_sdp_mapping = self.read_attribute("Antenna_to_SDP_Mapping_R") + + + subband_weights = numpy.zeros((nr_antennas, N_subbands, VALUES_PER_COMPLEX), dtype=numpy.float64) + + # compute real_imag weight for each subband + for antenna_nr in range(nr_antennas): + fpga_nr, input_nr = antenna_to_sdp_mapping[antenna_nr] + if input_nr == -1: + continue + + delay, phase_offset, amplitude = delay_phase_amplitude[antenna_nr, :] + + for subband_nr in range(N_subbands): + frequency = subband_frequency(subband_nr, clock, nyquist_zone[fpga_nr, input_nr]) + + # turn signal backwards to compensate for the provided delay and offset + phase_shift = -(2 * numpy.pi * frequency * delay + phase_offset) + + real = numpy.cos(phase_shift) * amplitude + imag = numpy.sin(phase_shift) * amplitude + + subband_weights[antenna_nr, subband_nr, :] = (real, imag) + + return subband_weights.reshape(nr_antennas, N_subbands * VALUES_PER_COMPLEX) + + 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") + # default table to use if no calibration table is available default_table = self.read_attribute("Calibration_SDP_Subband_Weights_Default_R") @@ -455,17 +504,29 @@ class AntennaField(lofar_device): 1: self.Calibration_SDP_Subband_Weights_200MHz or default_table, 4: self.Calibration_SDP_Subband_Weights_250MHz or default_table, } - + + # 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_subbands, 2) + + 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. - subband_weights = numpy.zeros((self.read_attribute("nr_antennas_R"), 3), dtype=numpy.float64) + nr_antennas = self.read_attribute("nr_antennas_R") + subband_weights = numpy.zeros((nr_antennas, 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: @@ -475,13 +536,13 @@ class AntennaField(lofar_device): if antenna_to_sdp_mapping[antenna_nr, 1] == -1: continue - subband_weights[antenna_nr, :] = rcu_band_to_caltable[rcu_band][antenna_nr, :] + subband_weights[antenna_nr, :, :] = rcu_band_to_caltable[rcu_band][antenna_nr, :, :] - return subband_weights + return subband_weights.reshape(nr_antennas, 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") + 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) @@ -653,6 +714,9 @@ class AntennaField(lofar_device): # Commands # -------- @command() + @only_in_states(DEFAULT_COMMAND_STATES) + @DebugIt() + @log_exceptions() def configure_recv(self): """ Configure RECV to process our antennas. """ @@ -664,6 +728,9 @@ class AntennaField(lofar_device): self.proxy.write_attribute('RCU_PWR_ANT_on_RW', self.Antenna_Needs_Power) @command() + @only_in_states(DEFAULT_COMMAND_STATES) + @DebugIt() + @log_exceptions() def configure_sdp(self): """ Configure SDP to process our antennas. """ @@ -747,11 +814,8 @@ class AntennaField(lofar_device): # skip unconnected antennas continue - # compute weights - weights = SDP.subband_weights(caltable[antenna_nr][0], caltable[antenna_nr][1], caltable[antenna_nr][2], clock, nyquist_zone[fpga_nr, input_nr]) - # set weights - fpga_subband_weights[fpga_nr, input_nr, :] = weights + fpga_subband_weights[fpga_nr, input_nr, :] = real_imag_to_weights(caltable[antenna_nr, :], SDP.SUBBAND_UNIT_WEIGHT) self.sdp_proxy.FPGA_subband_weights_RW = fpga_subband_weights.reshape(N_pn, S_pn * N_subbands) diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/common.py b/tangostationcontrol/tangostationcontrol/devices/sdp/common.py index 891dbb648b1417f5f8504e878658fbe6773bf66b..b9f0065a97fe5d65ef567cbe70221897c394ceb4 100644 --- a/tangostationcontrol/tangostationcontrol/devices/sdp/common.py +++ b/tangostationcontrol/tangostationcontrol/devices/sdp/common.py @@ -50,6 +50,20 @@ def phases_to_weights(phases: numpy.ndarray, unit_weight: float, amplitudes: num return weights.reshape(orig_shape) +def real_imag_to_weights(real_imag_pairs: numpy.ndarray, unit_weight: float) -> numpy.ndarray: + """ Convert complex values (as (real, imag) pairs) into FPGA weights (complex numbers packed into uint32). """ + + # The FPGA accepts weights as a 16-bit (imag,real) complex pair packed into an uint32. + + # Interleave into (real, imag) pairs, and store as int16 + # Round to nearest integer instead of rounding down + real_imag = numpy.round(real_imag_pairs * unit_weight).astype(numpy.int16) + + # Cast each (real, imag) pair into an uint32 + weights = real_imag.view(numpy.uint32) + + return weights + def weight_to_complex(weight: numpy.uint32, unit: int) -> complex: """Unpack an FPGA weight (uint32) into a complex number.