solarviewer 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solar_radio_image_viewer/__init__.py +12 -0
- solar_radio_image_viewer/assets/add_tab_default.png +0 -0
- solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/browse.png +0 -0
- solar_radio_image_viewer/assets/browse_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
- solar_radio_image_viewer/assets/profile.png +0 -0
- solar_radio_image_viewer/assets/profile_light.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
- solar_radio_image_viewer/assets/reset.png +0 -0
- solar_radio_image_viewer/assets/reset_light.png +0 -0
- solar_radio_image_viewer/assets/ruler.png +0 -0
- solar_radio_image_viewer/assets/ruler_light.png +0 -0
- solar_radio_image_viewer/assets/search.png +0 -0
- solar_radio_image_viewer/assets/search_light.png +0 -0
- solar_radio_image_viewer/assets/settings.png +0 -0
- solar_radio_image_viewer/assets/settings_light.png +0 -0
- solar_radio_image_viewer/assets/splash.fits +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_in.png +0 -0
- solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_out.png +0 -0
- solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
- solar_radio_image_viewer/create_video.py +1345 -0
- solar_radio_image_viewer/dialogs.py +2665 -0
- solar_radio_image_viewer/from_simpl/__init__.py +184 -0
- solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
- solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
- solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
- solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
- solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
- solar_radio_image_viewer/from_simpl/utils.py +984 -0
- solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
- solar_radio_image_viewer/helioprojective.py +1916 -0
- solar_radio_image_viewer/helioprojective_viewer.py +817 -0
- solar_radio_image_viewer/helioviewer_browser.py +1514 -0
- solar_radio_image_viewer/main.py +148 -0
- solar_radio_image_viewer/move_phasecenter.py +1269 -0
- solar_radio_image_viewer/napari_viewer.py +368 -0
- solar_radio_image_viewer/noaa_events/__init__.py +32 -0
- solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
- solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
- solar_radio_image_viewer/norms.py +293 -0
- solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
- solar_radio_image_viewer/searchable_combobox.py +220 -0
- solar_radio_image_viewer/solar_context/__init__.py +41 -0
- solar_radio_image_viewer/solar_context/active_regions.py +371 -0
- solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
- solar_radio_image_viewer/solar_context/context_images.py +297 -0
- solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
- solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
- solar_radio_image_viewer/styles.py +643 -0
- solar_radio_image_viewer/utils/__init__.py +32 -0
- solar_radio_image_viewer/utils/rate_limiter.py +255 -0
- solar_radio_image_viewer/utils.py +952 -0
- solar_radio_image_viewer/video_dialog.py +2629 -0
- solar_radio_image_viewer/video_utils.py +656 -0
- solar_radio_image_viewer/viewer.py +11174 -0
- solarviewer-1.0.2.dist-info/METADATA +343 -0
- solarviewer-1.0.2.dist-info/RECORD +82 -0
- solarviewer-1.0.2.dist-info/WHEEL +5 -0
- solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
- solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
- solarviewer-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
NOAA Solar Events Parser - Core module for fetching and parsing NOAA solar events.
|
|
4
|
+
|
|
5
|
+
Data source: https://solarmonitor.org/data/{YYYY}/{MM}/{DD}/meta/noaa_events_raw_{YYYYMMDD}.txt
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import urllib.request
|
|
10
|
+
import urllib.error
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime, date
|
|
13
|
+
from typing import List, Optional, Dict, Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Event type definitions with descriptions and icons
|
|
17
|
+
EVENT_TYPES = {
|
|
18
|
+
"XRA": {
|
|
19
|
+
"name": "X-ray Flare",
|
|
20
|
+
"description": "Solar X-ray event observed by GOES spacecraft (1-8 Ångström)",
|
|
21
|
+
"icon": "☀️",
|
|
22
|
+
"category": "xray",
|
|
23
|
+
},
|
|
24
|
+
"FLA": {
|
|
25
|
+
"name": "Optical Flare",
|
|
26
|
+
"description": "Optical flare observed in H-alpha wavelength",
|
|
27
|
+
"icon": "🔥",
|
|
28
|
+
"category": "optical",
|
|
29
|
+
},
|
|
30
|
+
"RSP": {
|
|
31
|
+
"name": "Sweep Radio Burst",
|
|
32
|
+
"description": "Sweep-frequency radio burst (Type II/III/IV/V)",
|
|
33
|
+
"icon": "📻",
|
|
34
|
+
"category": "radio",
|
|
35
|
+
},
|
|
36
|
+
"RBR": {
|
|
37
|
+
"name": "Fixed-freq Radio Burst",
|
|
38
|
+
"description": "Fixed-frequency radio burst (245, 410, 610 MHz, etc.)",
|
|
39
|
+
"icon": "📡",
|
|
40
|
+
"category": "radio",
|
|
41
|
+
},
|
|
42
|
+
"RNS": {
|
|
43
|
+
"name": "Radio Noise Storm",
|
|
44
|
+
"description": "Prolonged enhanced radio emission from sunspot groups",
|
|
45
|
+
"icon": "🌊",
|
|
46
|
+
"category": "radio",
|
|
47
|
+
},
|
|
48
|
+
"CME": {
|
|
49
|
+
"name": "Coronal Mass Ejection",
|
|
50
|
+
"description": "Massive plasma/magnetic field ejection from corona",
|
|
51
|
+
"icon": "💥",
|
|
52
|
+
"category": "cme",
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# X-ray flare class colors (for UI)
|
|
57
|
+
FLARE_CLASS_COLORS = {
|
|
58
|
+
"A": "#808080", # Gray - minor
|
|
59
|
+
"B": "#4CAF50", # Green - weak
|
|
60
|
+
"C": "#FFC107", # Yellow/Amber - small
|
|
61
|
+
"M": "#FF9800", # Orange - moderate
|
|
62
|
+
"X": "#F44336", # Red - major
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Observatory codes
|
|
66
|
+
OBSERVATORY_CODES = {
|
|
67
|
+
"G16": "GOES-16",
|
|
68
|
+
"G18": "GOES-18",
|
|
69
|
+
"G17": "GOES-17",
|
|
70
|
+
"G15": "GOES-15",
|
|
71
|
+
"LEA": "Learmonth",
|
|
72
|
+
"SVI": "San Vito",
|
|
73
|
+
"PAL": "Palehua",
|
|
74
|
+
"HOL": "Holloman",
|
|
75
|
+
"SAG": "Sagamore Hill",
|
|
76
|
+
"RAM": "Ramey",
|
|
77
|
+
"CUL": "Culgoora",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class SolarEvent:
|
|
83
|
+
"""Represents a single NOAA solar event."""
|
|
84
|
+
event_id: str
|
|
85
|
+
is_followup: bool # Has '+' marker
|
|
86
|
+
begin_time: Optional[str] # HHMM format or None
|
|
87
|
+
max_time: Optional[str] # HHMM format or None (can be "////")
|
|
88
|
+
end_time: Optional[str] # HHMM format or None
|
|
89
|
+
observatory: str
|
|
90
|
+
quality: str
|
|
91
|
+
event_type: str # XRA, FLA, RSP, RBR, RNS, CME
|
|
92
|
+
location_or_freq: str
|
|
93
|
+
particulars: str
|
|
94
|
+
active_region: Optional[str]
|
|
95
|
+
raw_line: str = ""
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def type_info(self) -> Dict[str, Any]:
|
|
99
|
+
"""Get event type metadata."""
|
|
100
|
+
return EVENT_TYPES.get(self.event_type, {
|
|
101
|
+
"name": self.event_type,
|
|
102
|
+
"description": "Unknown event type",
|
|
103
|
+
"icon": "❓",
|
|
104
|
+
"category": "other",
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def observatory_name(self) -> str:
|
|
109
|
+
"""Get full observatory name."""
|
|
110
|
+
return OBSERVATORY_CODES.get(self.observatory, self.observatory)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def begin_time_formatted(self) -> str:
|
|
114
|
+
"""Format begin time as HH:MM."""
|
|
115
|
+
if not self.begin_time or self.begin_time == "////":
|
|
116
|
+
return "—"
|
|
117
|
+
# Handle B-prefixed times (began before monitoring started)
|
|
118
|
+
t = self.begin_time.lstrip("B")
|
|
119
|
+
if len(t) == 4:
|
|
120
|
+
return f"{t[:2]}:{t[2:]}"
|
|
121
|
+
return self.begin_time
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def max_time_formatted(self) -> str:
|
|
125
|
+
"""Format max time as HH:MM."""
|
|
126
|
+
if not self.max_time or self.max_time == "////":
|
|
127
|
+
return "—"
|
|
128
|
+
if len(self.max_time) == 4:
|
|
129
|
+
return f"{self.max_time[:2]}:{self.max_time[2:]}"
|
|
130
|
+
return self.max_time
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def end_time_formatted(self) -> str:
|
|
134
|
+
"""Format end time as HH:MM."""
|
|
135
|
+
if not self.end_time or self.end_time == "////":
|
|
136
|
+
return "—"
|
|
137
|
+
if len(self.end_time) == 4:
|
|
138
|
+
return f"{self.end_time[:2]}:{self.end_time[2:]}"
|
|
139
|
+
return self.end_time
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def time_range(self) -> str:
|
|
143
|
+
"""Get formatted time range string."""
|
|
144
|
+
begin = self.begin_time_formatted
|
|
145
|
+
end = self.end_time_formatted
|
|
146
|
+
if begin == "—" and end == "—":
|
|
147
|
+
return "—"
|
|
148
|
+
return f"{begin} – {end}"
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def duration_minutes(self) -> Optional[int]:
|
|
152
|
+
"""Calculate event duration in minutes."""
|
|
153
|
+
try:
|
|
154
|
+
if not self.begin_time or not self.end_time:
|
|
155
|
+
return None
|
|
156
|
+
begin = self.begin_time.lstrip("B")
|
|
157
|
+
end = self.end_time
|
|
158
|
+
if begin == "////" or end == "////":
|
|
159
|
+
return None
|
|
160
|
+
begin_mins = int(begin[:2]) * 60 + int(begin[2:])
|
|
161
|
+
end_mins = int(end[:2]) * 60 + int(end[2:])
|
|
162
|
+
if end_mins < begin_mins:
|
|
163
|
+
end_mins += 24 * 60 # Crosses midnight
|
|
164
|
+
return end_mins - begin_mins
|
|
165
|
+
except (ValueError, IndexError):
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def flare_class(self) -> Optional[str]:
|
|
170
|
+
"""Extract flare class for XRA events (e.g., 'M1.9')."""
|
|
171
|
+
if self.event_type == "XRA":
|
|
172
|
+
# Particulars contains something like "M1.9 1.5E-02"
|
|
173
|
+
parts = self.particulars.split()
|
|
174
|
+
if parts:
|
|
175
|
+
return parts[0]
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def flare_class_letter(self) -> Optional[str]:
|
|
180
|
+
"""Get just the letter class (A, B, C, M, X)."""
|
|
181
|
+
fc = self.flare_class
|
|
182
|
+
if fc and len(fc) > 0:
|
|
183
|
+
return fc[0].upper()
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def flare_class_color(self) -> str:
|
|
188
|
+
"""Get color for flare class."""
|
|
189
|
+
letter = self.flare_class_letter
|
|
190
|
+
return FLARE_CLASS_COLORS.get(letter, "#808080")
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def optical_class(self) -> Optional[str]:
|
|
194
|
+
"""Extract optical flare class for FLA events (e.g., 'SF', '1N')."""
|
|
195
|
+
if self.event_type == "FLA":
|
|
196
|
+
parts = self.particulars.split()
|
|
197
|
+
if parts:
|
|
198
|
+
return parts[0]
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def fetch_events_raw(event_date: date) -> Optional[str]:
|
|
203
|
+
"""
|
|
204
|
+
Fetch raw NOAA events text from solarmonitor.org.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
event_date: The date to fetch events for
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Raw text content or None if fetch failed
|
|
211
|
+
"""
|
|
212
|
+
year = event_date.strftime("%Y")
|
|
213
|
+
month = event_date.strftime("%m")
|
|
214
|
+
day = event_date.strftime("%d")
|
|
215
|
+
date_str = event_date.strftime("%Y%m%d")
|
|
216
|
+
|
|
217
|
+
url = f"https://solarmonitor.org/data/{year}/{month}/{day}/meta/noaa_events_raw_{date_str}.txt"
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
from ..utils import get_global_session
|
|
221
|
+
except ImportError:
|
|
222
|
+
from solar_radio_image_viewer.utils import get_global_session
|
|
223
|
+
|
|
224
|
+
session = get_global_session()
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
response = session.get(url)
|
|
228
|
+
return response.text
|
|
229
|
+
except Exception as e:
|
|
230
|
+
# Handle 404 or other errors
|
|
231
|
+
if hasattr(e, 'response') and e.response is not None:
|
|
232
|
+
if e.response.status_code == 404:
|
|
233
|
+
return None # No events file for this date
|
|
234
|
+
print(f"Error fetching NOAA events: {e}")
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def parse_events(raw_text: str) -> List[SolarEvent]:
|
|
240
|
+
"""
|
|
241
|
+
Parse raw NOAA events text into structured SolarEvent objects.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
raw_text: Raw text from NOAA events file
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of SolarEvent objects
|
|
248
|
+
"""
|
|
249
|
+
events = []
|
|
250
|
+
|
|
251
|
+
for line in raw_text.split("\n"):
|
|
252
|
+
# Skip comments and empty lines
|
|
253
|
+
line = line.strip()
|
|
254
|
+
if not line or line.startswith("#") or line.startswith(":"):
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
# Parse event line
|
|
258
|
+
# Format: Event# + Begin Max End Obs Q Type Loc/Frq Particulars Reg#
|
|
259
|
+
# Example: 4470 + 1235 1246 1258 G16 5 XRA 1-8A M1.9 1.5E-02 3455
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
# Event ID (first 4-5 chars)
|
|
263
|
+
event_id = line[:5].strip()
|
|
264
|
+
|
|
265
|
+
# Check for '+' marker (follow-up event)
|
|
266
|
+
is_followup = "+" in line[5:7]
|
|
267
|
+
|
|
268
|
+
# Times: positions 7-11, 14-18, 24-28 (approximate, use regex)
|
|
269
|
+
# Use regex for more reliable parsing
|
|
270
|
+
# Event ID can be 3-5 digits (e.g., 250, 4470)
|
|
271
|
+
pattern = r"(\d{3,5})\s*\+?\s+([B]?\d{4}|////)\s+(\d{4}|////)\s+(\d{4}|////)\s+(\w{3})\s+(\S+)\s+(\w{3})\s+(\S+)\s+(.*)"
|
|
272
|
+
match = re.match(pattern, line)
|
|
273
|
+
|
|
274
|
+
if match:
|
|
275
|
+
event_id = match.group(1)
|
|
276
|
+
begin_time = match.group(2) if match.group(2) != "////" else None
|
|
277
|
+
max_time = match.group(3) if match.group(3) != "////" else None
|
|
278
|
+
end_time = match.group(4) if match.group(4) != "////" else None
|
|
279
|
+
observatory = match.group(5)
|
|
280
|
+
quality = match.group(6)
|
|
281
|
+
event_type = match.group(7)
|
|
282
|
+
location_or_freq = match.group(8)
|
|
283
|
+
rest = match.group(9).strip()
|
|
284
|
+
|
|
285
|
+
# Split rest into particulars and region
|
|
286
|
+
parts = rest.split()
|
|
287
|
+
if parts and parts[-1].isdigit() and len(parts[-1]) == 4:
|
|
288
|
+
active_region = parts[-1]
|
|
289
|
+
particulars = " ".join(parts[:-1])
|
|
290
|
+
else:
|
|
291
|
+
active_region = None
|
|
292
|
+
particulars = rest
|
|
293
|
+
|
|
294
|
+
events.append(SolarEvent(
|
|
295
|
+
event_id=event_id,
|
|
296
|
+
is_followup="+" in line[4:7],
|
|
297
|
+
begin_time=begin_time,
|
|
298
|
+
max_time=max_time,
|
|
299
|
+
end_time=end_time,
|
|
300
|
+
observatory=observatory,
|
|
301
|
+
quality=quality,
|
|
302
|
+
event_type=event_type,
|
|
303
|
+
location_or_freq=location_or_freq,
|
|
304
|
+
particulars=particulars,
|
|
305
|
+
active_region=active_region,
|
|
306
|
+
raw_line=line,
|
|
307
|
+
))
|
|
308
|
+
except Exception:
|
|
309
|
+
# Skip malformed lines
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
return events
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def fetch_and_parse_events(event_date: date) -> Optional[List[SolarEvent]]:
|
|
316
|
+
"""
|
|
317
|
+
Fetch and parse NOAA events for a given date.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
event_date: The date to fetch events for
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
List of SolarEvent objects or None if fetch failed
|
|
324
|
+
"""
|
|
325
|
+
raw_text = fetch_events_raw(event_date)
|
|
326
|
+
if raw_text is None:
|
|
327
|
+
return None
|
|
328
|
+
return parse_events(raw_text)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def categorize_events(events: List[SolarEvent]) -> Dict[str, List[SolarEvent]]:
|
|
332
|
+
"""
|
|
333
|
+
Categorize events by type category.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Dict with keys: 'xray', 'optical', 'radio', 'cme', 'other'
|
|
337
|
+
"""
|
|
338
|
+
categories = {
|
|
339
|
+
"xray": [],
|
|
340
|
+
"optical": [],
|
|
341
|
+
"radio": [],
|
|
342
|
+
"cme": [],
|
|
343
|
+
"other": [],
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for event in events:
|
|
347
|
+
category = event.type_info.get("category", "other")
|
|
348
|
+
if category in categories:
|
|
349
|
+
categories[category].append(event)
|
|
350
|
+
else:
|
|
351
|
+
categories["other"].append(event)
|
|
352
|
+
|
|
353
|
+
return categories
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def get_event_statistics(events: List[SolarEvent]) -> Dict[str, Any]:
|
|
357
|
+
"""
|
|
358
|
+
Calculate statistics for a list of events.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Dict with statistics like counts, max flare class, etc.
|
|
362
|
+
"""
|
|
363
|
+
stats = {
|
|
364
|
+
"total": len(events),
|
|
365
|
+
"by_type": {},
|
|
366
|
+
"max_xray_class": None,
|
|
367
|
+
"max_xray_event": None,
|
|
368
|
+
"active_regions": set(),
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
max_class_order = {"A": 0, "B": 1, "C": 2, "M": 3, "X": 4}
|
|
372
|
+
max_class_value = -1
|
|
373
|
+
|
|
374
|
+
for event in events:
|
|
375
|
+
# Count by type
|
|
376
|
+
t = event.event_type
|
|
377
|
+
stats["by_type"][t] = stats["by_type"].get(t, 0) + 1
|
|
378
|
+
|
|
379
|
+
# Track active regions
|
|
380
|
+
if event.active_region:
|
|
381
|
+
stats["active_regions"].add(event.active_region)
|
|
382
|
+
|
|
383
|
+
# Find max X-ray class
|
|
384
|
+
if event.event_type == "XRA":
|
|
385
|
+
letter = event.flare_class_letter
|
|
386
|
+
if letter and letter in max_class_order:
|
|
387
|
+
class_val = max_class_order[letter]
|
|
388
|
+
if class_val > max_class_value:
|
|
389
|
+
max_class_value = class_val
|
|
390
|
+
stats["max_xray_class"] = event.flare_class
|
|
391
|
+
stats["max_xray_event"] = event
|
|
392
|
+
elif class_val == max_class_value:
|
|
393
|
+
# Compare numeric part
|
|
394
|
+
try:
|
|
395
|
+
current_num = float(event.flare_class[1:])
|
|
396
|
+
max_num = float(stats["max_xray_class"][1:])
|
|
397
|
+
if current_num > max_num:
|
|
398
|
+
stats["max_xray_class"] = event.flare_class
|
|
399
|
+
stats["max_xray_event"] = event
|
|
400
|
+
except (ValueError, IndexError):
|
|
401
|
+
pass
|
|
402
|
+
|
|
403
|
+
stats["active_regions"] = list(stats["active_regions"])
|
|
404
|
+
return stats
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
# Test with sample date
|
|
409
|
+
from datetime import date
|
|
410
|
+
|
|
411
|
+
test_date = date(2023, 10, 2)
|
|
412
|
+
print(f"Fetching events for {test_date}...")
|
|
413
|
+
|
|
414
|
+
events = fetch_and_parse_events(test_date)
|
|
415
|
+
if events:
|
|
416
|
+
print(f"Found {len(events)} events")
|
|
417
|
+
|
|
418
|
+
categories = categorize_events(events)
|
|
419
|
+
for cat, cat_events in categories.items():
|
|
420
|
+
if cat_events:
|
|
421
|
+
print(f"\n{cat.upper()} ({len(cat_events)} events):")
|
|
422
|
+
for e in cat_events[:3]:
|
|
423
|
+
print(f" {e.time_range} | {e.event_type} | {e.particulars} | AR {e.active_region}")
|
|
424
|
+
|
|
425
|
+
stats = get_event_statistics(events)
|
|
426
|
+
print(f"\nStatistics:")
|
|
427
|
+
print(f" Max X-ray class: {stats['max_xray_class']}")
|
|
428
|
+
print(f" Active regions: {stats['active_regions']}")
|
|
429
|
+
else:
|
|
430
|
+
print("No events found or fetch failed")
|