Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Distortion Solution #284

Open
wants to merge 62 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
9b0f4fd
final updates to e2e testing params
manduhmia Dec 17, 2024
edb9404
updating TVAC data path and removing unnecessary ascii import
manduhmia Dec 17, 2024
34c71f4
fixing input error in measuring star offsets for computing plate scal…
manduhmia Dec 17, 2024
02f6bfd
Merge branch 'main' of https://github.com/roman-corgi/corgidrp into a…
manduhmia Dec 17, 2024
b987bac
adding function to fit the legendre coeffs for distortion correction
manduhmia Dec 17, 2024
0a5812f
adding function to compute the distortion map coeffs
manduhmia Dec 17, 2024
e99c62b
adding a distortion coefficient atribute to the astrometric calibrati…
manduhmia Jan 13, 2025
3b9d4d6
create function to format the input for distortion map computation
manduhmia Jan 13, 2025
b59720e
create function to compute the distortion coefficients
manduhmia Jan 13, 2025
63c8c6b
update the boresight function to compute distortion map coefficients …
manduhmia Jan 13, 2025
965788f
update the input data length check
manduhmia Jan 13, 2025
53fe0f5
Merge branch 'main' of https://github.com/roman-corgi/corgidrp into a…
manduhmia Jan 13, 2025
5554d78
Merge pull request #283 from roman-corgi/main
manduhmia Jan 13, 2025
eddf6b5
Merge branch 'astrom_e2e' of https://github.com/roman-corgi/corgidrp …
manduhmia Jan 13, 2025
f08e3cd
updating docstrings
manduhmia Jan 13, 2025
79f9176
updates to measured_offset functioni (to return errors)
manduhmia Jan 22, 2025
b4abcba
updating astrometry fitting function for distortion map calculation (…
manduhmia Jan 22, 2025
d6057f2
updating variable assignment after changing measure_offset outputs
manduhmia Jan 22, 2025
d8fcca2
updating previously hard-coded variables
manduhmia Jan 22, 2025
fbf0acf
updating AstromCal data object input formatting
manduhmia Jan 22, 2025
62ab820
updating create_astrom_data function to include optical distortion
manduhmia Jan 22, 2025
ed4240c
adding distortion coeffs as an attribute of the AstromCal data object
manduhmia Jan 22, 2025
86ac1f3
updating unit test to consider more stars
manduhmia Feb 3, 2025
b7d066b
adding sample legendre coeffs for unit testing of distortion map extr…
manduhmia Feb 3, 2025
c04d99d
updating the formatting code for distortion to consider star pairs wi…
manduhmia Feb 3, 2025
1cb9c8b
updating the distortion fitting function to take a new initial guess …
manduhmia Feb 3, 2025
88aa5f5
updating mock function to include magnitude in guess table
manduhmia Feb 3, 2025
c5f3431
creating a new unit test for the distortion mapping
manduhmia Feb 3, 2025
0001c5f
updating the cost function to remove for loop through frames and upda…
manduhmia Feb 4, 2025
ec5a64e
finalizing the distortion correction unit test
manduhmia Feb 4, 2025
ee48061
updating boresight function to handle the change in star pair formatt…
manduhmia Feb 4, 2025
100b381
updating the default boresight calibration to exclude distortion corr…
manduhmia Feb 4, 2025
fa09870
changing inital boresight conditions back to match original unit test
manduhmia Feb 4, 2025
19bea7d
changing from np.nan placeholder to np.inf to be able to evaluate pic…
manduhmia Feb 5, 2025
9fb9fb0
write the distortion correction function
manduhmia Feb 7, 2025
25ddedd
begin writing distortion correction unit test
manduhmia Feb 7, 2025
7832abc
updating docstring to reflect new output in measure_offset function
manduhmia Feb 7, 2025
5879a9b
updating function name to be more intuitive: fit_distortion_solution
manduhmia Feb 7, 2025
fe75222
adding parameter in boresight_cal to pass in initial guess for distor…
manduhmia Feb 10, 2025
c0820a6
adding the create_wcs() function
manduhmia Feb 10, 2025
36a5f5a
assuming a 'ROLL' header keyword in the primary header and not saving…
manduhmia Feb 10, 2025
e4ea2a7
creating a unit test for create_wcs() in l2b_to_l3 step function
manduhmia Feb 10, 2025
594d550
fixing incorrect call to image data
manduhmia Feb 10, 2025
16154fb
removing dependence on image ext_hdr values before wcs is created
manduhmia Feb 10, 2025
6a505ea
Merge pull request #298 from roman-corgi/astrom_e2e
manduhmia Feb 11, 2025
4489bd2
fixing indexing issue when matches are not passed into boresight cal
manduhmia Feb 11, 2025
49e44ff
updating distortion correction test
manduhmia Feb 11, 2025
d0612c5
altering boresight_cal to handle just a single filepath of field_matc…
manduhmia Feb 23, 2025
0b9ae6e
altering distortion coefficient formatting to be more intuitive
manduhmia Feb 25, 2025
285977e
setting default distortion coeffs to produce zero distortion instead …
manduhmia Feb 25, 2025
b5199ba
fixing boresight_calibration dependency on WCS info
manduhmia Feb 26, 2025
49cc1a8
updating specifics of input array format for the AstrometricCalibrati…
manduhmia Feb 26, 2025
6987078
placing the distortion map testing function inside the test astrom file
manduhmia Feb 26, 2025
6719543
updating unit test docstrings to be more descriptive
manduhmia Feb 26, 2025
770f4f6
Merge branch 'astrom_e2e' into astrom_b2
manduhmia Feb 26, 2025
4ee1bb7
Merge pull request #299 from roman-corgi/astrom_b2
manduhmia Feb 26, 2025
f2a1577
removing test_distortion_map_creation function because it was merged …
manduhmia Feb 26, 2025
c712b66
adding a check that the error in distortion within the central 1arcse…
manduhmia Mar 4, 2025
d1c7419
adding a platescale ext_hdr keyword into the create wcs function
manduhmia Mar 4, 2025
3101585
using arctan function to handle pa differences
manduhmia Mar 4, 2025
48379da
fixing incorrect syntax that was causing auto-pass for unit test
manduhmia Mar 5, 2025
0a27e5a
fixing input formatting across several functions
manduhmia Mar 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 240 additions & 12 deletions corgidrp/astrom.py

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions corgidrp/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1311,27 +1311,29 @@ class AstrometricCalibration(Image):
Class for astrometric calibration file.

Args:
data_or_filepath (str or np.array); either the filepath to the FITS file to read in OR the 1D array of calibration measurements
data_or_filepath (str or np.array): either the filepath to the FITS file to read in OR an array of calibration measurements (boresight, plate scale, north angle, distortion coeffs)
pri_hdr (astropy.io.fits.Header): the primary header (required only if raw 2D data is passed in)
ext_hdr (astropy.io.fits.Header): the image extension header (required only if raw 2D data is passed in)

Attrs:
boresight (np.array): the [(RA, Dec)] of the center pixel in ([deg], [deg])
platescale (float): the platescale value in [mas/pixel]
northangle (float): the north angle value in [deg]
distortion_coeffs (np.array): the array of legendre polynomial coefficients that describe the distortion map (if distortion map is not computed this is an array of nans)

"""
def __init__(self, data_or_filepath, pri_hdr=None, ext_hdr=None, input_dataset=None):
def __init__(self, data_or_filepath, pri_hdr=None, ext_hdr=None, err=None, input_dataset=None):
# run the image class constructor
super().__init__(data_or_filepath, pri_hdr=pri_hdr, ext_hdr=ext_hdr)
super().__init__(data_or_filepath, pri_hdr=pri_hdr, ext_hdr=ext_hdr, err=err)

# File format checks
if self.data.shape != (4,):
raise ValueError("The AstrometricCalibration data should be a 1D array of four values")
if type(self.data) != np.ndarray:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could still be checking for a minimum length (appears to be 6)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the length now depends on the order of leg polynomial being fit

raise ValueError("The AstrometricCalibration data should be an array of calibration measurements")
else:
self.boresight = self.data[:2]
self.platescale = self.data[2]
self.northangle = self.data[3]
self.distortion_coeffs = (self.data[4:-1], self.data[-1])

# if this is a new astrometric calibration file, bookkeep it in the header
# we need to check if it is new
Expand Down
85 changes: 75 additions & 10 deletions corgidrp/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -866,17 +866,19 @@ def make_fluxmap_image(
dq = dq)
return image

def create_astrom_data(field_path, filedir=None, subfield_radius=0.02, platescale=21.8, rotation=45, add_gauss_noise=True):
def create_astrom_data(field_path, filedir=None, image_shape=(1024, 1024), subfield_radius=0.02, platescale=21.8, rotation=45, add_gauss_noise=True, distortion_coeffs_path=None):
"""
Create simulated data for astrometric calibration.

Args:
field_path (str): Full path to directory with test field data (ra, dec, vmag, etc.)
filedir (str): (Optional) Full path to directory to save to.
image_shape (tuple of ints): The desired shape of the image (num y pixels, num x pixels), (default: (1024, 1024))
subfield_radius (float): The radius [deg] around the target coordinate for creating a subfield to produce the image from
platescale (float): The plate scale of the created image data (default: 21.8 [mas/pixel])
rotation (float): The north angle of the created image data (default: 45 [deg])
add_gauss_noise (boolean): Argument to determine if gaussian noise should be added to the data (default: True)
distortion_coeffs_path (str): Full path to csv with the distortion coefficients and the order of polynomial used to describe distortion (default: None))

