Skip to content

Commit

Permalink
Merge pull request #12 from jmbhughes/v1.0
Browse files Browse the repository at this point in the history
V1.0
  • Loading branch information
jmbhughes authored Jul 16, 2023
2 parents d765cdc + 6f036da commit 849ccbc
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 11 deletions.
45 changes: 45 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: CI

on:
push:
pull_request:
schedule:
- cron: '0 8 * * *'

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10"]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest pytest-cov hypothesis
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pip install .
pytest --cov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
fail_ci_if_error: true
verbose: true
13 changes: 13 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
pyqt5 >= 5.15.9
matplotlib >= 3.7.2
astropy >= 5.3.1
numpy >= 1.25.1
goes-solar-retriever >= 0.4.0
scipy >= 1.11.1
scikit-image >= 0.21.0
pillow >= 10.0.0
sunpy >= 5.0.0
lxml >= 4.9.3
reproject >= 0.11.0
zeep >= 4.2.1
drms >= 0.6.4
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
name='solarannotator',
long_description=long_description,
long_description_content_type='text/markdown',
version='0.3.1',
version='1.0.0',
packages=['solarannotator'],
url='',
license='',
Expand Down
17 changes: 12 additions & 5 deletions solarannotator/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import PyQt5
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QWidget, QLabel, QAction, QTabWidget, QPushButton, QFileDialog, QRadioButton, QMessageBox, \
QComboBox, QLineEdit, QSizePolicy
QComboBox, QLineEdit, QSizePolicy, QCheckBox
from PyQt5.QtCore import QDateTime
from PyQt5.QtGui import QIcon, QDoubleValidator
from datetime import datetime, timedelta
Expand All @@ -18,6 +18,8 @@
from matplotlib.backends.backend_qt5agg import FigureCanvas, NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure

from solarannotator.template import create_thmap_template

if hasattr(QtCore.Qt, 'AA_EnableHighDpiScaling'):
PyQt5.QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)

Expand Down Expand Up @@ -69,7 +71,7 @@ def __init__(self, config):
self.pix = np.vstack((xv.flatten(), yv.flatten())).T

lineprops = dict(color="red", linewidth=2)
self.lasso = LassoSelector(self.axs[0], self.onlasso, lineprops=lineprops)
self.lasso = LassoSelector(self.axs[0], self.onlasso, props=lineprops)
self.fig.tight_layout()

def onlasso(self, verts):
Expand Down Expand Up @@ -209,7 +211,7 @@ def clearBoundaries(self):
self.region_patches = []
self.fig.canvas.draw_idle()

def loadThematicMap(self, thmap):
def loadThematicMap(self, thmap, template=True):
try:
download_message = QMessageBox.information(self,
'Downloading',
Expand All @@ -220,9 +222,11 @@ def loadThematicMap(self, thmap):
self.data_does_not_exist_popup()
else:
self.thmap = thmap
if template:
self.thmap = create_thmap_template(self.composites)
self.thmap.copy_195_metadata(self.composites)
self.history = [thmap.data.copy()]
self.thmap_data = thmap.data
self.thmap_data = self.thmap.data
self.thmap_axesimage.set_data(self.thmap_data)
self.preview_axesimage.set_data(self.composites['94'].data)
self.fig.canvas.draw_idle()
Expand Down Expand Up @@ -479,9 +483,12 @@ def initUI(self):
layout = QtWidgets.QHBoxLayout()
instructions = QLabel("Please select a time for the new file.", self)
self.dateEdit = QtWidgets.QDateTimeEdit(QDateTime.currentDateTime())
self.template_option = QCheckBox("Use template")
self.template_option.setChecked(True)
submit_button = QPushButton("Submit")
layout.addWidget(instructions)
layout.addWidget(self.dateEdit)
layout.addWidget(self.template_option)
layout.addWidget(submit_button)
self.setLayout(layout)
submit_button.clicked.connect(self.onSubmit)
Expand All @@ -493,7 +500,7 @@ def onSubmit(self):
{'DATE-OBS': str(self.parent.date),
'DATE': str(datetime.today())},
self.parent.config.solar_class_name)
self.parent.annotator.loadThematicMap(new_thmap)
self.parent.annotator.loadThematicMap(new_thmap, self.template_option.isChecked())
self.parent.controls.onTabChange() # Us
self.close()
self.parent.setWindowTitle("SolarAnnotator: {}".format(new_thmap.date_obs))
Expand Down
53 changes: 48 additions & 5 deletions solarannotator/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import sunpy.map
from sunpy.coordinates import Helioprojective


Image = namedtuple('Image', 'data header')


