initial project code commit

This commit is contained in:
2026-04-22 13:15:55 +03:00
parent e624299842
commit 9dcc2f9473
69 changed files with 7387 additions and 0 deletions

BIN
artpipeline/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
[build-system]
requires = [
"setuptools>=42",
]
build-backend = "setuptools.build_meta"

17
artpipeline/setup.py Normal file
View File

@@ -0,0 +1,17 @@
from setuptools import setup
exec(open('src/artpipeline/version.py').read())
setup(
name="artpipeline",
version=__version__,
author="M.Pavlinsky SRG/ART-XC software team",
description="data reduction pipeline",
package_dir={"": "src"},
packages=["artpipeline"],
entry_points={
"console_scripts": [
"artpipeline = artpipeline.pipeline:main",
]
},
)

BIN
artpipeline/src/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,325 @@
#!/usr/bin/env python3
# -*- coding: utf8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2020-2025 IKI RAS (http://iki.rssi.ru/)
# Space Research Institute of the Russian Academy of Science
#
# This file is part of the M. Pavlinsky SRG/ART-XC software project.
# -----------------------------------------------------------------------------
import os
import sys
from pathlib import Path
from datetime import datetime as dt
import numpy as np
from scipy.interpolate import interp1d
import polars as pl
from astropy.io import fits
# from astropy.table import Table
from arttools.datatable import DataTable, FitsAdapter
# if 'ARTCALDB' in os.environ:
# ARTCALDBPATH = os.getenv('ARTCALDB')
# else:
# print('error: ARTCALDB environment variable not found')
# sys.exit(-1)
class CaldbInfo(object):
def __init__(self, caldbdir):
self._caldbpath = caldbdir
self._indexfile = 'caldb.indx'
self._caldbindex = os.path.join(self._caldbpath, self._indexfile)
with fits.open(self._caldbindex) as caldbfile:
self._version = caldbfile['CIF'].header['ARTCALDB']
# self._index = Table(caldbfile['CIF'].data).to_pandas()
self._index = DataTable().adapter(FitsAdapter(self._caldbindex, 'CIF'))
self._index.read()
def get(self):
return {
'CALDBDIR': self._caldbpath,
'CALDBVER': self._version
}
class Caldb(object):
def __init__(self, caldbdir, instname, silent=True):
self._instname = instname
self._silent = silent
self._version = None
self._index = None
# --- read version, index
self._caldbpath = caldbdir
self._indexfile = 'caldb.indx'
self._caldbindex = os.path.join(self._caldbpath, self._indexfile)
with fits.open(self._caldbindex) as caldbfile:
self._version = caldbfile['CIF'].header['ARTCALDB']
# self._index = Table(caldbfile['CIF'].data).to_pandas()
self._index = DataTable().adapter(FitsAdapter(self._caldbindex, 'CIF'))
self._index.read()
# print('CALDB: version {} [{}]'.format(self._version, self._caldbfilespath))
@property
def silent(self):
return self._silent
def cif(self, instname, calcname):
iname = instname; cname = calcname
if not self._silent:
print(self._silent)
print('CALDB: select: {}'.format(
'INSTRUME==\'{}\' and CAL_CNAM==\'{}\''.format(iname, cname)))
# iname = iname.ljust(10, ' ')
# cname = cname.ljust(20, ' ')
# return self._index.query(
# 'INSTRUME==\'{}\' and CAL_CNAM==\'{}\''.format(iname, cname))
iname = iname.ljust(10, ' ')
cname = cname.ljust(20, ' ')
return self._index.frame.filter(
pl.col('INSTRUME') == iname,
pl.col('CAL_CNAM') == cname
)
def file(self, calcname):
return self.instfile(self._instname, calcname)
def instfile(self, instname, calcname):
selected = self.cif(instname, calcname)
for cdbpath, cdbfile in zip(selected['CAL_DIR'], selected['CAL_FILE']):
cdbpath = cdbpath.strip()
cdbfile = cdbfile.strip()
if not self._silent:
print('CALDB: product: {}'.format(cdbfile))
p = Path(cdbpath)
p = Path(*p.parts[2:]) # chop off data/artxc_caldb
cdbpath = p
# return first record only, @fixme
return os.path.join(cdbpath, cdbfile)
return None
def path(self, folder='bcf'):
return os.path.join(self._caldbpath, folder)
def instname(self):
return self._instname
def version(self):
return self._version
class CaldbTelescope(object):
def __init__(self, caldb):
self._caldb = caldb
self._silent = self._caldb.silent
self._oaxis_file \
= os.path.join(self._caldb.path(), self._caldb.file('OPTAXIS'))
with fits.open(self._oaxis_file) as hdul:
self._oaxis = {
'X': hdul[1].data['X'][0],
'Y': hdul[1].data['Y'][0]
}
if not self._silent:
print('CALDB: selected OPTAXIS: X={} Y={}'.format(self._oaxis['X'], self._oaxis['Y']))
def oaxis(self):
return self._oaxis
def info(self):
meta = {
'OPTAXIS': self._oaxis
}
# class CaldbEnergy(object):
# def __init__(self, instname):
# self._caldb = Caldb(instname)
# self._thresh_file \
# = self._caldb.file('THRESHOL')
# self._escale_file \
# = self._caldb.file('ESCALE' )
# #todo: check files is not None
# self._thresh = fits.open(os.path.join(self._caldb.path(), self._thresh_file))
# self._escale = fits.open(os.path.join(self._caldb.path(), self._escale_file))
# def bot(self):
# return self._thresh['BOT'].data
# def top(self):
# return self._thresh['TOP'].data
# def scale(self):
# # escale_data = self._escale['ESCALE'].data
# escale_data = self._escale['E_COEFF'].data
# obt_data = escale_data['OBT']
# c0_data = escale_data['C0' ]
# c1_data = escale_data['C1' ]
# scale_fn = interp1d(obt_data, c0_data, bounds_error=False, fill_value=tuple(c0_data[[0, -1]]))
# shift_fn = interp1d(obt_data, c1_data, bounds_error=False, fill_value=tuple(c1_data[[0, -1]]))
# # print last time (obt_data)
# # save it to header
# return {
# 'C0': scale_fn,
# 'C1': shift_fn}
# def version(self):
# return self._caldb.version()
# def info(self):
# meta = {
# 'THRESHOL': self._thresh_file,
# 'ESCALE' : self._escale_file,
# 'CALDBVER': self.version()
# }
# return meta
class CaldbEnergy(object):
def __init__(self, caldbdir, instname):
self._caldb = Caldb(caldbdir, instname)
self._chen_file \
= self._caldb.file('CHENERGY')
self._chen = fits.open(os.path.join(self._caldb.path(), self._chen_file))
def bot(self):
return self._chen['BOT'].data
def top(self):
return self._chen['TOP'].data
def version(self):
return self._caldb.version()
def info(self):
meta = {
'CHENERGY': self._chen_file,
'CALDBVER': self.version()
}
return meta
class CaldbMask(object):
def __init__(self, caldbdir, instname):
self._caldb = Caldb(caldbdir, instname)
self._dmask_file \
= os.path.join(self._caldb.path(), self._caldb.file('DETMASK'))
self._dmask = np.copy(fits.getdata(self._dmask_file, 1)).astype(bool).T
# self._dmask = fits.open(os.path.join(self._caldb.path(), self._dmask_file))
def get(self):
return self._dmask
def version(self):
return self._caldb.version()
def info(self):
meta = {
'DETMASK': self._dmask_file,
'CALDBVER': self.version()
}
return meta
class CaldbOrientation(object):
_INST_QUAT = {
'GYRO': [ 0. , 0. , 0. , 1. ],
'BOKZ': [ 0. , -0.707106781186548, 0. , 0.707106781186548],
'SED1': [ 0.183012701892219, 0.683012701892219, -0.183012701892219, 0.683012701892219],
'SED2': [-0.183012701892219, 0.683012701892219, 0.183012701892219, 0.683012701892219]}
def __init__(self, caldbdir, instname):
self._delay_file = None
self._bs_file = None
self._caldb = Caldb(caldbdir, instname)
self._delay = self.read_time_delay()
self._instquat = self.read_instquat ()
self._boresight = self.read_boresight ()
def read_time_delay(self):
delay_file = self._caldb.file('{instname}LAG'.format(instname=self._caldb.instname()))
delay_hdul = fits.open(os.path.join(self._caldb.path(), delay_file))
delay = delay_hdul[1].data['DELAY'][0]
self._delay_file = delay_file
# print('CALDB: select {} DELAY={}'.format(self._caldb.instname(), delay))
return delay
def read_instquat(self):
return self._INST_QUAT[self._caldb.instname()]
def read_boresight(self, instname=None):
if instname is None:
instname = self._caldb.instname()
bs_file = self._caldb.instfile(instname, 'BORESIGH')
bs_hdul = fits.open(os.path.join(self._caldb.path(), bs_file))
self._bs_file = bs_file
return bs_hdul[1].data[0]
def timeshift(self):
return self._delay
def instquat(self):
return self._instquat
def boresight(self):
return self._boresight
def version(self):
return self._caldb.version()
def info(self):
meta = {
'INSTRUME': self._caldb.instname(),
'TIMELAG' : self._delay_file,
'BORESIGH': self._bs_file
}
return meta
if __name__ == '__main__':
pass

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python3
# -*- coding: utf8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2020-2025 IKI RAS (http://iki.rssi.ru/)
# Space Research Institute of the Russian Academy of Science
#
# This file is part of the SRG/ART-XC software project.
# -----------------------------------------------------------------------------
import os
import sys
import shutil
import numpy as np
from scipy.interpolate import interp1d
from .time import Mission
class Energy(object):
def __init__(self, caldb_energy, seed=None):
self._caldb_energy = caldb_energy
self._seed = seed
self.GRADES = np.ones(256) * (-1)
self.GRADES[18] = 0 # single central events
self.GRADES[[50, 26, 22, 19, 54, 51, 30, 27]] = [1, 2, 3, 4, 5, 6, 7, 8] # double events
self.GRADES[[58, 62, 59, 23, 55, 31, 63]] = [9, 10, 11, 12, 13, 14, 15] # triple events
def calc(self, evtdata, hkdata):
time = evtdata['time']
tstart = Mission(time[0]).to_datetime()
escale = self._caldb_energy.scale()
ebot = self._caldb_energy.bot()
etop = self._caldb_energy.top()
tempdata = hkdata['td1']
temperature = interp1d(
hkdata['time'], tempdata, bounds_error=False, kind='linear',
fill_value=(tempdata[0], tempdata[-1]))(evtdata['time'])
if self._seed is not None:
sseq = np.random.SeedSequence(self._seed)
else:
sseq = np.random.SeedSequence()
rng = np.random.default_rng(sseq)
seed = sseq.entropy
energybot, sigmabot, maskbot \
= self._calc(
evtdata,
('raw_x' ,
'pha_bot_sub1',
'pha_bot' ,
'pha_bot_add1'),
temperature, ebot, rng)
energytop, sigmatop, masktop \
= self._calc(
evtdata,
('raw_y' ,
'pha_top_sub1',
'pha_top' ,
'pha_top_add1'),
temperature, etop, rng)
energy = np.zeros(evtdata.size, np.double)
mask = np.zeros((8, evtdata.size), bool)
mask[2:5,:] = masktop
mask[5:8,:] = maskbot
emask = np.any(mask[2:,:], axis=0)
energybot = np.sum(energybot * maskbot , axis=0)
sigmabot2 = np.sum(np.power(sigmabot, 2.) * maskbot, axis=0)
energytop = np.sum(energytop * masktop , axis=0)
sigmatop2 = np.sum(np.power(sigmatop, 2.) * masktop, axis=0)
energy[emask] \
= ((energybot * sigmatop2 + energytop * sigmabot2) / (sigmabot2 + sigmatop2))[emask]
energy[emask] = self._scale(evtdata['time'][emask], energy[emask], escale)
# energy = self._scale(energy, escale)
pi = self._en2pi(energy, emask )
grade = self._grade(mask )
return energy, pi, grade, temperature, energybot, energytop, seed
def _calc(self, evtdata, coln, temperature, energycal, rng):
strip, mask \
= self._eventindex(evtdata[coln[0]])
pha = np.array([evtdata[coln[1]], evtdata[coln[2]], evtdata[coln[3]]])
e1 = self._pha2en(pha , strip, temperature, energycal)
e2 = self._pha2en(pha + 1., strip, temperature, energycal)
# energy = np.random.uniform(e1, e2)
energy = rng.uniform(e1, e2)
sigma = energycal['fwhm_0'][strip] + energycal['fwhm_1'][strip] * temperature
mask = np.logical_and(mask, energy > energycal['threshold'][strip])
return energy, sigma, mask
def _pha2en(self, pha, strip, temperature, energycal):
''' Energy = c00 + c01*T + (c10 + c11*T)*PHA + (c20 + c21*T)*PHA**2 '''
pha2 = np.power(pha, 2.)
energy = \
energycal["c0_0"][strip] + energycal["c0_1"][strip] * temperature + \
(energycal["c1_0"][strip] + energycal["c1_1"][strip] * temperature) * pha + \
(energycal["c2_0"][strip] + energycal["c2_1"][strip] * temperature) * pha2
return energy
def _scale(self, times, energy, escale):
''' Еnergy = C0 + C1*Еnergy + С2*Еnergy**2 '''
# return escale['C0'] + escale['C1'] * energy + escale['C2'] * np.power(energy, 2.)
C0 = escale['C0'](times)
C1 = escale['C1'](times)
return energy * C0 + C1
def _en2pi(self, energy, mask):
PHA2PI = 10 # channels are in units of 100 eV
pi = np.int16(energy * PHA2PI) - 1
pi = np.maximum(pi, 0)
pi[~mask] = 0
return pi
def _eventindex(self, strip):
coord = np.empty((3, strip.size), int) #(strip, (3, 1))
coord[0, :] = strip - 1
coord[1, :] = strip
coord[2, :] = strip + 1
mask = (coord >= 0) & (coord <= 47)
coord[np.logical_not(mask)] = -1
return coord, mask
def _grade(self, mask):
grademask = np.packbits(mask, axis=0)[0]
return self.GRADES[grademask].astype(int)
if __name__ == '__main__':
pass