Returns:
corgidrp.data.Dataset:
Expand All @@ -891,9 +893,8 @@ def create_astrom_data(field_path, filedir=None, subfield_radius=0.02, platescal
os.mkdir(filedir)

# hard coded image properties
size = (1024, 1024)
sim_data = np.zeros(size)
ny, nx = size
sim_data = np.zeros(image_shape)
ny, nx = image_shape
center = [nx //2, ny //2]
target = (80.553428801, -69.514096821)
fwhm = 3
Expand Down Expand Up @@ -936,6 +937,8 @@ def create_astrom_data(field_path, filedir=None, subfield_radius=0.02, platescal

xpix = xpix[pix_inds]
ypix = ypix[pix_inds]
ras = cal_SkyCoords[pix_inds]
decs = cal_SkyCoords[pix_inds]

amplitudes = np.power(10, ((subfield['VMAG'][pix_inds] - 22.5) / (-2.5))) * 10

Expand Down Expand Up @@ -984,9 +987,69 @@ def create_astrom_data(field_path, filedir=None, subfield_radius=0.02, platescal
noise_rng = np.random.default_rng(10)
gain = 1
ref_flux = 10
noise = noise_rng.normal(scale= ref_flux/gain * 0.1, size= size)
noise = noise_rng.normal(scale= ref_flux/gain * 0.1, size= image_shape)
sim_data = sim_data + noise

# add distortion (optional)
if distortion_coeffs_path is not None:
# load in distortion coeffs and fitorder
coeff_data = np.genfromtxt(distortion_coeffs_path, delimiter=',', dtype=None)
fitorder = int(coeff_data[-1])

# convert legendre polynomials into distortin maps in x and y
yorig, xorig = np.indices(image_shape)
y0, x0 = image_shape[0]//2, image_shape[1]//2
yorig -= y0
xorig -= x0

# get the number of fitting params from the order
fitparams = (fitorder + 1)**2

# reshape the coeff arrays
best_params_x = coeff_data[:fitparams]
best_params_x = best_params_x.reshape(fitorder+1, fitorder+1)

total_orders = np.arange(fitorder+1)[:,None] + np.arange(fitorder+1)[None, :]

best_params_x = best_params_x / 500**(total_orders)

# evaluate the polynomial at all pixel positions
x_corr = np.polynomial.legendre.legval2d(xorig.ravel(), yorig.ravel(), best_params_x)
x_corr = x_corr.reshape(xorig.shape)
distmapx = x_corr - xorig

# reshape and evaluate the same for y
best_params_y = coeff_data[fitparams:-1]
best_params_y = best_params_y.reshape(fitorder+1, fitorder+1)

best_params_y = best_params_y / 500**(total_orders)

# evaluate the polynomial at all pixel positions
y_corr = np.polynomial.legendre.legval2d(xorig.ravel(), yorig.ravel(), best_params_y)
y_corr = y_corr.reshape(yorig.shape)
distmapy = y_corr - yorig

## distort image based on coeffs
if (nx >= ny): imgsize = nx
else: imgsize = ny

gridx, gridy = np.meshgrid(np.arange(imgsize), np.arange(imgsize))
gridx = gridx + distmapx
gridy = gridy + distmapy

sim_data = scipy.ndimage.map_coordinates(sim_data, [gridy, gridx])

# transform the source coordinates
dist_xpix, dist_ypix = [], []
for (x,y) in zip(xpix, ypix):
x_new = x + distmapx[int(y)][int(x)]
y_new = y + distmapy[int(y)][int(x)]

dist_xpix.append(x_new)
dist_ypix.append(y_new)

xpix, ypix = dist_xpix, dist_ypix

# load as an image object
frames = []
prihdr, exthdr = create_default_headers()
Expand All @@ -997,14 +1060,16 @@ def create_astrom_data(field_path, filedir=None, subfield_radius=0.02, platescal
newhdr = fits.Header(new_hdr)
frame = data.Image(sim_data, pri_hdr= prihdr, ext_hdr= newhdr)
filename = "simcal_astrom.fits"
guessname = "guesses.csv"
if filedir is not None:
# save source SkyCoord locations and pixel location estimates
guess = Table()
guess['x'] = [int(x) for x in xpix]
guess['y'] = [int(y) for y in ypix]
guess['RA'] = cal_SkyCoords[pix_inds].ra
guess['DEC'] = cal_SkyCoords[pix_inds].dec
ascii.write(guess, filedir+'/guesses.csv', overwrite=True)
guess['x'] = [x for x in xpix]
guess['y'] = [y for y in ypix]
guess['RA'] = ras.ra
guess['DEC'] = decs.dec
guess['VMAG'] = subfield['VMAG'][pix_inds]
ascii.write(guess, filedir+'/'+guessname, overwrite=True)

frame.save(filedir=filedir, filename=filename)

Expand Down
3 changes: 1 addition & 2 deletions tests/e2e_tests/astrom_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import numpy as np
import astropy.time as time
import astropy.io.fits as fits
import astropy.io.ascii as ascii
import corgidrp
import corgidrp.data as data
import corgidrp.mocks as mocks
Expand Down Expand Up @@ -178,7 +177,7 @@ def test_astrom_e2e(tvacdata_path, e2eoutput_path):
assert dec == pytest.approx(target[1], abs=8.333e-7)

if __name__ == "__main__":
tvacdata_dir = "/Users/macuser/Roman/corgi_contributions/Callibration_Notebooks/TVAC"
tvacdata_dir = "/Users/macuser/Roman/corgidrp_develop/calibration_notebooks/TVAC"
outputdir = thisfile_dir

ap = argparse.ArgumentParser(description="run the l1->l2b->boresight end-to-end test")
Expand Down
1 change: 0 additions & 1 deletion tests/test_astrom.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ def test_astrom():

# perform the astrometric calibration
astrom_cal = astrom.boresight_calibration(input_dataset=dataset, field_path=field_path, find_threshold=5)
assert len(astrom_cal.data) == 4

# the data was generated to have the following image properties
expected_platescale = 21.8
Expand Down
33 changes: 33 additions & 0 deletions tests/test_data/distortion_expected_coeffs.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
0.00000000e+00
5.37588246e-01
1.45977937e-01
-5.71312982e-02
5.00412506e+02
2.96600421e-01
-1.92137365e-01
-7.80792573e-01
-4.85109411e-01
-2.75618370e-02
-1.58717081e-01
-1.94784434e-01
-1.39807000e-02
-5.13642791e-02
-1.33704172e-02
1.08517713e-02
0.00000000e+00
5.00855736e+02
3.15740375e-01
-1.46063718e-01
-1.87760787e-02
-4.02194305e-01
-2.91035718e-02
1.72557478e-01
-4.50792397e-01
-2.38286770e-01
1.81843117e-01
2.03185301e-01
5.01096120e-01
2.54169286e-02
4.85195275e-02
5.61938400e-02
3
119 changes: 119 additions & 0 deletions tests/test_distortion_map_creation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import os
import pickle
import numpy as np
import pytest

import corgidrp
import corgidrp.mocks as mocks
import corgidrp.astrom as astrom
import corgidrp.data as data

import astropy
from astropy.io import ascii

def test_distortion():
"""
Generate a simulated image and test the distortion map creation as part of the boresight calibration.

"""
# create a simulated image with source guesses and true positions
# check that the simulated image folder exists and create if not
datadir = os.path.join(os.path.dirname(__file__), "test_data", "simastrom")
if not os.path.exists(datadir):
os.mkdir(datadir)

field_path = os.path.join(os.path.dirname(__file__), "test_data", "JWST_CALFIELD2020.csv")
distortion_coeffs_path = os.path.join(os.path.dirname(__file__), "test_data", "distortion_expected_coeffs.csv")

mocks.create_astrom_data(field_path=field_path, filedir=datadir, distortion_coeffs_path=distortion_coeffs_path)

image_path = os.path.join(datadir, 'simcal_astrom.fits')
source_match_path = os.path.join(datadir, 'guesses.csv')
matches = ascii.read(source_match_path)

# open the image
dataset = data.Dataset([image_path])

# perform the astrometric calibration
# feed in the correct matches and use only ~100 randomly selected stars
rng = np.random.default_rng(seed=17)
select_stars = rng.choice(len(matches), size=150, replace=False)

astrom_cal = astrom.boresight_calibration(input_dataset=dataset, field_path=field_path, field_matches=[matches[select_stars]], find_distortion=True)

## check that the distortion map does not create offsets greater than 4[mas]
# compute the distortion maps created from the best fit coeffs
coeffs = astrom_cal.distortion_coeffs
expected_coeffs = np.genfromtxt(distortion_coeffs_path)

# note the image shape and center around the image center
image_shape = np.shape(dataset[0].data)
yorig, xorig = np.indices(image_shape)
y0, x0 = image_shape[0]//2, image_shape[1]//2
yorig -= y0
xorig -= x0

# get the number of fitting params from the order
fitorder = int(coeffs[1])
fitparams = (fitorder + 1)**2
true_fitorder = int(expected_coeffs[-1])
true_fitparams = (true_fitorder + 1)**2

# reshape the coeff arrays
best_params_x = coeffs[0][:fitparams]
best_params_x = best_params_x.reshape(fitorder+1, fitorder+1)
total_orders = np.arange(fitorder+1)[:,None] + np.arange(fitorder+1)[None, :]
best_params_x = best_params_x / 500**(total_orders)

true_params_x = expected_coeffs[:-1][:true_fitparams]
true_params_x = true_params_x.reshape(true_fitorder+1, true_fitorder+1)
true_total_orders = np.arange(true_fitorder+1)[:,None] + np.arange(true_fitorder+1)[None, :]
true_params_x = true_params_x / 500**(true_total_orders)

# evaluate the polynomial at all pixel positions
x_corr = np.polynomial.legendre.legval2d(xorig.ravel(), yorig.ravel(), best_params_x)
x_corr = x_corr.reshape(xorig.shape)
x_diff = x_corr - xorig

true_x_corr = np.polynomial.legendre.legval2d(xorig.ravel(), yorig.ravel(), true_params_x)
true_x_corr = true_x_corr.reshape(xorig.shape)
true_x_diff = true_x_corr - xorig

# reshape and evaluate the same for y
best_params_y = coeffs[0][fitparams:]
best_params_y = best_params_y.reshape(fitorder+1, fitorder+1)
best_params_y = best_params_y / 500**(total_orders)

true_params_y = expected_coeffs[:-1][true_fitparams:]
true_params_y = true_params_y.reshape(true_fitorder+1, true_fitorder+1)
true_params_y = true_params_y / 500**(true_total_orders)

# evaluate the polynomial at all pixel positions
y_corr = np.polynomial.legendre.legval2d(xorig.ravel(), yorig.ravel(), best_params_y)
y_corr = y_corr.reshape(yorig.shape)
y_diff = y_corr - yorig

true_y_corr = np.polynomial.legendre.legval2d(xorig.ravel(), yorig.ravel(), true_params_y)
true_y_corr = true_y_corr.reshape(yorig.shape)
true_y_diff = true_y_corr - yorig

# check the distortion maps are less than the maximum injected distortion (~3 pixels)
assert np.all(np.abs(x_diff)) < np.max(np.abs(true_x_diff))
assert np.all(np.abs(y_diff)) < np.max(np.abs(true_y_diff))

# check they can be pickled (for CTC operations)
pickled = pickle.dumps(astrom_cal)
pickled_astrom = pickle.loads(pickled)
assert np.all((astrom_cal.data == pickled_astrom.data))

# save and check it can be pickled after save
astrom_cal.save(filedir=datadir, filename="astrom_cal_output.fits")
astrom_cal_2 = data.AstrometricCalibration(os.path.join(datadir, "astrom_cal_output.fits"))

# check they can be pickled (for CTC operations)
pickled = pickle.dumps(astrom_cal_2)
pickled_astrom = pickle.loads(pickled)
assert np.all((astrom_cal.data == pickled_astrom.data)) # check it is the same as the original

if __name__ == "__main__":
test_distortion()