Skip to content

Commit

Permalink
WIP Run Launcher widget
Browse files Browse the repository at this point in the history
  • Loading branch information
mwcraig committed Jul 16, 2024
1 parent 796d55c commit a7bd40c
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 62 deletions.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies = [
"ipywidgets",
"jupyter-app-launcher",
"matplotlib",
"papermill",
"pandas",
"photutils >=1.9",
"pydantic >=2",
Expand Down Expand Up @@ -178,4 +179,6 @@ filterwarnings = [
'ignore:RADECSYS=:',
# photutils changed the name of a function again
'ignore:The make_gaussian_sources_image function is deprecated',
# papermill is using deprecated jupyter paths
'ignore:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning',
]
82 changes: 20 additions & 62 deletions stellarphot/notebooks/run-launcher.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,38 @@
"cells": [
{
"cell_type": "code",
"execution_count": null,
"execution_count": 1,
"id": "e5f2fc8f-b893-400e-8fb7-4dd8e871334f",
"metadata": {},
"outputs": [],
"source": [
"import ipywidgets as ipw\n",
"import papermill as pm\n",
"\n",
"from ipyautoui.custom import FileChooser\n",
"\n",
"from stellarphot.gui_tools import FitsOpener\n",
"from stellarphot.settings.custom_widgets import Confirm\n",
"from stellarphot.settings import PhotometryRunSettings"
"from stellarphot.settings.custom_widgets import Kaboodle"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2389ff77-c8ac-4bad-afb7-79cdd5c0f365",
"metadata": {},
"outputs": [],
"source": [
"class Kaboodle(ipw.VBox):\n",
" def __init__(self, *args, **kwargs):\n",
" super().__init__(*args, **kwargs)\n",
" self.fo = FitsOpener(title=\"Choose any image in the folder of images to do photometry on that contains the object of interest\")\n",
" self.info_box = ipw.HTML()\n",
" self.run_output = ipw.Output()\n",
" self.confirm = Confirm(message=\"Is this correct?\")\n",
" self.children = (self.fo.file_chooser, self.info_box, self.confirm, self.run_output)\n",
" self.fo.file_chooser.observe(self._file_chosen, \"_value\")\n",
" self.confirm.observe(self._confirmation, \"value\")\n",
" self.run_settings = None\n",
"\n",
" def _file_chosen(self, change=None):\n",
" self.run_settings = PhotometryRunSettings(\n",
" directory_with_images=self.fo.path.parent, \n",
" object_of_interest=self.fo.header['object']\n",
" )\n",
" self.info_box.value = \"<h2>\" + self.info_message + \"</br>Is this correct?\" + \"</h2>\"\n",
" self.confirm.show()\n",
"\n",
" @property\n",
" def info_message(self):\n",
" return (f\"Photomery will be done on all images of the object \"\n",
" f\"'<code>{self.run_settings.object_of_interest}</code>' in the \"\n",
" f\"folder '<code>{self.run_settings.directory_with_images}</code>'\")\n",
" \n",
" def _confirmation(self, change=None):\n",
" if change[\"new\"]:\n",
" # User said yes\n",
"\n",
" # Update informational message\n",
" self.info_box.value = \"<h2>\" + self.info_message + \"</br>Photometry is running...\" + \"</h2>\"\n",
" with self.run_output:\n",
" pm.execute_notebook(\n",
" \"stellarphot/notebooks/photometry_runner.ipynb\",\n",
" \"MEEEPhonk.ipynb\",\n",
" parameters=self.run_settings.model_dump(mode=\"json\")\n",
" )\n",
" else:\n",
" # User said no, so reset to initial state.\n",
" self.fo.file_chooser.reset()\n",
" self.info_box.value = \"\"\n",
" self.run_settings = None\n",
" "
]
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 2,
"id": "c5eed8a2-7533-4411-8f6e-303b854f4e3e",
"metadata": {},
"outputs": [],
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "415ea731dc1d42279fcce2ff8a718672",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Kaboodle(children=(FileChooser(path='/Users/mattcraig/development/astronomy/stellarphot/stellarphot/notebooks'…"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"kab = Kaboodle()\n",
"kab"
Expand Down
72 changes: 72 additions & 0 deletions stellarphot/settings/custom_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ class StrEnum(str, Enum):


import ipywidgets as ipw
import papermill as pm
import traitlets as tr
from astropy.utils.data import get_pkg_data_filename
from camel_converter import to_snake
from ipyautoui.autoobject import AutoObject
from ipyautoui.custom.iterable import ItemBox
Expand All @@ -25,10 +27,12 @@ class StrEnum(str, Enum):
Observatory,
PartialPhotometrySettings,
PassbandMap,
PhotometryRunSettings,
PhotometryWorkingDirSettings,
SavedSettings,
ui_generator,
)
from stellarphot.settings.fits_opener import FitsOpener

__all__ = ["ChooseOrMakeNew", "Confirm", "SettingWithTitle"]

Expand Down Expand Up @@ -987,3 +991,71 @@ def save_wd(_=None):
raise ValueError(
f"The widget {setting_widget} is not a recognized type of widget."
)


class PhotometryRunner(ipw.VBox):
def __init__(
self, photometry_notebook_name="photometry_run.ipynb", *args, **kwargs
):
super().__init__(*args, **kwargs)
self.photometry_notebook_name = photometry_notebook_name
self.fo = FitsOpener(
title=(
"Choose any image in the folder of images to do photometry on that "
"contains the object of interest"
)
)
self.info_box = ipw.HTML()
self.run_output = ipw.Output()
self.confirm = Confirm(message="Is this correct?")
self.children = (
self.fo.file_chooser,
self.info_box,
self.confirm,
self.run_output,
)
self.fo.file_chooser.observe(self._file_chosen, "_value")
self.confirm.observe(self._confirmation, "value")
self.run_settings = None

def _file_chosen(self, _):
self.run_settings = PhotometryRunSettings(
directory_with_images=self.fo.path.parent,
object_of_interest=self.fo.header["object"],
)
self.info_box.value = (
"<h2>" + self.info_message + "</br>Is this correct?" + "</h2>"
)
self.confirm.show()

@property
def info_message(self):
return (
f"Photometry will be done on all images of the object "
f"'<code>{self.run_settings.object_of_interest}</code>' in the "
f"folder '<code>{self.run_settings.directory_with_images}</code>'"
)

def _confirmation(self, change=None):
if change["new"]:
# User said yes

# Update informational message
self.info_box.value = (
"<h2>" + self.info_message + "</br>Photometry is running..." + "</h2>"
)
template_nb = get_pkg_data_filename(
"photometry_runner.ipynb", package="stellarphot.notebooks"
)
print(template_nb)
with self.run_output:
pm.execute_notebook(
template_nb,
self.photometry_notebook_name,
parameters=self.run_settings.model_dump(mode="json"),
)
else:
# User said no, so reset to initial state.
self.fo.file_chooser.reset()
self.info_box.value = ""
self.run_settings = None
100 changes: 100 additions & 0 deletions stellarphot/settings/tests/test_photometry_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import os
from pathlib import Path

import ipywidgets as ipw
import pytest
from astropy.io import fits

from stellarphot.settings import (
PhotometrySettings,
PhotometryWorkingDirSettings,
settings_files,
)
from stellarphot.settings.custom_widgets import PhotometryRunner
from stellarphot.settings.tests.test_models import DEFAULT_PHOTOMETRY_SETTINGS


# See test_settings_file.TestSavedSettings for a detailed description of what the
# following fixture does. In brief, it patches the settings_files.PlatformDirs class
# so that the user_data_dir method returns the temporary directory.
@pytest.fixture(autouse=True)
def fake_settings_dir(mocker, tmp_path):
mocker.patch.object(
settings_files.PlatformDirs, "user_data_dir", tmp_path / "stellarphot"
)


class TestPhotometryRunner:

# This auto-used fixture changes the working directory to the temporary directory
# and then changes back to the original directory after the test is done.
@pytest.fixture(autouse=True)
def change_to_tmp_dir(self, tmp_path):
original_dir = os.getcwd()
os.chdir(tmp_path)
# Yielding here is important. It means that when the test is done, the remainder
# of the function will be executed. This is important because the test is run in
# a temporary directory and we want to change back to the original directory
# when the test is done.
yield
os.chdir(original_dir)

def test_photometry_runner_creation(self):
# This test simply makes sure we can create the object
photometry_runner = PhotometryRunner()
assert isinstance(photometry_runner, ipw.Box)

@pytest.mark.parametrize("do_photometry", [True, False])
def test_photometry_runner_with_valid_settings(self, do_photometry):
# This test makes sure that the PhotometryRunner widget can be used
# to *start* a photometry run. It does not check the actual photometry
# or the results of the photometry run, except to ensure that the expected
# files are created.
# Make a settings
phot_settings = PhotometrySettings.model_validate(DEFAULT_PHOTOMETRY_SETTINGS)

# Save a copy to the working directory
wd_settings = PhotometryWorkingDirSettings()
wd_settings.save(phot_settings)

# Make sure a source list file exists
sources = Path(phot_settings.source_location_settings.source_list_file)
sources.touch()

# Make sure a fake fits file exists
fake_fits = Path("fake.fits")
fits_data = fits.PrimaryHDU(data=[[1, 2], [3, 4]])
object_name = "Fake Object"
fits_data.header["object"] = object_name
fits_data.writeto(fake_fits)

photometry_runner = PhotometryRunner()
# Select a fits file
photometry_runner.fo.file_chooser.reset(".", fake_fits.name)
photometry_runner.fo.file_chooser._apply_selection()
photometry_runner.fo.file_chooser.value = fake_fits

# Check that the message in the information box is as expected
assert "Photometry will be done" in photometry_runner.info_box.value
assert object_name in photometry_runner.info_box.value

if do_photometry:
# Run the photometry
photometry_runner.confirm._yes.click()
assert "Photometry is running" in photometry_runner.info_box.value

# Check that the expected file was created, in this case just the notebook.
# No photometry is actually done because the source is empty and the "image"
# has no stars.
assert Path(photometry_runner.photometry_notebook_name).exists()

# Make sure we do get the error we expect, which is generated when
# the source list is read. It is an empty file, not actually an ecsv.
with open(photometry_runner.photometry_notebook_name) as f:
notebook_text = f.read()
assert "InconsistentTableError" in notebook_text
else:
# Cancel the photometry
photometry_runner.confirm._no.click()

assert photometry_runner.info_box.value == ""

0 comments on commit a7bd40c

Please sign in to comment.