From 62a458a5b18be9cc058796c0b3564176f13f3b25 Mon Sep 17 00:00:00 2001 From: Nicky Hochmuth Date: Fri, 5 Dec 2025 13:32:54 +0100 Subject: [PATCH 1/6] change internal time system of L1 products to be SCETime or UTC --- .../io/product_processors/fits/processors.py | 19 ++-- .../tests/test_processors.py | 11 ++- stixcore/processing/tests/test_publish.py | 39 +++++--- stixcore/products/level1/quicklookL1.py | 14 +++ stixcore/products/product.py | 95 +++++++++++++++---- 5 files changed, 135 insertions(+), 43 deletions(-) diff --git a/stixcore/io/product_processors/fits/processors.py b/stixcore/io/product_processors/fits/processors.py index e8a1351a..6a0826d9 100644 --- a/stixcore/io/product_processors/fits/processors.py +++ b/stixcore/io/product_processors/fits/processors.py @@ -723,18 +723,19 @@ def generate_primary_header(cls, filename, product, *, version=0): # if not isinstance(product.obt_beg, SCETime): # raise ValueError("Expected SCETime as time format") + scet_timerange = product.scet_timerange headers = FitsProcessor.generate_common_header(filename, product, version=version) + ( # Name, Value, Comment # ('MJDREF', product.obs_beg.mjd), # ('DATEREF', product.obs_beg.fits), - ("OBT_BEG", product.scet_timerange.start.as_float().value, "Start acquisition time in OBT"), - ("OBT_END", product.scet_timerange.end.as_float().value, "End acquisition time in OBT"), + ("OBT_BEG", scet_timerange.start.as_float().value, "Start acquisition time in OBT"), + ("OBT_END", scet_timerange.end.as_float().value, "End acquisition time in OBT"), ("TIMESYS", "OBT", "System used for time keywords"), ("LEVEL", "L0", "Processing level of the data"), - ("DATE-OBS", product.scet_timerange.start.to_string(), "Depreciated, same as DATE-BEG"), - ("DATE-BEG", product.scet_timerange.start.to_string(), "Start time of observation"), - ("DATE-AVG", product.scet_timerange.avg.to_string(), "Average time of observation"), - ("DATE-END", product.scet_timerange.end.to_string(), "End time of observation"), + ("DATE-OBS", scet_timerange.start.to_string(), "Depreciated, same as DATE-BEG"), + ("DATE-BEG", scet_timerange.start.to_string(), "Start time of observation"), + ("DATE-AVG", scet_timerange.avg.to_string(), "Average time of observation"), + ("DATE-END", scet_timerange.end.to_string(), "End time of observation"), ("DATAMIN", product.dmin, "Minimum valid physical value"), ("DATAMAX", product.dmax, "Maximum valid physical value"), ("BUNIT", product.bunit, "Units of physical value, after application of BSCALE, BZERO"), @@ -899,10 +900,8 @@ def write_fits(self, product, *, version=0): # In TM sent as uint in units of 0.1 so convert to cs as the time center # can be on 0.5ds points - data["time"] = np.atleast_1d( - np.around((data["time"] - prod.scet_timerange.start).as_float().to(u.cs)).astype("uint32") - ) - data["timedel"] = np.atleast_1d(np.uint32(np.around(data["timedel"].as_float().to(u.cs)))) + data["time"] = np.atleast_1d(np.around((data["time"] - prod.utc_timerange.start).to(u.cs)).astype("uint32")) + data["timedel"] = np.atleast_1d(np.uint32(np.around(data["timedel"].to(u.cs)))) try: control["time_stamp"] = control["time_stamp"].as_float() diff --git a/stixcore/io/product_processors/tests/test_processors.py b/stixcore/io/product_processors/tests/test_processors.py index bb0fe4ec..7877d2fc 100644 --- a/stixcore/io/product_processors/tests/test_processors.py +++ b/stixcore/io/product_processors/tests/test_processors.py @@ -15,6 +15,7 @@ from stixcore.products.product import Product from stixcore.soop.manager import SOOPManager from stixcore.time import SCETime, SCETimeRange +from stixcore.time.datetime import SCETimeDelta @pytest.fixture @@ -189,10 +190,16 @@ def test_level0_processor_generate_primary_header(datetime, product): def test_count_data_mixin(p_file): processor = FitsL0Processor("some/path") p = Product(p_file) + + if isinstance(p.data["timedel"], SCETimeDelta): + assert p.exposure == p.data["timedel"].as_float().min().to_value("s") + assert p.max_exposure == p.data["timedel"].as_float().max().to_value("s") + else: + assert p.exposure == p.data["timedel"].min().to_value("s") + assert p.max_exposure == p.data["timedel"].max().to_value("s") + assert p.dmin == p.data["counts"].min().value assert p.dmax == p.data["counts"].max().value - assert p.exposure == p.data["timedel"].min().as_float().to_value() - assert p.max_exposure == p.data["timedel"].max().as_float().to_value() test_data = { "DATAMAX": p.dmax, diff --git a/stixcore/processing/tests/test_publish.py b/stixcore/processing/tests/test_publish.py index fee75ec3..d5274453 100644 --- a/stixcore/processing/tests/test_publish.py +++ b/stixcore/processing/tests/test_publish.py @@ -121,7 +121,15 @@ def test_publish_fits_to_esa_incomplete(product, out_dir): ) t = SCETime(coarse=[beg.coarse, end.coarse]) - product.data = QTable({"time": t, "timedel": t - beg, "fcounts": np.array([1, 2]), "control_index": [1, 1]}) + t_utc = t.to_time() + product.data = QTable( + { + "time": t_utc, + "timedel": (t - beg).as_float().to("cs"), + "fcounts": np.array([1, 2]), + "control_index": [1, 1], + } + ) product.raw = ["packet1.xml", "packet2.xml"] product.parent = ["packet1.xml", "packet2.xml"] product.level = "L1" @@ -135,9 +143,9 @@ def test_publish_fits_to_esa_incomplete(product, out_dir): product.NAME = "background" product.obt_beg = beg product.obt_end = end - product.date_obs = beg - product.date_beg = beg - product.date_end = end + product.date_obs = beg.to_datetime() + product.date_beg = beg.to_datetime() + product.date_end = end.to_datetime() product.exposure = 2 product.max_exposure = 3 product.dmin = 2 @@ -222,10 +230,12 @@ def test_fits_incomplete_switch_over(out_dir): ) t = SCETime(coarse=[beg.coarse, end.coarse]) + t_utc = t.to_time() + product.data = QTable( { - "time": t, - "timedel": t - beg, + "time": t_utc, + "timedel": (t - beg).as_float().to("cs"), "fcounts": np.array([1, 2]), "counts": np.array([1, 2]) * u.deg_C, "control_index": [1, 1], @@ -244,9 +254,9 @@ def test_fits_incomplete_switch_over(out_dir): product.name = "background" product.obt_beg = beg product.obt_end = end - product.date_obs = beg - product.date_beg = beg - product.date_end = end + product.date_obs = beg.to_datetime() + product.date_beg = beg.to_datetime() + product.date_end = end.to_datetime() product.exposure = 2 product.max_exposure = 3 product.dmin = 2 @@ -371,7 +381,10 @@ def test_publish_fits_to_esa(product, out_dir): ) t = SCETime(coarse=[beg.coarse, end.coarse]) - product.data = QTable({"time": t, "timedel": t - beg, "fcounts": np.array([1, 2]), "control_index": [1, 1]}) + t_utc = t.to_time() + product.data = QTable( + {"time": t_utc, "timedel": (t - beg).as_float().to("cs"), "fcounts": np.array([1, 2]), "control_index": [1, 1]} + ) product.raw = ["packet1.xml", "packet2.xml"] product.parent = ["packet1.xml", "packet2.xml"] product.level = "L1" @@ -382,9 +395,9 @@ def test_publish_fits_to_esa(product, out_dir): product.name = "xray-spec" product.obt_beg = beg product.obt_end = end - product.date_obs = beg - product.date_beg = beg - product.date_end = end + product.date_obs = beg.to_datetime() + product.date_beg = beg.to_datetime() + product.date_end = end.to_datetime() product.exposure = 2 product.max_exposure = 3 product.dmin = 2 diff --git a/stixcore/products/level1/quicklookL1.py b/stixcore/products/level1/quicklookL1.py index 6265c506..c1b62784 100644 --- a/stixcore/products/level1/quicklookL1.py +++ b/stixcore/products/level1/quicklookL1.py @@ -11,6 +11,7 @@ from stixcore.products.level0.quicklookL0 import QLProduct from stixcore.products.product import L1Mixin from stixcore.time import SCETimeRange +from stixcore.time.datetime import SCETime, SCETimeDelta from stixcore.util.logging import get_logger __all__ = ["LightCurve", "Background", "Spectra", "Variance", "FlareFlag", "EnergyCalibration", "TMStatusFlareList"] @@ -246,6 +247,19 @@ def from_level0(cls, l0product, parent=""): l1.level = "L1" engineering.raw_to_engineering_product(l1, IDBManager.instance) + # convert SCETimes to UTC Time + if "time" in l1.data.colnames and isinstance(l1.data["time"], SCETime): + l1.data.replace_column( + "time", + l1.data["time"].to_time(), + ) + # convert SCETimesDelta to Quantity (s) + if "timedel" in l1.data.colnames and isinstance(l1.data["timedel"], SCETimeDelta): + l1.data.replace_column( + "timedel", + l1.data["timedel"].as_float(), + ) + # fix for wrong calibration in IDB https://github.com/i4Ds/STIXCore/issues/432 # nix00122 was wrong assumed to be in ds but it is plain s l1.control["integration_time"] = l1.control["integration_time"] * 10 diff --git a/stixcore/products/product.py b/stixcore/products/product.py index 15c9d246..cea8fff8 100644 --- a/stixcore/products/product.py +++ b/stixcore/products/product.py @@ -3,6 +3,8 @@ from itertools import chain import numpy as np +import pytz +from sunpy.time.timerange import TimeRange from sunpy.util.datatype_factory_base import ( BasicRegistrationFactory, MultipleMatchError, @@ -17,6 +19,7 @@ import stixcore.processing.decompression as decompression import stixcore.processing.engineering as engineering +from stixcore.ephemeris.manager import Spice from stixcore.idb.manager import IDBManager from stixcore.time import SCETime, SCETimeDelta, SCETimeRange from stixcore.tmtc.packet_factory import Packet @@ -47,7 +50,7 @@ # date when the min integration time was changed from 1.0s to 0.5s needed to fix count and time # offset issue -MIN_INT_TIME_CHANGE = datetime(2021, 9, 6, 13) +MIN_INT_TIME_CHANGE = datetime(2021, 9, 6, 13, tzinfo=pytz.UTC) def read_qtable(file, hdu, hdul=None): @@ -258,8 +261,18 @@ def __call__(self, *args, **kwargs): ssid = 34 if level not in ["LB", "LL01"] and "timedel" in data.colnames and "time" in data.colnames: - data["timedel"] = SCETimeDelta(data["timedel"]) - offset = SCETime.from_float(pri_header["OBT_BEG"] * u.s) + # select the time format based on available header keywords + offset = None + if "TIMESYS" in pri_header and pri_header["TIMESYS"] == "UTC": + try: + offset = Time(pri_header["DATE-OBS"]) + except Exception: + offset = None + + # fallback to OBT_BEG if no TIMESYS=UTC or DATE-OBS is present or not parseable + if offset is None: + offset = SCETime.from_float(pri_header["OBT_BEG"] * u.s) + data["timedel"] = SCETimeDelta(data["timedel"]) try: control["time_stamp"] = SCETime.from_float(control["time_stamp"]) @@ -535,10 +548,22 @@ def __init__( @property def scet_timerange(self): - return SCETimeRange( - start=self.data["time"][0] - self.data["timedel"][0] / 2, - end=self.data["time"][-1] + self.data["timedel"][-1] / 2, - ) + if isinstance(self.data["time"], SCETime): + return SCETimeRange( + start=self.data["time"][0] - self.data["timedel"][0] / 2, + end=self.data["time"][-1] + self.data["timedel"][-1] / 2, + ) + else: + start_str = Spice.instance.datetime_to_scet((self.data["time"][0] - self.data["timedel"][0] / 2).datetime) + end_str = Spice.instance.datetime_to_scet((self.data["time"][-1] + self.data["timedel"][-1] / 2).datetime) + if "/" in start_str: + start_str = start_str.split("/")[-1] + if "/" in end_str: + end_str = end_str.split("/")[-1] + return SCETimeRange( + start=SCETime.from_string(start_str), + end=SCETime.from_string(end_str), + ) @property def raw(self): @@ -656,7 +681,7 @@ def __add__(self, other): other_data["old_index"] = [f"o{i}" for i in other_data["control_index"]] self_data["old_index"] = [f"s{i}" for i in self_data["control_index"]] - if (self.service_type, self.service_subtype) == (3, 25): + if (self.service_type, self.service_subtype) == (3, 25) and self.level in ["L0", "LB"]: self_data["time"] = SCETime(self_control["scet_coarse"], self_control["scet_fine"]) other_data["time"] = SCETime(other_control["scet_coarse"], other_control["scet_fine"]) @@ -667,8 +692,10 @@ def __add__(self, other): # Fits write we do np.around(time - start_time).as_float().to(u.cs)).astype("uint32")) # So need to do something similar here to avoid comparing un-rounded value to rounded values - data["time_float"] = np.around((data["time"] - data["time"].min()).as_float().to("cs")) - + if isinstance(data["time"], SCETime): + data["time_float"] = np.around((data["time"] - data["time"].min()).as_float().to("cs")) + else: + data["time_float"] = np.around((data["time"] - data["time"].min()).to("cs")) # remove duplicate data based on time bin and sort the data data = unique(data, keys=["time_float"]) # data.sort(["time_float"]) @@ -777,12 +804,12 @@ def split_to_files(self): yield out else: # L1+ - utc_timerange = self.scet_timerange.to_timerange() + utc_timerange = self.utc_timerange for day in utc_timerange.get_dates(): ds = day de = day + 1 * u.day - utc_times = self.data["time"].to_time() + utc_times = self.data["time"] i = np.where((utc_times >= ds) & (utc_times < de)) if len(i[0]) > 0: @@ -864,11 +891,17 @@ def bunit(self): @property def exposure(self): - return self.data["timedel"].as_float().min().to_value("s") + if isinstance(self.data["timedel"], SCETimeDelta): + return self.data["timedel"].as_float().min().to_value("s") + else: + return self.data["timedel"].min().to_value("s") @property def max_exposure(self): - return self.data["timedel"].as_float().max().to_value("s") + if isinstance(self.data["timedel"], SCETimeDelta): + return self.data["timedel"].as_float().max().to_value("s") + else: + return self.data["timedel"].max().to_value("s") class EnergyChannelsMixin: @@ -925,7 +958,13 @@ class L1Mixin(FitsHeaderMixin): @property def utc_timerange(self): - return self.scet_timerange.to_timerange() + if isinstance(self.data["time"], SCETime): + self.scet_timerange.to_timerange() + else: + return TimeRange( + (self.data["time"][0] - self.data["timedel"][0] / 2).datetime, + (self.data["time"][-1] + self.data["timedel"][-1] / 2).datetime, + ) @classmethod def from_level0(cls, l0product, parent=""): @@ -951,10 +990,10 @@ def from_level0(cls, l0product, parent=""): if idbs[0] < (2, 26, 36) and len(l1.data) > 1: # Check if request was at min configured time resolution if ( - l1.utc_timerange.start.datetime < MIN_INT_TIME_CHANGE + l0product.scet_timerange.start.to_datetime() < MIN_INT_TIME_CHANGE and l1.data["timedel"].as_float().min() == 1 * u.s ) or ( - l1.utc_timerange.start.datetime >= MIN_INT_TIME_CHANGE + l0product.scet_timerange.start.to_datetime() >= MIN_INT_TIME_CHANGE and l1.data["timedel"].as_float().min() == 0.5 * u.s ): l1.data["timedel"][1:-1] = l1.data["timedel"][:-2] @@ -966,13 +1005,33 @@ def from_level0(cls, l0product, parent=""): l1.control.replace_column("parent", [parent] * len(l1.control)) l1.level = "L1" engineering.raw_to_engineering_product(l1, IDBManager.instance) + + # convert SCETimes to UTC Time + if "time" in l1.data.colnames and isinstance(l1.data["time"], SCETime): + l1.data.replace_column( + "time", + l1.data["time"].to_time(), + ) + # convert SCETimesDelta to Quantity (s) + if "timedel" in l1.data.colnames and isinstance(l1.data["timedel"], SCETimeDelta): + l1.data.replace_column( + "timedel", + l1.data["timedel"].as_float(), + ) + return l1 class L2Mixin(FitsHeaderMixin): @property def utc_timerange(self): - return self.scet_timerange.to_timerange() + if isinstance(self.data["time"], SCETime): + self.scet_timerange.to_timerange() + else: + return TimeRange( + (self.data["time"][0] - self.data["timedel"][0] / 2).datetime, + (self.data["time"][-1] + self.data["timedel"][-1] / 2).datetime, + ) @classmethod def get_additional_extensions(cls): From 5a7c6708bf824ba4e6d4b95cbf655df20efbb7aa Mon Sep 17 00:00:00 2001 From: Nicky Hochmuth Date: Thu, 29 Jan 2026 15:38:18 +0100 Subject: [PATCH 2/6] fixes after review --- stixcore/io/product_processors/fits/processors.py | 6 +++--- .../io/product_processors/tests/test_processors.py | 8 ++++---- stixcore/processing/tests/test_publish.py | 6 +++--- stixcore/products/level3/flarelist.py | 4 ++-- stixcore/products/product.py | 14 +++++++------- stixcore/soop/manager.py | 2 ++ 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/stixcore/io/product_processors/fits/processors.py b/stixcore/io/product_processors/fits/processors.py index 6a0826d9..6b74fef6 100644 --- a/stixcore/io/product_processors/fits/processors.py +++ b/stixcore/io/product_processors/fits/processors.py @@ -739,7 +739,7 @@ def generate_primary_header(cls, filename, product, *, version=0): ("DATAMIN", product.dmin, "Minimum valid physical value"), ("DATAMAX", product.dmax, "Maximum valid physical value"), ("BUNIT", product.bunit, "Units of physical value, after application of BSCALE, BZERO"), - ("XPOSURE", product.exposure, "[s] shortest exposure time"), + ("XPOSURE", product.min_exposure, "[s] shortest exposure time"), ("XPOMAX", product.max_exposure, "[s] maximum exposure time"), ) @@ -783,7 +783,7 @@ def generate_primary_header(self, filename, product, *, version=0): ("DATAMIN", empty_if_nan(product.dmin), "Minimum valid physical value"), ("DATAMAX", empty_if_nan(product.dmax), "Maximum valid physical value"), ("BUNIT", product.bunit, "Units of physical value, after application of BSCALE, BZERO"), - ("XPOSURE", empty_if_nan(product.exposure), "[s] shortest exposure time"), + ("XPOSURE", empty_if_nan(product.min_exposure), "[s] shortest exposure time"), ("XPOMAX", empty_if_nan(product.max_exposure), "[s] maximum exposure time"), ) @@ -999,7 +999,7 @@ def generate_primary_header(self, filename, product, *, version=0): ("DATAMIN", empty_if_nan(product.dmin), "Minimum valid physical value"), ("DATAMAX", empty_if_nan(product.dmax), "Maximum valid physical value"), ("BUNIT", product.bunit, "Units of physical value, after application of BSCALE, BZERO"), - ("XPOSURE", empty_if_nan(product.exposure), "[s] shortest exposure time"), + ("XPOSURE", empty_if_nan(product.min_exposure), "[s] shortest exposure time"), ("XPOMAX", empty_if_nan(product.max_exposure), "[s] maximum exposure time"), ) diff --git a/stixcore/io/product_processors/tests/test_processors.py b/stixcore/io/product_processors/tests/test_processors.py index 7877d2fc..f78e1303 100644 --- a/stixcore/io/product_processors/tests/test_processors.py +++ b/stixcore/io/product_processors/tests/test_processors.py @@ -192,10 +192,10 @@ def test_count_data_mixin(p_file): p = Product(p_file) if isinstance(p.data["timedel"], SCETimeDelta): - assert p.exposure == p.data["timedel"].as_float().min().to_value("s") + assert p.min_exposure == p.data["timedel"].as_float().min().to_value("s") assert p.max_exposure == p.data["timedel"].as_float().max().to_value("s") else: - assert p.exposure == p.data["timedel"].min().to_value("s") + assert p.min_exposure == p.data["timedel"].min().to_value("s") assert p.max_exposure == p.data["timedel"].max().to_value("s") assert p.dmin == p.data["counts"].min().value @@ -204,7 +204,7 @@ def test_count_data_mixin(p_file): test_data = { "DATAMAX": p.dmax, "DATAMIN": p.dmin, - "XPOSURE": p.exposure, + "XPOSURE": p.min_exposure, "XPOMAX": p.max_exposure, "BUNIT": "counts", } @@ -264,7 +264,7 @@ def test_level1_processor_generate_primary_header(product, soop_manager): product.dmax = 1 product.dunit = "" product.max_exposure = 1 - product.exposure = 1 + product.min_exposure = 1 product.service_type = 1 product.service_subtype = 2 product.ssid = 3 diff --git a/stixcore/processing/tests/test_publish.py b/stixcore/processing/tests/test_publish.py index d5274453..00013493 100644 --- a/stixcore/processing/tests/test_publish.py +++ b/stixcore/processing/tests/test_publish.py @@ -146,7 +146,7 @@ def test_publish_fits_to_esa_incomplete(product, out_dir): product.date_obs = beg.to_datetime() product.date_beg = beg.to_datetime() product.date_end = end.to_datetime() - product.exposure = 2 + product.min_exposure = 2 product.max_exposure = 3 product.dmin = 2 product.dmax = 3 @@ -257,7 +257,7 @@ def test_fits_incomplete_switch_over(out_dir): product.date_obs = beg.to_datetime() product.date_beg = beg.to_datetime() product.date_end = end.to_datetime() - product.exposure = 2 + product.min_exposure = 2 product.max_exposure = 3 product.dmin = 2 product.dmax = 3 @@ -398,7 +398,7 @@ def test_publish_fits_to_esa(product, out_dir): product.date_obs = beg.to_datetime() product.date_beg = beg.to_datetime() product.date_end = end.to_datetime() - product.exposure = 2 + product.min_exposure = 2 product.max_exposure = 3 product.dmin = 2 product.dmax = 3 diff --git a/stixcore/products/level3/flarelist.py b/stixcore/products/level3/flarelist.py index aef66704..23538103 100644 --- a/stixcore/products/level3/flarelist.py +++ b/stixcore/products/level3/flarelist.py @@ -515,7 +515,7 @@ def dmax(self): return (self.data["lc_peak"].sum(axis=1)).max().value if len(self.data) > 0 else np.nan @property - def exposure(self): + def min_exposure(self): return self.data["duration"].min().to_value("s") if len(self.data) > 0 else np.nan @property @@ -649,7 +649,7 @@ def dmax(self): return (self.data["lc_peak"].sum(axis=1)).max().value if len(self.data) > 0 else np.nan @property - def exposure(self): + def min_exposure(self): return self.data["duration"].min().to_value("s") if len(self.data) > 0 else np.nan @property diff --git a/stixcore/products/product.py b/stixcore/products/product.py index cea8fff8..73ed5450 100644 --- a/stixcore/products/product.py +++ b/stixcore/products/product.py @@ -263,13 +263,13 @@ def __call__(self, *args, **kwargs): if level not in ["LB", "LL01"] and "timedel" in data.colnames and "time" in data.colnames: # select the time format based on available header keywords offset = None - if "TIMESYS" in pri_header and pri_header["TIMESYS"] == "UTC": + if pri_header.get("TIMESYS", "") == "UTC": try: offset = Time(pri_header["DATE-OBS"]) - except Exception: + except ValueError: offset = None - # fallback to OBT_BEG if no TIMESYS=UTC or DATE-OBS is present or not parseable + # fallback to OBT_BEG if no TIMESYS=UTC or DATE-OBS is present or can not be parsed if offset is None: offset = SCETime.from_float(pri_header["OBT_BEG"] * u.s) data["timedel"] = SCETimeDelta(data["timedel"]) @@ -589,7 +589,7 @@ def bunit(self): return " " @property - def exposure(self): + def min_exposure(self): # default for FITS HEADER return 0.0 @@ -890,7 +890,7 @@ def bunit(self): return "counts" @property - def exposure(self): + def min_exposure(self): if isinstance(self.data["timedel"], SCETimeDelta): return self.data["timedel"].as_float().min().to_value("s") else: @@ -962,8 +962,8 @@ def utc_timerange(self): self.scet_timerange.to_timerange() else: return TimeRange( - (self.data["time"][0] - self.data["timedel"][0] / 2).datetime, - (self.data["time"][-1] + self.data["timedel"][-1] / 2).datetime, + (self.data["time"][0] - self.data["timedel"][0] / 2), + (self.data["time"][-1] + self.data["timedel"][-1] / 2), ) @classmethod diff --git a/stixcore/soop/manager.py b/stixcore/soop/manager.py index b3dd242e..b1d997a8 100644 --- a/stixcore/soop/manager.py +++ b/stixcore/soop/manager.py @@ -529,6 +529,8 @@ def add_soop_file_to_index(self, path, *, rebuild_index=True, **args): all_soop_file = Path(CONFIG.get("SOOP", "soop_files_download")) / f"{plan}.{version}.all.json" if not all_soop_file.exists(): + # TODO reactivate when API is back + return self.download_all_soops_from_api(plan, version, all_soop_file) with open(all_soop_file) as f_all: From 604d0063e56931c96572e40e539dd64884fcf1b6 Mon Sep 17 00:00:00 2001 From: Nicky Hochmuth Date: Thu, 12 Mar 2026 14:36:43 +0100 Subject: [PATCH 3/6] add warning when time out converting with spice --- stixcore/products/level3/flarelist.py | 6 ++++++ stixcore/products/level3/flarelistproduct.py | 6 ++++++ stixcore/products/product.py | 3 +++ stixcore/soop/manager.py | 2 -- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/stixcore/products/level3/flarelist.py b/stixcore/products/level3/flarelist.py index 23538103..188f6193 100644 --- a/stixcore/products/level3/flarelist.py +++ b/stixcore/products/level3/flarelist.py @@ -499,6 +499,9 @@ def utc_timerange(self): @property def scet_timerange(self): tr = self.utc_timerange + logger.warning( + "scet_timerange will be approximated using Spice. Better to work with utc_timerange property to avoid automatic time conversion" + ) start = SCETime.from_string(Spice.instance.datetime_to_scet(tr.start)[2:]) end = SCETime.from_string(Spice.instance.datetime_to_scet(tr.end)[2:]) return SCETimeRange(start=start, end=end) @@ -633,6 +636,9 @@ def utc_timerange(self): @property def scet_timerange(self): tr = self.utc_timerange + logger.warning( + "scet_timerange will be approximated using Spice. Better to work with utc_timerange property to avoid automatic time conversion" + ) start = SCETime.from_string(Spice.instance.datetime_to_scet(tr.start)[2:]) end = SCETime.from_string(Spice.instance.datetime_to_scet(tr.end)[2:]) return SCETimeRange(start=start, end=end) diff --git a/stixcore/products/level3/flarelistproduct.py b/stixcore/products/level3/flarelistproduct.py index 4a3fbdaa..ae7cb0d4 100644 --- a/stixcore/products/level3/flarelistproduct.py +++ b/stixcore/products/level3/flarelistproduct.py @@ -4,9 +4,12 @@ from stixcore.ephemeris.manager import Spice from stixcore.products.product import GenericProduct, L3Mixin from stixcore.time.datetime import SCETime, SCETimeRange +from stixcore.util.logging import get_logger __all__ = ["FlareListProduct", "PeekPreviewImage"] +logger = get_logger(__name__) + class FlareListProduct(GenericProduct, L3Mixin): """Product not based on direct TM data but on time ranges defined in flare lists. @@ -47,6 +50,9 @@ def utc_timerange(self): @property def scet_timerange(self): tr = self.utc_timerange + logger.warning( + "scet_timerange will be approximated using Spice. Better to work with utc_timerange property to avoid automatic time conversion" + ) start = SCETime.from_string(Spice.instance.datetime_to_scet(tr.start)[2:]) end = SCETime.from_string(Spice.instance.datetime_to_scet(tr.end)[2:]) return SCETimeRange(start=start, end=end) diff --git a/stixcore/products/product.py b/stixcore/products/product.py index 73ed5450..e873b4f8 100644 --- a/stixcore/products/product.py +++ b/stixcore/products/product.py @@ -554,6 +554,9 @@ def scet_timerange(self): end=self.data["time"][-1] + self.data["timedel"][-1] / 2, ) else: + logger.warning( + "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" + ) start_str = Spice.instance.datetime_to_scet((self.data["time"][0] - self.data["timedel"][0] / 2).datetime) end_str = Spice.instance.datetime_to_scet((self.data["time"][-1] + self.data["timedel"][-1] / 2).datetime) if "/" in start_str: diff --git a/stixcore/soop/manager.py b/stixcore/soop/manager.py index b1d997a8..b3dd242e 100644 --- a/stixcore/soop/manager.py +++ b/stixcore/soop/manager.py @@ -529,8 +529,6 @@ def add_soop_file_to_index(self, path, *, rebuild_index=True, **args): all_soop_file = Path(CONFIG.get("SOOP", "soop_files_download")) / f"{plan}.{version}.all.json" if not all_soop_file.exists(): - # TODO reactivate when API is back - return self.download_all_soops_from_api(plan, version, all_soop_file) with open(all_soop_file) as f_all: From d0afb27d6bb8fe7d4b4160839d30457043835941 Mon Sep 17 00:00:00 2001 From: Nicky Hochmuth Date: Tue, 17 Mar 2026 16:24:42 +0100 Subject: [PATCH 4/6] rework to not auto convert times in dta table at processing time only on fits open --- .../io/product_processors/fits/processors.py | 6 +- .../tests/test_processors.py | 11 +--- stixcore/processing/tests/test_end2end.py | 2 +- stixcore/processing/tests/test_publish.py | 39 ++++-------- stixcore/products/level1/quicklookL1.py | 14 ----- stixcore/products/product.py | 62 +++++++++---------- stixcore/products/tests/test_factory.py | 20 ++++++ stixcore/util/scripts/end2end_testing.py | 13 ++++ 8 files changed, 84 insertions(+), 83 deletions(-) diff --git a/stixcore/io/product_processors/fits/processors.py b/stixcore/io/product_processors/fits/processors.py index 6b74fef6..90630eea 100644 --- a/stixcore/io/product_processors/fits/processors.py +++ b/stixcore/io/product_processors/fits/processors.py @@ -900,8 +900,10 @@ def write_fits(self, product, *, version=0): # In TM sent as uint in units of 0.1 so convert to cs as the time center # can be on 0.5ds points - data["time"] = np.atleast_1d(np.around((data["time"] - prod.utc_timerange.start).to(u.cs)).astype("uint32")) - data["timedel"] = np.atleast_1d(np.uint32(np.around(data["timedel"].to(u.cs)))) + data["time"] = np.atleast_1d( + np.around((data["time"] - prod.scet_timerange.start).as_float().to(u.cs)).astype("uint32") + ) + data["timedel"] = np.atleast_1d(np.uint32(np.around(data["timedel"].as_float().to(u.cs)))) try: control["time_stamp"] = control["time_stamp"].as_float() diff --git a/stixcore/io/product_processors/tests/test_processors.py b/stixcore/io/product_processors/tests/test_processors.py index f78e1303..32c66cd6 100644 --- a/stixcore/io/product_processors/tests/test_processors.py +++ b/stixcore/io/product_processors/tests/test_processors.py @@ -15,7 +15,6 @@ from stixcore.products.product import Product from stixcore.soop.manager import SOOPManager from stixcore.time import SCETime, SCETimeRange -from stixcore.time.datetime import SCETimeDelta @pytest.fixture @@ -190,16 +189,10 @@ def test_level0_processor_generate_primary_header(datetime, product): def test_count_data_mixin(p_file): processor = FitsL0Processor("some/path") p = Product(p_file) - - if isinstance(p.data["timedel"], SCETimeDelta): - assert p.min_exposure == p.data["timedel"].as_float().min().to_value("s") - assert p.max_exposure == p.data["timedel"].as_float().max().to_value("s") - else: - assert p.min_exposure == p.data["timedel"].min().to_value("s") - assert p.max_exposure == p.data["timedel"].max().to_value("s") - assert p.dmin == p.data["counts"].min().value assert p.dmax == p.data["counts"].max().value + assert p.min_exposure == p.data["timedel"].min().as_float().to_value() + assert p.max_exposure == p.data["timedel"].max().as_float().to_value() test_data = { "DATAMAX": p.dmax, diff --git a/stixcore/processing/tests/test_end2end.py b/stixcore/processing/tests/test_end2end.py index 52a2cfeb..e3091873 100644 --- a/stixcore/processing/tests/test_end2end.py +++ b/stixcore/processing/tests/test_end2end.py @@ -73,8 +73,8 @@ def test_complete(orig_fits, current_fits): raise ValueError(f"{error_c} errors out of {len(orig_fits)}\nnumber of fits files differ") +# @pytest.mark.end2end @pytest.mark.remote_data -@pytest.mark.end2end def test_identical(orig_fits, current_fits): error_c = 0 error_files = list() diff --git a/stixcore/processing/tests/test_publish.py b/stixcore/processing/tests/test_publish.py index 00013493..a7affcc5 100644 --- a/stixcore/processing/tests/test_publish.py +++ b/stixcore/processing/tests/test_publish.py @@ -121,15 +121,7 @@ def test_publish_fits_to_esa_incomplete(product, out_dir): ) t = SCETime(coarse=[beg.coarse, end.coarse]) - t_utc = t.to_time() - product.data = QTable( - { - "time": t_utc, - "timedel": (t - beg).as_float().to("cs"), - "fcounts": np.array([1, 2]), - "control_index": [1, 1], - } - ) + product.data = QTable({"time": t, "timedel": t - beg, "fcounts": np.array([1, 2]), "control_index": [1, 1]}) product.raw = ["packet1.xml", "packet2.xml"] product.parent = ["packet1.xml", "packet2.xml"] product.level = "L1" @@ -143,9 +135,9 @@ def test_publish_fits_to_esa_incomplete(product, out_dir): product.NAME = "background" product.obt_beg = beg product.obt_end = end - product.date_obs = beg.to_datetime() - product.date_beg = beg.to_datetime() - product.date_end = end.to_datetime() + product.date_obs = beg + product.date_beg = beg + product.date_end = end product.min_exposure = 2 product.max_exposure = 3 product.dmin = 2 @@ -230,12 +222,10 @@ def test_fits_incomplete_switch_over(out_dir): ) t = SCETime(coarse=[beg.coarse, end.coarse]) - t_utc = t.to_time() - product.data = QTable( { - "time": t_utc, - "timedel": (t - beg).as_float().to("cs"), + "time": t, + "timedel": t - beg, "fcounts": np.array([1, 2]), "counts": np.array([1, 2]) * u.deg_C, "control_index": [1, 1], @@ -254,9 +244,9 @@ def test_fits_incomplete_switch_over(out_dir): product.name = "background" product.obt_beg = beg product.obt_end = end - product.date_obs = beg.to_datetime() - product.date_beg = beg.to_datetime() - product.date_end = end.to_datetime() + product.date_obs = beg + product.date_beg = beg + product.date_end = end product.min_exposure = 2 product.max_exposure = 3 product.dmin = 2 @@ -381,10 +371,7 @@ def test_publish_fits_to_esa(product, out_dir): ) t = SCETime(coarse=[beg.coarse, end.coarse]) - t_utc = t.to_time() - product.data = QTable( - {"time": t_utc, "timedel": (t - beg).as_float().to("cs"), "fcounts": np.array([1, 2]), "control_index": [1, 1]} - ) + product.data = QTable({"time": t, "timedel": t - beg, "fcounts": np.array([1, 2]), "control_index": [1, 1]}) product.raw = ["packet1.xml", "packet2.xml"] product.parent = ["packet1.xml", "packet2.xml"] product.level = "L1" @@ -395,9 +382,9 @@ def test_publish_fits_to_esa(product, out_dir): product.name = "xray-spec" product.obt_beg = beg product.obt_end = end - product.date_obs = beg.to_datetime() - product.date_beg = beg.to_datetime() - product.date_end = end.to_datetime() + product.date_obs = beg + product.date_beg = beg + product.date_end = end product.min_exposure = 2 product.max_exposure = 3 product.dmin = 2 diff --git a/stixcore/products/level1/quicklookL1.py b/stixcore/products/level1/quicklookL1.py index c1b62784..6265c506 100644 --- a/stixcore/products/level1/quicklookL1.py +++ b/stixcore/products/level1/quicklookL1.py @@ -11,7 +11,6 @@ from stixcore.products.level0.quicklookL0 import QLProduct from stixcore.products.product import L1Mixin from stixcore.time import SCETimeRange -from stixcore.time.datetime import SCETime, SCETimeDelta from stixcore.util.logging import get_logger __all__ = ["LightCurve", "Background", "Spectra", "Variance", "FlareFlag", "EnergyCalibration", "TMStatusFlareList"] @@ -247,19 +246,6 @@ def from_level0(cls, l0product, parent=""): l1.level = "L1" engineering.raw_to_engineering_product(l1, IDBManager.instance) - # convert SCETimes to UTC Time - if "time" in l1.data.colnames and isinstance(l1.data["time"], SCETime): - l1.data.replace_column( - "time", - l1.data["time"].to_time(), - ) - # convert SCETimesDelta to Quantity (s) - if "timedel" in l1.data.colnames and isinstance(l1.data["timedel"], SCETimeDelta): - l1.data.replace_column( - "timedel", - l1.data["timedel"].as_float(), - ) - # fix for wrong calibration in IDB https://github.com/i4Ds/STIXCore/issues/432 # nix00122 was wrong assumed to be in ds but it is plain s l1.control["integration_time"] = l1.control["integration_time"] * 10 diff --git a/stixcore/products/product.py b/stixcore/products/product.py index e873b4f8..af08ff34 100644 --- a/stixcore/products/product.py +++ b/stixcore/products/product.py @@ -214,7 +214,9 @@ def get_cls_processing_version(cls): class ProductFactory(BasicRegistrationFactory): def __call__(self, *args, **kwargs): - if len(args) == 1 and len(kwargs) == 0: + get_timeformat_from_TIMESYS = kwargs.get("get_timeformat_from_TIMESYS", False) + + if len(args) == 1: if isinstance(args[0], (str, Path)): file_path = Path(args[0]) pri_header = fits.getheader(file_path) @@ -261,18 +263,24 @@ def __call__(self, *args, **kwargs): ssid = 34 if level not in ["LB", "LL01"] and "timedel" in data.colnames and "time" in data.colnames: - # select the time format based on available header keywords - offset = None - if pri_header.get("TIMESYS", "") == "UTC": - try: - offset = Time(pri_header["DATE-OBS"]) - except ValueError: - offset = None - - # fallback to OBT_BEG if no TIMESYS=UTC or DATE-OBS is present or can not be parsed - if offset is None: - offset = SCETime.from_float(pri_header["OBT_BEG"] * u.s) + if level in ["L0", "L1"] and not get_timeformat_from_TIMESYS: + # L0 and L1 date are open by default in SCETime format so we can directly apply the timedelta data["timedel"] = SCETimeDelta(data["timedel"]) + offset = SCETime.from_float(pri_header["OBT_BEG"] * u.s) + else: + # in L2 and higher the time format should not be in SCETime format + # select the time format based on available header keywords + offset = None + if pri_header.get("TIMESYS", "") == "UTC": + try: + offset = Time(pri_header["DATE-OBS"]) + except ValueError: + offset = None + + # fallback to OBT_BEG if no TIMESYS=UTC or DATE-OBS is present or can not be parsed + if offset is None: + offset = SCETime.from_float(pri_header["OBT_BEG"] * u.s) + data["timedel"] = SCETimeDelta(data["timedel"]) try: control["time_stamp"] = SCETime.from_float(control["time_stamp"]) @@ -672,6 +680,12 @@ def __add__(self, other): if not isinstance(other, type(self)): raise TypeError(f"Products must of same type not {type(self)} and {type(other)}") + if "time" in self.data.colnames and "time" in other.data.colnames: + if type(self.data["time"]) is not type(other.data["time"]): + raise TypeError( + f"Products must have the same time format not {type(self.data['time'])} and {type(other.data['time'])}" + ) + # make a deep copy of the data and control other_control = other.control[:] other_data = other.data[:] @@ -684,7 +698,7 @@ def __add__(self, other): other_data["old_index"] = [f"o{i}" for i in other_data["control_index"]] self_data["old_index"] = [f"s{i}" for i in self_data["control_index"]] - if (self.service_type, self.service_subtype) == (3, 25) and self.level in ["L0", "LB"]: + if (self.service_type, self.service_subtype) == (3, 25): self_data["time"] = SCETime(self_control["scet_coarse"], self_control["scet_fine"]) other_data["time"] = SCETime(other_control["scet_coarse"], other_control["scet_fine"]) @@ -697,7 +711,7 @@ def __add__(self, other): # So need to do something similar here to avoid comparing un-rounded value to rounded values if isinstance(data["time"], SCETime): data["time_float"] = np.around((data["time"] - data["time"].min()).as_float().to("cs")) - else: + else: # datetime or Time data["time_float"] = np.around((data["time"] - data["time"].min()).to("cs")) # remove duplicate data based on time bin and sort the data data = unique(data, keys=["time_float"]) @@ -807,12 +821,12 @@ def split_to_files(self): yield out else: # L1+ - utc_timerange = self.utc_timerange + utc_timerange = self.scet_timerange.to_timerange() for day in utc_timerange.get_dates(): ds = day de = day + 1 * u.day - utc_times = self.data["time"] + utc_times = self.data["time"].to_time() i = np.where((utc_times >= ds) & (utc_times < de)) if len(i[0]) > 0: @@ -962,7 +976,7 @@ class L1Mixin(FitsHeaderMixin): @property def utc_timerange(self): if isinstance(self.data["time"], SCETime): - self.scet_timerange.to_timerange() + return self.scet_timerange.to_timerange() else: return TimeRange( (self.data["time"][0] - self.data["timedel"][0] / 2), @@ -1008,20 +1022,6 @@ def from_level0(cls, l0product, parent=""): l1.control.replace_column("parent", [parent] * len(l1.control)) l1.level = "L1" engineering.raw_to_engineering_product(l1, IDBManager.instance) - - # convert SCETimes to UTC Time - if "time" in l1.data.colnames and isinstance(l1.data["time"], SCETime): - l1.data.replace_column( - "time", - l1.data["time"].to_time(), - ) - # convert SCETimesDelta to Quantity (s) - if "timedel" in l1.data.colnames and isinstance(l1.data["timedel"], SCETimeDelta): - l1.data.replace_column( - "timedel", - l1.data["timedel"].as_float(), - ) - return l1 diff --git a/stixcore/products/tests/test_factory.py b/stixcore/products/tests/test_factory.py index 68a140ed..8e0d5074 100644 --- a/stixcore/products/tests/test_factory.py +++ b/stixcore/products/tests/test_factory.py @@ -2,12 +2,16 @@ import pytest +from astropy.time import Time +from astropy.units import Quantity + from stixcore.data.test import test_data from stixcore.products.level0.quicklookL0 import LightCurve as LCL0 from stixcore.products.level1.quicklookL1 import LightCurve as LCL1 from stixcore.products.levelb.binary import LevelB from stixcore.products.product import Product from stixcore.time import SCETime +from stixcore.time.datetime import SCETimeDelta def test_ql_lb(): @@ -22,6 +26,22 @@ def test_ql_lb(): assert lb_prod.obt_beg == SCETime(coarse=664148503, fine=10710) +def test_read_timeformat(): + lq_scet = Product(test_data.products.L1_LightCurve_fits[0]) + + assert type(lq_scet.data["time"][0]) is SCETime + assert type(lq_scet.data["timedel"][0]) is SCETimeDelta + + lq_utc = Product(test_data.products.L1_LightCurve_fits[0], get_timeformat_from_TIMESYS=True) + assert type(lq_utc.data["time"][0]) is Time + assert type(lq_utc.data["timedel"][0]) is Quantity + + assert abs((lq_scet.scet_timerange.start - lq_utc.scet_timerange.start).coarse) < 1 + assert abs((lq_scet.scet_timerange.end - lq_utc.scet_timerange.end).coarse) < 1 + assert abs((lq_scet.utc_timerange.start - lq_utc.utc_timerange.start).to("s").value) < 0.2 + assert abs((lq_scet.utc_timerange.end - lq_utc.utc_timerange.end).to("s").value) < 0.2 + + # The fits file times maybe off by onescet time bin need to regenerate and test @pytest.mark.xfail def test_ql_l0(): diff --git a/stixcore/util/scripts/end2end_testing.py b/stixcore/util/scripts/end2end_testing.py index cf8cfa49..eda001c7 100644 --- a/stixcore/util/scripts/end2end_testing.py +++ b/stixcore/util/scripts/end2end_testing.py @@ -169,6 +169,19 @@ def end2end_pipeline(indir, fitsdir): if __name__ == "__main__": + p_scet = Product(Path("/data/stix/out/test/e2e/orig/solo_L1_stix-ql-variance_20210626_V02U.fits")) + p_utc = Product( + Path("/data/stix/out/test/e2e/orig/solo_L1_stix-ql-variance_20210626_V02U.fits"), + get_timeformat_from_TIMESYS=True, + ) + + end2end_pipeline( + indir=Path("/data/stix/out/test/e2e/orig"), + fitsdir=Path("/data/stix/out/test/e2e/current"), + ) + + quit() + if len(sys.argv) > 2: zippath = Path(sys.argv[1]) datapath = Path(sys.argv[2]) From 8d8e3df7b0151cbbf6a1f133f93e06ece9e0a1a3 Mon Sep 17 00:00:00 2001 From: Nicky Hochmuth Date: Mon, 23 Mar 2026 13:39:54 +0100 Subject: [PATCH 5/6] cleanup after review --- stixcore/processing/tests/test_end2end.py | 2 +- stixcore/products/CAL/energy.py | 2 +- stixcore/products/product.py | 5 ++--- stixcore/util/scripts/end2end_testing.py | 13 ------------- 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/stixcore/processing/tests/test_end2end.py b/stixcore/processing/tests/test_end2end.py index e3091873..64ba1112 100644 --- a/stixcore/processing/tests/test_end2end.py +++ b/stixcore/processing/tests/test_end2end.py @@ -73,7 +73,7 @@ def test_complete(orig_fits, current_fits): raise ValueError(f"{error_c} errors out of {len(orig_fits)}\nnumber of fits files differ") -# @pytest.mark.end2end +@pytest.mark.end2end @pytest.mark.remote_data def test_identical(orig_fits, current_fits): error_c = 0 diff --git a/stixcore/products/CAL/energy.py b/stixcore/products/CAL/energy.py index 9b3e6053..a0882e50 100644 --- a/stixcore/products/CAL/energy.py +++ b/stixcore/products/CAL/energy.py @@ -81,7 +81,7 @@ def bunit(self): return "keV" @property - def exposure(self): + def min_exposure(self): # default for FITS HEADER return self.control["integration_time"].min().to_value(u.s) diff --git a/stixcore/products/product.py b/stixcore/products/product.py index af08ff34..9dc7c8ef 100644 --- a/stixcore/products/product.py +++ b/stixcore/products/product.py @@ -3,7 +3,6 @@ from itertools import chain import numpy as np -import pytz from sunpy.time.timerange import TimeRange from sunpy.util.datatype_factory_base import ( BasicRegistrationFactory, @@ -50,7 +49,7 @@ # date when the min integration time was changed from 1.0s to 0.5s needed to fix count and time # offset issue -MIN_INT_TIME_CHANGE = datetime(2021, 9, 6, 13, tzinfo=pytz.UTC) +MIN_INT_TIME_CHANGE = datetime(2021, 9, 6, 13, tzinfo=datetime.timezone.utc) def read_qtable(file, hdu, hdul=None): @@ -1029,7 +1028,7 @@ class L2Mixin(FitsHeaderMixin): @property def utc_timerange(self): if isinstance(self.data["time"], SCETime): - self.scet_timerange.to_timerange() + return self.scet_timerange.to_timerange() else: return TimeRange( (self.data["time"][0] - self.data["timedel"][0] / 2).datetime, diff --git a/stixcore/util/scripts/end2end_testing.py b/stixcore/util/scripts/end2end_testing.py index eda001c7..cf8cfa49 100644 --- a/stixcore/util/scripts/end2end_testing.py +++ b/stixcore/util/scripts/end2end_testing.py @@ -169,19 +169,6 @@ def end2end_pipeline(indir, fitsdir): if __name__ == "__main__": - p_scet = Product(Path("/data/stix/out/test/e2e/orig/solo_L1_stix-ql-variance_20210626_V02U.fits")) - p_utc = Product( - Path("/data/stix/out/test/e2e/orig/solo_L1_stix-ql-variance_20210626_V02U.fits"), - get_timeformat_from_TIMESYS=True, - ) - - end2end_pipeline( - indir=Path("/data/stix/out/test/e2e/orig"), - fitsdir=Path("/data/stix/out/test/e2e/current"), - ) - - quit() - if len(sys.argv) > 2: zippath = Path(sys.argv[1]) datapath = Path(sys.argv[2]) From badd965d1837959ae7f578d4b38b37a25d4679b2 Mon Sep 17 00:00:00 2001 From: Nicky Hochmuth Date: Mon, 23 Mar 2026 13:44:36 +0100 Subject: [PATCH 6/6] pytz.UTC --- stixcore/products/product.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stixcore/products/product.py b/stixcore/products/product.py index 9dc7c8ef..02712266 100644 --- a/stixcore/products/product.py +++ b/stixcore/products/product.py @@ -3,6 +3,7 @@ from itertools import chain import numpy as np +import pytz from sunpy.time.timerange import TimeRange from sunpy.util.datatype_factory_base import ( BasicRegistrationFactory, @@ -49,7 +50,7 @@ # date when the min integration time was changed from 1.0s to 0.5s needed to fix count and time # offset issue -MIN_INT_TIME_CHANGE = datetime(2021, 9, 6, 13, tzinfo=datetime.timezone.utc) +MIN_INT_TIME_CHANGE = datetime(2021, 9, 6, 13, tzinfo=pytz.UTC) def read_qtable(file, hdu, hdul=None):