View File

@@ -0,0 +1,206 @@
#!/usr/bin/env python3
# -*- coding: utf8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2020-2025 IKI RAS (http://iki.rssi.ru/)
# Space Research Institute of the Russian Academy of Science
#
# This file is part of the SRG/ART-XC software project.
# -----------------------------------------------------------------------------
import os
import sys
import shutil
import warnings
import numpy as np
from scipy.interpolate import interp1d
import astropy.units as u
from astropy.time.formats import erfa
from astropy.time import Time
from astropy.time import TimezoneInfo
# from .time import Mission
from arttools.telescope import Teldef
np.seterr(all="ignore")
warnings.filterwarnings('ignore')
class Energy(object):
def __init__(self, caldb, seed=None):
self._caldb = caldb
self._seed = seed
self.MJDCDB = Time(58677.6464931, format='mjd')
self.MJDREF = Time(51543.8750000, format='mjd') #corresponds to date 01.01.2000 00:00:00 (UTC+3)
self.TZ_UTC = TimezoneInfo(utc_offset=0*u.hour) #UTC time zone
self.TZ_MSK = TimezoneInfo(utc_offset=3*u.hour) #UTC+3 (Moscow) time zone
self.GRADES = np.ones(256) * (-1)
self.GRADES[18] = 0 # single central events
self.GRADES[[50, 26, 22, 19, 54, 51, 30, 27]] = [1, 2, 3, 4, 5, 6, 7, 8] # double events
self.GRADES[[58, 62, 59, 23, 55, 31, 63]] = [9, 10, 11, 12, 13, 14, 15] # triple events
def _calc_mjdtime(self, missiontime):
return self.MJDREF + Time(missiontime / erfa.DAYSEC, format='mjd').mjd
def _calc_dt(self, missiontime):
mjdt = self._calc_mjdtime(missiontime)
return (mjdt - self.MJDCDB).jd
def _eventindex(self, strip):
# coord = np.empty((3, strip.size), int) #(strip, (3, 1))
coord = np.empty((3, strip.count()), int) #(strip, (3, 1))
coord[0, :] = strip - 1
coord[1, :] = strip
coord[2, :] = strip + 1
mask = (coord >= 0) & (coord <= 47)
coord[np.logical_not(mask)] = -1
return coord, mask
def _calc(self, evtdata, coln, dt, dt2, random, energycal):
data_size = dt.size
strip, mask_strip \
= self._eventindex(evtdata.column(coln[0]))
pha0 = np.array([
evtdata.column(coln[1]),
evtdata.column(coln[2]),
evtdata.column(coln[3])
], dtype=np.uint32)
C0 = energycal['C0_0'][strip] + energycal['C0_1'][strip] * dt + energycal['C0_2'][strip] * dt2
C1 = energycal['C1_0'][strip] + energycal['C1_1'][strip] * dt + energycal['C1_2'][strip] * dt2
C2 = energycal['C2_0'][strip] + energycal['C2_1'][strip] * dt + energycal['C2_2'][strip] * dt2
C3 = energycal['C3_0'][strip] + energycal['C3_1'][strip] * dt + energycal['C3_2'][strip] * dt2
C4 = energycal['C4_0'][strip] + energycal['C4_1'][strip] * dt + energycal['C4_2'][strip] * dt2
if random is not None:
pha_rand = random.uniform(0, 1, pha0.shape)
else:
pha_rand = np.zeros(pha0.shape)
pha = pha0 + pha_rand
energy3 = C0 + C1 * pha + C2 * np.power(pha, 2)
kofA = [0.9, 1.15]
dayA = [223., 1480.]
C1_thr = (kofA[0] - kofA[1]) / (dayA[0] - dayA[1])
C0_thr = ((kofA[0] + kofA[1]) - (dayA[0] + dayA[1]) * C1_thr) / 2.
kof = C0_thr + C1_thr * dt
mask_threshold = energy3 > energycal['THRESHOLD'][strip] * kof
mask_threshold[1] = np.full_like(mask_threshold[1], True)
mask3 = np.logical_and(mask_strip, mask_threshold)
energy3[~mask3] = 0
energy = np.sum(energy3, axis=0)
# energy = np.sum(energy3 * mask3, axis=0)
fwhm = (C3[1] + C4[1] * np.sqrt(energy))
return energy, fwhm, mask3
def _grade(self, mask):
grademask = np.packbits(mask, axis=0)[0]
return self.GRADES[grademask].astype(int)
def calc(self, teln, evtdata, random=None):
dt = self._calc_dt(evtdata.column('TIME'))
dt2 = np.power(dt, 2)
energybot, fwhmbot, maskbot3 =\
self._calc(
evtdata,
('RAW_X' ,
'PHA_BOT_SUB1',
'PHA_BOT' ,
'PHA_BOT_ADD1'),
dt, dt2, random, self._caldb.bot()
)
fwhmbot_2 = np.power(fwhmbot, 2.)
energytop, fwhmtop, masktop3 =\
self._calc(
evtdata,
('RAW_Y' ,
'PHA_TOP_SUB1',
'PHA_TOP' ,
'PHA_TOP_ADD1'),
dt, dt2, random, self._caldb.top()
)
fwhmtop_2 = np.power(fwhmtop, 2.)
# -- calculate mask
mask = np.zeros((8, evtdata.size), bool)
mask[2:5,:] = masktop3
mask[5:8,:] = maskbot3
# energy = np.zeros(dt.size, float)
energy =\
((energybot * fwhmtop_2 + energytop * fwhmbot_2) / (fwhmbot_2 + fwhmtop_2))
grades = self._grade(mask)
return energy, energybot, energytop, grades
class Flag(object):
def __init__(self, caldb):
self._caldb = caldb
def _mask_events(self, evtdata, tstart, tstop):
times = evtdata.column('TIME')
# try:
# tstart = evtdata.tstart
# tstop = evtdata.tstop
# except KeyError:
# tstart = times[ 0]
# tstop = times[-1]
# print('WARNING: TSTART or TSTOP keywords not found')
# print('WARNING: will use event times TSTART = {}, TSTOP = {}'.format(tstart, tstop))
return (times > tstart) & (times < tstop)
def _mask_shadow(self, evtdata):
evtrawx = evtdata.column('RAW_X')
evtrawy = evtdata.column('RAW_Y')
shadowmask = self._caldb.get()
return np.logical_not(shadowmask[evtrawx, evtrawy])
def _mask_edges(self, evtdata):
evtrawx = evtdata.column('RAW_X')
evtrawy = evtdata.column('RAW_Y')
return np.any([
evtrawx == 0, evtrawx == 47,
evtrawy == 0, evtrawy == 47], axis=0)
def calc(self, evtdata, tstart, tstop):
flag = np.ones(evtdata.size, dtype=np.uint8)
eventsmask = self._mask_events(evtdata, tstart, tstop)
shadowmask = self._mask_shadow(evtdata)
edgesmask = self._mask_edges (evtdata)
flag[eventsmask] = 0
flag[shadowmask] = 2
flag[edgesmask ] = 3
return flag
if __name__ == '__main__':
pass

