Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maybe support raw kotekan format? #505

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions baseband/kotekan/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Licensed under the GPLv3 - see LICENSE.rst
"""Distributed Acquisition and Data Analysis (DADA) format reader/writer."""
from .base import open, info, KotekanFileNameSequencer # noqa
from .header import KotekanHeader # noqa
from .payload import KotekanPayload # noqa
from .frame import KotekanFrame # noqa
106 changes: 106 additions & 0 deletions baseband/kotekan/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Licensed under the GPLv3 - see LICENSE
import astropy.units as u

from baseband.helpers import sequentialfile as sf
from baseband.base.base import (
FileBase,
FileOpener, FileInfo)
from baseband.base.file_info import FileReaderInfo
from .header import KotekanHeader
from .frame import KotekanFrame


__all__ = ['KotekanFileNameSequencer',
'KotekanFileReader',
'open', 'info']


class KotekanFileNameSequencer(sf.FileNameSequencer):
"""List-like generator of Kotekan filenames using a template.

Parameters
----------
template : str
Template to format to get specific filenames. Curly bracket item
keywords are not case-sensitive.
header : dict-like
Structure holding key'd values that are used to fill in the format.
Keys must be in all caps (eg. ``DATE``), as with DADA header keys.
ndisk : int
Number of disks involved.

Examples
--------

>>> from baseband import kotekan
>>> dfs = kotekan.KOTEKANFileNameSequencer(
... '/drives/CHA/{disk}/20210311T000929Z_no_name_set_raw/{file_nr:09d}.raw',
... ndisk=8)
>>> dfs[10]
'2018-01-01_010.dada'
>>> from baseband.data import SAMPLE_DADA
>>> with open(SAMPLE_DADA, 'rb') as fh:
... header = dada.DADAHeader.fromfile(fh)
>>> template = '{utc_start}.{obs_offset:016d}.000000.dada'
>>> dfs = dada.DADAFileNameSequencer(template, header)
>>> dfs[0]
'2013-07-02-01:37:40.0000006400000000.000000.dada'
>>> dfs[1]
'2013-07-02-01:37:40.0000006400064000.000000.dada'
>>> dfs[10]
'2013-07-02-01:37:40.0000006400640000.000000.dada'
"""

def __init__(self, template, header={}, ndisk=None):
super().__init__(template, header=header)
self.ndisk = ndisk
if ndisk is not None:
self.items.setdefault('disk', 0)
self._disk_0 = self.items['disk']

def _process_items(self, file_nr):
super()._process_items(file_nr)
if self.ndisk is not None:
self.items['disk'] = (self._disk_0
+ self.items['file_nr']) % self.ndisk


class KotekanFileReader(FileBase):
"""Simple reader for raw Kotekan files."""
info = FileReaderInfo()

def read_header(self):
"""Read a single header from the file.

Returns
-------
header : `~baseband.kotekan.KotekanHeader`
"""
return KotekanHeader.fromfile(self.fh_raw)

def read_frame(self, memmap=False, verify=True):
"""Read the frame header and read or map the corresponding payload."""
return KotekanFrame.fromfile(self.fh_raw, memmap=memmap, verify=verify)

def get_frame_rate(self):
"""Determine the number of frames per second.

The routine uses the sample rate and number of samples per frame
from the first header in the file.

Returns
-------
frame_rate : `~astropy.units.Quantity`
Frames per second.
"""
with self.temporary_offset(0):
header = self.read_header()
return (header.sample_rate / header.samples_per_frame).to(u.Hz)


class KotekanFileOpener(FileOpener):
FileNameSequencer = KotekanFileNameSequencer


open = KotekanFileOpener('kotekan', {'rb': KotekanFileReader}, KotekanHeader)
info = FileInfo(open)
12 changes: 12 additions & 0 deletions baseband/kotekan/frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Licensed under the GPLv3 - see LICENSE
from baseband.base.frame import FrameBase
from .header import KotekanHeader
from .payload import KotekanPayload


__all__ = ['KotekanFrame']


class KotekanFrame(FrameBase):
_header_class = KotekanHeader
_payload_class = KotekanPayload
145 changes: 145 additions & 0 deletions baseband/kotekan/header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import numpy as np
from astropy.time import Time
from astropy import units as u

