From e7eee3b13972127940d2ce4a0fb6f5ffe2cf0d33 Mon Sep 17 00:00:00 2001
From: Marko Mecina <marko.mecina@univie.ac.at>
Date: Fri, 3 Nov 2023 17:54:50 +0100
Subject: [PATCH] implement pipeline to generate FITS files from S13 data

---
 Ccs/ccs_function_lib.py                       |  19 +-
 Ccs/decompression.py                          |  14 +-
 Ccs/packet_config_SMILE.py                    |   1 +
 .../dataprocessing/smile_raw_ce_converter.py  | 162 ++++++++++++++++++
 4 files changed, 189 insertions(+), 7 deletions(-)
 create mode 100755 Ccs/tools/dataprocessing/smile_raw_ce_converter.py

diff --git a/Ccs/ccs_function_lib.py b/Ccs/ccs_function_lib.py
index 62f1d0f..43dd85a 100644
--- a/Ccs/ccs_function_lib.py
+++ b/Ccs/ccs_function_lib.py
@@ -5507,6 +5507,7 @@ def add_tst_import_paths():
     sys.path.append(cfg.get('paths', 'tst') + '/sketch_desk')
     sys.path.append(cfg.get('paths', 'tst') + '/test_specs')
     sys.path.append(cfg.get('paths', 'tst') + '/testing_library')
+    sys.path.append(cfg.get('paths', 'tst') + '/testing_library/testlib')
     # insert this to import the tst view.py, not the one in .local folder
     sys.path.insert(0, cfg.get('paths', 'tst') + '/tst')
 
@@ -5688,10 +5689,15 @@ def collect_13(pool_name, starttime=None, endtime=None, startidx=None, endidx=No
             continue
         else:
             pkts = [a.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN] for a in tm_132.filter(DbTelemetry.idx > i.idx, DbTelemetry.idx < j.idx)]
+
+            # check for padding bytes in last packet
+            datalen = int.from_bytes(j.raw[S13_DATALEN_PAR_OFFSET:S13_DATALEN_PAR_OFFSET + S13_DATALEN_PAR_SIZE], 'big')
+            lastpktdata = j.raw[S13_HEADER_LEN_TOTAL:S13_HEADER_LEN_TOTAL + datalen]
+
             if join:
-                ces[float(i.timestamp[:-1])] = i.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN] + b''.join(pkts) + j.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN]
+                ces[float(i.timestamp[:-1])] = i.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN] + b''.join(pkts) + lastpktdata
             else:
-                ces[float(i.timestamp[:-1])] = [i.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN]] + [b''.join(pkts)] + [j.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN]]
+                ces[float(i.timestamp[:-1])] = [i.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN]] + pkts + [lastpktdata]
             k += 2
 
         cnt += 1