View File

@@ -0,0 +1,176 @@
#!/usr/bin/env python3
# -*- coding: utf8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2021-2025 IKI RAS (http://iki.rssi.ru/)
# Space Research Institute of the Russian Academy of Science
#
# This file is part of the M. Pavlinsky SRG/ART-XC software project.
# -----------------------------------------------------------------------------
import os
import shutil
import numpy as np
from astropy.io import fits
from arttools.gti import Gti
from arttools.datatable import DataTable, FitsAdapter
from artpipeline.version import __version__
class Events(object):
def __init__(self, srcfile):
super().__init__()
self._srcfile = srcfile
self._telname = 0
self._teln = 0
self._tstart = 0
self._tstop = 0
self._events = DataTable()
self._hk = DataTable()
self._gti = Gti()
@property
def size(self):
return self._events.size
@property
def teln(self): return self._teln
@property
def telname(self): return self._telname
@property
def tstart(self): return self._tstart
@property
def tstop(self): return self._tstop
@property
def events(self): return self._events
@property
def hk(self): return self._hk
@property
def gti(self): return self._gti
def _read_header(self):
with fits.open(self._srcfile) as hdul:
self._teln = hdul['events'].header['teln' ]
self._telname = hdul['events'].header['instrume']
self._tstart = hdul['events'].header['tstart' ]
self._tstop = hdul['events'].header['tstop' ]
def read(self):
self._read_header()
self._events = DataTable().adapter(FitsAdapter(self._srcfile, 'events')).read()
self._hk = DataTable().adapter(FitsAdapter(self._srcfile, 'hk' )).read()
self._gti = Gti.from_hduname(self._srcfile, 'stdgti')
return self
def write_cl(self, dstfile, cdbinfo):
srchdul = fits.open(self._srcfile)
# --- primary HDU ---
hdu = fits.PrimaryHDU()
# --- EVENTS HDU ---
a00 = np.array(self._events.column('TIME' ))
a01 = np.array(self._events.column('TIME_I' ))
a02 = np.array(self._events.column('TIME_F' ))
a03 = np.array(self._events.column('TIME_CORR' ))
a04 = np.array(self._events.column('ENERGY' ))
a05 = np.array(self._events.column('ENERGY_TOP' ))
a06 = np.array(self._events.column('ENERGY_BOT' ))
a07 = np.array(self._events.column('GRADE' ))
a08 = np.array(self._events.column('FLAG' ))
a09 = np.array(self._events.column('RA' ))
a10 = np.array(self._events.column('DEC' ))
a11 = np.array(self._events.column('RAW_X' ))
a12 = np.array(self._events.column('RAW_Y' ))
a13 = np.array(self._events.column('PHA_BOT' ))
a14 = np.array(self._events.column('PHA_BOT_ADD1'))
a15 = np.array(self._events.column('PHA_BOT_SUB1'))
a16 = np.array(self._events.column('PHA_TOP' ))
a17 = np.array(self._events.column('PHA_TOP_ADD1'))
a18 = np.array(self._events.column('PHA_TOP_SUB1'))
a19 = np.array(self._events.column('TRIGGER' ))
c00 = fits.Column(name='TIME' , format='1D', unit='sec', array=a00 )
c01 = fits.Column(name='TIME_I' , format='1J', unit='sec', array=a01, bzero=2147483648)
c02 = fits.Column(name='TIME_F' , format='1D', unit='sec', array=a02 )
c03 = fits.Column(name='TIME_CORR' , format='1D', unit='sec', array=a03 )
c04 = fits.Column(name='ENERGY' , format='1D', unit='keV', array=a04 )
c05 = fits.Column(name='ENERGY_TOP' , format='1D', unit='keV', array=a05 )
c06 = fits.Column(name='ENERGY_BOT' , format='1D', unit='keV', array=a06 )
c07 = fits.Column(name='GRADE' , format='1I', unit='' , array=a07 )
c08 = fits.Column(name='FLAG' , format='1I', unit='' , array=a08 )
c09 = fits.Column(name='RA' , format='1D', unit='deg', array=a09 )
c10 = fits.Column(name='DEC' , format='1D', unit='deg', array=a10 )
c11 = fits.Column(name='RAW_X' , format='1I', unit='pix', array=a11 )
c12 = fits.Column(name='RAW_Y' , format='1I', unit='pix', array=a12 )
c13 = fits.Column(name='PHA_BOT' , format='1I', unit='' , array=a13 )
c14 = fits.Column(name='PHA_BOT_ADD1', format='1I', unit='' , array=a14 )
c15 = fits.Column(name='PHA_BOT_SUB1', format='1I', unit='' , array=a15 )
c16 = fits.Column(name='PHA_TOP' , format='1I', unit='' , array=a16 )
c17 = fits.Column(name='PHA_TOP_ADD1', format='1I', unit='' , array=a17 )
c18 = fits.Column(name='PHA_TOP_SUB1', format='1I', unit='' , array=a18 )
c19 = fits.Column(name='TRIGGER' , format='1I', unit='' , array=a19 )
cols = fits.ColDefs([
c00, c01, c02, c03, c04, c05, c06, c07, c08, c09,
c10, c11, c12, c13, c14, c15, c16, c17, c18, c19
])
evthdu = fits.BinTableHDU.from_columns(cols)
evthdu.name = 'EVENTS'
creator = 'artpipeline v.{}'.format(__version__)
evthdu.header['creator' ] = (creator , 'program and version that created this file' )
evthdu.header['origin' ] = ('IKI RAS' , 'institution that created this file' )
evthdu.header['telescop'] = ('SRG/ART-XC', 'mission name' )
# copy header keys
COPYKEYS = [
'instrume', 'teln', 'detn', 'urdn',
'mjdref', 'artday', 'tstart', 'tstop',
'date', 'date-obs', 'time-obs', 'date-end', 'time-end',
'maxticks', 'timedel', 'timeunit', 'timeref', 'tassign'
]
for key in COPYKEYS:
try:
evthdu.header[key] = srchdul['EVENTS'].header[key]
except KeyError:
pass
# add caldb key
for key in cdbinfo:
evthdu.header[key] = cdbinfo[key]
# --- make file ---
# hdulist = fits.HDUList([hdu, evthdu, srchdul['']])
hdulist = fits.HDUList([hdu, evthdu, srchdul['KVEA'], srchdul['HK'], srchdul['STDGTI']])
hdulist.writeto(dstfile, overwrite=True, checksum=True)
def setcol(self, name, arr):
self._events.setcol(name, arr)
if __name__ == '__main__':
pass

