diff --git a/README.md b/README.md index d6d38d3..84d627b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,31 @@ # PSI -PSI package for focal-plane wavefront sensing, developed primarily for ELT/METIS +Python implementation of Phase Sorting Interferometry (PSI) for estimating non-common path errors in High Contrast Imaging (HCI) instruments. + +This PSI package is developed primarily for ELT/METIS, but also for VLT/ERIS. + + +## Legacy contribution +This work is based on the initial work of Emiel Por, followed-up by the work of Matthew Willson†: +- [fepsi](https://github.com/mkenworthy/fepsi) : written by Emiel Por and modified by Matthew Kenworthy (Leiden University) +- [psi](https://github.com/mwillson-astro/PSI/tree/master) : preliminary package developed by Matthew Willson† for METIS (Liège University) + +The initial commit of this repository is a copy of [psi](https://github.com/mwillson-astro/PSI/tree/master). + +## References +- ["Focal Plane Wavefront Sensing Using Residual Adaptive Optics Speckles" by Codona and Kenworthy (2013)](https://iopscience.iop.org/article/10.1088/0004-637X/767/2/100), ApJ, 767, 100. + +## Dependencies + +Code requires the following Python packages: + * `astropy` + * `matplotlib` + * `hcipy` + +The ffmpeg tools are required for generating movies. + +[HCIPy](https://github.com/ehpor/hcipy) is a Python software package written and developed by Emiel Por for performing end-to-end simulations of high contrast imaging instruments for astronomy. + +It can be installed from PyPI with: +``` +pip install hcipy +``` diff --git a/__init__ .py b/__init__ .py new file mode 100644 index 0000000..2232918 --- /dev/null +++ b/__init__ .py @@ -0,0 +1,19 @@ +# Import all submodules. +from . import aperture +from . import psi + +# Import all core submodules in default namespace. +from .aperture import * +from .psi import * + +# Export default namespaces. +__all__ = [] +__all__.extend(aperture.__all__) +__all__.extend(psi.__all__) + +from pkg_resources import get_distribution, DistributionNotFound +try: + __version__ = get_distribution(__name__).version +except DistributionNotFound: + # package is not installed + pass diff --git a/aperture/__init__.py b/aperture/__init__.py new file mode 100644 index 0000000..1f69d5e --- /dev/null +++ b/aperture/__init__.py @@ -0,0 +1,3 @@ +__all__ = ['make_COMPASS_aperture', 'resize_img', 'pad_img', 'crop_img'] + +from .realistic import * diff --git a/aperture/realistic.py b/aperture/realistic.py new file mode 100644 index 0000000..af90660 --- /dev/null +++ b/aperture/realistic.py @@ -0,0 +1,127 @@ +import numpy as np +# from ..field import CartesianGrid, UnstructuredCoords, make_hexagonal_grid, Field +# from .generic import * +from hcipy.field import CartesianGrid, UnstructuredCoords, make_hexagonal_grid, Field # These two lines maybe needed in the future for making custom apertures so I have left them in +from hcipy.aperture.generic import * + +def make_vlt_aperture(): + pass + +def make_subaru_aperture(): + pass + +def make_lbt_aperture(): + pass + +def make_elt_aperture(): + pass + +def make_COMPASS_aperture(npupil=256, input_folder='/Users/matt/Documents/METIS/TestArea/fepsi/COMPASSPhaseScreens/Test/', nimg=720): + '''Create an aperture from a COMPASS product. + + Parameters + ---------- + npupil : scalar + Number of pixels across each dimension of the array. + input_folder : string + Location of the aperture file from COMPASS. + nimg : scalar + The size the aperture file needs to be cut down to as it comes with some padding. 720 should be the default unless something + changes in the COMPASS products. + + Returns + ------- + Field generator + The resized COMPASS aperture. + ''' + + mask = fits.getdata(os.path.join(input_folder, 'mask_256.fits')) + if mask.shape[0] < nimg: + mask = crop_img(mask, nimg, verbose=False) + mask_pupil = resize_img(mask, npupil) + #mask_pupil[mask_pupil<0.8] = 0 + #mask_pupil = mask_pupil.transpose() # Testing for wind direction dependencies. Should be commented out. + aperture = np.ravel(mask_pupil) + + nimg = 720 + npupil = 256 + + def func(grid): + return Field(aperture, grid) + return func + +def resize_img(img, new_size, preserve_range=True, mode='reflect', + anti_aliasing=True): + ''' Resize an image. Handles even and odd sizes. + ''' + requirement = "new_size must be an int or a tuple/list of size 2." + assert type(new_size) in [int, tuple, list], requirement + if type(new_size) is int: + new_size = (new_size, new_size) + else: + assert len(new_size) == 2, requirement + assert img.ndim in [2, 3], 'image must be a frame (2D) or a cube (3D)' + if img.ndim == 3: + new_size = (len(img), *new_size) + if new_size != img.shape: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") # when anti_aliasing=False, and NANs + img = np.float32(resize(np.float32(img), new_size, \ + preserve_range=preserve_range, mode=mode, anti_aliasing=anti_aliasing)) + return img + +def pad_img(img, padded_size, pad_value=0): + ''' Pad an img with a value (default is zero). Handles even and odd sizes. + ''' + requirement = "padded_size must be an int or a tuple/list of size 2." + assert type(padded_size) in [int, tuple, list], requirement + if type(padded_size) is int: + (x1, y1) = (padded_size, padded_size) + else: + assert len(padded_size) == 2, requirement + (x1, y1) = padded_size + (x2, y2) = img.shape + # determine padding region + assert not (x1 rad_inner + rad_out = rd < rad_outer + rad = rad_in*rad_out + mean_flux = np.append(mean_flux, np.mean(frame[np.ravel(rad)])) + return mean_flux + +def remove_piston(data, mask_): + data += -np.mean(data[mask_!=0.0]) # Remove piston + data[mask_==0.0] = 0.0 + return data + +def gauss_2Dalt(size_x=400, size_y=None, sigma_x=5, sigma_y=None, x0=None, y0=None, theta=0): + # 2D Guassian which can be elongated in x,y and angled. + if size_y == None: + size_y = size_x + if sigma_y == None: + sigma_y = sigma_x + if x0 == None: + x0 = size_x//2 + if y0 == None: + y0 = x0 + size_x = int(size_x) + size_y = int(size_y) + x_g,y_g = np.meshgrid(np.arange(0, size_x), np.arange(0, size_y)) + x_g = x_g - x0 + y_g = y_g - y0 + a = np.cos(theta)**2 / (2*sigma_x**2) + np.sin(theta)**2 / (2*sigma_y**2) + b = -np.sin(2*theta) / (4*sigma_x**2) + np.sin(2*theta) / (4*sigma_y**2) + c = np.sin(theta)**2 / (2*sigma_x**2) + np.cos(theta)**2 / (2*sigma_y**2) + exp_part = a*x_g**2 - 2*b*x_g*y_g + c*y_g**2 + return 1/(2*np.pi*sigma_x*sigma_y) * np.exp(-exp_part) + +def resetVariables(a): + return np.copy(a), 0, 0, 0+0j + +def resetPSIVariables(): + return 0, 0, 0, 0