from baseband.base.header import ParsedHeaderBase
from baseband.base.utils import fixedvalue


__all__ = ['KotekanHeader']


class KotekanHeader(ParsedHeaderBase):
"""Base for header represented by C structs.

The struct is captured using a `numpy.dtype`, which should be
defined on the class as ``_dtype``.
"""

_dtype = np.dtype(
[("fpga_seq_start", "<u8"), # sample number at start of frame.
("ctime", [("tv", "<i8"), ("tv_nsec", "<u8")]), # unix time
("stream_id", "<u8"), # ???
("dataset_id", "(2,)<u8"), # same for all
("beam_number", "<u4"), # beam 11
("ra", "<f4"), # 2 different RA?
("dec", "<f4"), # 2 different dec?
("scaling", "<u4"), # all 48
("frequency_bin", "<u4"), # between 0 and 1023.
], align=True)
_properties = ('sample_shape', 'time')
_properties = ('payload_nbytes', 'frame_nbytes', 'bps', 'complex_data',
'samples_per_frame', 'sample_shape', 'sample_rate', 'time')

def __init__(self, words, verify=True):
if words is None:
self.words = np.zeros((), self._dtype)
else:
self.words = words
if verify:
self.verify()

def verify(self):
assert self.words.dtype == self._dtype

def keys(self):
return self._dtype.names

@property
def mutable(self):
"""Whether the header can be modified."""
return self.words.flags['WRITEABLE']

@mutable.setter
def mutable(self, mutable):
self.words.flags['WRITEABLE'] = mutable

def __getitem__(self, item):
try:
return self.words[item]
except ValueError:
raise KeyError(f"{self.__class__.__name__} header does not "
f"contain {item}") from None

def __setitem__(self, item, value):
try:
self.words[item] = value
except ValueError:
if not self.mutable:
raise TypeError("header is immutable. Set '.mutable` attribute"
" or make a copy.")
elif item not in self.words.dtype.names:
raise KeyError(f"{self.__class__.__name__} header does not "
f"contain {item}") from None

else:
raise

@classmethod
def fromfile(cls, fh, verify=True, **kwargs):
"""Read from file.

Arguments are the same as for class initialisation. The header
constructed will be immutable.
"""
s = fh.read(cls._dtype.itemsize)
if len(s) < cls._dtype.itemsize:
raise EOFError('reached EOF while reading frame header')
words = np.ndarray(buffer=s, shape=(), dtype=cls._dtype)
self = cls(words, verify=verify, **kwargs)
self.mutable = False
return self

def tofile(self, fh):
"""Write header to filehandle."""
return fh.write(self.words.tobytes())

@property
def nbytes(self):
return self._dtype.itemsize

@fixedvalue
def sample_shape(cls):
npol = 2
return npol,

@property
def time(self):
"""Start time."""
return Time(self['ctime']['tv'], self['ctime']['tv_nsec']*1e-9,
format='unix')

def __eq__(self, other):
return (type(other) is type(self)
and other.words == self.words)

def invariant_pattern(self):
mask = self.__class__(None)
for key in {'dateset_id', 'beam_number', 'scaling'}:
mask[key] = -1
return (np.atleast_1d(self.words).view('<u4'),
np.atleast_1d(mask.words).view('<u4'))

@fixedvalue
def payload_nbytes(self):
return 49152 * 2

@property
def frame_nbytes(self):
return self.payload_nbytes + self.nbytes

@fixedvalue
def bps(cls):
return 4

@fixedvalue
def complex_data(cls):
return True

@fixedvalue
def samples_per_frame(self):
return 49152

@fixedvalue
def sample_rate(self):
return 8e8 / 2048 / u.s
24 changes: 24 additions & 0 deletions baseband/kotekan/payload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Licensed under the GPLv3 - see LICENSE
"""Payload for DADA format."""
from collections import namedtuple

import numpy as np

from baseband.base.payload import PayloadBase


__all__ = ['KotekanPayload']


levels = np.array(list(range(-8, 8)))
lut = (levels * 1j + levels[:, np.newaxis]).ravel().astype('c8')


def decode_4bit(words):
return lut[words.view(np.uint8)]


class KotekanPayload(PayloadBase):
_dtype_word = np.dtype('u1')
_decoders = {4: decode_4bit}
_sample_shape_maker = namedtuple('SampleShape', 'npol')