11from pathlib import Path
22from datetime import datetime
3+ from itertools import groupby
34
45import numpy as np
56from stixpy .calibration .visibility import (
1011from stixpy .coordinates .transforms import get_hpc_info
1112from stixpy .net .client import STIXClient
1213from stixpy .product import Product as STIXPYProduct
13- from sunpy .coordinates import HeliographicStonyhurst , Helioprojective
14+ from sunpy .coordinates import HeliographicStonyhurst , Helioprojective , SphericalScreen
1415from sunpy .map import make_fitswcs_header
1516from sunpy .net import attrs as a
1617from sunpy .time import TimeRange
@@ -95,12 +96,26 @@ def add_flare_position(
9596 # helio_frame = Helioprojective(observer="earth")
9697 # SkyCoord(HeliographicStonyhurst(0 * u.deg, 0 * u.deg))
9798 # SkyCoord(0 * u.deg, 0 * u.deg, frame=helio_frame)
98- data ["flare_position" ] = [SkyCoord (HeliographicStonyhurst (0 * u .deg , 0 * u .deg )) for i in range (0 , len (data ))]
99+
100+ n = len (data )
101+
102+ data ["flareposition_obs_hgs_x" ] = Column (
103+ np .zeros (n , dtype = float ) * u .km , description = "HeliographicStonyhurst X of observer"
104+ )
105+ data ["flareposition_obs_hgs_y" ] = Column (
106+ np .zeros (n , dtype = float ) * u .km , description = "HeliographicStonyhurst Y of observer"
107+ )
108+ data ["flareposition_obs_hgs_z" ] = Column (
109+ np .zeros (n , dtype = float ) * u .km , description = "HeliographicStonyhurst Z of observer"
110+ )
111+ data ["flareposition_hp_tx" ] = Column (np .zeros (n , dtype = float ) * u .arcsec , description = "Helioprojective Tx" )
112+ data ["flareposition_hp_ty" ] = Column (np .zeros (n , dtype = float ) * u .arcsec , description = "Helioprojective Ty" )
99113
100114 data ["anc_ephemeris_path" ] = Column (" " * 500 , dtype = str , description = "TDB" )
101115 data ["cpd_path" ] = Column (" " * 500 , dtype = str , description = "TDB" )
102116 data ["_position_status" ] = Column (False , dtype = bool , description = "TDB" )
103117 data ["_position_message" ] = Column (" " * 500 , dtype = str , description = "TDB" )
118+
104119 to_remove = []
105120 pass_filter = 0
106121 no_ephemeris = 0
@@ -110,9 +125,9 @@ def add_flare_position(
110125 total_flares = len (data )
111126
112127 day_asp_ephemeris_cache = dict ()
113- flare_positions = []
128+
114129 for i , row in enumerate (data ):
115- if filter_function (row ) and i < 200 :
130+ if filter_function (row ): # and i < 200:
116131 pass_filter += 1
117132 peak_time = row [peak_time_colname ]
118133 start_time = row [start_time_colname ]
@@ -183,7 +198,7 @@ def add_flare_position(
183198 cpd_res ["duration" ][i ] = header ["OBT_END" ] - header ["OBT_BEG" ]
184199
185200 # TODO: add more criteria to select the best CPD file
186- cpd_res .sort (["tbins" , "duration" ])
201+ cpd_res .sort (["inc_peak" , " tbins" , "duration" ], reverse = True )
187202 # cpd_res.pprint()
188203 best_cpd_idx = 0
189204 else :
@@ -193,24 +208,63 @@ def add_flare_position(
193208
194209 try :
195210 stixpy_cpd = STIXPYProduct (Path (data [i ]["cpd_path" ]))
196- coord , map = estimate_stix_flare_location (stixpy_cpd )
211+ time_range = TimeRange (max (peak_time - 20 * u .s , start_time ), min (peak_time + 20 * u .s , end_time ))
212+ overlaps = calculate_overlap (stixpy_cpd .time_range , time_range )
213+ if overlaps is None :
214+ logger .warning (
215+ f"CPD data does not cover time range around peak time { time_range .start } to { time_range .end } "
216+ )
217+ time_range = stixpy_cpd .time_range
218+ contains_peak_time = False
219+ else :
220+ contains_peak_time = True
221+ time_range = overlaps
222+
223+ mask = (stixpy_cpd .data ["time" ] >= time_range .start ) & (stixpy_cpd .data ["time" ] <= time_range .end )
224+ data_at_peak = stixpy_cpd .data [mask ]
225+ if len (np .unique (data_at_peak ["rcr" ])) > 1 :
226+ logger .warning (
227+ f"Multiple rcr values found for flare at time { time_range .start } : { time_range .end } "
228+ )
229+ # allow a larger time range for finding a constant rcr sequence
230+ if contains_peak_time :
231+ time_range = TimeRange (
232+ max (peak_time - 40 * u .s , start_time ), min (peak_time + 40 * u .s , end_time )
233+ )
234+ mask = (stixpy_cpd .data ["time" ] >= time_range .start ) & (
235+ stixpy_cpd .data ["time" ] <= time_range .end
236+ )
237+ data_at_peak = stixpy_cpd .data [mask ]
238+ length , start_idx , rcr = longest_constant_sequence (data_at_peak ["rcr" ].value )
239+ time_range = TimeRange (
240+ data_at_peak ["time" ][start_idx ], data_at_peak ["time" ][start_idx + length - 1 ]
241+ )
242+ logger .info (
243+ f"Using time range { time_range .start } to { time_range .end } for flare at around { peak_time } with constant rcr={ rcr } "
244+ )
245+
246+ coord , map = estimate_stix_flare_location (stixpy_cpd , time_range = time_range )
197247
198248 roll , solo_xyz , pointing = get_hpc_info (start_time , end_time )
199249 solo = HeliographicStonyhurst (* solo_xyz , obstime = peak_time , representation_type = "cartesian" )
250+ with SphericalScreen (solo , only_off_disk = True ):
251+ center_hpc = coord .transform_to (Helioprojective (observer = solo ))
252+
253+ data [i ]["flareposition_obs_hgs_x" ] = solo_xyz [0 ].to (u .km )
254+ data [i ]["flareposition_obs_hgs_y" ] = solo_xyz [1 ].to (u .km )
255+ data [i ]["flareposition_obs_hgs_z" ] = solo_xyz [2 ].to (u .km )
256+ data [i ]["flareposition_hp_tx" ] = center_hpc .Tx .to (u .arcsec )
257+ data [i ]["flareposition_hp_ty" ] = center_hpc .Ty .to (u .arcsec )
200258
201- # data[i]["flare_position"] = coord.transform_to(Helioprojective(observer=solo))
202- flare_positions .append (coord .transform_to (Helioprojective (observer = solo )))
203259 data [i ]["_position_status" ] = True
204260 data [i ]["_position_message" ] = "OK"
205261 except Exception as e :
206- flare_positions . append ( None )
262+ data [ i ][ "_position_status" ] = False
207263 data [i ]["_position_message" ] = f"Error: { type (e )} "
208-
264+ logger . warn ( f"Error calculating flare position for flare at time { start_time } : { end_time } : { e } " )
209265 else :
210266 to_remove .append (i )
211- flare_positions .append (None )
212267
213- data ["flare_position" ] = flare_positions
214268 if not keep_all_flares :
215269 data .remove_rows (to_remove )
216270
@@ -762,3 +816,44 @@ def add_peak_preview(cls, data, energies, parent, fido_client: STIXClient, img_p
762816 @classmethod
763817 def is_datasource_for (cls , * , service_type , service_subtype , ssid , ** kwargs ):
764818 return kwargs ["level" ] == "L3" and service_type == 0 and service_subtype == 0 and ssid == 8
819+
820+
821+ def longest_constant_sequence (state_array ):
822+ """Find the longest sequence where state is constant.
823+ In case of equal length, prefer the one with the lower state value."""
824+ if len (state_array ) == 0 :
825+ return 0 , None , None
826+
827+ max_length = 0
828+ max_state = None
829+ max_start_idx = None
830+ current_idx = 0
831+
832+ for state , group in groupby (state_array ):
833+ length = len (list (group ))
834+ # Update if longer, OR if equal length but lower state value
835+ if length > max_length or (length == max_length and (max_state is None or state < max_state )):
836+ max_length = length
837+ max_state = state
838+ max_start_idx = current_idx
839+ current_idx += length
840+
841+ return max_length , max_start_idx , max_state
842+
843+
844+ def calculate_overlap (range1 , range2 ):
845+ """Calculate the overlap between two TimeRanges.
846+ Returns the overlap duration and the overlapping TimeRange, or None if no overlap."""
847+
848+ # Check if they intersect first
849+ if not range1 .intersects (range2 ):
850+ return None
851+
852+ # Calculate intersection boundaries
853+ overlap_start = max (range1 .start , range2 .start )
854+ overlap_end = min (range1 .end , range2 .end )
855+
856+ # Create the overlapping TimeRange
857+ overlap_range = TimeRange (overlap_start , overlap_end )
858+
859+ return overlap_range
0 commit comments