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.
Files changed (82) hide show
  1. solar_radio_image_viewer/__init__.py +12 -0
  2. solar_radio_image_viewer/assets/add_tab_default.png +0 -0
  3. solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
  4. solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
  5. solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
  6. solar_radio_image_viewer/assets/browse.png +0 -0
  7. solar_radio_image_viewer/assets/browse_light.png +0 -0
  8. solar_radio_image_viewer/assets/close_tab_default.png +0 -0
  9. solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
  10. solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
  11. solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
  12. solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
  13. solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
  14. solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
  15. solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
  16. solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
  17. solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
  18. solar_radio_image_viewer/assets/profile.png +0 -0
  19. solar_radio_image_viewer/assets/profile_light.png +0 -0
  20. solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
  21. solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
  22. solar_radio_image_viewer/assets/reset.png +0 -0
  23. solar_radio_image_viewer/assets/reset_light.png +0 -0
  24. solar_radio_image_viewer/assets/ruler.png +0 -0
  25. solar_radio_image_viewer/assets/ruler_light.png +0 -0
  26. solar_radio_image_viewer/assets/search.png +0 -0
  27. solar_radio_image_viewer/assets/search_light.png +0 -0
  28. solar_radio_image_viewer/assets/settings.png +0 -0
  29. solar_radio_image_viewer/assets/settings_light.png +0 -0
  30. solar_radio_image_viewer/assets/splash.fits +0 -0
  31. solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
  32. solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
  33. solar_radio_image_viewer/assets/zoom_in.png +0 -0
  34. solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
  35. solar_radio_image_viewer/assets/zoom_out.png +0 -0
  36. solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
  37. solar_radio_image_viewer/create_video.py +1345 -0
  38. solar_radio_image_viewer/dialogs.py +2665 -0
  39. solar_radio_image_viewer/from_simpl/__init__.py +184 -0
  40. solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
  41. solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
  42. solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
  43. solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
  44. solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
  45. solar_radio_image_viewer/from_simpl/utils.py +984 -0
  46. solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
  47. solar_radio_image_viewer/helioprojective.py +1916 -0
  48. solar_radio_image_viewer/helioprojective_viewer.py +817 -0
  49. solar_radio_image_viewer/helioviewer_browser.py +1514 -0
  50. solar_radio_image_viewer/main.py +148 -0
  51. solar_radio_image_viewer/move_phasecenter.py +1269 -0
  52. solar_radio_image_viewer/napari_viewer.py +368 -0
  53. solar_radio_image_viewer/noaa_events/__init__.py +32 -0
  54. solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
  55. solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
  56. solar_radio_image_viewer/norms.py +293 -0
  57. solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
  58. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
  59. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
  60. solar_radio_image_viewer/searchable_combobox.py +220 -0
  61. solar_radio_image_viewer/solar_context/__init__.py +41 -0
  62. solar_radio_image_viewer/solar_context/active_regions.py +371 -0
  63. solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
  64. solar_radio_image_viewer/solar_context/context_images.py +297 -0
  65. solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
  66. solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
  67. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
  68. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
  69. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
  70. solar_radio_image_viewer/styles.py +643 -0
  71. solar_radio_image_viewer/utils/__init__.py +32 -0
  72. solar_radio_image_viewer/utils/rate_limiter.py +255 -0
  73. solar_radio_image_viewer/utils.py +952 -0
  74. solar_radio_image_viewer/video_dialog.py +2629 -0
  75. solar_radio_image_viewer/video_utils.py +656 -0
  76. solar_radio_image_viewer/viewer.py +11174 -0
  77. solarviewer-1.0.2.dist-info/METADATA +343 -0
  78. solarviewer-1.0.2.dist-info/RECORD +82 -0
  79. solarviewer-1.0.2.dist-info/WHEEL +5 -0
  80. solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
  81. solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
  82. 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")