Source code for punchbowl.level1.stray_light
import os
import pathlib
import warnings
import numpy as np
from astropy.wcs import WCS
from ndcube import NDCube
from punchbowl.data import NormalizedMetadata, load_ndcube_from_fits
from punchbowl.exceptions import IncorrectPolarizationStateWarning, IncorrectTelescopeWarning, InvalidDataError
from punchbowl.prefect import punch_flow, punch_task
from punchbowl.util import interpolate_data, nan_percentile
[docs]
@punch_flow
def estimate_stray_light(filepaths: list[str],
percentile: float = 1,
do_uncertainty: bool = True) -> np.ndarray | tuple[np.ndarray, np.ndarray]:
"""Estimate the fixed stray light pattern using a percentile."""
data = None
uncertainties = None
for i, path in enumerate(filepaths):
cube = load_ndcube_from_fits(path, include_provenance=False, include_uncertainty=do_uncertainty)
if i == 0:
first_meta = cube.meta
if i == len(filepaths) - 1:
last_meta = cube.meta
if data is None:
data = np.empty((len(filepaths), *cube.data.shape))
data[i] = cube.data
if do_uncertainty:
if uncertainties is None:
uncertainties = np.empty_like(data)
if cube.uncertainty is not None:
uncertainties[i] = cube.uncertainty.array
else:
uncertainties[i] = np.zeros_like(cube.data)
stray_light_estimate = nan_percentile(data, percentile).squeeze()
out_type = "S" + cube.meta.product_code[1:]
meta = NormalizedMetadata.load_template(out_type, "1")
meta["DATE-OBS"] = first_meta["DATE-OBS"].value
meta.history.add_now("stray light",
f"Generated with {len(filepaths)} files running from "
f"{first_meta['DATE-OBS'].value} to {last_meta['DATE-OBS'].value}")
meta["FILEVRSN"] = first_meta["FILEVRSN"].value
uncertainty = np.sqrt(np.sum(uncertainties ** 2, axis=0)) / len(filepaths) if do_uncertainty else None
out_cube = NDCube(data=stray_light_estimate, meta=meta, wcs=WCS(naxis=2), uncertainty=uncertainty)
return [out_cube]
[docs]
@punch_task
def remove_stray_light_task(data_object: NDCube,
stray_light_before_path: pathlib.Path | str,
stray_light_after_path: pathlib.Path | str) -> NDCube:
"""
Prefect task to remove stray light from an image.
Stray light is light in an optical system which was not intended in the
design.
The PUNCH instrument stray light will be mapped periodically as part of the
ongoing in-flight calibration effort. The stray light maps will be
generated directly from the L0 and L1 science data. Separating instrumental
stray light from the F-corona. This has been demonstrated with SOHO/LASCO
and with STEREO/COR2 observations. It requires an instrumental roll to hold
the stray light pattern fixed while the F-corona rotates in the field of
view. PUNCH orbital rolls will be used to create similar effects.
Uncertainty across the image plane is calculated using a known stray light
model and the difference between the calculated stray light and the ground
truth. The uncertainty is convolved with the input uncertainty layer to
produce the output uncertainty layer.
Parameters
----------
data_object : NDCube
data to operate on
stray_light_before_path: pathlib
path to stray light model before observation to apply to data
stray_light_after_path: pathlib
path to stray light model after observation to apply to data
Returns
-------
NDCube
modified version of the input with the stray light removed
"""
if stray_light_before_path is None or stray_light_after_path is None:
data_object.meta.history.add_now("LEVEL1-remove_stray_light", "Stray light correction skipped")
return data_object
stray_light_before_path = pathlib.Path(stray_light_before_path)
stray_light_after_path = pathlib.Path(stray_light_after_path)
if not stray_light_before_path.exists() or not stray_light_after_path.exists():
msg = f"File {stray_light_before_path} or {stray_light_after_path} does not exist."
raise InvalidDataError(msg)
stray_light_before_model = load_ndcube_from_fits(stray_light_before_path)
stray_light_after_model = load_ndcube_from_fits(stray_light_after_path)
if stray_light_before_model.meta["TELESCOP"].value != data_object.meta["TELESCOP"].value:
msg=f"Incorrect TELESCOP value within {stray_light_before_path}"
warnings.warn(msg, IncorrectTelescopeWarning)
elif stray_light_before_model.meta["OBSLAYR1"].value != data_object.meta["OBSLAYR1"].value:
msg=f"Incorrect polarization state within {stray_light_before_path}"
warnings.warn(msg, IncorrectPolarizationStateWarning)
elif stray_light_before_model.data.shape != data_object.data.shape:
msg = f"Incorrect stray light function shape within {stray_light_before_path}"
raise InvalidDataError(msg)
elif stray_light_after_model.meta["TELESCOP"].value != data_object.meta["TELESCOP"].value:
msg=f"Incorrect TELESCOP value within {stray_light_after_path}"
warnings.warn(msg, IncorrectTelescopeWarning)
elif stray_light_after_model.meta["OBSLAYR1"].value != data_object.meta["OBSLAYR1"].value:
msg=f"Incorrect polarization state within {stray_light_after_path}"
warnings.warn(msg, IncorrectPolarizationStateWarning)
elif stray_light_after_model.data.shape != data_object.data.shape:
msg = f"Incorrect stray light function shape within {stray_light_after_path}"
raise InvalidDataError(msg)
else:
stray_light_model = interpolate_data(stray_light_before_model,
stray_light_after_model,
data_object.meta.datetime)
data_object.data[:, :] -= stray_light_model
uncertainty = 0
# TODO: when we have real uncertainties, use them
# uncertainty = stray_light_model.uncertainty.array # noqa: ERA001
data_object.uncertainty.array[...] = np.sqrt(data_object.uncertainty.array**2 + uncertainty**2)
data_object.meta.history.add_now("LEVEL1-remove_stray_light",
f"stray light removed with {os.path.basename(str(stray_light_before_path))}"
f"and {os.path.basename(str(stray_light_after_path))}")
return data_object