Local background estimation
opticam_new supports specification of local background estimators, which can improve the background estimation in cases where the background varies considerably across an image. In this notebook, I will demonstrate how to define a local background estimator for use with opticam_new, as well as explain opticam_new’s default local background estimator.
Generating Data
First thing’s first, we need to generate some dummy data:
[1]:
import opticam_new
opticam_new.generate_observations(
out_dir='local_backgrounds_tutorial/data',
n_images=20,
circular_aperture=False,
)
[OPTICAM] variable source is at (122, 104)
Generating observations: 100%|██████████|[00:04<00:00]
Now we can use these dummy observations to compute and compare local backgrounds.
Generating Catalogs
Before we can look at the local background estimator, we need to create our source catalogs:
[2]:
cat = opticam_new.Catalog(
data_directory='local_backgrounds_tutorial/data',
out_directory='local_backgrounds_tutorial/reduced',
remove_cosmic_rays=False,
)
[OPTICAM] local_backgrounds_tutorial/reduced not found, attempting to create ...
[OPTICAM] local_backgrounds_tutorial/reduced created.
[OPTICAM] Scanning data directory: 100%|██████████|[00:00<00:00]
[OPTICAM] Binning: 4x4
[OPTICAM] Filters: g-band, r-band, i-band
[OPTICAM] 20 g-band images.
[OPTICAM] 20 r-band images.
[OPTICAM] 20 i-band images.
[3]:
cat.create_catalogs()
[OPTICAM] Initialising catalogs
[OPTICAM] Aligning g-band images: 100%|██████████|[00:00<00:00]
[OPTICAM] Done.
[OPTICAM] 20 image(s) aligned.
[OPTICAM] 0 image(s) could not be aligned.
[OPTICAM] Aligning r-band images: 100%|██████████|[00:00<00:00]
[OPTICAM] Done.
[OPTICAM] 20 image(s) aligned.
[OPTICAM] 0 image(s) could not be aligned.
[OPTICAM] Aligning i-band images: 100%|██████████|[00:00<00:00]
[OPTICAM] Done.
[OPTICAM] 20 image(s) aligned.
[OPTICAM] 0 image(s) could not be aligned.
DefaultLocalBackground
Now we look at the default local background estimator, DefaultLocalBackground. The default local background estimator uses an elliptical annulus that scales the semimajor_sigma and semiminor_sigma parameters of the catalogue’s psf_params dictionary. Let’s see what this dictionary looks like:
[5]:
cat.psf_params
[5]:
{'g-band': {'semimajor_sigma': np.float64(1.947867010701578),
'semiminor_sigma': np.float64(1.9362408131678852),
'orientation': np.float64(47.13371883619817)},
'r-band': {'semimajor_sigma': np.float64(1.9478670106350826),
'semiminor_sigma': np.float64(1.9362408131119144),
'orientation': np.float64(47.152263419301484)},
'i-band': {'semimajor_sigma': np.float64(1.9478670107825269),
'semiminor_sigma': np.float64(1.936240813226689),
'orientation': np.float64(47.17119043872118)}}
We can see that all three cameras have the same PSF parameters, since these simulated data are the same for each camera. In practise, the PSF parameters will generally be different between cameras.
We can see each camera has three PSF parameters:
semimajor_sigma: the standard deviation of the PSF along the semi-major axis (assuming a 2D Gaussian PSF)semiminor_sigma: the standard deviation of the PSF along the semi-minor axis (assuming a 2D Gaussian PSF)orientation: the orientation of the PSF in degrees
When defining a custom local background estimator, it’s a good idea to use these values to define your annulus (more on this later). For now, let’s perform photometry using the default local background estimator:
[6]:
phot = opticam_new.OptimalPhotometer(
local_background_estimator=opticam_new.DefaultLocalBackground(),
)
cat.photometry(phot)
[OPTICAM] Photometry results will be saved to optimal_annulus_light_curves in local_backgrounds_tutorial/reduced.
[OPTICAM] Performing photometry on g-band images: 100%|██████████|[00:00<00:00]
[OPTICAM] Performing photometry on r-band images: 100%|██████████|[00:00<00:00]
[OPTICAM] Performing photometry on i-band images: 100%|██████████|[00:00<00:00]
Let’s take a look at the resulting light curve file for a random source:
[7]:
import pandas as pd
default_g_band_source_2_df = pd.read_csv(
'local_backgrounds_tutorial/reduced/optimal_annulus_light_curves/g-band_source_2.csv',
)
default_g_band_source_2_df
[7]:
| flux | flux_err | background | background_err | BMJD | |
|---|---|---|---|---|---|
| 0 | 432.999425 | 10.673493 | 100.541360 | 9.986540 | 60310.000000 |
| 1 | 433.165506 | 10.905346 | 98.669093 | 10.235830 | 60310.000012 |
| 2 | 431.942271 | 10.963515 | 100.166224 | 10.298477 | 60310.000023 |
| 3 | 431.465017 | 9.964454 | 99.602199 | 9.228097 | 60310.000035 |
| 4 | 434.025681 | 10.709357 | 101.071948 | 10.023068 | 60310.000046 |
| 5 | 432.006028 | 10.339840 | 99.736465 | 9.632121 | 60310.000058 |
| 6 | 429.911557 | 10.072193 | 100.981359 | 9.345138 | 60310.000069 |
| 7 | 431.166918 | 10.540508 | 100.106304 | 9.848330 | 60310.000081 |
| 8 | 431.436103 | 10.140895 | 99.966898 | 9.418578 | 60310.000093 |
| 9 | 429.945455 | 11.011182 | 101.971043 | 10.348042 | 60310.000104 |
| 10 | 433.208687 | 9.382505 | 100.238254 | 8.592982 | 60310.000116 |
| 11 | 432.252908 | 10.341411 | 100.470962 | 9.631887 | 60310.000127 |
| 12 | 433.925916 | 11.075005 | 99.826508 | 10.414112 | 60310.000139 |
| 13 | 433.588557 | 10.043910 | 100.640733 | 9.309566 | 60310.000150 |
| 14 | 432.451536 | 11.068595 | 99.581468 | 10.410437 | 60310.000162 |
| 15 | 433.622799 | 11.497434 | 99.335452 | 10.863324 | 60310.000174 |
| 16 | 434.072779 | 11.780972 | 99.630559 | 11.161835 | 60310.000185 |
| 17 | 432.774604 | 10.741582 | 99.689833 | 10.059892 | 60310.000197 |
| 18 | 434.283844 | 10.639444 | 99.759756 | 9.949243 | 60310.000208 |
| 19 | 433.472601 | 10.069855 | 99.464752 | 9.340178 | 60310.000220 |
We can see that the light curve file has background and background_err columns, which are only included if a local background estimator was used to compute the light curve. The average background is about 100, which is the expected value, and the average background error is about \(\sqrt{100} = 10\), which is also the expected value, showing that the default local background estimator is working well.
Custom Local Background
The default local background estimator uses the mean and standard deviation in the sigma-clipped annulus to estimate the background and its error, respectively. We will therefore define a custom local background estimator that works slightly differently, using the median instead of the mean, and the median absolute deviation instead of the standard deviation.
To define a custom local background estimator, you should either inherit from the BaseLocalBackground base class, or define a class that implements a __call__() method with the following input parameters:
data: the image data as a 2Dnumpyarrayerror: the error in the image data as a 2Dnumpyarrayposition: the position of a source as a tuple (x, y)semimajor_axis: the semi-major standard deviation of the PSF (assumed to be a 2D Gaussian)semiminor_axis: the semi-minor standard deviation of the PSF (assumed to be a 2D Gaussian)orientation: the orientation of the PSF in degrees
For simplicity, I will inherit from the base class since this is how I’d recommend defining a custom local background estimator:
[9]:
from opticam_new.reduction.local_background import BaseLocalBackground
from photutils.aperture import EllipticalAnnulus, ApertureStats
class CustomLocalBackground(BaseLocalBackground):
def __call__(
self,
data, # the image data as a 2D numpy array
error, # the error in the image data as a 2D numpy array
position, # the position of a source as a tuple (x, y)
semimajor_axis, # the semimajor axis of the PSF in pixels (assuming a 2D Gaussian PSF)
semiminor_axis, # the semiminor axis of the PSF in pixels (assuming a 2D Gaussian PSF)
orientation, # the orientation of the PSF in degrees
):
# define the annulus using photutils
annulus = EllipticalAnnulus(
positions=position,
a_in=self.r_in_scale * semimajor_axis,
a_out=self.r_out_scale * semimajor_axis,
b_in=self.r_in_scale * semiminor_axis,
b_out=self.r_out_scale * semiminor_axis,
theta=orientation,
)
# compute the statistics in the annulus
stats = ApertureStats(
data,
annulus,
error=error,
sigma_clip=self.sigma_clip,
)
median_local_background = float(stats.median)
# median absolute deviation (MAD) of the local background
mad_std_local_background = float(stats.mad_std)
return median_local_background, mad_std_local_background
When inheriting from the BaseLocalBackground base class, there is no need to implement an __init__() method.
The order of the parameters in the __call__() method is important, since opticam_new’s photometry routines assume a local background estimator’s __call__() method takes the above parameters in the above order. These parameters are handled by the catalogue, and so you don’t need to worry about computing them. In particular, the semimajor_axis, semiminor_axis, and orientation parameters are passed from the catalogue’s psf_params dictionary using the appropriate values
for the camera corresponding to the image, so you don’t need to worry about unpacking this dictionary.
Finally, there is no requirement to use photutils for estimating local backgrounds, but photutils does make it a lot easier.
Let’s now perform photometry using our custom local background estimator:
[10]:
from astropy.stats import SigmaClip
custom_phot = opticam_new.OptimalPhotometer(
local_background_estimator=CustomLocalBackground(
r_in_scale=5,
r_out_scale=6,
sigma_clip=SigmaClip(
sigma=3,
maxiters=10,
),
),
)
cat.photometry(custom_phot)
[OPTICAM] Photometry results will be saved to optimal_annulus_light_curves in local_backgrounds_tutorial/reduced.
[OPTICAM] Performing photometry on g-band images: 100%|██████████|[00:00<00:00]
[OPTICAM] Performing photometry on r-band images: 100%|██████████|[00:00<00:00]
[OPTICAM] Performing photometry on i-band images: 100%|██████████|[00:00<00:00]
[11]:
custom_g_band_source_2_df = pd.read_csv(
'local_backgrounds_tutorial/reduced/optimal_annulus_light_curves/g-band_source_2.csv',
)
custom_g_band_source_2_df
[11]:
| flux | flux_err | background | background_err | BMJD | |
|---|---|---|---|---|---|
| 0 | 431.851133 | 11.442116 | 101.689652 | 10.804145 | 60310.000000 |
| 1 | 434.001516 | 10.229813 | 97.833082 | 9.512871 | 60310.000012 |
| 2 | 431.630545 | 10.741558 | 100.477951 | 10.061861 | 60310.000023 |
| 3 | 431.275066 | 9.604017 | 99.792150 | 8.837679 | 60310.000035 |
| 4 | 434.935973 | 9.908621 | 100.161657 | 9.162550 | 60310.000046 |
| 5 | 432.821894 | 10.756675 | 98.920598 | 10.078270 | 60310.000058 |
| 6 | 430.910770 | 10.220638 | 99.982146 | 9.504944 | 60310.000069 |
| 7 | 430.927339 | 11.450960 | 100.345884 | 10.817199 | 60310.000081 |
| 8 | 431.325232 | 11.154775 | 100.077770 | 10.502422 | 60310.000093 |
| 9 | 430.179853 | 9.423111 | 101.736645 | 8.638915 | 60310.000104 |
| 10 | 433.081687 | 10.431433 | 100.365254 | 9.727422 | 60310.000116 |
| 11 | 431.612039 | 9.632335 | 101.111831 | 8.866247 | 60310.000127 |
| 12 | 435.330265 | 10.517162 | 98.422159 | 9.818792 | 60310.000139 |
| 13 | 434.030394 | 10.933374 | 100.198896 | 10.262873 | 60310.000150 |
| 14 | 431.408444 | 11.198730 | 100.624560 | 10.548694 | 60310.000162 |
| 15 | 433.000181 | 10.971761 | 99.958070 | 10.305356 | 60310.000174 |
| 16 | 433.630169 | 11.667831 | 100.073168 | 11.042352 | 60310.000185 |
| 17 | 431.732221 | 11.156288 | 100.732216 | 10.501553 | 60310.000197 |
| 18 | 434.233694 | 11.975030 | 99.809906 | 11.366223 | 60310.000208 |
| 19 | 433.103346 | 10.593071 | 99.834007 | 9.902025 | 60310.000220 |
Again, the background and its error are both close to the expected values. Let’s therefore compare the local background estimations more clearly:
[15]:
from matplotlib import pyplot as plt
fig, axes = plt.subplots(
nrows=2,
tight_layout=True,
sharex=True,
)
axes[0].plot(
default_g_band_source_2_df['BMJD'],
default_g_band_source_2_df['background'],
label='Default',
)
axes[0].plot(
custom_g_band_source_2_df['BMJD'],
custom_g_band_source_2_df['background'],
label='Custom',
)
axes[0].axhline(
100,
color='k',
zorder=0,
)
axes[1].plot(
default_g_band_source_2_df['BMJD'],
default_g_band_source_2_df['background_err'],
label='Default',
)
axes[1].plot(
custom_g_band_source_2_df['BMJD'],
custom_g_band_source_2_df['background_err'],
label='Custom',
)
axes[1].axhline(
10,
color='k',
zorder=0,
)
axes[1].set_xlabel('BMJD [days]')
axes[0].set_ylabel('Background [Counts]')
axes[1].set_ylabel('Background error [Counts]')
axes[0].legend()
axes[1].legend()
plt.show()
As we can see, both methods are around the expected values, and so either local background estimator would likely give pretty good results in practise.
That concludes the local background tutorial for opticam_new! The default local background estimator should be “good enough” in most cases, but we have seen how to implement custom local background estimators if required. Once again, I refer to the excellent photutils documentation for further details on implementing custom local background estimators: https://photutils.readthedocs.io/en/stable/user_guide/aperture.html#local-background-subtraction.