@@ -5738,6 +5744,11 @@ def dump_large_data(pool_name, starttime=0, endtime=None, outdir="", dump_all=Fa
 
         try:
             obsid, ctime, ftime, ctr = s13_unpack_data_header(ldt_dict[buf])
+        except NotImplementedError:
+            obsid = int(datetime.datetime.utcnow().strftime('%j'))
+            ctime = int(buf)
+            ftime = 0
+            ctr = crc(ldt_dict[buf])
         except ValueError as err:
             logger.error('Incompatible definition of S13 data header.')
             raise err
@@ -6940,7 +6951,11 @@ try:
     SDU_PAR_LENGTH = _s13_info[0][-1]
     # length of PUS + source header in S13 packets (i.e. data to be removed when collecting S13)
     S13_HEADER_LEN_TOTAL = TM_HEADER_LEN + sum([p[-1] for p in _s13_info])
+    S13_DATALEN_PAR_OFFSET, S13_DATALEN_PAR_SIZE = TM_HEADER_LEN + sum([x[1] for x in _s13_info[:-1]]), _s13_info[-1][1]
 except (SQLOperationalError, NotImplementedError, IndexError):
     logger.warning('Could not get S13 info from MIB, using default values')
     SDU_PAR_LENGTH = 1
     S13_HEADER_LEN_TOTAL = 21
+    S13_DATALEN_PAR_OFFSET = 19
+    S13_DATALEN_PAR_SIZE = 2
+
diff --git a/Ccs/decompression.py b/Ccs/decompression.py
index 7966865..c97bcce 100644
--- a/Ccs/decompression.py
+++ b/Ccs/decompression.py
@@ -76,9 +76,9 @@ def convert_fullframe_to_cheopssim(fname):
     hdulist.writeto(fname[:-5] + '_CHEOPSSIM.fits')
 
 
-def ce_decompress(outdir, pool_name=None, sdu=None, starttime=None, endtime=None, startidx=None, endidx=None,
+def ce_decompress(pool_name, outdir, sdu=None, starttime=None, endtime=None, startidx=None, endidx=None,
                   ce_exec=None):
-    decomp = CeDecompress(outdir, pool_name=pool_name, sdu=sdu, starttime=starttime, endtime=endtime, startidx=startidx,
+    decomp = CeDecompress(pool_name, outdir, sdu=sdu, starttime=starttime, endtime=endtime, startidx=startidx,
                           endidx=endidx, ce_exec=ce_exec)
     decomp.start()
 
@@ -94,7 +94,7 @@ def ce_decompress_stop(name=None):
 
 class CeDecompress:
 
-    def __init__(self, outdir, pool_name=None, sdu=None, starttime=None, endtime=None, startidx=None, endidx=None,
+    def __init__(self, pool_name, outdir, sdu=None, starttime=None, endtime=None, startidx=None, endidx=None,
                  ce_exec=None):
         self.outdir = outdir
         self.pool_name = pool_name
@@ -156,7 +156,8 @@ class CeDecompress:
             fitspath = cefile[:-2] + 'fits'
             if os.path.isfile(fitspath):
                 subprocess.run(["rm", fitspath])
-            subprocess.run([self.ce_exec, cefile, fitspath], stdout=open(cefile[:-2] + 'log', 'w'))
+            with open(cefile[:-2] + 'log', 'w') as logfd:
+                subprocess.run([self.ce_exec, cefile, fitspath], stdout=logfd, stderr=logfd)
 
         # first, get all TM13s already complete in pool
         try:
@@ -164,8 +165,11 @@ class CeDecompress:
                                            outdir=self.outdir, dump_all=True, sdu=self.sdu, startidx=self.startidx,
                                            endidx=self.endidx)
             for ce in filedict:
-                self.last_ce_time = ce
                 decompress(filedict[ce])
+                self.last_ce_time = ce
+
+            self.last_ce_time += self.ldt_minimum_ce_gap
+
         except (ValueError, TypeError, AttributeError) as err:
             ce_decompressors.pop(self.init_time)
             raise err
diff --git a/Ccs/packet_config_SMILE.py b/Ccs/packet_config_SMILE.py
index ed9753f..802c4e4 100644
--- a/Ccs/packet_config_SMILE.py
+++ b/Ccs/packet_config_SMILE.py
@@ -793,4 +793,5 @@ _S13_HEADER_FMT = S13_FMT_OBSID + S13_FMT_TIME + S13_FMT_FTIME + S13_FMT_COUNTER
 
 
 def s13_unpack_data_header(buf):
+    raise NotImplementedError
     return struct.unpack('>' + _S13_HEADER_FMT, buf[:struct.calcsize(_S13_HEADER_FMT)])
diff --git a/Ccs/tools/dataprocessing/smile_raw_ce_converter.py b/Ccs/tools/dataprocessing/smile_raw_ce_converter.py
new file mode 100755
index 0000000..4930f74
--- /dev/null
+++ b/Ccs/tools/dataprocessing/smile_raw_ce_converter.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python
+
+"""
+Convert unprocessed CE raw data to FITS files. Product type is determined (guessed) based on CE size.
+"""
+
+import datetime
+import os
+import sys
+
+import numpy as np
+from astropy.io import fits
+
+# expected CE sizes in bytes
+SIZE_FF = 4511 * 4608 * 2
+SIZE_FT = 639 * 384 * 2  # 1 node
+SIZE_ED = 64  # 1 event
+
+FILE_PREFIX = 'SMILE_SXI_L1'
+
+ED_BIN_DTYPE = np.dtype(
+    [('TIME', '>f8'), ('CCDFRAME', '>i4'), ('CCDNR', 'u1'), ('RAWX', '>i2'), ('RAWY', '>i2'), ('AMP', 'u1'),
+     ('PHAS', '>i2', (25,))])
+
+
+def convert_ce(cefile, fitsfile=None):
+
+    cedata = open(cefile, 'rb').read()
+
+    # guess product based on CE size
+    if len(cedata) == SIZE_FF:
+        mode, hdl = mk_ff(cedata)
+    elif len(cedata) // SIZE_FT in [1, 2, 4]:
+        mode, hdl = mk_ft(cedata)
+    elif len(cedata) % SIZE_ED == 0:
+        mode, hdl = mk_ed(cedata)
+    else:
+        print('Cannot determine product type for CE of length {}, aborting.'.format(len(cedata)))
+        sys.exit()
+
+    if fitsfile is None:
+        outdir = os.path.dirname(os.path.abspath(cefile))
+        fitsfile = os.path.join(outdir, '{}_{}_{}.fits'.format(FILE_PREFIX, mode, _mk_ts()))
+    else:
+        fitsfile = fitsfile.replace('.fits', '_{}.fits'.format(mode))
+
+    hdl.writeto(fitsfile, overwrite=True)
+
+
+def mk_ff(data):
+    # create uint16 array from raw data and reshape
+    arr = np.frombuffer(data, dtype='>H').reshape(4511, -1)
+    fnode = arr[:, ::2]
+    enode = arr[:, 1::2][:, ::-1]
+    ff = np.concatenate((fnode, enode), axis=1)
+
+    # write array to FITS file
+    hdl = _mk_hdl('FF')
+    hdl.append(fits.ImageHDU(data=ff, name='FULLFRAME'))
+
+    return 'FF', hdl
+
+
+def mk_ft(data):
+    arr = np.frombuffer(data, dtype='>H').reshape(-1, 639, 384)
+
+    hdl = _mk_hdl('FT')
+    for n in range(arr.shape[0]):
+        hdl.append(fits.ImageHDU(data=arr[n, :, :], name='FT_CCD_NODE_{}'.format(n)))
+
+    # arrange all nodes to full CCD view
+    if arr.shape[0] == 4:
+        n00 = arr[0, :, :]  # CCD2 F-side
+        n01 = arr[1, :, ::-1]  # CCD2 E-side
+        n10 = arr[2, ::-1, :]  # CCD4 F-side
+        n11 = arr[3, ::-1, ::-1]  # CCD4 E-side
+
+        n0 = np.concatenate((n00, n01), 1)
+        n1 = np.concatenate((n10, n11), 1)
+
+        nn = np.concatenate((n0, n1), 0)  # [::-1, :]
+
+        hdl.append(fits.ImageHDU(data=nn, name='FULLCCD'))
+
+    return 'FT', hdl
+
+
+def mk_ed(data):
+    # reshape into array of evt packets
+    arr = np.frombuffer(data, dtype='>H').reshape(-1, 32)
+
+    hdl = _mk_hdl('FT')
+    ts = int(hdl['PRIMARY'].header['OBS_ID'])
+    bindata = np.array([_mk_bin_entry(evt, ts) for evt in arr], dtype=ED_BIN_DTYPE)
+
+    # also add an HDU with event map
+    nodes = np.zeros((2, 2, 639, 384))
+    nodes[:] = np.nan
+
+    for _, _, ccd, col, row, node, fx in bindata:
+        try:
+            nodes[ccd, node, row - 2:row + 3, col - 2:col + 3] = fx.reshape(5, 5)
+        except:
+            print(col, row, 'FAILED')
+
+    n00 = nodes[0, 0, :, :]  # CCD2 F-side
+    n01 = nodes[0, 1, :, :][:, ::-1]  # CCD2 E-side
+    n10 = nodes[1, 0, :, :][::-1, :]  # CCD4 F-side
+    n11 = nodes[1, 1, :, :][::-1, ::-1]  # CCD4 E-side
+
+    n0 = np.concatenate((n00, n01), 1)
+    n1 = np.concatenate((n10, n11), 1)
+
+    ed_img = np.concatenate((n0, n1), 0)  # [::-1, :]
+
+    hdl.append(fits.ImageHDU(data=ed_img, name='EVTMAP'))
+
+    hdl.append(fits.BinTableHDU(data=bindata, name='EVENTS'))
+
+    return 'ED', hdl
+
+
+def _mk_bin_entry(data, timestamp):
+    spw, dlen, dtyp, fc, sc, col, row, *evts = data
+
+    ccdnr = (dtyp >> 4) & 1
+    node = (dtyp >> 5) & 0b11
+
+    return timestamp, fc, ccdnr, col, row, node, evts
+
+
+def _mk_hdl(dmode):
+    hdl = fits.HDUList()
+    phdu = fits.PrimaryHDU()
+
+    phdu.header['TELESCOP'] = 'SMILE'
+    phdu.header['INSTRUME'] = 'SXI'
+    phdu.header['DATAMODE'] = dmode
+    phdu.header['OBS_ID'] = datetime.datetime.utcnow().strftime('%s')
+
+    hdl.append(phdu)
+
+    return hdl
+
+
+def _mk_ts(cefile=None):
+
+    if cefile is None:
+        return datetime.datetime.utcnow().strftime('%j_%H%M%S%f')[:-3]
+    else:
+        return cefile.split('_')[-1]
+
+
+if __name__ == "__main__":
+
+    if len(sys.argv) > 2:
+        cefile, fitsfile = sys.argv[1:3]
+    else:
+        cefile = sys.argv[1]
+        fitsfile = None
+
+    convert_ce(cefile, fitsfile=fitsfile)
-- 
GitLab