View File

@@ -0,0 +1,302 @@
#!/usr/bin/env python3
# -*- coding: utf8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2020-2025 IKI RAS (http://iki.rssi.ru/)
# Space Research Institute of the Russian Academy of Science
#
# This file is part of the M. Pavlinsky SRG/ART-XC software project.
# -----------------------------------------------------------------------------
import os
import sys
import shutil
import numpy as np
from scipy.spatial.transform import Slerp
from scipy.spatial.transform import Rotation
from astropy.io import fits
from astropy.nddata import bitmask
from arttools.telescope import Teldef
class AttitudeStatus(object):
INTERP = 1
SCIREADY = 2
PRECSTAB = 4
SED1_ON = 8
SED2_ON = 16
BOKZ_ON = 32
SED1_MEAS = 64
SED2_MEAS = 128
BOKZ_MEAS = 256
flags = np.array([
INTERP ,
SCIREADY ,
PRECSTAB ,
SED1_ON ,
SED2_ON ,
BOKZ_ON ,
SED1_MEAS,
SED2_MEAS,
BOKZ_MEAS
])
@classmethod
def flags_except(cls, flags):
indices = np.searchsorted(cls.flags, flags)
return np.delete(cls.flags, indices)
@classmethod
def pack(cls, arr):
bits = [cls.flags[x.astype(bool)] for x in arr.T]
return [bitmask.interpret_bit_flags(x) for x in bits]
@classmethod
def flagval(cls, val, flags):
return bitmask.bitfield_to_boolean_mask(val, ignore_flags=flags)
@classmethod
def extract(cls, arr, flags):
return np.array([AttitudeStatus.flagval(x, flags) for x in arr])
@staticmethod
def tostr(val, fmt='{0:016b}'):
return fmt.format(val)
class Orientation(object):
UNITY_QUAT = np.array([0., 0., 0., 1.])
VSC = {
'X': np.array([1., 0., 0.]),
'Y': np.array([0., 1., 0.]),
'Z': np.array([0., 0., 1.])}
OPTICAL_AXIS = VSC['X']
NORTH = VSC['Z']
RELCORR_GTI = [
[624390347, 624399808],
[624410643, 630954575]]
def __init__(self, caldb):
self._caldb = caldb
def calc_events_coords(self, oridata, evtdata, telname, random=None):
oritime = np.array(oridata['time'])
evttime = np.array(evtdata.column('TIME' ))
rawx0 = np.array(evtdata.column('RAW_X'))
rawy0 = np.array(evtdata.column('RAW_Y'))
mask = np.logical_and(evttime > oritime[0], evttime < oritime[-1])
if random is not None:
rawx_rand = random.uniform(-0.5, 0.5, rawx0.shape)
rawy_rand = random.uniform(-0.5, 0.5, rawy0.shape)
else:
rawx_rand = 0
rawy_rand = 0
rawx = rawx0 + rawx_rand
rawy = rawy0 + rawy_rand
orient = Slerp(oridata['time'], oridata['quat'])
telquat = Rotation(self._caldb.read_boresight(telname))
evtquat = np.zeros_like(evttime)
ra = np.zeros_like(evttime)
dec = np.zeros_like(evttime)
evtquat = orient(evttime[mask]) * telquat
ra, dec = self.rawxy_to_radec(evtquat, rawx[mask], rawy[mask])
ra_deg = np.zeros_like(evttime)
dec_deg = np.zeros_like(evttime)
ra_deg[mask] = np.rad2deg(ra)
dec_deg[mask] = np.rad2deg(dec)
return ra_deg, dec_deg, mask
def calc_attitudes(self, oridata, instname=None):
instquat = self.instquat(instname)
quatdata = oridata['quat_sc']
if instname != None:
quatdata = oridata['quat']
orient = Slerp (oridata['time'], quatdata)
attquat = orient(oridata['time']) * Rotation(instquat)
ra, dec, roll \
= self.calc_radecroll(attquat)
return np.rad2deg(ra), np.rad2deg(dec), np.rad2deg(roll)
def calc_rawxy(self, oritime, oriquat, evttime, ra, dec, telname):
ra = np.deg2rad(ra )
dec = np.deg2rad(dec)
orient = Slerp(oritime, oriquat)
telquat = Rotation(self._caldb.read_boresight(telname))
evtquat = orient(evttime) * telquat
rawx, rawy \
= self.radec_to_rawxy(evtquat, ra, dec)
return rawx, rawy
def calc_radec(self, oritime, oriquat, evttime, evtrawx, evtrawy, telname):
orient = Slerp(oritime, oriquat)
telquat = Rotation(self._caldb.read_boresight(telname))
evtquat = orient(evttime) * telquat
ra, dec = self.rawxy2radec(evtquat, evtrawx, evtrawy)
return np.rad2deg(ra), np.rad2deg(dec)
def calc_radecroll(self, quat):
oaxis = self.vec_normalize(quat.apply(self.OPTICAL_AXIS))
ra, dec \
= self.vec_to_pol(oaxis)
YZ = self.vec_normalize(np.cross(oaxis, self.NORTH))
roll = np.arctan2(
np.sum(YZ * quat.apply(self.VSC['Z']), axis=1),
np.sum(YZ * quat.apply(self.VSC['Y']), axis=1))
return ra, dec, roll
def instquat(self, instname=None):
if instname == None:
return self.UNITY_QUAT
if instname != 'GYRO':
return self._caldb.read_boresight(instname)
return self.UNITY_QUAT
def read(self, orifile):
times, quats, state = self._read_gyro(orifile)
quat = Rotation(quats) * Rotation(self._caldb.instquat()) * Rotation(self._caldb.boresight())
quat_sc = Rotation(quats) * Rotation(self._caldb.instquat())
return {
'size' : times.size,
'time' : times ,
'quat' : quat ,
'quat_sc': quat_sc ,
'state' : state }
def _read_gyro(self, orifile):
ori_hdul = fits.open(orifile)
data_size = ori_hdul[1].data.size
times = ori_hdul[1].data['TIME' ]
q0 = ori_hdul[1].data['QORT_1']
q1 = ori_hdul[1].data['QORT_2']
q2 = ori_hdul[1].data['QORT_3']
q3 = ori_hdul[1].data['QORT_0']
times -= self._caldb.timeshift()
quats = np.array([q0, q1, q2, q3]).T
state = np.array([
np.zeros(data_size, dtype=int),
ori_hdul[1].data['ST_SCIREADY' ],
ori_hdul[1].data['ST_PRECSTAB' ],
ori_hdul[1].data['ST_SED1_ON' ],
ori_hdul[1].data['ST_SED2_ON' ],
ori_hdul[1].data['ST_BOKZ_ON' ],
ori_hdul[1].data['ST_SED1_MEAS' ],
ori_hdul[1].data['ST_SED2_MEAS' ],
ori_hdul[1].data['ST_BOKZ_MEAS' ]
])
return times, quats, state
def vec_normalize(self, vec):
norm = np.sqrt(np.sum(vec**2, axis=-1))[..., np.newaxis]
return vec / norm
def rawxy_to_offset(self, rawx, rawy):
xoffset = (rawx - Teldef.centeroffset) * Teldef.d
yoffset = (rawy - Teldef.centeroffset) * Teldef.d
return xoffset, yoffset
def offset_to_rawxy(self, xoffset, yoffset):
x = xoffset / Teldef.d + Teldef.centeroffset + 0.5
y = yoffset / Teldef.d + Teldef.centeroffset + 0.5
# return x.astype(np.int) - (x < 0), y.astype(np.int) - (y < 0)
# return x.astype(np.int), y.astype(np.int)
return x.astype(int), y.astype(int)
def offset_to_vec(self, x, y):
vec = np.empty(shape=x.shape+(3,), dtype=np.double)
vec[..., 0] = Teldef.F
vec[..., 1] = x
vec[..., 2] = -y
return vec
def vec_to_offset(self, vec):
x = vec[..., 0]
y = vec[..., 1]
z = vec[..., 2]
xoffset = y * Teldef.F / x
yoffset = -z * Teldef.F / x
return xoffset, yoffset
def vec_to_pol(self, vec):
x = vec[..., 0]
y = vec[..., 1]
z = vec[..., 2]
h = np.hypot(x, y)
phi = np.arctan2(y, x) % (2. * np.pi)
theta = np.arctan2(z, h)
return phi, theta
def pol_to_vec(self, phi, theta):
vec = np.empty(shape=phi.shape+(3,), dtype=np.double)
vec[..., 0] = np.cos(theta) * np.cos(phi)
vec[..., 1] = np.cos(theta) * np.sin(phi)
vec[..., 2] = np.sin(theta)
return vec
def rawxy_to_radec(self, quat, rawx, rawy):
xoffset, yoffset \
= self.rawxy_to_offset(rawx, rawy)
xyvec = self.offset_to_vec(xoffset, yoffset)
ra, dec = self.vec_to_pol(quat.apply(xyvec))
return ra, dec
def radec_to_rawxy(self, quat, ra, dec):
skyvec = self.pol_to_vec(ra, dec)
detvec = quat.apply(skyvec, inverse=True)
xoffset, yoffset \
= self.vec_to_offset(detvec)
rawx, rawy \
= self.offset_to_rawxy(xoffset, yoffset)
return rawx, rawy
if __name__ == '__main__':
pass

