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,293 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from matplotlib.colors import Normalize
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SqrtNorm(Normalize):
|
|
7
|
+
"""
|
|
8
|
+
Square root normalization for enhancing faint features.
|
|
9
|
+
|
|
10
|
+
Applies sqrt(x) scaling which compresses bright values and enhances faint ones.
|
|
11
|
+
Commonly used in solar EUV imaging.
|
|
12
|
+
"""
|
|
13
|
+
def __call__(self, value, clip=None):
|
|
14
|
+
normed = super().__call__(value, clip)
|
|
15
|
+
# Clip to avoid sqrt of negative values (can happen with edge cases)
|
|
16
|
+
normed = np.clip(normed, 0, 1)
|
|
17
|
+
# Handle masked arrays
|
|
18
|
+
if hasattr(normed, 'mask'):
|
|
19
|
+
result = np.ma.sqrt(normed)
|
|
20
|
+
else:
|
|
21
|
+
result = np.sqrt(normed)
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AsinhNorm(Normalize):
|
|
26
|
+
"""
|
|
27
|
+
Inverse hyperbolic sine (arcsinh) normalization.
|
|
28
|
+
|
|
29
|
+
Similar to logarithmic scaling but handles zero and negative values gracefully.
|
|
30
|
+
Excellent for high dynamic range solar images like AIA EUV channels.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
linear_width : float, optional
|
|
35
|
+
Controls the transition between linear and logarithmic behavior.
|
|
36
|
+
Smaller values = more compression of bright features. Default is 1e-3.
|
|
37
|
+
"""
|
|
38
|
+
def __init__(self, vmin=None, vmax=None, clip=False, linear_width=1e-3):
|
|
39
|
+
super().__init__(vmin, vmax, clip)
|
|
40
|
+
# Validate linear_width
|
|
41
|
+
if linear_width <= 0:
|
|
42
|
+
warnings.warn("linear_width must be positive, using default 1e-3")
|
|
43
|
+
linear_width = 1e-3
|
|
44
|
+
self.linear_width = linear_width
|
|
45
|
+
|
|
46
|
+
def __call__(self, value, clip=None):
|
|
47
|
+
normed = super().__call__(value, clip)
|
|
48
|
+
# Protect against division by zero
|
|
49
|
+
with np.errstate(invalid='ignore', divide='ignore'):
|
|
50
|
+
denominator = np.arcsinh(1.0 / self.linear_width)
|
|
51
|
+
if denominator == 0:
|
|
52
|
+
return normed # Fall back to linear
|
|
53
|
+
result = np.arcsinh(normed / self.linear_width) / denominator
|
|
54
|
+
# Handle any NaN that might result
|
|
55
|
+
if hasattr(result, 'mask'):
|
|
56
|
+
return result
|
|
57
|
+
return np.nan_to_num(result, nan=0.0, posinf=1.0, neginf=0.0)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PowerNorm(Normalize):
|
|
61
|
+
"""
|
|
62
|
+
Power-law (gamma) normalization.
|
|
63
|
+
|
|
64
|
+
Applies x^gamma scaling. Gamma < 1 enhances faint features,
|
|
65
|
+
gamma > 1 enhances bright features.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
gamma : float, optional
|
|
70
|
+
The power exponent. Default is 1.0 (linear).
|
|
71
|
+
Common values: 0.5 (sqrt-like), 0.3 (strong faint enhancement)
|
|
72
|
+
"""
|
|
73
|
+
def __init__(self, vmin=None, vmax=None, clip=False, gamma=1.0):
|
|
74
|
+
super().__init__(vmin, vmax, clip)
|
|
75
|
+
# Validate gamma
|
|
76
|
+
if gamma <= 0:
|
|
77
|
+
warnings.warn("gamma must be positive, using default 1.0")
|
|
78
|
+
gamma = 1.0
|
|
79
|
+
self.gamma = gamma
|
|
80
|
+
|
|
81
|
+
def __call__(self, value, clip=None):
|
|
82
|
+
normed = super().__call__(value, clip)
|
|
83
|
+
# Clip to ensure non-negative values for power operation
|
|
84
|
+
normed = np.clip(normed, 0, 1)
|
|
85
|
+
# Use safe power operation
|
|
86
|
+
with np.errstate(invalid='ignore'):
|
|
87
|
+
result = np.power(normed, self.gamma)
|
|
88
|
+
return np.nan_to_num(result, nan=0.0, posinf=1.0, neginf=0.0)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ZScaleNorm(Normalize):
|
|
92
|
+
"""
|
|
93
|
+
ZScale normalization using iterative sigma clipping (IRAF algorithm).
|
|
94
|
+
|
|
95
|
+
Automatically determines optimal display range by fitting a line to
|
|
96
|
+
sorted pixel values and rejecting outliers. Widely used in astronomy.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
contrast : float, optional
|
|
101
|
+
Contrast parameter that adjusts the slope (default is 0.25).
|
|
102
|
+
num_samples : int, optional
|
|
103
|
+
Number of samples to use if the image is large (default is 600).
|
|
104
|
+
max_iterations : int, optional
|
|
105
|
+
Maximum iterations for sigma clipping (default is 5).
|
|
106
|
+
krej : float, optional
|
|
107
|
+
Rejection threshold in units of sigma (default is 2.5).
|
|
108
|
+
min_npixels : int, optional
|
|
109
|
+
Minimum number of pixels required to continue fitting (default is 5).
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
vmin=None,
|
|
115
|
+
vmax=None,
|
|
116
|
+
clip=False,
|
|
117
|
+
contrast=0.25,
|
|
118
|
+
num_samples=600,
|
|
119
|
+
max_iterations=5,
|
|
120
|
+
krej=2.5,
|
|
121
|
+
min_npixels=5,
|
|
122
|
+
):
|
|
123
|
+
super().__init__(vmin, vmax, clip)
|
|
124
|
+
# Validate parameters
|
|
125
|
+
self.contrast = max(0.01, contrast) # Prevent division by zero
|
|
126
|
+
self.num_samples = max(10, num_samples)
|
|
127
|
+
self.max_iterations = max(1, max_iterations)
|
|
128
|
+
self.krej = max(0.5, krej)
|
|
129
|
+
self.min_npixels = max(2, min_npixels)
|
|
130
|
+
self._zscale_computed = False
|
|
131
|
+
self._zmin = None
|
|
132
|
+
self._zmax = None
|
|
133
|
+
|
|
134
|
+
def _compute_zscale(self, data):
|
|
135
|
+
"""Compute ZScale limits using iterative sigma clipping."""
|
|
136
|
+
try:
|
|
137
|
+
# Flatten data and remove NaNs/Infs
|
|
138
|
+
flat_data = np.asarray(data).flatten()
|
|
139
|
+
mask = np.isfinite(flat_data)
|
|
140
|
+
flat_data = flat_data[mask]
|
|
141
|
+
|
|
142
|
+
if flat_data.size == 0:
|
|
143
|
+
return 0, 1
|
|
144
|
+
|
|
145
|
+
if flat_data.size < self.min_npixels:
|
|
146
|
+
return float(np.min(flat_data)), float(np.max(flat_data))
|
|
147
|
+
|
|
148
|
+
# Sample the data if necessary
|
|
149
|
+
if flat_data.size > self.num_samples:
|
|
150
|
+
indices = np.linspace(0, flat_data.size - 1, self.num_samples).astype(int)
|
|
151
|
+
samples = np.sort(flat_data[indices])
|
|
152
|
+
else:
|
|
153
|
+
samples = np.sort(flat_data)
|
|
154
|
+
|
|
155
|
+
# Iterative sigma clipping
|
|
156
|
+
for _ in range(self.max_iterations):
|
|
157
|
+
if len(samples) < self.min_npixels:
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
x = np.arange(len(samples))
|
|
161
|
+
try:
|
|
162
|
+
slope, intercept = np.polyfit(x, samples, 1)
|
|
163
|
+
except (np.linalg.LinAlgError, ValueError):
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
fitted = slope * x + intercept
|
|
167
|
+
residuals = samples - fitted
|
|
168
|
+
sigma = np.std(residuals)
|
|
169
|
+
|
|
170
|
+
if sigma == 0:
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
mask = np.abs(residuals) < self.krej * sigma
|
|
174
|
+
if np.sum(mask) < self.min_npixels:
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
new_samples = samples[mask]
|
|
178
|
+
if new_samples.size == samples.size:
|
|
179
|
+
break
|
|
180
|
+
samples = new_samples
|
|
181
|
+
|
|
182
|
+
# Adjust slope by contrast parameter
|
|
183
|
+
if slope != 0:
|
|
184
|
+
slope /= self.contrast
|
|
185
|
+
|
|
186
|
+
# Use median as pivot
|
|
187
|
+
median_index = len(samples) // 2
|
|
188
|
+
median_val = samples[median_index]
|
|
189
|
+
|
|
190
|
+
# Compute zscale limits
|
|
191
|
+
zmin = median_val - slope * median_index
|
|
192
|
+
zmax = median_val + slope * (len(samples) - median_index)
|
|
193
|
+
|
|
194
|
+
# Ensure limits are within data range
|
|
195
|
+
zmin = max(zmin, samples[0])
|
|
196
|
+
zmax = min(zmax, samples[-1])
|
|
197
|
+
|
|
198
|
+
# Ensure zmin < zmax
|
|
199
|
+
if zmin >= zmax:
|
|
200
|
+
zmin = samples[0]
|
|
201
|
+
zmax = samples[-1]
|
|
202
|
+
|
|
203
|
+
return float(zmin), float(zmax)
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
warnings.warn(f"ZScale computation failed: {e}, using min/max")
|
|
207
|
+
flat = np.asarray(data).flatten()
|
|
208
|
+
flat = flat[np.isfinite(flat)]
|
|
209
|
+
if flat.size > 0:
|
|
210
|
+
return float(np.min(flat)), float(np.max(flat))
|
|
211
|
+
return 0, 1
|
|
212
|
+
|
|
213
|
+
def __call__(self, value, clip=None):
|
|
214
|
+
if not self._zscale_computed and isinstance(value, np.ndarray):
|
|
215
|
+
self._zmin, self._zmax = self._compute_zscale(value)
|
|
216
|
+
self._zscale_computed = True
|
|
217
|
+
self.vmin = self._zmin
|
|
218
|
+
self.vmax = self._zmax
|
|
219
|
+
return super().__call__(value, clip)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class HistEqNorm(Normalize):
|
|
223
|
+
"""
|
|
224
|
+
Histogram equalization normalization.
|
|
225
|
+
|
|
226
|
+
Enhances contrast by redistributing intensity values so that the
|
|
227
|
+
cumulative distribution function becomes approximately linear.
|
|
228
|
+
Useful for images with poor contrast or non-uniform intensity distribution.
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
n_bins : int, optional
|
|
233
|
+
Number of bins for the histogram (default is 256).
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def __init__(self, vmin=None, vmax=None, clip=False, n_bins=256):
|
|
237
|
+
super().__init__(vmin, vmax, clip)
|
|
238
|
+
self.n_bins = max(16, min(4096, n_bins)) # Clamp to reasonable range
|
|
239
|
+
self._hist_eq_computed = False
|
|
240
|
+
self._hist_eq_map = None
|
|
241
|
+
|
|
242
|
+
def _compute_hist_eq(self, data):
|
|
243
|
+
"""Compute histogram equalization mapping."""
|
|
244
|
+
try:
|
|
245
|
+
flat_data = np.asarray(data).flatten()
|
|
246
|
+
flat_data = flat_data[np.isfinite(flat_data)]
|
|
247
|
+
|
|
248
|
+
if flat_data.size == 0:
|
|
249
|
+
return np.linspace(0, 1, self.n_bins), np.linspace(0, 1, self.n_bins)
|
|
250
|
+
|
|
251
|
+
# Compute histogram and CDF
|
|
252
|
+
hist, bin_edges = np.histogram(flat_data, bins=self.n_bins)
|
|
253
|
+
|
|
254
|
+
# Avoid division by zero
|
|
255
|
+
hist_sum = hist.sum()
|
|
256
|
+
if hist_sum == 0:
|
|
257
|
+
return np.linspace(0, 1, self.n_bins), np.linspace(0, 1, self.n_bins)
|
|
258
|
+
|
|
259
|
+
cdf = hist.cumsum() / hist_sum
|
|
260
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
261
|
+
|
|
262
|
+
return bin_centers, cdf
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
warnings.warn(f"Histogram equalization failed: {e}")
|
|
266
|
+
return np.linspace(0, 1, self.n_bins), np.linspace(0, 1, self.n_bins)
|
|
267
|
+
|
|
268
|
+
def __call__(self, value, clip=None):
|
|
269
|
+
try:
|
|
270
|
+
if not self._hist_eq_computed and isinstance(value, np.ndarray):
|
|
271
|
+
normed = super().__call__(value, clip)
|
|
272
|
+
bin_centers, cdf = self._compute_hist_eq(normed)
|
|
273
|
+
self._hist_eq_map = (bin_centers, cdf)
|
|
274
|
+
self._hist_eq_computed = True
|
|
275
|
+
return self._apply_hist_eq(normed, bin_centers, cdf)
|
|
276
|
+
elif self._hist_eq_computed and self._hist_eq_map is not None:
|
|
277
|
+
normed = super().__call__(value, clip)
|
|
278
|
+
bin_centers, cdf = self._hist_eq_map
|
|
279
|
+
return self._apply_hist_eq(normed, bin_centers, cdf)
|
|
280
|
+
else:
|
|
281
|
+
return super().__call__(value, clip)
|
|
282
|
+
except Exception:
|
|
283
|
+
return super().__call__(value, clip)
|
|
284
|
+
|
|
285
|
+
def _apply_hist_eq(self, normed, bin_centers, cdf):
|
|
286
|
+
"""Apply histogram equalization mapping."""
|
|
287
|
+
if np.isscalar(normed):
|
|
288
|
+
idx = np.abs(bin_centers - normed).argmin()
|
|
289
|
+
return cdf[idx]
|
|
290
|
+
else:
|
|
291
|
+
normed_flat = np.asarray(normed).flatten()
|
|
292
|
+
result = np.interp(normed_flat, bin_centers, cdf)
|
|
293
|
+
return result.reshape(normed.shape)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Radio Data Downloader Package
|
|
3
|
+
|
|
4
|
+
This package provides tools for downloading and processing radio solar data from various observatories:
|
|
5
|
+
- Learmonth Solar Observatory (Australia)
|
|
6
|
+
|
|
7
|
+
The package includes:
|
|
8
|
+
- Core downloader module (radio_data_downloader.py)
|
|
9
|
+
- Graphical user interface (radio_data_downloader_gui.py)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .radio_data_downloader import (
|
|
13
|
+
download_learmonth,
|
|
14
|
+
srs_to_dataframe,
|
|
15
|
+
dataframe_to_fits,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .radio_data_downloader_gui import launch_gui
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"download_learmonth",
|
|
22
|
+
"srs_to_dataframe",
|
|
23
|
+
"dataframe_to_fits",
|
|
24
|
+
"launch_gui",
|
|
25
|
+
]
|