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,528 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Solar Conditions Data Fetcher - Fetches solar conditions for a specific date.
|
|
4
|
+
|
|
5
|
+
Data sources:
|
|
6
|
+
- Historical Kp Index: services.swpc.noaa.gov/text/daily-geomagnetic-indices.txt (30-day history)
|
|
7
|
+
- Historical F10.7 Flux: services.swpc.noaa.gov/text/daily-solar-indices.txt (30-day history)
|
|
8
|
+
- Current Solar Wind: services.swpc.noaa.gov/products/solar-wind/plasma-7-day.json (7-day only)
|
|
9
|
+
|
|
10
|
+
Note: For dates older than 30 days, data may not be available from SWPC.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import json
|
|
15
|
+
import urllib.request
|
|
16
|
+
import urllib.error
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from datetime import datetime, date, timedelta
|
|
19
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class SolarWindData:
|
|
24
|
+
"""Current solar wind conditions."""
|
|
25
|
+
timestamp: datetime
|
|
26
|
+
speed: float # km/s
|
|
27
|
+
density: float # protons/cm³
|
|
28
|
+
temperature: float # Kelvin
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def speed_category(self) -> str:
|
|
32
|
+
"""Categorize solar wind speed."""
|
|
33
|
+
if self.speed < 350:
|
|
34
|
+
return "Slow"
|
|
35
|
+
elif self.speed < 500:
|
|
36
|
+
return "Normal"
|
|
37
|
+
elif self.speed < 700:
|
|
38
|
+
return "Elevated"
|
|
39
|
+
else:
|
|
40
|
+
return "High"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class KpIndexData:
|
|
45
|
+
"""Kp geomagnetic index data for a date."""
|
|
46
|
+
event_date: date
|
|
47
|
+
ap_value: int # Daily Ap index
|
|
48
|
+
kp_values: List[float] # 8 Kp values for the day (one per 3-hour interval)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def kp_max(self) -> float:
|
|
52
|
+
"""Get maximum Kp for the day."""
|
|
53
|
+
return max(self.kp_values) if self.kp_values else 0
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def kp_avg(self) -> float:
|
|
57
|
+
"""Get average Kp for the day."""
|
|
58
|
+
return sum(self.kp_values) / len(self.kp_values) if self.kp_values else 0
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def storm_level(self) -> str:
|
|
62
|
+
"""Get NOAA geomagnetic storm scale level based on max Kp."""
|
|
63
|
+
kp = self.kp_max
|
|
64
|
+
if kp >= 9:
|
|
65
|
+
return "G5 (Extreme)"
|
|
66
|
+
elif kp >= 8:
|
|
67
|
+
return "G4 (Severe)"
|
|
68
|
+
elif kp >= 7:
|
|
69
|
+
return "G3 (Strong)"
|
|
70
|
+
elif kp >= 6:
|
|
71
|
+
return "G2 (Moderate)"
|
|
72
|
+
elif kp >= 5:
|
|
73
|
+
return "G1 (Minor)"
|
|
74
|
+
else:
|
|
75
|
+
return "Quiet"
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def color_code(self) -> str:
|
|
79
|
+
"""Get color code for Kp value."""
|
|
80
|
+
kp = self.kp_max
|
|
81
|
+
if kp >= 7:
|
|
82
|
+
return "#F44336" # Red
|
|
83
|
+
elif kp >= 5:
|
|
84
|
+
return "#FF9800" # Orange
|
|
85
|
+
elif kp >= 4:
|
|
86
|
+
return "#FFC107" # Amber
|
|
87
|
+
elif kp >= 2:
|
|
88
|
+
return "#4CAF50" # Green
|
|
89
|
+
else:
|
|
90
|
+
return "#2196F3" # Blue (very quiet)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class F107FluxData:
|
|
95
|
+
"""F10.7 cm radio flux data for a date."""
|
|
96
|
+
event_date: date
|
|
97
|
+
flux_value: float # Solar Flux Units (sfu)
|
|
98
|
+
sunspot_number: int # SESC sunspot number
|
|
99
|
+
sunspot_area: int # 10E-6 Hemisphere
|
|
100
|
+
xray_background: str # Daily background X-ray flux (e.g., B1.5, *)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def activity_level(self) -> str:
|
|
104
|
+
"""Categorize solar activity based on F10.7."""
|
|
105
|
+
if self.flux_value < 70:
|
|
106
|
+
return "Very Low"
|
|
107
|
+
elif self.flux_value < 90:
|
|
108
|
+
return "Low"
|
|
109
|
+
elif self.flux_value < 120:
|
|
110
|
+
return "Moderate"
|
|
111
|
+
elif self.flux_value < 150:
|
|
112
|
+
return "Elevated"
|
|
113
|
+
elif self.flux_value < 200:
|
|
114
|
+
return "High"
|
|
115
|
+
else:
|
|
116
|
+
return "Very High"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class SolarConditions:
|
|
121
|
+
"""Combined solar conditions for a specific date."""
|
|
122
|
+
event_date: date
|
|
123
|
+
kp_index: Optional[KpIndexData]
|
|
124
|
+
f107_flux: Optional[F107FluxData]
|
|
125
|
+
is_historical: bool # True if data is from archive, False if current
|
|
126
|
+
data_source: str # Description of data source
|
|
127
|
+
solar_wind: Optional[SolarWindData] = None # Only for current date
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def summary(self) -> str:
|
|
131
|
+
"""Get a one-line summary of conditions."""
|
|
132
|
+
parts = []
|
|
133
|
+
|
|
134
|
+
if self.kp_index:
|
|
135
|
+
parts.append(f"Kp max: {self.kp_index.kp_max:.0f} ({self.kp_index.storm_level})")
|
|
136
|
+
|
|
137
|
+
if self.solar_wind:
|
|
138
|
+
parts.append(f"Wind: {self.solar_wind.speed:.0f} km/s")
|
|
139
|
+
|
|
140
|
+
if self.f107_flux:
|
|
141
|
+
parts.append(f"F10.7: {self.f107_flux.flux_value:.0f} sfu ({self.f107_flux.activity_level})")
|
|
142
|
+
|
|
143
|
+
return " | ".join(parts) if parts else "No data available"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def fetch_json(url: str, timeout: int = 30) -> Optional[Any]:
|
|
147
|
+
"""Fetch and parse JSON from URL."""
|
|
148
|
+
try:
|
|
149
|
+
from ..utils import get_global_session
|
|
150
|
+
session = get_global_session()
|
|
151
|
+
response = session.get(url)
|
|
152
|
+
return json.loads(response.text)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
print(f"Error fetching {url}: {e}")
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def fetch_solar_wind(target_date: Optional[date] = None) -> Optional[SolarWindData]:
|
|
159
|
+
"""
|
|
160
|
+
Fetch solar wind conditions from SWPC 7-day history.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
target_date: If provided, get the latest data for this specific date.
|
|
164
|
+
If None, get the absolute latest data available.
|
|
165
|
+
"""
|
|
166
|
+
url = "https://services.swpc.noaa.gov/products/solar-wind/plasma-7-day.json"
|
|
167
|
+
data = fetch_json(url)
|
|
168
|
+
|
|
169
|
+
if not data or len(data) < 2:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Data format: [["time_tag", "density", "speed", "temperature"], [...], ...]
|
|
173
|
+
# Iterate backwards to find latest valid entry for the target date
|
|
174
|
+
for entry in reversed(data[1:]):
|
|
175
|
+
try:
|
|
176
|
+
timestamp_str = entry[0]
|
|
177
|
+
timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S.%f")
|
|
178
|
+
|
|
179
|
+
# If target_date is specified, verify match
|
|
180
|
+
if target_date and timestamp.date() != target_date:
|
|
181
|
+
# Optimized: Since data is chronological, if we see a date NEWER than target, skip.
|
|
182
|
+
# If we see a date OLDER, we missed our window (data not present).
|
|
183
|
+
if timestamp.date() > target_date:
|
|
184
|
+
continue
|
|
185
|
+
else:
|
|
186
|
+
# Found data older than target, so target date has no data in this file
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
# Found potential match (or no target date), check values
|
|
190
|
+
density = float(entry[1]) if entry[1] else None
|
|
191
|
+
speed = float(entry[2]) if entry[2] else None
|
|
192
|
+
temperature = float(entry[3]) if entry[3] else None
|
|
193
|
+
|
|
194
|
+
if speed is None or density is None:
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
return SolarWindData(
|
|
198
|
+
timestamp=timestamp,
|
|
199
|
+
speed=speed,
|
|
200
|
+
density=density,
|
|
201
|
+
temperature=temperature or 0,
|
|
202
|
+
)
|
|
203
|
+
except (ValueError, IndexError):
|
|
204
|
+
continue
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def fetch_text(url: str, timeout: int = 30) -> Optional[str]:
|
|
209
|
+
"""Fetch text content from URL."""
|
|
210
|
+
try:
|
|
211
|
+
from ..utils import get_global_session
|
|
212
|
+
session = get_global_session()
|
|
213
|
+
response = session.get(url)
|
|
214
|
+
return response.text
|
|
215
|
+
except Exception as e:
|
|
216
|
+
print(f"Error fetching {url}: {e}")
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def parse_daily_geomagnetic_indices(text: str) -> Dict[date, KpIndexData]:
|
|
221
|
+
"""
|
|
222
|
+
Parse SWPC daily-geomagnetic-indices.txt file.
|
|
223
|
+
|
|
224
|
+
Returns dict mapping dates to KpIndexData.
|
|
225
|
+
"""
|
|
226
|
+
results = {}
|
|
227
|
+
|
|
228
|
+
for line in text.split("\n"):
|
|
229
|
+
line = line.strip()
|
|
230
|
+
if not line or line.startswith("#") or line.startswith(":"):
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Format: YYYY MM DD A K K K K K K K K A K K K K K K K K A Kp Kp Kp Kp Kp Kp Kp Kp
|
|
234
|
+
# We want the planetary (Estimated) Kp values at the end
|
|
235
|
+
try:
|
|
236
|
+
parts = line.split()
|
|
237
|
+
if len(parts) < 30:
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
year = int(parts[0])
|
|
241
|
+
month = int(parts[1])
|
|
242
|
+
day = int(parts[2])
|
|
243
|
+
event_date = date(year, month, day)
|
|
244
|
+
|
|
245
|
+
# Get planetary A value (index 21 approximately, may vary)
|
|
246
|
+
# Actually the format shows 3 sets of A + 8 K values
|
|
247
|
+
# Fredericksburg (10), College (10), Planetary (10)
|
|
248
|
+
# So planetary Kp starts at index 22
|
|
249
|
+
|
|
250
|
+
# Parse planetary Kp values (last 8 values)
|
|
251
|
+
kp_values = []
|
|
252
|
+
for i in range(-8, 0):
|
|
253
|
+
try:
|
|
254
|
+
kp = float(parts[i])
|
|
255
|
+
kp_values.append(kp)
|
|
256
|
+
except ValueError:
|
|
257
|
+
kp_values.append(0.0)
|
|
258
|
+
|
|
259
|
+
# Get planetary A index (before the 8 Kp values)
|
|
260
|
+
try:
|
|
261
|
+
ap_value = int(parts[-9])
|
|
262
|
+
except (ValueError, IndexError):
|
|
263
|
+
ap_value = 0
|
|
264
|
+
|
|
265
|
+
results[event_date] = KpIndexData(
|
|
266
|
+
event_date=event_date,
|
|
267
|
+
ap_value=ap_value,
|
|
268
|
+
kp_values=kp_values,
|
|
269
|
+
)
|
|
270
|
+
except (ValueError, IndexError) as e:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
return results
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def parse_daily_solar_indices(text: str) -> Dict[date, F107FluxData]:
|
|
277
|
+
"""
|
|
278
|
+
Parse SWPC daily-solar-indices.txt file.
|
|
279
|
+
|
|
280
|
+
Returns dict mapping dates to F107FluxData.
|
|
281
|
+
"""
|
|
282
|
+
results = {}
|
|
283
|
+
|
|
284
|
+
for line in text.split("\n"):
|
|
285
|
+
line = line.strip()
|
|
286
|
+
if not line or line.startswith("#") or line.startswith(":"):
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
# Format: YYYY MM DD F10.7 SSN Area NewReg Field Bkgd C M X S 1 2 3
|
|
290
|
+
try:
|
|
291
|
+
parts = line.split()
|
|
292
|
+
if len(parts) < 5:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
year = int(parts[0])
|
|
296
|
+
month = int(parts[1])
|
|
297
|
+
day = int(parts[2])
|
|
298
|
+
event_date = date(year, month, day)
|
|
299
|
+
|
|
300
|
+
flux = float(parts[3])
|
|
301
|
+
sunspot_number = int(parts[4])
|
|
302
|
+
sunspot_area = int(parts[5])
|
|
303
|
+
xray_background = parts[8]
|
|
304
|
+
|
|
305
|
+
results[event_date] = F107FluxData(
|
|
306
|
+
event_date=event_date,
|
|
307
|
+
flux_value=flux,
|
|
308
|
+
sunspot_number=sunspot_number,
|
|
309
|
+
sunspot_area=sunspot_area,
|
|
310
|
+
xray_background=xray_background,
|
|
311
|
+
)
|
|
312
|
+
except (ValueError, IndexError) as e:
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
return results
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def fetch_conditions_for_date(event_date: date) -> SolarConditions:
|
|
319
|
+
"""
|
|
320
|
+
Fetch comprehensive solar conditions for a specific date.
|
|
321
|
+
|
|
322
|
+
Strategy:
|
|
323
|
+
1. Geomagnetic & Solar Indices:
|
|
324
|
+
- < 35 days: Daily text files
|
|
325
|
+
- > 35 days: FTP Archive
|
|
326
|
+
2. Solar Wind (Speed/Density):
|
|
327
|
+
- < 7 days: 7-day JSON history
|
|
328
|
+
- > 7 days: Not available (set to None)
|
|
329
|
+
"""
|
|
330
|
+
kp_data = None
|
|
331
|
+
f107_data = None
|
|
332
|
+
solar_wind = None
|
|
333
|
+
data_source = "NOAA SWPC daily indices (30-day archive)"
|
|
334
|
+
|
|
335
|
+
today = date.today()
|
|
336
|
+
days_ago = (today - event_date).days
|
|
337
|
+
|
|
338
|
+
# 1. Fetch Solar Wind (if within 7 days)
|
|
339
|
+
if 0 <= days_ago <= 7:
|
|
340
|
+
try:
|
|
341
|
+
solar_wind = fetch_solar_wind(event_date)
|
|
342
|
+
except Exception as e:
|
|
343
|
+
print(f"Solar wind fetch failed: {e}")
|
|
344
|
+
|
|
345
|
+
# 2. Fetch Kp/F10.7 Indices
|
|
346
|
+
# Fetch from daily files (last 30 days)
|
|
347
|
+
if 0 <= days_ago <= 35:
|
|
348
|
+
# Fetch geomagnetic indices (Kp)
|
|
349
|
+
geo_text = fetch_text("https://services.swpc.noaa.gov/text/daily-geomagnetic-indices.txt")
|
|
350
|
+
if geo_text:
|
|
351
|
+
kp_dict = parse_daily_geomagnetic_indices(geo_text)
|
|
352
|
+
kp_data = kp_dict.get(event_date)
|
|
353
|
+
|
|
354
|
+
# Fetch solar indices (F10.7)
|
|
355
|
+
solar_text = fetch_text("https://services.swpc.noaa.gov/text/daily-solar-indices.txt")
|
|
356
|
+
if solar_text:
|
|
357
|
+
f107_dict = parse_daily_solar_indices(solar_text)
|
|
358
|
+
f107_data = f107_dict.get(event_date)
|
|
359
|
+
|
|
360
|
+
return SolarConditions(
|
|
361
|
+
event_date=event_date,
|
|
362
|
+
kp_index=kp_data,
|
|
363
|
+
f107_flux=f107_data,
|
|
364
|
+
is_historical=days_ago > 0,
|
|
365
|
+
data_source=data_source,
|
|
366
|
+
solar_wind=solar_wind,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# For older data, try FTP archives
|
|
370
|
+
if days_ago > 35:
|
|
371
|
+
try:
|
|
372
|
+
kp_data, f107_data = fetch_historical_from_ftp(event_date)
|
|
373
|
+
if kp_data or f107_data:
|
|
374
|
+
return SolarConditions(
|
|
375
|
+
event_date=event_date,
|
|
376
|
+
kp_index=kp_data,
|
|
377
|
+
f107_flux=f107_data,
|
|
378
|
+
is_historical=True,
|
|
379
|
+
data_source="NOAA SWPC FTP Archive",
|
|
380
|
+
)
|
|
381
|
+
except Exception as e:
|
|
382
|
+
print(f"FTP fetch error: {e}")
|
|
383
|
+
|
|
384
|
+
# Fallback / No data found
|
|
385
|
+
return SolarConditions(
|
|
386
|
+
event_date=event_date,
|
|
387
|
+
kp_index=None,
|
|
388
|
+
f107_flux=None,
|
|
389
|
+
is_historical=True,
|
|
390
|
+
data_source="Data not available",
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# Cache for FTP data to avoid repeated connections
|
|
395
|
+
_ftp_cache = {}
|
|
396
|
+
|
|
397
|
+
def fetch_historical_from_ftp(event_date: date) -> Tuple[Optional[KpIndexData], Optional[F107FluxData]]:
|
|
398
|
+
"""
|
|
399
|
+
Fetch historical data from SWPC FTP archives.
|
|
400
|
+
|
|
401
|
+
Files are typically:
|
|
402
|
+
- YYYYQx_DGD.txt (Geomagnetic)
|
|
403
|
+
- YYYYQx_DSD.txt (Solar/F10.7)
|
|
404
|
+
|
|
405
|
+
Legacy might be YYYY_DGD.txt
|
|
406
|
+
"""
|
|
407
|
+
from ftplib import FTP
|
|
408
|
+
import io
|
|
409
|
+
|
|
410
|
+
year = event_date.year
|
|
411
|
+
quarter = (event_date.month - 1) // 3 + 1
|
|
412
|
+
|
|
413
|
+
# Try quarterly format first, then yearly
|
|
414
|
+
prefixes = [f"{year}Q{quarter}", f"{year}"]
|
|
415
|
+
|
|
416
|
+
kp_data = None
|
|
417
|
+
f107_data = None
|
|
418
|
+
|
|
419
|
+
# Check cache first
|
|
420
|
+
cache_key_kp = f"{year}_Q{quarter}_Kp"
|
|
421
|
+
cache_key_solar = f"{year}_Q{quarter}_Solar"
|
|
422
|
+
|
|
423
|
+
if cache_key_kp in _ftp_cache:
|
|
424
|
+
kp_dict = _ftp_cache[cache_key_kp]
|
|
425
|
+
kp_data = kp_dict.get(event_date)
|
|
426
|
+
else:
|
|
427
|
+
# Need to fetch Kp
|
|
428
|
+
pass # Will fetch below
|
|
429
|
+
|
|
430
|
+
if cache_key_solar in _ftp_cache:
|
|
431
|
+
f107_dict = _ftp_cache[cache_key_solar]
|
|
432
|
+
f107_data = f107_dict.get(event_date)
|
|
433
|
+
|
|
434
|
+
# If we have both from cache, return
|
|
435
|
+
if kp_data and f107_data:
|
|
436
|
+
return kp_data, f107_data
|
|
437
|
+
|
|
438
|
+
# If missing, fetch from FTP
|
|
439
|
+
try:
|
|
440
|
+
ftp = FTP('ftp.swpc.noaa.gov')
|
|
441
|
+
ftp.login()
|
|
442
|
+
ftp.cwd('pub/indices/old_indices')
|
|
443
|
+
|
|
444
|
+
# Fetch Geomagnetic Data (DGD) if needed
|
|
445
|
+
if cache_key_kp not in _ftp_cache:
|
|
446
|
+
dgd_content = None
|
|
447
|
+
for prefix in prefixes:
|
|
448
|
+
filename = f"{prefix}_DGD.txt"
|
|
449
|
+
try:
|
|
450
|
+
# Download file
|
|
451
|
+
bio = io.BytesIO()
|
|
452
|
+
ftp.retrbinary(f"RETR {filename}", bio.write)
|
|
453
|
+
dgd_content = bio.getvalue().decode('utf-8', errors='ignore')
|
|
454
|
+
break # Success
|
|
455
|
+
except Exception:
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
if dgd_content:
|
|
459
|
+
kp_dict = parse_daily_geomagnetic_indices(dgd_content)
|
|
460
|
+
_ftp_cache[cache_key_kp] = kp_dict
|
|
461
|
+
kp_data = kp_dict.get(event_date)
|
|
462
|
+
|
|
463
|
+
# Fetch Solar Data (DSD) if needed
|
|
464
|
+
if cache_key_solar not in _ftp_cache:
|
|
465
|
+
dsd_content = None
|
|
466
|
+
for prefix in prefixes:
|
|
467
|
+
filename = f"{prefix}_DSD.txt"
|
|
468
|
+
try:
|
|
469
|
+
# Download file
|
|
470
|
+
bio = io.BytesIO()
|
|
471
|
+
ftp.retrbinary(f"RETR {filename}", bio.write)
|
|
472
|
+
dsd_content = bio.getvalue().decode('utf-8', errors='ignore')
|
|
473
|
+
break # Success
|
|
474
|
+
except Exception:
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
if dsd_content:
|
|
478
|
+
f107_dict = parse_daily_solar_indices(dsd_content)
|
|
479
|
+
_ftp_cache[cache_key_solar] = f107_dict
|
|
480
|
+
f107_data = f107_dict.get(event_date)
|
|
481
|
+
|
|
482
|
+
ftp.quit()
|
|
483
|
+
|
|
484
|
+
except Exception as e:
|
|
485
|
+
print(f"FTP connection failed: {e}")
|
|
486
|
+
return None, None
|
|
487
|
+
|
|
488
|
+
return kp_data, f107_data
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# Legacy function for backwards compatibility
|
|
492
|
+
def fetch_current_conditions() -> SolarConditions:
|
|
493
|
+
"""
|
|
494
|
+
Fetch current solar conditions (today's data).
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
SolarConditions object with today's data
|
|
498
|
+
"""
|
|
499
|
+
return fetch_conditions_for_date(date.today())
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
if __name__ == "__main__":
|
|
503
|
+
from datetime import date, timedelta
|
|
504
|
+
|
|
505
|
+
# Test with a recent date
|
|
506
|
+
test_date = date.today() - timedelta(days=3)
|
|
507
|
+
print(f"Fetching solar conditions for {test_date}...")
|
|
508
|
+
|
|
509
|
+
conditions = fetch_conditions_for_date(test_date)
|
|
510
|
+
|
|
511
|
+
print(f"\nDate: {conditions.event_date}")
|
|
512
|
+
print(f"Data Source: {conditions.data_source}")
|
|
513
|
+
print(f"Summary: {conditions.summary}")
|
|
514
|
+
|
|
515
|
+
if conditions.kp_index:
|
|
516
|
+
kp = conditions.kp_index
|
|
517
|
+
print(f"\nKp Index:")
|
|
518
|
+
print(f" Ap: {kp.ap_value}")
|
|
519
|
+
print(f" Kp max: {kp.kp_max:.1f}")
|
|
520
|
+
print(f" Kp values: {kp.kp_values}")
|
|
521
|
+
print(f" Storm Level: {kp.storm_level}")
|
|
522
|
+
|
|
523
|
+
if conditions.f107_flux:
|
|
524
|
+
f107 = conditions.f107_flux
|
|
525
|
+
print(f"\nF10.7 Radio Flux:")
|
|
526
|
+
print(f" Value: {f107.flux_value:.1f} sfu")
|
|
527
|
+
print(f" Sunspot #: {f107.sunspot_number}")
|
|
528
|
+
print(f" Activity: {f107.activity_level}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Solar Data Downloader Package
|
|
3
|
+
|
|
4
|
+
This package provides tools for downloading and processing solar data from various observatories:
|
|
5
|
+
- SDO/AIA (Atmospheric Imaging Assembly)
|
|
6
|
+
- SDO/HMI (Helioseismic and Magnetic Imager)
|
|
7
|
+
- IRIS (Interface Region Imaging Spectrograph)
|
|
8
|
+
- SOHO (Solar and Heliospheric Observatory)
|
|
9
|
+
|
|
10
|
+
The package includes:
|
|
11
|
+
- Core downloader module (solar_data_downloader.py)
|
|
12
|
+
- Command-line interface (solar_data_downloader_cli.py)
|
|
13
|
+
- Graphical user interface (solar_data_downloader_gui.py)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .solar_data_downloader import (
|
|
17
|
+
download_aia,
|
|
18
|
+
download_aia_with_fido,
|
|
19
|
+
download_hmi,
|
|
20
|
+
download_hmi_with_fido,
|
|
21
|
+
download_iris,
|
|
22
|
+
download_soho,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from .solar_data_downloader_gui import launch_gui
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"download_aia",
|
|
29
|
+
"download_aia_with_fido",
|
|
30
|
+
"download_hmi",
|
|
31
|
+
"download_hmi_with_fido",
|
|
32
|
+
"download_iris",
|
|
33
|
+
"download_soho",
|
|
34
|
+
"launch_gui",
|
|
35
|
+
]
|