Source code for prysm.otf

"""A base optical transfer function interface."""
from scipy import interpolate

from .psf import PSF
from .fttools import forward_ft_unit
from .util import share_fig_ax
from .coordinates import polar_to_cart, uniform_cart_to_polar

from prysm import mathops as m


[docs]class MTF(object): """Modulation Transfer Function. Properties ---------- tan: slice along the X axis of the MTF object. sag: slice along the Y axis of the MTF object. Attributes ---------- center_x : `int` x center pixel location (MTF == 1 here by definition) center_y : `int` y center pixel location (MTF == 1 here by definition) data : `numpy.ndarray` MTF data array, 2D interpf_2d : `scipy.interpolate.RegularGridInterpolator` 2D interpolation function used to compute exact values of the 2D MTF interpf_sag : `scipy.interpolate.interp1d` 1D interpolation function used to compute exact values of the sagittal MTF interpf_tan : `scipy.interpolate.interp1d` 1D interpolation function used to compute exact values of the tangential MTF unit_x : `numpy.ndarray` ordinate x coordinates unit_y : TYPE ordinate y coordinates """ def __init__(self, data, unit_x, unit_y=None): """Create an MTF object. Parameters ---------- data : `numpy.ndarray` MTF values on 2D grid unit_x : `numpy.ndarray` array of x units, 1D unit_y : `numpy.ndarray` array of y units, 1D """ if unit_y is None: unit_y = unit_x self.data = data self.unit_x = unit_x self.unit_y = unit_y self.samples_y, self.samples_x = data.shape self.center_x = self.samples_x // 2 self.center_y = self.samples_y // 2 self.interpf_2d = None self.interpf_tan = None self.interpf_sag = None # quick-access slices ------------------------------------------------------ @property def tan(self): """Retrieve the tangential MTF. Notes ----- Assumes the object is extended in y. If the object is extended along a different azimuth, this will not return the tangential MTF. Returns ------- self.unit_x : `numpy.ndarray` ordinate self.data : `numpy.ndarray` coordiante """ return self.unit_x[self.center_x:], self.data[self.center_y:, self.center_x] @property def sag(self): """Retrieve the sagittal MTF. Notes ----- Assumes the object is extended in y. If the object is extended along a different azimuth, this will not return the sagittal MTF. Returns ------- self.unit_x : `numpy.ndarray` ordinate self.data : `numpy.ndarray` coordiante """ return self.unit_y[self.center_y:], self.data[self.center_y, self.center_x:]
[docs] def exact_polar(self, freqs, azimuths=None): """Retrieve the MTF at the specified frequency-azimuth pairs. Parameters ---------- freqs : iterable radial frequencies to retrieve MTF for azimuths : iterable corresponding azimuths to retrieve MTF for Returns ------- `list` MTF at the given points """ self._make_interp_function_2d() # handle user-unspecified azimuth if azimuths is None: if type(freqs) in (int, float): # single azimuth azimuths = 0 else: azimuths = [0] * len(freqs) # handle single azimuth, multiple freqs elif type(azimuths) in (int, float): azimuths = [azimuths] * len(freqs) azimuths = m.radians(azimuths) # handle single value case if type(freqs) in (int, float): x, y = polar_to_cart(freqs, azimuths) return float(self.interpf_2d((x, y), method='linear')) outs = [] for freq, az in zip(freqs, azimuths): x, y = polar_to_cart(freq, az) outs.append(float(self.interpf_2d((x, y), method='linear'))) return m.asarray(outs)
[docs] def exact_xy(self, x, y=None): """Retrieve the MTF at the specified X-Y frequency pairs. Parameters ---------- x : iterable X frequencies to retrieve the MTF at y : iterable Y frequencies to retrieve the MTF at Returns ------- `list` MTF at the given points """ self._make_interp_function_2d() # handle data along just x if y is None: if type(x) in (int, float): # single azimuth y = 0 else: y = [0] * len(x) elif type(y) in (int, float): y = [y] * len(x) # handle data just along y if type(x) in (int, float): x = [x] * len(y) x, y = m.asarray(x), m.asarray(y) outs = [] for x, y in zip(x, y): outs.append(float(self.interpf_2d((x, y), method='linear'))) return m.asarray(outs)
[docs] def exact_tan(self, freq): """Return data at an exact x coordinate along the y=0 axis. Parameters ---------- freq : `number` or `numpy.ndarray` frequency or frequencies to return Returns ------- `numpy.ndarray` ndarray of MTF values """ self._make_interp_function_tansag() return self.interpf_tan(freq)
[docs] def exact_sag(self, freq): """Return data at an exact y coordinate along the x=0 axis. Parameters ---------- freq : `number` or `numpy.ndarray` frequency or frequencies to return Returns ------- `numpy.ndarray` ndarray of MTF values """ self._make_interp_function_tansag() return self.interpf_sag(freq)
[docs] def azimuthal_average(self): """Return the azimuthally averaged MTF. Returns ------- nu : `numpy.ndarray` spatial frequencies mtf : `numpy.ndarray` mtf values """ nu, theta, mtf = uniform_cart_to_polar(self.unit_x, self.unit_y, self.data) l = len(nu) // 2 return nu[l:], mtf.mean(axis=0)
# quick-access slices ------------------------------------------------------ # plotting -----------------------------------------------------------------
[docs] def plot2d(self, max_freq=200, power=1, fig=None, ax=None): """Create a 2D plot of the MTF. Parameters ---------- max_freq : `float` Maximum frequency to plot to. Axis limits will be ((-max_freq, max_freq), (-max_freq, max_freq)). power : `float` inverse of power to stretch the MTF to/by, e.g. power=2 will plot MTF^(1/2) fig : `matplotlib.figure.Figure`, optional: Figure to draw plot in ax : `matplotlib.axes.Axis`, optional: Axis to draw plot in Returns ------- fig : `matplotlib.figure.Figure` Figure to draw plot in ax : `matplotlib.axes.Axis` Axis to draw plot in """ from matplotlib import colors left, right = self.unit_x[0], self.unit_x[-1] bottom, top = self.unit_y[0], self.unit_y[-1] fig, ax = share_fig_ax(fig, ax) im = ax.imshow(self.data, extent=[left, right, bottom, top], origin='lower', cmap='Greys_r', clim=(0, 1), norm=colors.PowerNorm(1/power), interpolation='lanczos') cb = fig.colorbar(im, label='MTF [Rel 1.0]', ax=ax, fraction=0.046) cb.outline.set_edgecolor('k') cb.outline.set_linewidth(0.5) ax.set(xlabel=r'$\nu_x$ [cy/mm]', ylabel=r'$\nu_y$ [cy/mm]', xlim=(-max_freq, max_freq), ylim=(-max_freq, max_freq)) return fig, ax
[docs] def plot_tan_sag(self, max_freq=200, fig=None, ax=None, labels=('Tangential', 'Sagittal')): """Create a plot of the tangential and sagittal MTF. Parameters ---------- max_freq : `float` Maximum frequency to plot to. Axis limits will be ((-max_freq, max_freq), (-max_freq, max_freq)) fig : `matplotlib.figure.Figure`, optional: Figure to draw plot in ax : `matplotlib.axes.Axis`, optional: Axis to draw plot in labels : `iterable` set of labels for the two lines that will be plotted Returns ------- fig : `matplotlib.figure.Figure` Figure to draw plot in ax : `matplotlib.axes.Axis` Axis to draw plot in """ ut, tan = self.tan us, sag = self.sag fig, ax = share_fig_ax(fig, ax) ax.plot(ut, tan, label=labels[0], linestyle='-', lw=3) ax.plot(us, sag, label=labels[1], linestyle='--', lw=3) ax.set(xlabel='Spatial Frequency [cy/mm]', ylabel='MTF [Rel 1.0]', xlim=(0, max_freq), ylim=(0, 1)) ax.legend(loc='lower left') return fig, ax
# plotting ----------------------------------------------------------------- # helpers ------------------------------------------------------------------ def _make_interp_function_2d(self): """Generate a 2D interpolation function for this instance of MTF, used to procure MTF at exact frequencies. Returns ------- `scipy.interpolate.RegularGridInterpolator`, this instance of an MTF object. """ if self.interpf_2d is None: self.interpf_2d = interpolate.RegularGridInterpolator((self.unit_x, self.unit_y), self.data) return self.interpf_2d def _make_interp_function_tansag(self): """Generate two interpolation functions for tangential and sagittal MTF. Returns ------- self.interpf_tan : `scipy.interpolate.interp1d` tangential interpolator self.interpf_sag : `scipy.interpolate.interp1d` sagittal interpolator """ if self.interpf_tan is None or self.interpf_sag is None: ut, tan = self.tan us, sag = self.sag self.interpf_tan = interpolate.interp1d(ut, tan) self.interpf_sag = interpolate.interp1d(us, sag) return self.interpf_tan, self.interpf_sag
[docs] @staticmethod def from_psf(psf): """Generate an MTF from a PSF. Parameters ---------- psf : `PSF` PSF to compute an MTF from Returns ------- `MTF` A new MTF instance """ if getattr(psf, '_mtf', None) is not None: return psf._mtf else: dat = abs(m.fftshift(m.fft2(psf.data))) unit_x = forward_ft_unit(psf.sample_spacing / 1e3, psf.samples_x) # 1e3 for microns => mm unit_y = forward_ft_unit(psf.sample_spacing / 1e3, psf.samples_y) return MTF(dat / dat[psf.center_y, psf.center_x], unit_x, unit_y)
[docs] @staticmethod def from_pupil(pupil, efl, Q=2): """Generate an MTF from a pupil, given a focal length (propagation distance). Parameters ---------- pupil : `Pupil` A pupil to propagate to a PSF, and convert to an MTF efl : `float` Effective focal length or propagation distance of the wavefunction Q : `number` ratio of pupil sample count to PSF sample count. Q > 2 satisfies nyquist Returns ------- `MTF` A new MTF instance """ psf = PSF.from_pupil(pupil, efl=efl, Q=Q) return MTF.from_psf(psf)
def diffraction_limited_mtf(fno, wavelength, frequencies=None, samples=128): """Give the diffraction limited MTF for a circular pupil and the given parameters. Parameters ---------- fno : `float` f/# of the lens. wavelength : `float` wavelength of light, in microns. frequencies : `numpy.ndarray` spatial frequencies of interest, in cy/mm if frequencies are given, samples is ignored. samples : `int` number of points in the output array, if frequencies not given. Returns ------- if frequencies not given: frequencies : `numpy.ndarray` array of ordinate data mtf : `numpy.ndarray` array of coordinate data else: mtf : `numpy.ndarray` array of MTF data Notes ----- If frequencies are given, just returns the MTF. If frequencies are not given, returns both the frequencies and the MTF. """ extinction = 1 / (wavelength / 1000 * fno) if frequencies is None: normalized_frequency = m.linspace(0, 1, samples) else: normalized_frequency = m.asarray(frequencies) / extinction try: normalized_frequency[normalized_frequency > 1] = 1 # clamp values except TypeError: # single freq if normalized_frequency > 1: normalized_frequency = 1 mtf = _difflim_mtf_core(normalized_frequency) if frequencies is None: return normalized_frequency * extinction, mtf else: return mtf def _difflim_mtf_core(normalized_frequency): """Compute the MTF at a given normalized spatial frequency. Parameters ---------- normalized_frequency : `numpy.ndarray` normalized frequency; function is defined over [0, and takes a value of 0 for [1, Returns ------- `numpy.ndarray` The diffraction MTF function at a given normalized spatial frequency """ return (2 / m.pi) * \ (m.arccos(normalized_frequency) - normalized_frequency * m.sqrt(1 - normalized_frequency ** 2))