{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Corrections\n", "\n", "`phoptic` provides a simple interface for creating and applying image corrections via the `phoptic.correctors` subpackge. Currently, corrections are limited to bias, flat-fields and darks. However, we plan to implement a flexible framework for applying bespoke corrections in future.\n", "\n", "In this notebook, I will demonstrate how to create a correction image, and how this correction image can be incorporated into the reduction process.\n", "\n", "## Flat-fielding\n", "\n", "First, let's take a look at flat-field corrections.\n", "\n", "### Generating Synthetic Data and Flats\n", "\n", "Before applying flat-field corrections, we need some flat-field images. For this tutorial, I will use the `generate_flats()` routine to generate some synethetic flats. I will also generate some synthetic observations using `generate_observations()` with `circular_aperture=True`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:35.050161Z", "iopub.status.busy": "2026-01-23T11:19:35.049956Z", "iopub.status.idle": "2026-01-23T11:19:41.846762Z", "shell.execute_reply": "2026-01-23T11:19:41.846525Z" } }, "outputs": [], "source": [ "from pathlib import Path\n", "\n", "import phoptic\n", "\n", "out_dir = Path('out')\n", "obs_dir = out_dir / 'data' / 'observations'\n", "flats_dir = out_dir / 'calibration_images' / 'flats'\n", "\n", "phoptic.generate_observations(\n", " out_directory=obs_dir,\n", " n_images=10, # number of images per camera\n", " circular_aperture=True, # impose circular aperture shadow on images\n", " )\n", "\n", "phoptic.generate_flats(\n", " out_directory=flats_dir,\n", " n_flats=5, # number of flats per camera\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's check the files in the newly created flats directory:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:41.847744Z", "iopub.status.busy": "2026-01-23T11:19:41.847589Z", "iopub.status.idle": "2026-01-23T11:19:41.850410Z", "shell.execute_reply": "2026-01-23T11:19:41.850250Z" } }, "outputs": [], "source": [ "flat_paths = sorted(list(flats_dir.glob('*')))\n", "\n", "flat_paths" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We have a total of 15 flats in three different filters, matching the three filters of our synthetic observations.\n", "\n", "Let's take a look at one of our newly-generated flats:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:41.851359Z", "iopub.status.busy": "2026-01-23T11:19:41.851159Z", "iopub.status.idle": "2026-01-23T11:19:41.974349Z", "shell.execute_reply": "2026-01-23T11:19:41.974104Z" } }, "outputs": [], "source": [ "from astropy.io import fits\n", "from astropy.visualization import simple_norm\n", "from matplotlib import pyplot as plt\n", "import numpy as np\n", "\n", "\n", "# open the first flat field image\n", "with fits.open(flat_paths[0]) as hdul:\n", " print(repr(hdul[0].header))\n", " flat = np.array(hdul[0].data)\n", "\n", "# plot the flat field image\n", "fig, ax = plt.subplots(tight_layout=True)\n", "\n", "im = ax.imshow(\n", " flat,\n", " norm=simple_norm(\n", " flat,\n", " stretch=\"log\",\n", " ),\n", " origin=\"lower\",\n", " cmap=\"Greys\",\n", " )\n", "\n", "fig.colorbar(im)\n", "\n", "ax.set_xlabel(\"X\")\n", "ax.set_ylabel(\"Y\")\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These synthetic flats show a circular shadow. You might imagine that this shadow was cast by the telescope's circular aperture.\n", "\n", "We see a similar shadow in the synthetic observations:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:41.975312Z", "iopub.status.busy": "2026-01-23T11:19:41.975147Z", "iopub.status.idle": "2026-01-23T11:19:42.102960Z", "shell.execute_reply": "2026-01-23T11:19:42.102664Z" } }, "outputs": [], "source": [ "# get the first image\n", "image_paths = sorted(list(obs_dir.glob('*')))\n", "\n", "# open the image\n", "with fits.open(image_paths[0]) as hdul:\n", " image = np.array(hdul[0].data)\n", "\n", "# plot the image\n", "fig, ax = plt.subplots(tight_layout=True)\n", "\n", "im = ax.imshow(\n", " image,\n", " norm=simple_norm(\n", " image,\n", " stretch=\"log\",\n", " ),\n", " origin=\"lower\",\n", " cmap=\"Greys\",\n", " )\n", "\n", "fig.colorbar(im)\n", "\n", "ax.set_xlabel(\"X\")\n", "ax.set_ylabel(\"Y\")\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Just like in the flats, we see a circular shadow at the corners of the image.\n", "\n", "### Ignoring flat-field corrections\n", "\n", "Let's see what happens if we neglect flat-field corrections:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:42.103911Z", "iopub.status.busy": "2026-01-23T11:19:42.103814Z", "iopub.status.idle": "2026-01-23T11:19:46.567421Z", "shell.execute_reply": "2026-01-23T11:19:46.567164Z" } }, "outputs": [], "source": [ "reducer = phoptic.Reducer(\n", " data_directory=obs_dir, # path to the simulated data\n", " out_directory=out_dir / 'reduced' / 'no_flats', # path to where output will be saved\n", " show_plots=True,\n", " )\n", "\n", "reducer.create_catalogs()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, systematic artefacts stack in the catalog image. Not only can these artefacts skew flux measurements, but they may also lead to spurious source detections. It's therefore important to correct for these effects.\n", "\n", "### Applying Flat-field Corrections\n", "\n", "In `phoptic`, flat-field corrections are handled by an `phoptic.FlatFieldCorrector` object:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:46.568359Z", "iopub.status.busy": "2026-01-23T11:19:46.568266Z", "iopub.status.idle": "2026-01-23T11:19:46.672864Z", "shell.execute_reply": "2026-01-23T11:19:46.672650Z" } }, "outputs": [], "source": [ "flat_corrector = phoptic.FlatFieldCorrector(\n", " out_directory=out_dir / 'correctors' / 'manual_flats', # where the master flat field images will be stored\n", " data_directory=flats_dir, # where the flat field images are stored\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When defining a `FlatFieldCorrector` object, an `out_directory` must be specified. This is the directory to which any output files (e.g., the master flats) will be written. \n", "\n", "After creating a `FlatFieldCorrector` instance, you will be able to see how many flats have been detected for each filter. In this case, we can see that each filter has five flat-field images.\n", "\n", "#### Creating Master Flats (Long Way)\n", "\n", "Now that we have a `FlatFieldCorrector` instance, we can either create master flats manually, or pass the `FlatFieldCorrector` object to a `Reducer` instance, which will automatically create the master flats for us as required. In this example, let's create the master flats manually using the `create_master_images()` method:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:46.673686Z", "iopub.status.busy": "2026-01-23T11:19:46.673602Z", "iopub.status.idle": "2026-01-23T11:19:47.376421Z", "shell.execute_reply": "2026-01-23T11:19:47.376204Z" } }, "outputs": [], "source": [ "flat_corrector.create_master_images()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For convenience, the location of the resulting master flats file is displayed once it has been created. This is a single FITS file that may contain several master flats. In this case, it should contain three images. \n", "\n", "We can either read the master flats from their new directory, or access them directly from the `FlatFieldCorrector` instance using the `master_images` attribute. Let's first look at the file:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:47.377324Z", "iopub.status.busy": "2026-01-23T11:19:47.377235Z", "iopub.status.idle": "2026-01-23T11:19:47.421154Z", "shell.execute_reply": "2026-01-23T11:19:47.420932Z" } }, "outputs": [], "source": [ "from astropy.io import fits\n", "\n", "with fits.open(flat_corrector.master_image_path) as hdul:\n", " for hdu in hdul:\n", " print(repr(hdu.header))\n", " print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the master flats file, we can see that the first header-data unit (HDU) is blank. The remaining HDUs contain the master flat-field images for each filter and their master variance images (for error propagation). Let's now take a look at one of these master flats using the `master_images` attribute of the `FlatFieldCorrector` instance:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:47.422021Z", "iopub.status.busy": "2026-01-23T11:19:47.421941Z", "iopub.status.idle": "2026-01-23T11:19:47.488675Z", "shell.execute_reply": "2026-01-23T11:19:47.488432Z" } }, "outputs": [], "source": [ "master_flat = flat_corrector.master_images['1:g']\n", "\n", "fig, ax = plt.subplots(tight_layout=True)\n", "\n", "im = ax.imshow(\n", " master_flat,\n", " norm=simple_norm(\n", " master_flat,\n", " stretch=\"log\",\n", " ),\n", " origin=\"lower\",\n", " cmap=\"Greys\",\n", " )\n", "\n", "ax.set_xlabel(\"X\")\n", "ax.set_ylabel(\"Y\")\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Performing Flat-field Corrections\n", "\n", "Let's now use our `FlatFieldCorrector` instance to correct an image using its `correct()` method. `correct()` takes an image array and its corresponding filter as input, and then returns the corrected image and the variance introduced from the correction (used in error propagation):" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:47.489608Z", "iopub.status.busy": "2026-01-23T11:19:47.489510Z", "iopub.status.idle": "2026-01-23T11:19:47.677474Z", "shell.execute_reply": "2026-01-23T11:19:47.677253Z" } }, "outputs": [], "source": [ "with fits.open(image_paths[0]) as hdul:\n", " fltr = hdul[0].header['FILTER']\n", " image = np.array(hdul[0].data)\n", "\n", "if fltr == 'g':\n", " camera = '1'\n", "elif fltr == 'r':\n", " camera = '2'\n", "elif fltr == 'i':\n", " camera = '3'\n", "\n", "# apply the flat-field correction\n", "corrected_image, flat_var = flat_corrector.correct(image, camera=camera, fltr=fltr)\n", "\n", "fig, axes = plt.subplots(\n", " ncols=2,\n", " tight_layout=True,\n", " figsize=(10, 5),\n", " )\n", "\n", "axes[0].imshow(\n", " image,\n", " norm=simple_norm(\n", " image,\n", " stretch=\"log\",\n", " ),\n", " origin=\"lower\",\n", " cmap=\"Greys\",\n", " )\n", "axes[0].set_title('Original image')\n", "\n", "axes[1].imshow(\n", " corrected_image,\n", " norm=simple_norm(\n", " corrected_image,\n", " stretch=\"log\",\n", " ),\n", " origin=\"lower\",\n", " cmap=\"Greys\",\n", " )\n", "axes[1].set_title('Corrected image')\n", "\n", "for ax in axes:\n", " ax.set_xlabel(\"X\")\n", " ax.set_ylabel(\"Y\")\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, the aperture shadow has been successfully removed!\n", "\n", "We can automate the process of applying master flats by passing an `phoptic.FlatFieldCorrector` instance to `phoptic.Reducer`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:47.679263Z", "iopub.status.busy": "2026-01-23T11:19:47.678987Z", "iopub.status.idle": "2026-01-23T11:19:49.396979Z", "shell.execute_reply": "2026-01-23T11:19:49.396750Z" } }, "outputs": [], "source": [ "reducer = phoptic.Reducer(\n", " data_directory=obs_dir, # path to the simulated data\n", " out_directory=out_dir / 'reduced' / 'manual_flats', # path to where output will be saved\n", " show_plots=True,\n", " flat_corrector=flat_corrector, # pass the flat field corrector\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After initialising our `Reducer`, we are told that our `FlatFieldCorrector` successfully passed all checks. This is a safety measure to ensure that the binnings of the flat-field images match those of the science images. Let's now create source catalogs:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:49.398057Z", "iopub.status.busy": "2026-01-23T11:19:49.397964Z", "iopub.status.idle": "2026-01-23T11:19:52.390256Z", "shell.execute_reply": "2026-01-23T11:19:52.390027Z" } }, "outputs": [], "source": [ "reducer.create_catalogs()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, the catalog images no longer show an aperture shadow. \n", "\n", "### Creating Master Flats (Short Way)\n", "\n", "We can create master flat-field images more easily by letting `phoptic` handle the master flat creation automatically:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:52.391155Z", "iopub.status.busy": "2026-01-23T11:19:52.391064Z", "iopub.status.idle": "2026-01-23T11:19:54.260895Z", "shell.execute_reply": "2026-01-23T11:19:54.260620Z" } }, "outputs": [], "source": [ "auto_flat_corrector = phoptic.FlatFieldCorrector(\n", " out_directory=out_dir / 'correctors' / 'auto_flats', # where the master flat field images will be stored\n", " data_directory=flats_dir, # where the flat field images are stored\n", " )\n", "\n", "auto_reducer = phoptic.Reducer(\n", " data_directory=obs_dir, # path to the simulated data\n", " out_directory=out_dir / 'reduced' / 'auto_flats', # path to where output will be saved\n", " show_plots=True,\n", " flat_corrector=auto_flat_corrector, # pass the flat field corrector\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Currently, the flat corrector has not been used, and so if we check for the master flats we will find that they don't yet exist:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:54.261958Z", "iopub.status.busy": "2026-01-23T11:19:54.261862Z", "iopub.status.idle": "2026-01-23T11:19:54.264028Z", "shell.execute_reply": "2026-01-23T11:19:54.263867Z" } }, "outputs": [], "source": [ "auto_reducer.flat_corrector.master_image_path.is_file()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However, if we initialise the catalog we will see that the master flats will be created automatically so that they can be applied to the images:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:54.264738Z", "iopub.status.busy": "2026-01-23T11:19:54.264665Z", "iopub.status.idle": "2026-01-23T11:19:57.819600Z", "shell.execute_reply": "2026-01-23T11:19:57.819371Z" } }, "outputs": [], "source": [ "auto_reducer.create_catalogs()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, the master flats were automatically created so that the corrections could be applied when needed. For completeness, let's also check if the master flats directory now exists:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:57.820621Z", "iopub.status.busy": "2026-01-23T11:19:57.820476Z", "iopub.status.idle": "2026-01-23T11:19:57.822482Z", "shell.execute_reply": "2026-01-23T11:19:57.822330Z" } }, "outputs": [], "source": [ "auto_reducer.flat_corrector.master_image_path.is_file()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With just a few lines of code, we have created master flats and used them to correct our observation images!\n", "\n", "## Dark noise\n", "\n", "OPTICAM-MX images include the nominal dark current in their headers, allowing the dark noise to be calculated by multiplying the dark current by the exposure time. If you're using `phoptic` to reduce data from OPTICAM-MX data, or data from an instrument that measures the dark current, then you can subtract the dark noise by passing an empty `DarkNoiseCorrector` instance to `Reducer`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "dark_current_reducer = phoptic.Reducer(\n", " data_directory=obs_dir, # path to the simulated data\n", " out_directory=out_dir / 'reduced' / 'dark_current', # path to where output will be saved\n", " show_plots=True,\n", " flat_corrector=auto_flat_corrector, # pass the flat field corrector\n", " dark_corrector=phoptic.DarkNoiseCorrector(\n", " instrument=phoptic.OPTICAM_MX(),\n", " )\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that we have passed an `Instrument` instance to `DarkNoiseCorrector`, but no directories. By doing this, `phoptic` will attempt to perform dark noise corrections using the exposure-integrated dark current; as we can see, the `DarkNoiseCorrector` instance passed all checks in this case. For instruments that do not report a dark current value, we'll need to use `DarkNoiseCorrector` to create a master dark image, which will then be subtracted from science images to remove the dark noise. The interface for `phoptic.DarkNoiseCorrector` is pretty much identical to that of `phoptic.FlatFieldCorrector` in this regard.\n", "\n", "### Generating Synthetic Darks\n", "\n", "To demonstrate how to apply dark noise corrections, we need to generate some dark images. For this example, we'll assume we have observed faint sources using a long exposure time to allow for a considerable dark noise contribution. I'll also ignore any randomness in the dark noise contribution for simplicity:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:57.823170Z", "iopub.status.busy": "2026-01-23T11:19:57.823102Z", "iopub.status.idle": "2026-01-23T11:19:57.834810Z", "shell.execute_reply": "2026-01-23T11:19:57.834628Z" } }, "outputs": [], "source": [ "import numpy as np\n", "\n", "IMG_SIZE = (256, 256)\n", "DARK_FLUX = 30.0 # average exposure-integrated dark current contribution\n", "EXPTIME = 120.0 # exposure time in seconds\n", "FLTR = 'U'\n", "BINNING = '1x1'\n", "\n", "for i in range(5):\n", " \n", " image = np.zeros(IMG_SIZE) + DARK_FLUX\n", " \n", " hdu = fits.PrimaryHDU(image) # image data\n", " \n", " hdu.header['FILTER'] = FLTR # the observation filter\n", " hdu.header['BINNING'] = BINNING # the image binning (1x1)\n", " hdu.header['EXPTIME'] = EXPTIME # the exposure time in seconds\n", " hdu.header['INSTRUME'] = 'SYNTHETICAM'\n", " hdu.header['RDNOISE'] = 0.\n", " \n", " save_path = out_dir / 'calibration_images' / 'darks' # directory to which image will be saved\n", " \n", " # create save directory if it does not already exist\n", " if not save_path.is_dir():\n", " save_path.mkdir(parents=True)\n", " \n", " # save image\n", " file_path = save_path / f'image_{i}.fits.gz'\n", " hdu.writeto(file_path, overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we have some dark images, we can create a `DarkNoiseCorrector`. To create a `DarkNoiseCorrector`, however, we need to define an `Instrument`: (see the [custom instruments tutorial](instruments.ipynb) for more details):" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:57.835584Z", "iopub.status.busy": "2026-01-23T11:19:57.835513Z", "iopub.status.idle": "2026-01-23T11:19:57.837670Z", "shell.execute_reply": "2026-01-23T11:19:57.837492Z" } }, "outputs": [], "source": [ "%%writefile out/custom_routines.py\n", "from astropy.coordinates import EarthLocation\n", "from astropy import units as u\n", "\n", "import phoptic\n", "\n", "\n", "class CustomInstrument(phoptic.Instrument):\n", " \n", " def __init__(\n", " self,\n", " diameter=1.0 * u.m,\n", " location = EarthLocation.from_geodetic(\n", " lon=0.0,\n", " lat=0.0,\n", " height=0.0,\n", " ),\n", " pixel_scales = {\n", " 'SYNTHETICAM': 1.0,\n", " },\n", " ):\n", " \n", " super().__init__(\n", " diameter=diameter,\n", " location=location,\n", " pixel_scales=pixel_scales,\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Due to compatibility issues, routines defined inside of IPython notebooks cannot be used by `multiprocessing` (used by `phoptic`). To overcome this issue, we have used the `%%writefile` magic command to write our `CustomInstrument` class to a temporary module called `custom_routines.py` in our `out` directory. This is required in Python $\\geq$ 3.14." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:57.838424Z", "iopub.status.busy": "2026-01-23T11:19:57.838352Z", "iopub.status.idle": "2026-01-23T11:19:57.843085Z", "shell.execute_reply": "2026-01-23T11:19:57.842913Z" } }, "outputs": [], "source": [ "from out.custom_routines import CustomInstrument\n", "\n", "instrument = CustomInstrument()\n", "\n", "dark_corr = phoptic.DarkNoiseCorrector(\n", " out_directory=out_dir / 'correctors' / 'darks',\n", " data_directory=out_dir / 'calibration_images' / 'darks',\n", " instrument=instrument,\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that our `DarkNoiseCorrector` has detected five U-band dark images. Let's now generate some science images that contain dark noise:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:19:57.843855Z", "iopub.status.busy": "2026-01-23T11:19:57.843775Z", "iopub.status.idle": "2026-01-23T11:20:07.923004Z", "shell.execute_reply": "2026-01-23T11:20:07.922730Z" } }, "outputs": [], "source": [ "from photutils.datasets import make_100gaussians_image\n", "\n", "# load a noiseless simulated HST image\n", "base_image = np.array(make_100gaussians_image(), dtype=np.float64)[:IMG_SIZE[0], :IMG_SIZE[1]]\n", "base_image /= base_image.max()\n", "base_image *= 100\n", "\n", "# add dark noise to the image\n", "noisy_image = base_image + DARK_FLUX\n", "\n", "for i in range(5):\n", " hdu.data = noisy_image\n", " hdu.header['FILTER'] = FLTR # the observation filter\n", " hdu.header['BINNING'] = BINNING # the image binning (1x1)\n", " hdu.header['EXPTIME'] = EXPTIME # the exposure time in seconds\n", " hdu.header['RA'] = '0' # pointing of the image\n", " hdu.header['DEC'] = '0' # pointing of the image\n", " hdu.header['GAIN'] = 1.0 # instrument gain\n", " hdu.header['DATE-OBS'] = f'2026-01-01T00:00:0{i}.000' # date of observation\n", " hdu.header['AIRMASS'] = 1 # airmass of observation\n", " \n", " save_path = out_dir / 'data' / 'darks' # directory to which image will be saved\n", " \n", " # create save directory if it does not already exist\n", " if not save_path.is_dir():\n", " save_path.mkdir(parents=True)\n", " \n", " # save image\n", " file_path = save_path / f'image_{i}.fits.gz'\n", " hdu.writeto(file_path, overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's first take a look at the reference image (i.e., without any dark noise):" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:07.924105Z", "iopub.status.busy": "2026-01-23T11:20:07.924001Z", "iopub.status.idle": "2026-01-23T11:20:08.022707Z", "shell.execute_reply": "2026-01-23T11:20:08.022441Z" } }, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "\n", "im = ax.imshow(base_image, origin='lower')\n", "\n", "fig.colorbar(im)\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's look at one of the noisy images:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:08.023702Z", "iopub.status.busy": "2026-01-23T11:20:08.023603Z", "iopub.status.idle": "2026-01-23T11:20:08.114397Z", "shell.execute_reply": "2026-01-23T11:20:08.114154Z" } }, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "\n", "im = ax.imshow(noisy_image, origin='lower')\n", "\n", "fig.colorbar(im)\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "While there is no obvious difference from the image alone, because we're assuming uniform dark noise, we can see from the colorbar that the dark noise has increased the flux. Let's compare the fluxes between the two images more explicitly:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:08.115268Z", "iopub.status.busy": "2026-01-23T11:20:08.115171Z", "iopub.status.idle": "2026-01-23T11:20:08.387595Z", "shell.execute_reply": "2026-01-23T11:20:08.387396Z" } }, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "\n", "ax.hist(\n", " base_image.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Reference',\n", " )\n", "ax.hist(\n", " noisy_image.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Noisy',\n", " )\n", "\n", "ax.set_yscale('log')\n", "\n", "ax.legend()\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, the reference image appears to have a median background flux of $\\sim 30$, while the dark noise image has its flux values shifted by the dark flux value of 30. Let's use our `DarkNoiseCorrector` to correct this:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:08.388461Z", "iopub.status.busy": "2026-01-23T11:20:08.388382Z", "iopub.status.idle": "2026-01-23T11:20:08.401637Z", "shell.execute_reply": "2026-01-23T11:20:08.401440Z" } }, "outputs": [], "source": [ "corrected_image, dark_var = dark_corr.correct(\n", " image=noisy_image,\n", " camera='SYNTHETICAM',\n", " fltr=FLTR,\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `correct()` method of `DarkNoiseCorrector` returns the corrected image and the dark noise variance:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:08.402330Z", "iopub.status.busy": "2026-01-23T11:20:08.402255Z", "iopub.status.idle": "2026-01-23T11:20:08.404263Z", "shell.execute_reply": "2026-01-23T11:20:08.404092Z" } }, "outputs": [], "source": [ "dark_var" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The dark noise variance is zero in this example since our dark images are all identical. In reality, of course, master dark images will have some non-zero variance that will contribute to the photometric error. This is covered in detail in a [dedicated notebook](../tests/calibration_error_propagation.ipynb).\n", "\n", "Let's take a look at the corrected image:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:08.404961Z", "iopub.status.busy": "2026-01-23T11:20:08.404893Z", "iopub.status.idle": "2026-01-23T11:20:08.482147Z", "shell.execute_reply": "2026-01-23T11:20:08.481903Z" } }, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "\n", "im = ax.imshow(corrected_image, origin='lower')\n", "\n", "fig.colorbar(im)\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "From the image alone, the difference isn't immediately obvious. Let's compare the corrected image to the base image more explicitly:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:08.483057Z", "iopub.status.busy": "2026-01-23T11:20:08.482974Z", "iopub.status.idle": "2026-01-23T11:20:08.573178Z", "shell.execute_reply": "2026-01-23T11:20:08.572962Z" } }, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "\n", "ax.hist(\n", " base_image.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Reference image',\n", " )\n", "ax.hist(\n", " corrected_image.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Corrected image',\n", " ls='-.',\n", " )\n", "\n", "ax.set_yscale('log')\n", "\n", "ax.legend()\n", "\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-cell" ] }, "outputs": [], "source": [ "assert np.allclose(base_image, corrected_image)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, the corrected image is now identical to the base image. Of course, in reality noise is random and so it can never be removed perfectly like we have done here. However, this demonstrates how `phoptic.DarkNoiseCorrector` can be used to correct for an instrument's dark noise.\n", "\n", "To then use this dark current corrector to reduce some images, we can pass it to a `Reducer` instance as we did for the flat-field corrections. However, since these data have not been designed to mimic OPTICAM-MX data, we'll need to specify custom [source identification](finders.ipynb) and [background estimation](backgrounds.ipynb) routines:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:08.574356Z", "iopub.status.busy": "2026-01-23T11:20:08.574213Z", "iopub.status.idle": "2026-01-23T11:20:10.478182Z", "shell.execute_reply": "2026-01-23T11:20:10.477871Z" } }, "outputs": [], "source": [ "dark_reducer = phoptic.Reducer(\n", " data_directory=out_dir / 'data' / 'darks',\n", " out_directory=out_dir / 'reduced' / 'darks',\n", " show_plots=True,\n", " dark_corrector=dark_corr, # pass the dark noise corrector\n", " instrument=instrument,\n", " finder=phoptic.DefaultFinder(n_pixels=8), # specify a custom source finder\n", " background=phoptic.DefaultBackground(box_size=16), # specify a custom background estimator\n", " )\n", "\n", "dark_reducer.create_catalogs(\n", " max_catalog_sources=50,\n", " n_alignment_sources=50,\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, our `DarkNoiseCorrector` successfully passed all checks. Similarly to `FlatFieldCorrector`, `DarkNoiseCorrector` instances are checked to ensure that any dark images have the same properties as the science images. In particular, `DarkNoiseCorrector` checks that the binning modes, exposure times, and filters all match those of the science images. Additionally, we can see that the median background is $\\sim 30$, suggesting that the dark noise has been successfully removed! If the dark noise had not been removed, then the median background would have been closer to $\\sim 60$, as we saw earlier.\n", "\n", "## Bias Corrections\n", "\n", "The OPTICAM-MX cameras perform real-time bias corrections, and so additional bias corrections are nominally not required. If you're using `phoptic` to reduce data from OPTICAM-MX, or another instrument that performs real-time bias corrections, then you can ignore this section. For other instruments, however, a `BiasCorrector` can be used to automate bias correcting. The interface for `phoptic.BiasCorrector` is almost identical to those of `phoptic.FlatFieldCorrector` and `phoptic.BiasCorrector`.\n", "\n", "Let's generate some bias images:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.479167Z", "iopub.status.busy": "2026-01-23T11:20:10.479073Z", "iopub.status.idle": "2026-01-23T11:20:10.487500Z", "shell.execute_reply": "2026-01-23T11:20:10.487251Z" } }, "outputs": [], "source": [ "BIAS = 100.0\n", "\n", "for i in range(3):\n", " \n", " image = np.zeros(IMG_SIZE) + BIAS\n", " \n", " hdu = fits.PrimaryHDU(image)\n", " \n", " hdu.header['BINNING'] = BINNING\n", " hdu.header['EXPTIME'] = 0.0 # bias images should have an exposure time of 0 s\n", " hdu.header['INSTRUME'] = 'SYNTHETICAM'\n", " \n", " save_path = out_dir / 'calibration_images' / 'biases' # directory to which image will be saved\n", " \n", " # create save directory if it does not already exist\n", " if not save_path.is_dir():\n", " save_path.mkdir(parents=True)\n", " \n", " # save image\n", " file_path = save_path / f'image_{i}.fits.gz'\n", " hdu.writeto(file_path, overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's generate some new dark images. Our previous dark images assumed no bias, which is not the case in reality; our new dark images will therefore contain a bias:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.488466Z", "iopub.status.busy": "2026-01-23T11:20:10.488287Z", "iopub.status.idle": "2026-01-23T11:20:10.496619Z", "shell.execute_reply": "2026-01-23T11:20:10.496434Z" } }, "outputs": [], "source": [ "for i in range(3):\n", " \n", " image = np.zeros(IMG_SIZE) + DARK_FLUX + BIAS\n", " \n", " hdu = fits.PrimaryHDU(image) # image data\n", " \n", " hdu.header['FILTER'] = FLTR # the observation filter\n", " hdu.header['BINNING'] = BINNING # the image binning (1x1)\n", " hdu.header['EXPTIME'] = EXPTIME # the exposure time in seconds\n", " hdu.header['INSTRUME'] = 'SYNTHETICAM'\n", " \n", " save_path = out_dir / 'calibration_images' / 'darks_with_bias' # directory to which image will be saved\n", " \n", " # create save directory if it does not already exist\n", " if not save_path.is_dir():\n", " save_path.mkdir(parents=True)\n", " \n", " # save image\n", " file_path = save_path / f'image_{i}.fits.gz'\n", " hdu.writeto(file_path, overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's generate some science images with both bias and dark noise contributions:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.497559Z", "iopub.status.busy": "2026-01-23T11:20:10.497471Z", "iopub.status.idle": "2026-01-23T11:20:10.565730Z", "shell.execute_reply": "2026-01-23T11:20:10.565441Z" } }, "outputs": [], "source": [ "# add dark noise to the image\n", "noisy_image = base_image + DARK_FLUX + BIAS\n", "\n", "for i in range(10):\n", " hdu.data = noisy_image\n", " hdu.header['FILTER'] = FLTR # the observation filter\n", " hdu.header['BINNING'] = BINNING # the image binning (1x1)\n", " hdu.header['EXPTIME'] = EXPTIME # the exposure time in seconds\n", " hdu.header['RA'] = '0' # pointing of the image\n", " hdu.header['DEC'] = '0' # pointing of the image\n", " hdu.header['GAIN'] = 1.0 # instrument gain\n", " hdu.header['DATE-OBS'] = f'2026-01-01T00:00:0{i}.000' # date of observation\n", " hdu.header['INSTRUME'] = 'SYNTHETICAM'\n", " \n", " save_path = out_dir / 'data' / 'darks_with_bias' # directory to which image will be saved\n", " \n", " # create save directory if it does not already exist\n", " if not save_path.is_dir():\n", " save_path.mkdir(parents=True)\n", "\n", " # save image\n", " file_path = save_path.joinpath(f'image_{i}.fits.gz')\n", " hdu.writeto(file_path, overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's compare the image with the noisy image:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.566649Z", "iopub.status.busy": "2026-01-23T11:20:10.566554Z", "iopub.status.idle": "2026-01-23T11:20:10.652342Z", "shell.execute_reply": "2026-01-23T11:20:10.652098Z" } }, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "\n", "ax.hist(\n", " base_image.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Base image',\n", " )\n", "ax.hist(\n", " noisy_image.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Noisy image',\n", " )\n", "\n", "ax.set_yscale('log')\n", "\n", "ax.legend()\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, there is a large offset between the base and noisy images since the noisy image now includes a bias offset in addition to the dark noise from previous example. Let's correct this:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.653370Z", "iopub.status.busy": "2026-01-23T11:20:10.653222Z", "iopub.status.idle": "2026-01-23T11:20:10.659907Z", "shell.execute_reply": "2026-01-23T11:20:10.659681Z" } }, "outputs": [], "source": [ "bias_corr = phoptic.BiasCorrector(\n", " out_directory=out_dir / 'correctors' / 'biases',\n", " data_directory=out_dir / 'calibration_images' / 'biases',\n", " instrument=instrument,\n", " )\n", "\n", "dark_corr = phoptic.DarkNoiseCorrector(\n", " out_directory=out_dir / 'correctors' / 'darks_with_bias',\n", " data_directory=out_dir / 'calibration_images' / 'darks_with_bias',\n", " instrument=instrument,\n", " )" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.660823Z", "iopub.status.busy": "2026-01-23T11:20:10.660611Z", "iopub.status.idle": "2026-01-23T11:20:10.670658Z", "shell.execute_reply": "2026-01-23T11:20:10.670414Z" } }, "outputs": [], "source": [ "bias_corrected_image, bias_var = bias_corr.correct(\n", " image=noisy_image,\n", " camera='SYNTHETICAM'\n", " )" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.671416Z", "iopub.status.busy": "2026-01-23T11:20:10.671334Z", "iopub.status.idle": "2026-01-23T11:20:10.765783Z", "shell.execute_reply": "2026-01-23T11:20:10.765488Z" } }, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "\n", "ax.hist(\n", " base_image.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Base image',\n", " )\n", "ax.hist(\n", " bias_corrected_image.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Bias corrected image',\n", " )\n", "\n", "ax.set_yscale('log')\n", "\n", "ax.legend()\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that the offset is smaller, but not yet zero (because we also have a dark noise contribution). Let's correct this using our new dark noise corrector:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.766705Z", "iopub.status.busy": "2026-01-23T11:20:10.766616Z", "iopub.status.idle": "2026-01-23T11:20:10.777821Z", "shell.execute_reply": "2026-01-23T11:20:10.777570Z" } }, "outputs": [], "source": [ "bias_and_dark_corrected_image, dark_var = dark_corr.correct(\n", " image=bias_corrected_image,\n", " camera='SYNTHETICAM',\n", " fltr='U',\n", " )" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.778648Z", "iopub.status.busy": "2026-01-23T11:20:10.778566Z", "iopub.status.idle": "2026-01-23T11:20:10.874948Z", "shell.execute_reply": "2026-01-23T11:20:10.874644Z" } }, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "\n", "ax.hist(\n", " base_image.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Noiseless',\n", " )\n", "ax.hist(\n", " bias_and_dark_corrected_image.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Noisy',\n", " )\n", "\n", "ax.set_yscale('log')\n", "\n", "ax.legend()\n", "\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Uh oh, we have negative fluxes! This is because the bias contribution is being subtracted twice: once by the master bias and again by the master dark. To fix this, we can pass a `BiasCorrectorInstance` to `DarkNoiseCorrector`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.875913Z", "iopub.status.busy": "2026-01-23T11:20:10.875826Z", "iopub.status.idle": "2026-01-23T11:20:10.890027Z", "shell.execute_reply": "2026-01-23T11:20:10.889769Z" } }, "outputs": [], "source": [ "new_dark_corr = phoptic.DarkNoiseCorrector(\n", " out_directory=out_dir / 'correctors' / 'bias-corrected_darks',\n", " data_directory=out_dir / 'calibration_images' / 'darks_with_bias',\n", " instrument=instrument,\n", " bias_corrector=bias_corr,\n", " )\n", "\n", "new_dark_corr.create_master_images()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.890803Z", "iopub.status.busy": "2026-01-23T11:20:10.890722Z", "iopub.status.idle": "2026-01-23T11:20:10.892419Z", "shell.execute_reply": "2026-01-23T11:20:10.892160Z" } }, "outputs": [], "source": [ "bias_and_dark_corrected_image_new, dark_var = new_dark_corr.correct(\n", " image=bias_corrected_image,\n", " camera='SYNTHETICAM',\n", " fltr='U',\n", " )" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.893134Z", "iopub.status.busy": "2026-01-23T11:20:10.893057Z", "iopub.status.idle": "2026-01-23T11:20:10.991201Z", "shell.execute_reply": "2026-01-23T11:20:10.990934Z" } }, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "\n", "ax.hist(\n", " base_image.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Base image',\n", " )\n", "ax.hist(\n", " bias_and_dark_corrected_image_new.flatten(),\n", " histtype='step',\n", " bins=100,\n", " label='Corrected image',\n", " ls='-.',\n", " )\n", "\n", "ax.set_yscale('log')\n", "\n", "ax.legend()\n", "\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-cell" ] }, "outputs": [], "source": [ "assert np.allclose(base_image, bias_and_dark_corrected_image_new)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now the corrected image is identical to the reference image! Again, however, it's worth stressing that noise is *random*, and so in practise it will never be removed perfectly as it has been here.\n", "\n", "## Calibrating calibration images\n", "\n", "Above, we saw that it is necessary to bias-correct dark images. Of course, the same is also true for flat-field images. Additionally, it may be necessary to subtract the dark noise contribution from your flats if they require a longer exposure time and/or your instrument has a high dark current. Let's generate some flats that contain bias and dark noise contributions:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:10.992081Z", "iopub.status.busy": "2026-01-23T11:20:10.991991Z", "iopub.status.idle": "2026-01-23T11:20:11.000649Z", "shell.execute_reply": "2026-01-23T11:20:11.000374Z" } }, "outputs": [], "source": [ "for i in range(3):\n", " image = np.ones(IMG_SIZE) + DARK_FLUX + BIAS\n", "\n", " hdu = fits.PrimaryHDU(image) # image data\n", "\n", " hdu.header['FILTER'] = FLTR # the observation filter\n", " hdu.header['BINNING'] = BINNING # the image binning (1x1)\n", " hdu.header['EXPTIME'] = EXPTIME # the exposure time in seconds\n", " hdu.header['INSTRUME'] = 'SYNTHETICAM'\n", "\n", " save_path = out_dir / 'calibration_images' / 'flats_with_bias_and_dark_noise' # directory to which image will be saved\n", "\n", " # create save directory if it does not already exist\n", " if not save_path.is_dir():\n", " save_path.mkdir(parents=True)\n", "\n", " # save image\n", " file_path = save_path / f'image_{i}.fits.gz'\n", " hdu.writeto(file_path, overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These flats have a flux of 1 (to avoid division by zero) before bias and dark noise contributions are added. As such, the master flat-field image should also have a flux of 1 (assuming the bias and dark noise are corrected properly):" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:11.001510Z", "iopub.status.busy": "2026-01-23T11:20:11.001427Z", "iopub.status.idle": "2026-01-23T11:20:11.017816Z", "shell.execute_reply": "2026-01-23T11:20:11.017531Z" } }, "outputs": [], "source": [ "flat_corr = phoptic.FlatFieldCorrector(\n", " out_directory=out_dir / 'correctors' / 'bias-corrected_and_dark-subtracted_flats',\n", " data_directory=save_path,\n", " bias_corrector=bias_corr, # pass bias corrector\n", " dark_corrector=new_dark_corr,\n", " instrument=instrument,\n", " )\n", "\n", "flat_corr.create_master_images()\n", "\n", "print(flat_corr.master_images['SYNTHETICAM:U'])" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-cell" ] }, "outputs": [], "source": [ "assert np.allclose(flat_corr.master_images['SYNTHETICAM:U'], 1.)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, all the pixels of the master flat have a flux of 1, suggesting that the bias and dark contributions were successfully removed.\n", "\n", "When passing a `DarkNoiseCorrector` instance to `FlatFieldCorrector`, `phoptic` also checks that the exposure times of the flats matches those of the darks; if they exposure times do not match, an error is raised:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:11.018562Z", "iopub.status.busy": "2026-01-23T11:20:11.018482Z", "iopub.status.idle": "2026-01-23T11:20:11.026726Z", "shell.execute_reply": "2026-01-23T11:20:11.026474Z" } }, "outputs": [], "source": [ "for i in range(3):\n", " image = np.ones(IMG_SIZE) + DARK_FLUX + BIAS\n", " \n", " hdu = fits.PrimaryHDU(image) # image data\n", " \n", " hdu.header['FILTER'] = FLTR # the observation filter\n", " hdu.header['BINNING'] = BINNING # the image binning (1x1)\n", " hdu.header['EXPTIME'] = 3. # the exposure time in seconds\n", " hdu.header['INSTRUME'] = 'SYNTHETICAM'\n", " \n", " save_path = out_dir / 'calibration_images' / 'short-exposure_flats_with_bias_and_dark' # directory to which image will be saved\n", " \n", " # create save directory if it does not already exist\n", " if not save_path.is_dir():\n", " save_path.mkdir(parents=True)\n", " \n", " # save image\n", " file_path = save_path / f'image_{i}.fits.gz'\n", " hdu.writeto(file_path, overwrite=True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-01-23T11:20:11.027534Z", "iopub.status.busy": "2026-01-23T11:20:11.027453Z", "iopub.status.idle": "2026-01-23T11:20:11.032836Z", "shell.execute_reply": "2026-01-23T11:20:11.032599Z" } }, "outputs": [], "source": [ "flat_corr = phoptic.FlatFieldCorrector(\n", " out_directory=out_dir / 'correctors' / 'short-exposure_flats',\n", " data_directory=save_path,\n", " bias_corrector=bias_corr, # pass bias corrector\n", " dark_corrector=new_dark_corr,\n", " instrument=instrument,\n", " )\n", "\n", "try:\n", " flat_corr.create_master_images()\n", "except Exception as e:\n", " print(f'ERROR: {e}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, the master flat-field image could not be created due to incompatible exposure times between the dark and flat-field images.\n", "\n", "That concludes the corrections tutorial for `phoptic`! For simplicity, we largely ignored the randomness of noise and, as such, did not discuss how `phoptic` propagates errors during calibration. To see `phoptic`'s calibration error propagation in action, there is a [dedicated notebook](../tests/calibration_error_propagation.ipynb)." ] } ], "metadata": { "kernelspec": { "display_name": "opticam7", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.14.6" } }, "nbformat": 4, "nbformat_minor": 2 }