diff --git a/pyproject.toml b/pyproject.toml
index 57774442..35fa1fab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,6 +25,7 @@ dependencies = [
"ipywidgets",
"jupyter-app-launcher",
"matplotlib",
+ "papermill",
"pandas",
"photutils >=1.9",
"pydantic >=2",
@@ -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',
]
diff --git a/stellarphot/notebooks/run-launcher.ipynb b/stellarphot/notebooks/run-launcher.ipynb
index 041c0e4d..a831bf6d 100644
--- a/stellarphot/notebooks/run-launcher.ipynb
+++ b/stellarphot/notebooks/run-launcher.ipynb
@@ -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 = \"
\" + self.info_message + \"Is this correct?\" + \"
\"\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\"'{self.run_settings.object_of_interest}
' in the \"\n",
- " f\"folder '{self.run_settings.directory_with_images}
'\")\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 = \"\" + self.info_message + \"Photometry is running...\" + \"
\"\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"
diff --git a/stellarphot/settings/custom_widgets.py b/stellarphot/settings/custom_widgets.py
index dd3b8340..0c6ed7a2 100644
--- a/stellarphot/settings/custom_widgets.py
+++ b/stellarphot/settings/custom_widgets.py
@@ -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
@@ -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"]
@@ -985,3 +989,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 = (
+ "" + self.info_message + "Is this correct?" + "
"
+ )
+ self.confirm.show()
+
+ @property
+ def info_message(self):
+ return (
+ f"Photometry will be done on all images of the object "
+ f"'{self.run_settings.object_of_interest}
' in the "
+ f"folder '{self.run_settings.directory_with_images}
'"
+ )
+
+ def _confirmation(self, change=None):
+ if change["new"]:
+ # User said yes
+
+ # Update informational message
+ self.info_box.value = (
+ "" + self.info_message + "Photometry is running..." + "
"
+ )
+ 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
diff --git a/stellarphot/settings/tests/test_photometry_runner.py b/stellarphot/settings/tests/test_photometry_runner.py
new file mode 100644
index 00000000..7f0e01f8
--- /dev/null
+++ b/stellarphot/settings/tests/test_photometry_runner.py
@@ -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 == ""