Expand All @@ -29,9 +28,9 @@ def retrieve(date):
@staticmethod
def _load_gong_image(date, suvi_195_image):
# Find an image and download it
results = Fido.search(a.Time(date-timedelta(hours=1), date+timedelta(hours=1)),
results = Fido.search(a.Time(date - timedelta(hours=1), date + timedelta(hours=1)),
a.Wavelength(6563 * u.Angstrom), a.Source("GONG"))
selection = results[0][len(results[0])//2] # only download the middle image
selection = results[0][len(results[0]) // 2] # only download the middle image
downloads = Fido.fetch(selection)
with fits.open(downloads[0]) as hdul:
gong_data = hdul[1].data
Expand All @@ -56,7 +55,6 @@ def _load_gong_image(date, suvi_195_image):

return Image(out.data, dict(out.meta))


@staticmethod
def _load_suvi_composites(date):
satellite = Satellite.GOES16
Expand All @@ -77,7 +75,6 @@ def _load_suvi_composites(date):
os.remove(fn)
return composites


@staticmethod
def create_empty():
mapping = {"94": Image(np.zeros((1280, 1280)), {}),
Expand All @@ -95,6 +92,52 @@ def __getitem__(self, key):
def channels(self):
return list(self.images.keys())

def get_solar_radius(self, channel="304", refine=True):
"""
Gets the solar radius from the header of the specified channel
:param channel: channel to get radius from
:param refine: whether to refine the metadata radius to better approximate the edge
:return: solar radius specified in the header
"""

# Return the solar radius
if channel not in self.channels():
raise RuntimeError("Channel requested must be one of {}".format(self.channels()))
try:
solar_radius = self.images[channel].header['DIAM_SUN'] / 2
if refine:
composite_img = self.images[channel].data
# Determine image size
image_size = np.shape(composite_img)[0]
# Find center and radial mesh grid
center = (image_size / 2) - 0.5
xm, ym = np.meshgrid(np.linspace(0, image_size - 1, num=image_size),
np.linspace(0, image_size - 1, num=image_size))
xm_c = xm - center
ym_c = ym - center
rads = np.sqrt(xm_c ** 2 + ym_c ** 2)
# Iterate through radii within a range past the solar radius
accuracy = 15
rad_iterate = np.linspace(solar_radius, solar_radius + 50, num=accuracy)
img_avgs = []
for rad in rad_iterate:
# Create a temporary solar image corresponding to the layer
solar_layer = np.zeros((image_size, image_size))
# Find indices in mask of the layer
indx_layer = np.where(rad >= rads)
# Set temporary image corresponding to indices to solar image values
solar_layer[indx_layer] = composite_img[indx_layer]
# Appends average to image averages
img_avgs.append(np.mean(solar_layer))
# Find "drop off" where mask causes average image brightness to drop
diff_avgs = np.asarray(img_avgs[0:accuracy - 1]) - np.asarray(img_avgs[1:accuracy])
# Return the radius that best represents the edge of the sun
solar_radius = rad_iterate[np.where(np.amax(diff_avgs))[0] + 1]
except KeyError:
raise RuntimeError("Header does not include the solar diameter or radius")
else:
return solar_radius


class ThematicMap:
def __init__(self, data, metadata, theme_mapping):
Expand Down
74 changes: 74 additions & 0 deletions solarannotator/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from solarannotator.io import ImageSet, ThematicMap
from datetime import datetime, timedelta
import numpy as np


def create_mask(radius, image_size):
"""
Inputs:
- Radius: Radius (pixels) within which a certain theme should be assigned
- Image size: tuple of (x, y) size (pixels) that represents size of image
"""
# Define image center
center_x = (image_size[0] / 2) - 0.5
center_y = (image_size[1] / 2) - 0.5

# Create mesh grid of image coordinates
xm, ym = np.meshgrid(np.linspace(0, image_size[0] - 1, num=image_size[0]),
np.linspace(0, image_size[1] - 1, num=image_size[1]))

# Center each mesh grid (zero at the center)
xm_c = xm - center_x
ym_c = ym - center_y

# Create array of radii
rad = np.sqrt(xm_c ** 2 + ym_c ** 2)

# Create empty mask of same size as the image
mask = np.zeros((image_size[0], image_size[1]))

# Apply the mask as true for anything within a radius
mask[(rad < radius)] = 1

# Return the mask
return mask.astype('bool')


def create_thmap_template(image_set, limb_thickness=10):
"""
Input: Image set object as input, and limb thickness in pixels
Output: thematic map object
Process:
- Get the solar radius with predefined function
- Create empty thematic map
- Define concentric layers separated by solar radius + limb thickness, and create thmap
- Return the thematic map object
Note copied from Alison Jarvis
"""

# Get the solar radius with class function
solar_radius = image_set.get_solar_radius()

# Define end of disk and end of limb radii
disk_radius = solar_radius - (limb_thickness / 2)
limb_radius = solar_radius + (limb_thickness / 2)

# Create concentric layers for disk, limb, and outer space
# First template layer, outer space (value 1) with same size as composites
imagesize = np.shape(image_set['171'].data)
thmap_data = np.ones(imagesize)
# Mask out the limb (value 8)
limb_mask = create_mask(limb_radius, imagesize)
thmap_data[limb_mask] = 8
# Mask out the disk with quiet sun (value 7)
qs_mask = create_mask(disk_radius, imagesize)
thmap_data[qs_mask] = 7

# Create a thematic map object with this data and return it
theme_mapping = {1: 'outer_space', 3: 'bright_region', 4: 'filament', 5: 'prominence', 6: 'coronal_hole',
7: 'quiet_sun', 8: 'limb', 9: 'flare'}
thmap_template = ThematicMap(thmap_data, {'DATE-OBS': image_set['171'].header['DATE-OBS']}, theme_mapping)

# Return the thematic map object
return thmap_template
7 changes: 7 additions & 0 deletions tests/test_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from solarannotator.template import create_mask


def test_mask_creation():
mask = create_mask(500, (2048, 2048))
assert not mask[0, 0]
assert mask[1024, 1024]

0 comments on commit 849ccbc

Please sign in to comment.