33from itertools import chain
44
55import numpy as np
6+ import pytz
7+ from sunpy .time .timerange import TimeRange
68from sunpy .util .datatype_factory_base import (
79 BasicRegistrationFactory ,
810 MultipleMatchError ,
1719
1820import stixcore .processing .decompression as decompression
1921import stixcore .processing .engineering as engineering
22+ from stixcore .ephemeris .manager import Spice
2023from stixcore .idb .manager import IDBManager
2124from stixcore .time import SCETime , SCETimeDelta , SCETimeRange
2225from stixcore .tmtc .packet_factory import Packet
4750
4851# date when the min integration time was changed from 1.0s to 0.5s needed to fix count and time
4952# offset issue
50- MIN_INT_TIME_CHANGE = datetime (2021 , 9 , 6 , 13 )
53+ MIN_INT_TIME_CHANGE = datetime (2021 , 9 , 6 , 13 , tzinfo = pytz . UTC )
5154
5255
5356def read_qtable (file , hdu , hdul = None ):
@@ -211,7 +214,9 @@ def get_cls_processing_version(cls):
211214
212215class ProductFactory (BasicRegistrationFactory ):
213216 def __call__ (self , * args , ** kwargs ):
214- if len (args ) == 1 and len (kwargs ) == 0 :
217+ get_timeformat_from_TIMESYS = kwargs .get ("get_timeformat_from_TIMESYS" , False )
218+
219+ if len (args ) == 1 :
215220 if isinstance (args [0 ], (str , Path )):
216221 file_path = Path (args [0 ])
217222 pri_header = fits .getheader (file_path )
@@ -258,8 +263,24 @@ def __call__(self, *args, **kwargs):
258263 ssid = 34
259264
260265 if level not in ["LB" , "LL01" ] and "timedel" in data .colnames and "time" in data .colnames :
261- data ["timedel" ] = SCETimeDelta (data ["timedel" ])
262- offset = SCETime .from_float (pri_header ["OBT_BEG" ] * u .s )
266+ if level in ["L0" , "L1" ] and not get_timeformat_from_TIMESYS :
267+ # L0 and L1 date are open by default in SCETime format so we can directly apply the timedelta
268+ data ["timedel" ] = SCETimeDelta (data ["timedel" ])
269+ offset = SCETime .from_float (pri_header ["OBT_BEG" ] * u .s )
270+ else :
271+ # in L2 and higher the time format should not be in SCETime format
272+ # select the time format based on available header keywords
273+ offset = None
274+ if pri_header .get ("TIMESYS" , "" ) == "UTC" :
275+ try :
276+ offset = Time (pri_header ["DATE-OBS" ])
277+ except ValueError :
278+ offset = None
279+
280+ # fallback to OBT_BEG if no TIMESYS=UTC or DATE-OBS is present or can not be parsed
281+ if offset is None :
282+ offset = SCETime .from_float (pri_header ["OBT_BEG" ] * u .s )
283+ data ["timedel" ] = SCETimeDelta (data ["timedel" ])
263284
264285 try :
265286 control ["time_stamp" ] = SCETime .from_float (control ["time_stamp" ])
@@ -535,10 +556,25 @@ def __init__(
535556
536557 @property
537558 def scet_timerange (self ):
538- return SCETimeRange (
539- start = self .data ["time" ][0 ] - self .data ["timedel" ][0 ] / 2 ,
540- end = self .data ["time" ][- 1 ] + self .data ["timedel" ][- 1 ] / 2 ,
541- )
559+ if isinstance (self .data ["time" ], SCETime ):
560+ return SCETimeRange (
561+ start = self .data ["time" ][0 ] - self .data ["timedel" ][0 ] / 2 ,
562+ end = self .data ["time" ][- 1 ] + self .data ["timedel" ][- 1 ] / 2 ,
563+ )
564+ else :
565+ logger .warning (
566+ "internal time format is not in SCETime format, scet_timerange will be approximated using Spice. Better to work with utc_timerange property to avoid automatic time conversion"
567+ )
568+ start_str = Spice .instance .datetime_to_scet ((self .data ["time" ][0 ] - self .data ["timedel" ][0 ] / 2 ).datetime )
569+ end_str = Spice .instance .datetime_to_scet ((self .data ["time" ][- 1 ] + self .data ["timedel" ][- 1 ] / 2 ).datetime )
570+ if "/" in start_str :
571+ start_str = start_str .split ("/" )[- 1 ]
572+ if "/" in end_str :
573+ end_str = end_str .split ("/" )[- 1 ]
574+ return SCETimeRange (
575+ start = SCETime .from_string (start_str ),
576+ end = SCETime .from_string (end_str ),
577+ )
542578
543579 @property
544580 def raw (self ):
@@ -564,7 +600,7 @@ def bunit(self):
564600 return " "
565601
566602 @property
567- def exposure (self ):
603+ def min_exposure (self ):
568604 # default for FITS HEADER
569605 return 0.0
570606
@@ -644,6 +680,12 @@ def __add__(self, other):
644680 if not isinstance (other , type (self )):
645681 raise TypeError (f"Products must of same type not { type (self )} and { type (other )} " )
646682
683+ if "time" in self .data .colnames and "time" in other .data .colnames :
684+ if type (self .data ["time" ]) is not type (other .data ["time" ]):
685+ raise TypeError (
686+ f"Products must have the same time format not { type (self .data ['time' ])} and { type (other .data ['time' ])} "
687+ )
688+
647689 # make a deep copy of the data and control
648690 other_control = other .control [:]
649691 other_data = other .data [:]
@@ -667,8 +709,10 @@ def __add__(self, other):
667709
668710 # Fits write we do np.around(time - start_time).as_float().to(u.cs)).astype("uint32"))
669711 # So need to do something similar here to avoid comparing un-rounded value to rounded values
670- data ["time_float" ] = np .around ((data ["time" ] - data ["time" ].min ()).as_float ().to ("cs" ))
671-
712+ if isinstance (data ["time" ], SCETime ):
713+ data ["time_float" ] = np .around ((data ["time" ] - data ["time" ].min ()).as_float ().to ("cs" ))
714+ else : # datetime or Time
715+ data ["time_float" ] = np .around ((data ["time" ] - data ["time" ].min ()).to ("cs" ))
672716 # remove duplicate data based on time bin and sort the data
673717 data = unique (data , keys = ["time_float" ])
674718 # data.sort(["time_float"])
@@ -863,12 +907,18 @@ def bunit(self):
863907 return "counts"
864908
865909 @property
866- def exposure (self ):
867- return self .data ["timedel" ].as_float ().min ().to_value ("s" )
910+ def min_exposure (self ):
911+ if isinstance (self .data ["timedel" ], SCETimeDelta ):
912+ return self .data ["timedel" ].as_float ().min ().to_value ("s" )
913+ else :
914+ return self .data ["timedel" ].min ().to_value ("s" )
868915
869916 @property
870917 def max_exposure (self ):
871- return self .data ["timedel" ].as_float ().max ().to_value ("s" )
918+ if isinstance (self .data ["timedel" ], SCETimeDelta ):
919+ return self .data ["timedel" ].as_float ().max ().to_value ("s" )
920+ else :
921+ return self .data ["timedel" ].max ().to_value ("s" )
872922
873923
874924class EnergyChannelsMixin :
@@ -925,7 +975,13 @@ class L1Mixin(FitsHeaderMixin):
925975
926976 @property
927977 def utc_timerange (self ):
928- return self .scet_timerange .to_timerange ()
978+ if isinstance (self .data ["time" ], SCETime ):
979+ return self .scet_timerange .to_timerange ()
980+ else :
981+ return TimeRange (
982+ (self .data ["time" ][0 ] - self .data ["timedel" ][0 ] / 2 ),
983+ (self .data ["time" ][- 1 ] + self .data ["timedel" ][- 1 ] / 2 ),
984+ )
929985
930986 @classmethod
931987 def from_level0 (cls , l0product , parent = "" ):
@@ -951,10 +1007,10 @@ def from_level0(cls, l0product, parent=""):
9511007 if idbs [0 ] < (2 , 26 , 36 ) and len (l1 .data ) > 1 :
9521008 # Check if request was at min configured time resolution
9531009 if (
954- l1 . utc_timerange .start .datetime < MIN_INT_TIME_CHANGE
1010+ l0product . scet_timerange .start .to_datetime () < MIN_INT_TIME_CHANGE
9551011 and l1 .data ["timedel" ].as_float ().min () == 1 * u .s
9561012 ) or (
957- l1 . utc_timerange .start .datetime >= MIN_INT_TIME_CHANGE
1013+ l0product . scet_timerange .start .to_datetime () >= MIN_INT_TIME_CHANGE
9581014 and l1 .data ["timedel" ].as_float ().min () == 0.5 * u .s
9591015 ):
9601016 l1 .data ["timedel" ][1 :- 1 ] = l1 .data ["timedel" ][:- 2 ]
@@ -972,7 +1028,13 @@ def from_level0(cls, l0product, parent=""):
9721028class L2Mixin (FitsHeaderMixin ):
9731029 @property
9741030 def utc_timerange (self ):
975- return self .scet_timerange .to_timerange ()
1031+ if isinstance (self .data ["time" ], SCETime ):
1032+ return self .scet_timerange .to_timerange ()
1033+ else :
1034+ return TimeRange (
1035+ (self .data ["time" ][0 ] - self .data ["timedel" ][0 ] / 2 ).datetime ,
1036+ (self .data ["time" ][- 1 ] + self .data ["timedel" ][- 1 ] / 2 ).datetime ,
1037+ )
9761038
9771039 @classmethod
9781040 def get_additional_extensions (cls ):
0 commit comments