View File

@@ -0,0 +1,518 @@
#!/usr/bin/env python3
# -*- coding: utf8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2021-2025 IKI RAS (http://iki.rssi.ru/)
# Space Research Institute of the Russian Academy of Science
#
# This file is part of the M. Pavlinsky SRG/ART-XC software project.
# -----------------------------------------------------------------------------
import os
import sys
import json
import shutil
import argparse
import datetime
import astropy.units as u
from astropy.time.formats import erfa
from astropy.time import Time
from astropy.time import TimezoneInfo
TZ_UTC = TimezoneInfo(utc_offset=0*u.hour) # UTC time zone
TZ_MSK = TimezoneInfo(utc_offset=3*u.hour) # UTC+3 (Moscow) time zone
import numpy as np
from arttools.cmd import Command, ExecutionResult
from arttools.task import (
Task, TaskError, TaskArgs, TaskCmds
)
from arttools.errors import Error
from arttools.fstools import Dir
from arttools.argparse import KvAction
from arttools.telescope import Teldef
from arttools.random import Random
from arttools.arttime import ArtDay, MissionTime
from artpipeline.version import __version__
from artpipeline.caldb import CaldbInfo
from artpipeline.caldb import CaldbMask
from artpipeline.caldb import CaldbEnergy
from artpipeline.caldb import CaldbOrientation
from artpipeline.events import Events
from artpipeline.energy import Flag
from artpipeline.energy import Energy
from artpipeline.skyaxis import SkyAxis
from artpipeline.skycoord import SkyCoord
class Args(TaskArgs):
def __init__(self):
super().__init__()
def read_env(self):
if not 'ARTCALDB' in os.environ:
raise TaskError('ARTCALDB not found (environment variable not set?)')
self.caldbdir = os.environ['ARTCALDB']
def read_cfg(self, cfgfile):
pass
def read_args(self):
argparser = argparse.ArgumentParser()
argparser.add_argument(
'--stem',
help='newfile creation stem',
action=KvAction,
default=None
)
argparser.add_argument(
'--stem-in',
type=str,
help='input files stem',
action=KvAction,
default=None
)
argparser.add_argument(
'srcdir',
help='input directory',
action=KvAction
)
argparser.add_argument(
'tmpdir',
help='temporary directory',
action=KvAction
)
argparser.add_argument(
'--dstdir',
help='output directory',
action=KvAction,
default=None
)
# argparser.add_argument(
# '--user-gti',
# type=str,
# help='user gti file',
# action=KvAction,
# default=None
# )
argparser.add_argument(
'--seed',
type=str,
help='random generator seed',
action=KvAction,
default=None
)
argparser.add_argument(
'--no-random',
help='turn randomization off',
action='store_true',
default=False
)
argparser.add_argument(
'--plain-energy',
help='calculate energy on a plain list of files',
action='store_true',
default=False
)
argparser.add_argument(
'--L0',
help='process L0 files',
action='store_true',
default=False
)
args = argparser.parse_args()
self.stem = args.stem
self.stem_in = args.stem_in
self.srcdir = args.srcdir
self.dstdir = args.dstdir
self.tmpdir = os.path.join(args.tmpdir, str(self.pid))
self.tmpdst = os.path.join(self.tmpdir, 'dst')
# self.usergti = args.usergti
self.seed = args.seed
self.no_random = args.no_random
self.plain_energy = args.plain_energy
self.process_l0 = args.L0
if self.dstdir is None:
self.dstdir = self.srcdir
# if self.seed is not None:
# self.seed = int(self.seed)
def print(self):
print('Input parameters:')
print(' stem = {}'.format(self.stem ))
print(' stem-in = {}'.format(self.stem_in ))
print(' srcdir = {}'.format(self.srcdir ))
print(' dstdir = {}'.format(self.dstdir ))
print(' tmpdir = {}'.format(self.tmpdir ))
# print(' user-gti = {}'.format(self.usergti ))
print(' seed = {}'.format(self.seed ))
print(' no random = {}'.format(self.no_random ))
print(' plain energy = {}'.format(self.plain_energy))
print(' peocess L0 = {}'.format(self.process_l0 ))
print()
caldbinfo = CaldbInfo(self.caldbdir).get()
print('CALDB version {} [{}]'.format(caldbinfo['CALDBVER'], caldbinfo['CALDBDIR']))
print()
def check(self):
if not Dir(self.srcdir).exists():
raise TaskError('error: srcdir not found')
Dir(self.dstdir).check()
Dir(self.tmpdir).check(clean=True)
Dir(self.tmpdst).check(clean=True)
class Pipeline(Task):
def __init__(self):
super().__init__()
print('*** artpipeline v.{}'.format(__version__))
self._name = 'artpipeline v.{}'.format(__version__)
self._args = Args()
self._orifile = None
def initialize(self):
self._args.read()
self._args.print()
self._args.check()
if not self._args.no_random:
self._random = Random(self._args.seed)
else:
self._random = None
def finalize(self):
Dir(self._args.tmpdir).delete()
def runmain(self):
layout = self._layout()
self._make_orient(layout)
self._make_attitude(layout)
self._process_events(layout)
print('*** artpipeline finished')
def _get_stem(self, fname, sep):
# get filenme from path
fn = os.path.basename(fname)
#find teln in fname
fi = fn.find(sep)
return fn[:fi]
def _list_files(self, srcdir, stem_in=None):
allfiles = [fi for fi in os.listdir(srcdir)]
allfiles.sort()
if stem_in==None:
return allfiles
return [fi for fi in allfiles if fi.startswith(stem_in)]
def _plain_layout(self):
layout = {}
layout['type'] = 'plain'
layout['post'] = ''
layout['uf' ] = {}
# pick events
srcdir = Dir(self._args.srcdir)
eventfiles = self._list_files(srcdir.get(), self._args.stem_in)
for TEL in Teldef.telnames:
telfiles = []
for fname in eventfiles:
if TEL in fname:
telfiles.append((srcdir / fname).get())
layout['uf'][TEL] = telfiles
return layout
def _L0_layout(self):
layout = {}
layout['type'] = 'L0'
layout['post'] = '_urd.L2'
layout['uf' ] = {}
# pick L0 events
ufdir = Dir(self._args.srcdir)
eventfiles = self._list_files(ufdir.get(), self._args.stem_in)
for TEL in Teldef.telnames:
uffiles = []
for fname in eventfiles:
if TEL in fname:
uffiles.append((ufdir / fname).get())
layout['uf'][TEL] = uffiles
# pick orientation
orientation = {}
auxdir = Dir(self._args.srcdir)
auxfiles = self._list_files(auxdir.get(), self._args.stem_in)
for otype in ['gyro', 'sed1', 'sed2', 'bokz']:
for auxf in auxfiles:
if auxf.endswith('_{}.L0.fits'.format(otype)):
ofile = auxdir / auxf
if ofile.exists():
orientation[otype] = ofile.get()
layout['orientation'] = orientation
return layout
def _science_layout(self):
layout = {}
layout['type'] = 'science'
layout['post'] = '_cl'
layout['uf' ] = {}
# pick uf events
ufdir = Dir(self._args.srcdir) / 'event_uf'
eventfiles = self._list_files(ufdir.get(), self._args.stem_in)
for TEL in Teldef.telnames:
uffiles = []
for fname in eventfiles:
if TEL in fname:
uffiles.append((ufdir / fname).get())
layout['uf'][TEL] = uffiles
# pick orientation
orientation = {}
auxdir = Dir(self._args.srcdir) / 'auxiliary'
auxfiles = self._list_files(auxdir.get(), self._args.stem_in)
for otype in ['gyro', 'sed1', 'sed2', 'bokz']:
for auxf in auxfiles:
if auxf.endswith('_{}.fits'.format(otype)):
ofile = auxdir / auxf
if ofile.exists():
orientation[otype] = ofile.get()
layout['orientation'] = orientation
return layout
def _layout(self):
if self._args.process_l0:
return self._L0_layout()
if self._args.plain_energy:
return self._plain_layout()
return self._science_layout()
def _make_orient(self, layout):
if layout['type'] == 'plain':
print('mode: plain: skipping orienation...')
return
print('making orienation file...')
gyrofile = layout['orientation']['gyro']
if self._args.stem is None:
stemout = self._get_stem(gyrofile, sep='_')
else:
stemout = self._args.stem
if self._args.process_l0:
auxdir = Dir(self._args.dstdir)
else:
auxdir = Dir(self._args.dstdir) / 'auxiliary'
auxdir.check()
orifile = '{}_ori.fits'.format(stemout)
orifull = (auxdir / orifile).get()
skyaxis = SkyAxis()
skyaxis.process(gyrofile).write(orifull)
self._orifile = orifull
print('written: {}'.format(orifull))
def _make_attitude(self, layout):
if layout['type'] == 'plain':
print('mode: plain: skipping attitude...')
return
if self._args.stem is None:
stemout = self._get_stem(self._orifile, sep='_')
else:
stemout = self._args.stem
print('making attitude file...')
attfile = '{}_att.fits'.format(stemout)
caldb = CaldbOrientation(self._args.caldbdir, 'GYRO')
# skycoord = SkyCoord(caldb, 'GYRO')
skycoord = SkyCoord(caldb, self._orifile, 'GYRO')
# skycoord.attitude(self._orifile).write(attfull)
for TEL in Teldef.telnames:
skycoord.attitude(TEL).collect()
if self._args.process_l0:
attfull = (Dir(self._args.dstdir) / attfile).get()
else:
attfull = (Dir(self._args.dstdir) / 'auxiliary' / attfile).get()
skycoord.write(attfull)
print('written: {}'.format(attfull))
def _process_events(self, layout):
processed = datetime.datetime.now(tz=TZ_MSK).strftime("%Y-%m-%d %H:%M:%S")
metainfo = {
'artpipeline': __version__,
'processed' : processed
}
if self._args.plain_energy:
metainfo['mode'] = 'plain'
elif self._args.process_l0:
metainfo['mode'] = 'L0'
else:
metainfo['mode'] = 'science'
caldbinfo = CaldbInfo(self._args.caldbdir)
metainfo['caldb'] = caldbinfo.get()['CALDBVER']
if not self._args.no_random:
random = self._random
metainfo['seed'] = random.seed
else:
random = None
metainfo['seed'] = None
metainfo['events'] = {}
for TEL in Teldef.telnames:
print('processing: {}'.format(TEL))
for fitsname in layout['uf'].get(TEL, []):
print('event file: {}'.format(fitsname))
if self._args.stem is None:
stemout = self._get_stem(fitsname, TEL)
else:
stemout = self._args.stem
# read events
events = Events(fitsname).read()
skyflag = np.zeros_like(events.events.column('TIME'))
if self._args.plain_energy:
times = events.events.column('TIME')
events.setcol('RA' , np.zeros_like(times))
events.setcol('DEC', np.zeros_like(times))
else:
# calc ra/dec
ocaldb = CaldbOrientation(self._args.caldbdir, 'GYRO')
skycoord = SkyCoord(ocaldb, self._orifile)
skyflag = skycoord.events(self._orifile, events, random)
# calc energies
ecaldb = CaldbEnergy(self._args.caldbdir, TEL)
energy = Energy(ecaldb)
en, enbot, entop, grade =\
energy.calc(events.teln, events.events, self._random)
events.events.setcol('ENERGY' , en )
events.events.setcol('ENERGY_BOT', enbot)
events.events.setcol('ENERGY_TOP', entop)
events.events.setcol('GRADE' , grade)
# calc flag
mcaldb = CaldbMask(self._args.caldbdir, TEL)
flag = Flag(mcaldb)
evtflag = flag.calc(events.events, events.tstart, events.tstop)
events.events.setcol('FLAG', skyflag + evtflag)
# print(events.events.frame)
if self._args.plain_energy:
stemout = self._get_stem(fitsname, '.fits')
dstfile = '{}.L2.fits'.format(stemout)
else:
dstfile = '{}{}{}.fits'.format(stemout, events.telname, layout['post'])
if self._args.plain_energy or self._args.process_l0:
dstdir = Dir(self._args.dstdir)
else:
dstdir = Dir(self._args.dstdir) / 'event_cl'
dstdir.check()
dstfull = (dstdir / dstfile).get()
events.write_cl(dstfull, caldbinfo.get())
aday_start = MissionTime(events.tstart).to_artday()
aday_stop = MissionTime(events.tstop ).to_artday()
metainfo['events'][dstfile] = {
'artday' : (aday_start, aday_stop),
'mission': (events.tstart, events.tstop),
'events' : events.size
}
print('written: {}'.format(dstfull))
# write meta
dstdir = Dir(self._args.dstdir)
metafname = dstdir.pathfor('meta.info')
with open(metafname, 'w') as metafile:
json.dump(metainfo, metafile, indent=4)
print('written: {}'.format(metafname))
def main():
try:
Pipeline().run()
except TaskError as e:
print('error: task execution:', e)
except Error as e:
print('error: unknown:', e)
if __name__ == '__main__':
pass
# PipelineTask().runall()

