diff --git a/CAL/CalibrationCommon/lib/CMakeLists.txt b/CAL/CalibrationCommon/lib/CMakeLists.txt index 649177b5079428855a15f071a9b3cdf29dc14047..33896b794a21bf367bc8673dbb6ae432bd29d9fa 100644 --- a/CAL/CalibrationCommon/lib/CMakeLists.txt +++ b/CAL/CalibrationCommon/lib/CMakeLists.txt @@ -26,6 +26,7 @@ python_install( datacontainers/holography_specification.py datacontainers/holography_observation.py datacontainers/holography_measurementset.py + datacontainers/calibration_table.py datacontainers/__init__.py coordinates.py utils.py diff --git a/CAL/CalibrationCommon/lib/datacontainers/calibration_table.py b/CAL/CalibrationCommon/lib/datacontainers/calibration_table.py new file mode 100644 index 0000000000000000000000000000000000000000..e4c406e1054de78c4a9aa7571160f843fd969f63 --- /dev/null +++ b/CAL/CalibrationCommon/lib/datacontainers/calibration_table.py @@ -0,0 +1,134 @@ +import logging +from datetime import datetime +from re import fullmatch +from struct import iter_unpack +from typing import BinaryIO +from typing import List + +from dataclasses import dataclass, asdict +from numpy import empty as empty_ndarray, ndarray, fromiter as array_from_iter, float64 + +logger = logging.getLogger(__name__) + +__MAX_HEADER_LINES = 100 +__HEADER_LINE_PATTERN = '(^[A-z]*\.[A-z]*\.[A-z]*\s=\s.*$)|(^[A-z]*\.[A-z]*\s=\s.*$)' + +_ATTRIBUTE_NAME_TO_SERIALIZED_NAME = { + 'observation_station': 'CalTableHeader.Observation.Station', + 'observation_mode': 'CalTableHeader.Observation.Mode', + 'observation_antennaset': 'CalTableHeader.Observation.AntennaSet', + 'observation_band': 'CalTableHeader.Observation.Band', + 'observation_source': 'CalTableHeader.Observation.Source', + 'observation_date': 'CalTableHeader.Observation.Date', + 'calibration_version': 'CalTableHeader.Calibration.Version', + 'calibration_name': 'CalTableHeader.Calibration.Name', + 'calibration_date': 'CalTableHeader.Calibration.Date', + 'calibration_ppsdelay': 'CalTableHeader.Calibration.PPSDelay', + 'comment': 'CalTableHeader.Comment' +} + + +class UnvalidFileException(Exception): + def __init__(self, message): + self.message = message + + +def _extract_header(fstream: BinaryIO): + header = {} + for i in range(__MAX_HEADER_LINES): + line = fstream.readline().decode('utf8').rstrip('\n') + + if line == 'HeaderStop': + break + elif line == 'HeaderStart': + continue + elif fullmatch(__HEADER_LINE_PATTERN, line): + + key, value = line.split('=') + + key = key.lower().replace('caltableheader.', '').strip().replace('.', '_') + value = value.strip() + header[key] = value + else: + logger.error('unrecognized line %s', line) + raise UnvalidFileException('unrecognized line %s' % line) + if len(header) == 0: + raise UnvalidFileException('empty header') + return header + + +__FREQUENCIES = 512 +__FLOATS_PER_FREQUENCY = 2 +__N_ANTENNAS_DUTCH = 96 +__N_ANTENNAS_INTERNATIONAL = 192 + + +def parse_data(data_buffer): + data = array_from_iter(map(lambda x: x[0], iter_unpack('d', data_buffer)), dtype=float) + n_antennas = data.shape[0] // __FREQUENCIES // __FLOATS_PER_FREQUENCY + if n_antennas not in [__N_ANTENNAS_DUTCH, __N_ANTENNAS_INTERNATIONAL]: + raise UnvalidFileException('invalid data range expected %s or %s antennas got %s' % + (__N_ANTENNAS_DUTCH, + __N_ANTENNAS_INTERNATIONAL, + n_antennas)) + + data = data.reshape((__FREQUENCIES, n_antennas, __FLOATS_PER_FREQUENCY)) + complex_data = empty_ndarray([__FREQUENCIES, n_antennas], dtype=complex) + complex_data.real = data[:, :, 0] + complex_data.imag = data[:, :, 1] + + return complex_data + + +@dataclass(init=True, repr=True, frozen=True) +class CalibrationTable: + observation_station: str + observation_mode: str + observation_antennaset: str + observation_band: str + observation_source: str + observation_date: datetime + calibration_version: int + calibration_name: str + calibration_date: datetime + calibration_ppsdelay: List[int] + data: ndarray + comment: str = '' + + @staticmethod + def load_from_file(file_path): + with open(file_path, 'rb') as file_stream: + header = _extract_header(file_stream) + data_raw = file_stream.read() + + data = parse_data(data_raw) + + calibration_table = CalibrationTable(**header, + data=data) + return calibration_table + + def __serialize_header(self, f_stream: BinaryIO): + f_stream.write(b'HeaderStart') + + for key, value in asdict(self).items(): + if key is 'data': + # skipping field data + continue + serialized_name = _ATTRIBUTE_NAME_TO_SERIALIZED_NAME[key] + serialized_line = '{} = {}\n'.format(serialized_name, value).encode('utf8') + f_stream.write(serialized_line) + + f_stream.write(b'HeaderStop') + + def __serialize_data(self, f_stream: BinaryIO): + dimensions = list(self.data.shape) + [2] + data_flatten = empty_ndarray(dimensions, dtype=float64) + data_flatten[:, :, 0] = self.data.real + data_flatten[:, :, 1] = self.data.imag + data_flattened = data_flatten.flatten().tolist() + f_stream.write() + + + def store_to_file(self, file_path): + with open(file_path, 'wb') as file_stream: + self.__serialize_header(file_stream) diff --git a/CAL/CalibrationCommon/test/CMakeLists.txt b/CAL/CalibrationCommon/test/CMakeLists.txt index 17249cdfb5adc3b4af1eb3457bb1be00e0afed75..2a5b743bbaaabb90bf35aa52b1d951770e2e1907 100644 --- a/CAL/CalibrationCommon/test/CMakeLists.txt +++ b/CAL/CalibrationCommon/test/CMakeLists.txt @@ -25,5 +25,7 @@ lofar_add_test(t_holography_datatable_class) lofar_add_test(t_holography_dataset_class) lofar_add_test(t_holography_dataset_class2) lofar_add_test(t_holography_observation) +lofar_add_test(t_calibration_table) + diff --git a/CAL/CalibrationCommon/test/t_calibration_table.in_CalTable-401-HBA-110_190.dat b/CAL/CalibrationCommon/test/t_calibration_table.in_CalTable-401-HBA-110_190.dat new file mode 100644 index 0000000000000000000000000000000000000000..6dd4108be0159aba328b674b8cf9a5d874258b31 Binary files /dev/null and b/CAL/CalibrationCommon/test/t_calibration_table.in_CalTable-401-HBA-110_190.dat differ diff --git a/CAL/CalibrationCommon/test/t_calibration_table.py b/CAL/CalibrationCommon/test/t_calibration_table.py new file mode 100644 index 0000000000000000000000000000000000000000..f833e3319135e563734f42f9ccaa7b8eefca1c9f --- /dev/null +++ b/CAL/CalibrationCommon/test/t_calibration_table.py @@ -0,0 +1,36 @@ +import unittest +from lofar.calibration.common.datacontainers.calibration_table import CalibrationTable, UnvalidFileException + +import logging +from tempfile import NamedTemporaryFile + +CALIBRATION_TABLE_FILENAME = 't_calibration_table.in_CalTable-401-HBA-110_190.dat' + + +class TestCalibrationTable(unittest.TestCase): + def test_loading(self): + calibration_table = CalibrationTable.load_from_file(CALIBRATION_TABLE_FILENAME) + self.assertEqual(calibration_table.observation_station, 'CS401') + + def test_unvalid_file(self): + with NamedTemporaryFile('w+b') as temp_file: + temp_file.write(b'stuff') + temp_file.flush() + + with self.assertRaises(UnvalidFileException): + _ = CalibrationTable.load_from_file(temp_file.name) + + def test_storing_file(self): + with NamedTemporaryFile('w+b') as temp_file: + test_calibration_table = CalibrationTable.load_from_file(CALIBRATION_TABLE_FILENAME) + + test_calibration_table.store_to_file(temp_file.name) + + stored_calibration_table = CalibrationTable.load_from_file(temp_file.name) + self.assertEqual(test_calibration_table.observation_station, + stored_calibration_table.observation_station) + + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG) + unittest.main() diff --git a/CAL/CalibrationCommon/test/t_calibration_table.run b/CAL/CalibrationCommon/test/t_calibration_table.run new file mode 100755 index 0000000000000000000000000000000000000000..c60df34d3bf1a38b2ccf4806c398abe0ca7187ad --- /dev/null +++ b/CAL/CalibrationCommon/test/t_calibration_table.run @@ -0,0 +1,22 @@ +#!/bin/bash + +# Copyright (C) 2018 ASTRON (Netherlands Institute for Radio Astronomy) +# P.O. Box 2, 7990 AA Dwingeloo, The Netherlands +# +# This file is part of the LOFAR software suite. +# The LOFAR software suite is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# The LOFAR software suite is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>. + +# Run the unit test +source python-coverage.sh +python_coverage_test "*calibration_table*" t_calibration_table.py diff --git a/CAL/CalibrationCommon/test/t_calibration_table.sh b/CAL/CalibrationCommon/test/t_calibration_table.sh new file mode 100755 index 0000000000000000000000000000000000000000..c4b6e470dab72bbbfb232195b7487564239af6c3 --- /dev/null +++ b/CAL/CalibrationCommon/test/t_calibration_table.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# Copyright (C) 2018 ASTRON (Netherlands Institute for Radio Astronomy) +# P.O. Box 2, 7990 AA Dwingeloo, The Netherlands +# +# This file is part of the LOFAR software suite. +# The LOFAR software suite is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# The LOFAR software suite is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>. + +./runctest.sh t_calibration_table diff --git a/CAL/CalibrationProcessing/test/convert/CMakeLists.txt b/CAL/CalibrationProcessing/test/convert/CMakeLists.txt index 1006e5ec9f93df055aa14a6a914df05542210968..2093e1068a2aebd49dca269fd99303d965efeefb 100644 --- a/CAL/CalibrationProcessing/test/convert/CMakeLists.txt +++ b/CAL/CalibrationProcessing/test/convert/CMakeLists.txt @@ -2,4 +2,4 @@ include(LofarCTest) -lofar_add_test(t_processing) \ No newline at end of file +lofar_add_test(t_convert_to_calibration_table) \ No newline at end of file diff --git a/CAL/CalibrationProcessing/test/convert/t_convert_to_calibration_table.py b/CAL/CalibrationProcessing/test/convert/t_convert_to_calibration_table.py index 8b137891791fe96927ad78e64b0aad7bded08bdc..a5aa6a0b3f2d0f838c283b10e9687deac10090c0 100644 --- a/CAL/CalibrationProcessing/test/convert/t_convert_to_calibration_table.py +++ b/CAL/CalibrationProcessing/test/convert/t_convert_to_calibration_table.py @@ -1 +1,6 @@ +import unittest + +class ConvertAcceptanceTest(unittest.TestCase): + def test_convertion(self): + pass