"""
A generalised module to provide phase or the EField through a "Line Of Sight"
Line of Sight Object
====================
The module contains a 'lineOfSight' object, which calculates the resulting phase or complex amplitude from propogating through the atmosphere in a given
direction. This can be done using either geometric propagation, where phase is simply summed for each layer, or physical propagation, where the phase is propagated between layers using an angular spectrum propagation method. Light can propogate either up or down.
The Object takes a 'config' as an argument, which is likely to be the same config object as the module using it (WFSs, ScienceCams, or LGSs). It should contain paramters required, such as the observation direction and light wavelength. The `config` also determines whether to use physical or geometric propagation through the 'propagationMode' parameter.
Examples::
from soapy import confParse, lineofsight
# Initialise a soapy conifuration file
config = confParse.loadSoapyConfig('conf/sh_8x8.py')
# Can make a 'LineOfSight' for WFSs
los = lineofsight.LineOfSight(config.wfss[0], config)
# Get resulting complex amplitude through line of sight
EField = los.frame(some_phase_screens)
"""
import numpy
from scipy.interpolate import interp2d
from . import logger
from .aotools import opticalpropagation, interp
DTYPE = numpy.float32
CDTYPE = numpy.complex64
# Python3 compatability
try:
xrange
except NameError:
xrange = range
RAD2ASEC = 206264.849159
ASEC2RAD = 1./RAD2ASEC
[docs]class LineOfSight(object):
"""
A "Line of sight" through a number of turbulence layers in the atmosphere, observing ing a given direction.
Parameters:
config: The soapy config for the line of sight
simConfig: The soapy simulation config object
propagationDirection (str, optional): Direction of light propagation, either `"up"` or `"down"`
outPxlScale (float, optional): The EField pixel scale required at the output (m/pxl)
nOutPxls (int, optional): Number of pixels to return in EFIeld
mask (ndarray, optional): Mask to apply at the *beginning* of propagation
metaPupilPos (list, dict, optional): A list or dictionary of the meta pupil position at each turbulence layer height ub metres. If None, works it out from GS position.
"""
def __init__(
self, config, soapyConfig,
propagationDirection="down", outPxlScale=None,
nOutPxls=None, mask=None, metaPupilPos=None):
self.config = config
self.simConfig = soapyConfig.sim
self.atmosConfig = soapyConfig.atmos
self.soapyConfig = soapyConfig
self.mask = mask
self.calcInitParams(outPxlScale, nOutPxls)
self.propagationDirection = propagationDirection
# If GS not at infinity, find meta-pupil radii for each layer
if self.height!=0:
self.radii = self.findMetaPupilSizes(self.height)
else:
self.radii = None
self.allocDataArrays()
# Can be set to use other values as metapupil position
self.metaPupilPos = metaPupilPos
# Some attributes for compatability between WFS and others
@property
def height(self):
try:
return self.config.height
except AttributeError:
return self.config.GSHeight
@height.setter
def height(self, height):
try:
self.config.height
self.config.height = height
except AttributeError:
self.config.GSHeight
self.config.GSHeight = height
@property
def position(self):
try:
return self.config.position
except AttributeError:
return self.config.GSPosition
@position.setter
def position(self, position):
try:
self.config.position
self.config.position = position
except AttributeError:
self.config.GSPosition
self.config.GSPosition = position
############################################################
# Initialisation routines
[docs] def calcInitParams(self, outPxlScale=None, nOutPxls=None):
"""
Calculates some parameters required later
Parameters:
outPxlScale (float): Pixel scale of required phase/EField (metres/pxl)
nOutPxls (int): Size of output array in pixels
"""
# Convert phase deviation to radians at wfs wavelength.
# (currently in nm remember...?)
self.phs2Rad = 2*numpy.pi/(self.config.wavelength * 10**9)
self.telDiam = float(self.simConfig.pupilSize) / self.simConfig.pxlScale
# Get the size of the phase required by the system
self.inPxlScale = self.simConfig.pxlScale**-1
if outPxlScale is None:
self.outPxlScale = self.simConfig.pxlScale**-1
else:
self.outPxlScale = outPxlScale
if nOutPxls is None:
self.nOutPxls = self.simConfig.simSize
else:
self.nOutPxls = nOutPxls
if self.mask is not None:
self.outMask = interp.zoom(
self.mask, self.nOutPxls).round()
[docs] def allocDataArrays(self):
"""
Allocate the data arrays the LOS will require
Determines and allocates the various arrays the LOS will require to
avoid having to re-alloc memory during the running of the LOS and
keep it fast. This includes arrays for phase
and the E-Field across the LOS
"""
self.phase = numpy.zeros([self.nOutPxls]*2, dtype=DTYPE)
self.EField = numpy.zeros([self.nOutPxls]*2, dtype=CDTYPE)
#############################################################
# Phase stacking routines for a WFS frame
######################################################
[docs] def zeroData(self, **kwargs):
"""
Sets the phase and complex amp data to zero
"""
self.EField[:] = 0
self.phase[:] = 0
[docs] def makePhase(self, radii=None, apos=None):
"""
Generates the required phase or EField. Uses difference approach depending on whether propagation is geometric or physical
(makePhaseGeometric or makePhasePhys respectively)
Parameters:
radii (dict, optional): Radii of each meta pupil of each screen height in pixels. If not given uses pupil radius.
apos (ndarray, optional): The angular position of the GS in radians. If not set, will use the config position
"""
# Check if geometric or physical
if self.config.propagationMode == "Physical":
return self.makePhasePhys(radii)
else:
return self.makePhaseGeometric(radii)
[docs] def makePhaseGeometric(self, radii=None, apos=None):
'''
Creates the total phase along line of sight offset by a given angle using a geometric ray tracing approach
Parameters:
radii (dict, optional): Radii of each meta pupil of each screen height in pixels. If not given uses pupil radius.
apos (ndarray, optional): The angular position of the GS in radians. If not set, will use the config position
'''
for i in range(len(self.scrns)):
logger.debug("Layer: {}".format(i))
if radii is None:
radius = None
else:
radius = radii[i]
if self.metaPupilPos is None:
pos = None
else:
pos = self.metaPupilPos[i]
phase = self.getMetaPupilPhase(
self.scrns[i], self.atmosConfig.scrnHeights[i],
pos=pos, radius=radius)
self.phase += phase
# Convert phase to radians
self.phase *= self.phs2Rad
# Change sign if propagating up
if self.propagationDirection == 'up':
self.phase *= -1
self.EField[:] = numpy.exp(1j*self.phase)
return self.EField
[docs] def makePhasePhys(self, radii=None, apos=None):
'''
Finds total line of sight complex amplitude by propagating light through phase screens
Parameters:
radii (dict, optional): Radii of each meta pupil of each screen height in pixels. If not given uses pupil radius.
apos (ndarray, optional): The angular position of the GS in radians. If not set, will use the config position
'''
scrnNo = len(self.scrns)
z_total = 0
scrnRange = range(0, scrnNo)
# Get initial up/down dependent params
if self.propagationDirection == "up":
ht = 0
ht_final = self.config.height
if ht_final==0:
raise ValueError("Can't propagate up to infinity")
scrnAlts = self.atmosConfig.scrnHeights
self.EFieldBuf = self.outMask.copy().astype(CDTYPE)
logger.debug("Create EField Buf of mask")
else:
ht = self.atmosConfig.scrnHeights[scrnNo-1]
ht_final = 0
scrnRange = scrnRange[::-1]
scrnAlts = self.atmosConfig.scrnHeights[::-1]
self.EFieldBuf = numpy.exp(
1j*numpy.zeros((self.nOutPxls,)*2)).astype(CDTYPE)
logger.debug("Create EField Buf of zero phase")
# Propagate to first phase screen (if not already there)
if ht!=scrnAlts[0]:
logger.debug("propagate to first phase screen")
z = abs(scrnAlts[0] - ht)
self.EFieldBuf[:] = opticalpropagation.angularSpectrum(
self.EFieldBuf, self.config.wavelength,
self.outPxlScale, self.outPxlScale, z)
# Go through and propagate between phase screens
for i in scrnRange:
# Check optional radii and position
if radii is None:
radius = None
else:
radius = radii[i]
if self.metaPupilPos is None:
pos = None
else:
pos = self.metaPupilPos[i]
# Get phase for this layer
phase = self.getMetaPupilPhase(
self.scrns[i],
self.atmosConfig.scrnHeights[i], radius=radius, pos=pos)
# Convert phase to radians
phase *= self.phs2Rad
# Change sign if propagating up
if self.propagationDirection == 'up':
self.phase *= -1
# Get propagation distance for this layer
if i==(scrnNo-1):
z = abs(ht_final - ht) - z_total
else:
z = abs(scrnAlts[i+1] - scrnAlts[i])
# Update total distance counter
z_total += z
# Apply phase to EField
self.EFieldBuf *= numpy.exp(1j*phase)
# Do ASP for last layer to next
self.EFieldBuf[:] = opticalpropagation.angularSpectrum(
self.EFieldBuf, self.config.wavelength,
self.outPxlScale, self.outPxlScale, z)
logger.debug("Propagation: {}, {} m. Total: {}".format(i, z, z_total))
self.EField[:] = self.EFieldBuf
return self.EField
[docs] def frame(self, scrns=None, correction=None):
'''
Runs one frame through a line of sight
Finds the phase or complex amplitude through line of sight for a
single simulation frame, with a given set of phase screens and
some optional correction.
Parameters:
scrns (list): A list or dict containing the phase screens
correction (ndarray, optional): The correction term to take from the phase screens before the WFS is run.
read (bool, optional): Should the WFS be read out? if False, then WFS image is calculated but slopes not calculated. defaults to True.
Returns:
ndarray: WFS Measurements
'''
self.zeroData()
if scrns is not None:
#If scrns is not dict or list, assume array and put in list
t = type(scrns)
if t != dict and t != list:
scrns = [scrns]
self.scrns = scrns
self.makePhase(self.radii)
self.residual = self.phase
if correction is not None:
self.performCorrection(correction)
return self.residual