View File

@@ -0,0 +1,573 @@
#!/usr/bin/env python3
# -*- coding: utf8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2021-2025 IKI RAS (http://iki.rssi.ru/)
# Space Research Institute of the Russian Academy of Science
#
# This file is part of the M. Pavlinsky SRG/ART-XC software project.
# -----------------------------------------------------------------------------
import os
import sys
import json
import shutil
import argparse
import datetime
import astropy.units as u
from astropy.time.formats import erfa
from astropy.time import Time
from astropy.time import TimezoneInfo
TZ_UTC = TimezoneInfo(utc_offset=0*u.hour) # UTC time zone
TZ_MSK = TimezoneInfo(utc_offset=3*u.hour) # UTC+3 (Moscow) time zone
import numpy as np
from arttools.cmd import Command, ExecutionResult
from arttools.task import (
Task, TaskError, TaskArgs, TaskCmds
)
from arttools.errors import Error
from arttools.fstools import Dir
from arttools.argparse import KvAction
from arttools.telescope import Teldef
from arttools.random import Random
from artdactools.arttime import ArtDay, MissionTime
from artpipeline.version import __version__
from artpipeline.caldb import CaldbInfo
from artpipeline.caldb import CaldbMask
from artpipeline.caldb import CaldbEnergy
from artpipeline.caldb import CaldbOrientation
from artpipeline.events import Events
from artpipeline.energy import Flag
from artpipeline.energy import Energy
from artpipeline.skyaxis import SkyAxis
from artpipeline.skycoord import SkyCoord
class Args(TaskArgs):
def __init__(self):
super().__init__()
def read_env(self):
if not 'ARTCALDB' in os.environ:
raise TaskError('ARTCALDB not found (environment variable not set?)')
self.caldbdir = os.environ['ARTCALDB']
def read_cfg(self, cfgfile):
pass
def read_args(self):
argparser = argparse.ArgumentParser()
argparser.add_argument(
'--stem',
help='newfile creation stem',
action=KvAction,
default=None
)
argparser.add_argument(
'--stem-in',
type=str,
help='input files stem',
action=KvAction,
default=None
)
argparser.add_argument(
'srcdir',
help='input directory',
action=KvAction
)
argparser.add_argument(
'tmpdir',
help='temporary directory',
action=KvAction
)
argparser.add_argument(
'--dstdir',
help='output directory',
action=KvAction,
default=None
)
# argparser.add_argument(
# '--user-gti',
# type=str,
# help='user gti file',
# action=KvAction,
# default=None
# )
argparser.add_argument(
'--seed',
type=str,
help='random generator seed',
action=KvAction,
default=None
)
argparser.add_argument(
'--no-random',
help='turn randomization off',
action='store_true',
default=False
)
argparser.add_argument(
'--plain-energy',
help='calculate energy on a plain list of files',
action='store_true',
default=False
)
argparser.add_argument(
'--L0',
help='process L0 files',
action='store_true',
default=False
)
args = argparser.parse_args()
self.stem = args.stem
self.stem_in = args.stem_in
self.srcdir = args.srcdir
self.dstdir = args.dstdir
self.tmpdir = os.path.join(args.tmpdir, str(self.pid))
self.tmpdst = os.path.join(self.tmpdir, 'dst')
# self.usergti = args.usergti
self.seed = args.seed
self.no_random = args.no_random
self.plain_energy = args.plain_energy
self.process_l0 = args.L0
if self.dstdir is None:
self.dstdir = self.srcdir
# if self.seed is not None:
# self.seed = int(self.seed)
def print(self):
print('Input parameters:')
print(' stem = {}'.format(self.stem ))
print(' stem-in = {}'.format(self.stem_in ))
print(' srcdir = {}'.format(self.srcdir ))
print(' dstdir = {}'.format(self.dstdir ))
print(' tmpdir = {}'.format(self.tmpdir ))
# print(' user-gti = {}'.format(self.usergti ))
print(' seed = {}'.format(self.seed ))
print(' no random = {}'.format(self.no_random ))
print(' plain energy = {}'.format(self.plain_energy))
print(' peocess L0 = {}'.format(self.process_l0 ))
print()
caldbinfo = CaldbInfo(self.caldbdir).get()
print('CALDB version {} [{}]'.format(caldbinfo['CALDBVER'], caldbinfo['CALDBDIR']))
print()
def check(self):
if not Dir(self.srcdir).exists():
raise TaskError('error: srcdir not found')
Dir(self.dstdir).check()
Dir(self.tmpdir).check(clean=True)
Dir(self.tmpdst).check(clean=True)
class PipeConfigL0(object):
def __init__(self, args):
self._args = args
self._layout = self._get_layout()
def _get_layout(self):
layout = {}
layout['type'] = 'L0'
layout['post'] = '_urd.L2'
layout['uf' ] = {}
# pick L0 events
ufdir = Dir(self._args.srcdir)
eventfiles = self._list_files(ufdir.get(), self._args.stem_in)
for TEL in Teldef.telnames:
uffiles = []
for fname in eventfiles:
if TEL in fname:
uffiles.append((ufdir / fname).get())
layout['uf'][TEL] = uffiles
# pick orientation
orientation = {}
auxdir = Dir(self._args.srcdir)
auxfiles = self._list_files(auxdir.get(), self._args.stem_in)
for otype in ['gyro', 'sed1', 'sed2', 'bokz']:
for auxf in auxfiles:
if auxf.endswith('_{}.fits'.format(otype)):
ofile = auxdir / auxf
if ofile.exists():
orientation[otype] = ofile.get()
layout['orientation'] = orientation
return layout
@property
def layout(self): return self._layout
class PipeConfigPlain(object):
def __init__(self, args):
self._args = args
self._layout = self._get_layout()
def _list_files(self, srcdir, stem_in=None):
allfiles = [fi for fi in os.listdir(srcdir)]
allfiles.sort()
if stem_in==None:
return allfiles
return [fi for fi in allfiles if fi.startswith(stem_in)]
def _get_layout(self):
layout = {}
layout['type'] = 'plain'
layout['post'] = ''
layout['uf' ] = {}
# pick events
srcdir = Dir(self._args.srcdir)
eventfiles = self._list_files(srcdir.get(), self._args.stem_in)
for TEL in Teldef.telnames:
telfiles = []
for fname in eventfiles:
if TEL in fname:
telfiles.append((srcdir / fname).get())
layout['uf'][TEL] = telfiles
return layout
@property
def layout(self): return self._layout
class PipeConfigScience(object):
def __init__(self, args):
self._args = args
self._layout = self._get_layout()
def _list_files(self, srcdir, stem_in=None):
allfiles = [fi for fi in os.listdir(srcdir)]
allfiles.sort()
if stem_in==None:
return allfiles
return [fi for fi in allfiles if fi.startswith(stem_in)]
def _get_layout(self):
layout = {}
layout['type'] = 'science'
layout['post'] = '_cl'
layout['uf' ] = {}
# pick uf events
ufdir = Dir(self._args.srcdir) / 'event_uf'
eventfiles = self._list_files(ufdir.get(), self._args.stem_in)
for TEL in Teldef.telnames:
uffiles = []
for fname in eventfiles:
if TEL in fname:
uffiles.append((ufdir / fname).get())
layout['uf'][TEL] = uffiles
# pick orientation
orientation = {}
auxdir = Dir(self._args.srcdir) / 'auxiliary'
auxfiles = self._list_files(auxdir.get(), self._args.stem_in)
for otype in ['gyro', 'sed1', 'sed2', 'bokz']:
for auxf in auxfiles:
if auxf.endswith('_{}.fits'.format(otype)):
ofile = auxdir / auxf
if ofile.exists():
orientation[otype] = ofile.get()
layout['orientation'] = orientation
return layout
@property
def layout(self): return self._layout
class EventProcContext(object):
pass
class Pipeline(Task):
def __init__(self):
super().__init__()
print('*** artpipeline v.{}'.format(__version__))
self._name = 'artpipeline v.{}'.format(__version__)
self._args = Args()
self._config = None
self._random = None
self._ofile = None
def initialize(self):
self._args.read()
self._args.print()
self._args.check()
if not self._args.no_random:
self._random = Random(self._args.seed)
if self._args.plain_energy:
self._config = PipeConfigPlain(self._args)
elif self._args.process_l0:
self._config = PipeConfigL0(self._args)
else:
self._config = PipeConfigScience(self._args)
def finalize(self):
Dir(self._args.tmpdir).delete()
def runmain(self):
# layout = self._layout()
layout = self._config.layout()
self._make_orient(layout)
self._make_attitude(layout)
self._process_events(layout)
print('*** artpipeline finished')
def _get_stem(self, fname, sep):
# get filenme from path
fn = os.path.basename(fname)
#find teln in fname
fi = fn.find(sep)
return fn[:fi]
def _list_files(self, srcdir, stem_in=None):
allfiles = [fi for fi in os.listdir(srcdir)]
allfiles.sort()
if stem_in==None:
return allfiles
return [fi for fi in allfiles if fi.startswith(stem_in)]
def _layout(self):
if self._args.process_l0:
return self._L0_layout()
if self._args.plain_energy:
return self._plain_layout()
return self._science_layout()
def _make_orient(self, layout):
if layout['type'] == 'plain':
print('mode: plain: skipping orienation...')
return
print('making orienation file...')
gyrofile = layout['orientation']['gyro']
if self._args.stem is None:
stemout = self._get_stem(gyrofile, sep='_')
else:
stemout = self._args.stem
if self._args.process_l0:
auxdir = Dir(self._args.dstdir)
else:
auxdir = Dir(self._args.dstdir) / 'auxiliary'
auxdir.check()
ofile = '{}_ori.fits'.format(stemout)
ofull = (auxdir / ofile).get()
skyaxis = SkyAxis()
skyaxis.process(gyrofile).write(ofull)
self._ofile = ofull
print('written: {}'.format(ofull))
def _make_attitude(self, layout):
if layout['type'] == 'plain':
print('mode: plain: skipping attitude...')
return
if self._args.stem is None:
stemout = self._get_stem(self._orifile, sep='_')
else:
stemout = self._args.stem
print('making attitude file...')
attfile = '{}_att.fits'.format(stemout)
if self._args.process_l0:
attfull = (Dir(self._args.dstdir) / attfile).get()
else:
attfull = (Dir(self._args.dstdir) / 'auxiliary' / attfile).get()
caldb = CaldbOrientation(self._args.caldbdir, 'GYRO')
skycoord = SkyCoord(caldb, 'GYRO')
skycoord.attitude(self._orifile).write(attfull)
print('written: {}'.format(attfull))
def _process_events(self, layout):
processed = datetime.datetime.now(tz=TZ_MSK).strftime("%Y-%m-%d %H:%M:%S")
metainfo = {
'artpipeline': __version__,
'processed' : processed
}
if self._args.plain_energy:
metainfo['mode'] = 'plain'
elif self._args.process_l0:
metainfo['mode'] = 'L0'
else:
metainfo['mode'] = 'science'
caldbinfo = CaldbInfo(self._args.caldbdir)
metainfo['caldb'] = caldbinfo.get()['CALDBVER']
if not self._args.no_random:
random = self._random
metainfo['seed'] = random.seed
else:
random = None
metainfo['seed'] = None
metainfo['events'] = {}
for TEL in Teldef.telnames:
print('processing: {}'.format(TEL))
for fitsname in layout['uf'].get(TEL, []):
print('event file: {}'.format(fitsname))
if self._args.stem is None:
stemout = self._get_stem(fitsname, TEL)
else:
stemout = self._args.stem
# read events
events = Events(fitsname).read()
if self._args.plain_energy:
times = events.events.column('TIME')
events.setcol('RA' , np.zeros_like(times))
events.setcol('DEC', np.zeros_like(times))
else:
# calc ra/dec
ocaldb = CaldbOrientation(self._args.caldbdir, 'GYRO')
skycoord = SkyCoord(ocaldb, 'GYRO')
skycoord.events(self._orifile, events, random)
# calc energies
ecaldb = CaldbEnergy(self._args.caldbdir, TEL)
energy = Energy(ecaldb)
en, enbot, entop, grade =\
energy.calc(events.teln, events.events, self._random)
events.events.setcol('ENERGY' , en )
events.events.setcol('ENERGY_BOT', enbot)
events.events.setcol('ENERGY_TOP', entop)
events.events.setcol('GRADE' , grade)
# calc flag
mcaldb = CaldbMask(self._args.caldbdir, TEL)
flag = Flag(mcaldb)
f = flag.calc(events.events, events.tstart, events.tstop)
events.events.setcol('FLAG', f)
# print(events.events.frame)
if self._args.plain_energy:
stemout = self._get_stem(fitsname, '.fits')
dstfile = '{}.L2.fits'.format(stemout)
else:
dstfile = '{}{}{}.fits'.format(stemout, events.telname, layout['post'])
if self._args.plain_energy or self._args.process_l0:
dstdir = Dir(self._args.dstdir)
else:
dstdir = Dir(self._args.dstdir) / 'event_cl'
dstdir.check()
dstfull = (dstdir / dstfile).get()
events.write_cl(dstfull, caldbinfo.get())
aday_start = MissionTime(events.tstart).to_artday()
aday_stop = MissionTime(events.tstop ).to_artday()
metainfo['events'][dstfile] = {
'artday' : (aday_start, aday_stop),
'mission': (events.tstart, events.tstop),
'events' : events.size
}
print('written: {}'.format(dstfull))
# write meta
metafname = (Dir(self._args.dstdir) / 'meta.info').get()
with open(metafname, 'w') as metafile:
json.dump(metainfo, metafile, indent=4)
print('written: {}'.format(metafname))
def main():
try:
Pipeline().run()
except TaskError as e:
print('error: task execution:', e)
except Error as e:
print('error: unknown:', e)
if __name__ == '__main__':
pass
# PipelineTask().runall()

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python3
# -*- coding: utf8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2021-2026 IKI RAS (http://iki.rssi.ru/)
# Space Research Institute of the Russian Academy of Science
#
# This file is part of the M. Pavlinsky SRG/ART-XC software project.
# -----------------------------------------------------------------------------
import os
import shutil
class SkyAxis(object):
def __init__(self):
super().__init__()
self._srcfile = None
def process(self, srcfile):
self._srcfile = srcfile
return self
def write(self, dstfile):
if os.path.exists(dstfile):
os.remove(dstfile)
shutil.copy(self._srcfile, dstfile)
if __name__ == '__main__':
pass

View File

@@ -0,0 +1,194 @@
#!/usr/bin/env python3
# -*- coding: utf8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2021-2025 IKI RAS (http://iki.rssi.ru/)
# Space Research Institute of the Russian Academy of Science
#
# This file is part of the M. Pavlinsky SRG/ART-XC software project.
# -----------------------------------------------------------------------------
import os
import sys
import numpy as np
from astropy.io import fits
from arttools.gti import Gti
from arttools.arttime import MJDREF
from arttools.telescope import Teldef
from artpipeline.orientation import Orientation
from artpipeline.orientation import AttitudeStatus
from artpipeline.events import Events
from artpipeline.version import __version__
class SkyCoord(object):
def __init__(self, caldb, orifile, instname=None):
super().__init__()
self._caldb = caldb
self._ofile = orifile
self._instname = instname
self._inst = None
self._mjdref = MJDREF.mjd
self._data = {}
self._adata = {}
self._process_orient()
self._calc_base_attitude()
def _process_orient(self):
self._orient = Orientation(self._caldb)
self._odata = self._orient.read(self._ofile)
self._gti = Gti.from_hduname(self._ofile, 'STDGTI')
self._otime = self._odata['time']
self._ostatus = AttitudeStatus.pack(self._odata['state'])
def _calc_base_attitude(self):
if self._instname is None:
return
att_ra, att_dec, att_roll \
= self._orient.calc_attitudes(self._odata, self._instname)
self._data['ATTITUDE'] = {
'time' : self._otime,
'ra' : att_ra ,
'dec' : att_dec ,
'roll' : att_roll ,
'status': self._ostatus
}
x_ra, x_dec, x_roll \
= self._orient.calc_attitudes(self._odata)
self._data['SCX'] = {
'time' : self._otime,
'ra' : x_ra ,
'dec' : x_dec ,
'roll' : x_roll ,
'status': self._ostatus
}
def attitude(self, instname=None):
if instname is not None:
self._inst = instname
else:
self._inst = self._instname
ra, dec, roll \
= self._orient.calc_attitudes(self._odata, self._inst)
self._adata[self._inst] = {
'time' : self._otime,
'ra' : ra ,
'dec' : dec ,
'roll' : roll ,
'status': self._ostatus
}
return self
def collect(self, name=None):
if self._inst is None:
return
if name is None:
name = self._inst
self._data[name] = self._adata.pop(self._inst, None)
self._inst = None
def _make_hdu(self, hduname):
if hduname not in self._data:
hdu = fits.BinTableHDU()
hdu.name = hduname
return hdu
# --- HDU ---
a00 = np.array(self._data[hduname]['time' ], dtype=np.double)
a01 = np.array(self._data[hduname]['ra' ], dtype=np.double)
a02 = np.array(self._data[hduname]['dec' ], dtype=np.double)
a03 = np.array(self._data[hduname]['roll' ], dtype=np.double)
a04 = np.array(self._data[hduname]['status'], dtype=int )
a05 = np.zeros(a00.size, dtype=int)
c00 = fits.Column(name='TIME' , format='1D', unit='sec', array=a00)
c01 = fits.Column(name='RA' , format='1D', unit='deg', array=a01)
c02 = fits.Column(name='DEC' , format='1D', unit='deg', array=a02)
c03 = fits.Column(name='ROLL' , format='1D', unit='deg', array=a03)
c04 = fits.Column(name='STATUS', format='1I', unit='' , array=a04)
c05 = fits.Column(name='FLAG' , format='1I', unit='' , array=a05)
cols = fits.ColDefs([
c00, c01, c02, c03, c04, c05
])
hdu = fits.BinTableHDU.from_columns(cols)
hdu.name = hduname
creator = 'artpipeline v.{}'.format(__version__)
hdu.header['CREATOR' ] = (creator , 'program and version that created this file' )
hdu.header['ORIGIN' ] = ('IKI RAS' , 'institution that created this file' )
hdu.header['TELESCOP'] = ('SRG/ART-XC', 'mission name' )
hdu.header['MJDREF' ] = (self._mjdref, 'MJD corresponding to SC clock start 2000.0 MSK')
hdu.header['TSTART' ] = self._gti.start
hdu.header['TSTOP' ] = self._gti.stop
hdu.header['CALDBVER'] = '{}'.format(self._caldb.version())
return hdu
def write(self, attfile):
print(self._data.keys())
prihdu = fits.PrimaryHDU()
# --- make file ---
hdulist = fits.HDUList([
prihdu,
self._make_hdu('ATTITUDE'),
self._make_hdu('T1' ),
self._make_hdu('T2' ),
self._make_hdu('T3' ),
self._make_hdu('T4' ),
self._make_hdu('T5' ),
self._make_hdu('T6' ),
self._make_hdu('T7' ),
self._make_hdu('SCX' ),
self._gti.make_hdu(self._mjdref)
])
hdulist.writeto(attfile, overwrite=True, checksum=True)
def events(self, orifile, events, random=None):
ra, dec, mask \
= self._orient.calc_events_coords(
self._odata, events.events, events.telname, random)
flag = np.zeros_like(events.events.column('TIME' ))
flag[~mask] = 100
events.setcol('RA' , ra )
events.setcol('DEC' , dec )
return flag
if __name__ == '__main__':
pass

View File

@@ -0,0 +1 @@
__version__ = '2.1.0'