def _compute_output_grid(convolvable1, convolvable2): """Calculate the output grid to be used when two convolvables are on different grids.""" # determine output spacing errorstr = 'when convolving two analytic objects, one must have a grid.' if m.isnan(convolvable1.sample_spacing): if m.isnan(convolvable2.sample_spacing): raise ValueError(errorstr) else: output_spacing = convolvable2.sample_spacing elif m.isnan(convolvable2.sample_spacing): if m.isnan(convolvable1.sample_spacing): raise ValueError(errorstr) else: output_spacing = convolvable1.sample_spacing else: output_spacing = min(convolvable1.sample_spacing, convolvable2.sample_spacing) # determine region of output if convolvable1.unit_x is None: c1ux, c1uy = (0, 0), (0, 0) else: c1ux, c1uy = convolvable1.unit_x, convolvable1.unit_y if convolvable2.unit_x is None: # this isn't totally DRY c2ux, c2uy = (0, 0), (0, 0) else: c2ux, c2uy = convolvable2.unit_x, convolvable2.unit_y output_x_left = min(c1ux[0], c2ux[0]) output_x_right = max(c1ux[-1], c2ux[-1]) output_y_left = min(c1uy[0], c2uy[0]) output_y_right = max(c1uy[-1], c2uy[-1]) # if region is not an integer multiple of sample spacings, enlarge to make this true x_rem = (output_x_right - output_x_left) % output_spacing y_rem = (output_y_right - output_y_left) % output_spacing if x_rem > 1e-3: adj = x_rem / 2 output_x_left -= adj output_x_right += adj if y_rem > 1e-3: adj = y_rem / 2 output_y_left -= adj output_y_right += adj # finally, compute the output window samples_x = int((output_x_right - output_x_left) // output_spacing) samples_y = int((output_y_right - output_y_left) // output_spacing) unit_out_x = m.linspace(output_x_left, output_x_right, samples_x) unit_out_y = m.linspace(output_y_left, output_y_right, samples_y) return unit_out_x, unit_out_y
def _encircled_energy_core(mtf_data, radius, nu_p, dx, dy): """Core computation of encircled energy, based on Baliga 1988. Parameters ---------- mtf_data : `numpy.ndarray` unaliased MTF data radius : `float` radius of "detector" nu_p : `numpy.ndarray` radial spatial frequencies dx : `float` x frequency delta dy : `float` y frequency delta Returns ------- `float` encircled energy for given radius """ integration_fourier = m.j1(2 * m.pi * radius * nu_p) / nu_p # division by nu_p will cause a NaN at the origin, 0.5 is the # analytical value of jinc there integration_fourier[m.isnan(integration_fourier)] = 0.5 dat = mtf_data * integration_fourier return radius * dat.sum() * dx * dy
def fcn(self): """Complex wavefunction associated with the pupil.""" phase = self.change_phase_unit(to='waves', inplace=False) fcn = m.exp(1j * 2 * m.pi * phase) # phase implicitly in units of waves, no 2pi/l # guard against nans in phase fcn[m.isnan(phase)] = 0 if self.mask_target in ('fcn', 'both'): fcn *= self._mask return fcn
def _phase_to_wavefunction(self): """Compute the wavefunction from the phase. Returns ------- `Pupil` this pupil instance """ phase = self.change_phase_unit(to='waves', inplace=False) self.fcn = m.exp(1j * 2 * m.pi * phase) # phase implicitly in units of waves, no 2pi/l # guard against nans in phase self.fcn[m.isnan(phase)] = 0 return self
def fill(self, _with=0): """Fill invalid (NaN) values. Parameters ---------- _with : `float`, optional value to fill with Returns ------- `Interferogram` self """ nans = m.isnan(self.phase) self.phase[nans] = _with return self
def dropout_percentage(self): """Percentage of pixels in the data that are invalid (NaN).""" return m.count_nonzero(m.isnan(self.phase)) / self.phase.size * 100
def __init__(self, samples=128, dia=1.0, wavelength=0.55, opd_unit='waves', mask='circle', mask_target='both', ux=None, uy=None, phase=None): """Create a new `Pupil` instance. Parameters ---------- samples : `int`, optional number of samples across the pupil interior dia : `float`, optional diameter of the pupil, mm wavelength : `float`, optional wavelength of light, um opd_unit : `str`, optional, {'waves', 'um', 'nm'} unit used to m.express the OPD. Equivalent strings may be used to the valid options, e.g. 'microns', or 'nanometers' mask : `str` or `numpy.ndarray` mask used to define the amplitude and boundary of the pupil; any regular polygon from `prysm.geometry` as a string, e.g. 'circle' is valid. A user-provided ndarray can also be used. mask_target : `str`, {'phase', 'fcn', 'both', None} which array to mask during pupil creation; only masking fcn is faster for numerical propagations but will make plot2d() and the phase array not be truncated properly. Note that if the mask is not binary and `phase` or `both` is used, phase plots will also not be correct, as they will be attenuated by the mask. ux : `np.ndarray` x axis units uy : `np.ndarray` y axis units phase : `np.ndarray` phase data Notes ----- If ux give, assume uy and phase also given; skip much of the pupil building process and simply copy values. Raises ------ ValueError if the OPD unit given is invalid """ if ux is None: # must build a pupil self.dia = dia ux = m.linspace(-dia / 2, dia / 2, samples) uy = m.linspace(-dia / 2, dia / 2, samples) self.samples = samples need_to_build = True else: # data already known need_to_build = False super().__init__(unit_x=ux, unit_y=uy, phase=phase, wavelength=wavelength, phase_unit=opd_unit, spatial_unit='mm') self.xaxis_label = 'Pupil ξ' self.yaxis_label = 'Pupil η' self.zaxis_label = 'OPD' self.rho = self.phi = None if need_to_build: if type(mask) is not m.ndarray: mask = mcache(mask, self.samples) self._mask = mask self.mask_target = mask_target self.build() self.mask(self._mask, self.mask_target) else: protomask = m.isnan(phase) mask = m.ones(protomask.shape) mask[protomask] = 0 self._mask = mask self.mask_target = 'fcn'
def write_zygo_ascii(file, phase, unit_x, unit_y, wavelength=0.6328, intensity=None, high_phase_res=False): # construct the header timestamp = datetime.datetime.now() line1 = 'Zygo ASCII Data File - Format 2' line2 = '0 0 0 0 ' + timestamp.strftime('"%a %b %d %H:%M:%S %Y').ljust( 30, ' ') + '"' if intensity is None: line3 = '0 0 0 0 0 0' else: raise NotImplementedError( 'writing of ASCII files with nonempty intensity not yet supported.' ) px, py = phase.shape ox = m.searchsorted(unit_x, 0) oy = m.searchsorted(unit_y, 0) line4 = f'{oy} {ox} {py} {px}' line5 = '"' + ' ' * 81 + '"' line6 = '"' + ' ' * 39 + '"' line7 = '"' + ' ' * 39 + '"' timestamp_int = int(str(timestamp.timestamp()).split('.')[0]) res = (unit_x[1] - unit_x[0]) * 1e-3 # mm to m line8 = f'0 0.5 {wavelength*1e-6} 0 1 0 {res} {timestamp_int}' # end is timestamp in integer seconds line9 = f'{py} {px} 0 0 0 0 ' + '"' + ' ' * 9 + '"' line10 = '0 0 0 0 0 0 0 0 0 0' line11 = f'{int(high_phase_res)} 1 20 2 0 0 0 0 0' line12 = '0 ' + '"' + ' ' * 12 + '"' line13 = '1 0' line14 = '"' + ' ' * 7 + '"' header_lines = (line1, line2, line3, line4, line5, line6, line7, line8, line9, line10, line11, line12, line13, line14) header = '\n'.join(header_lines) + '\n' if intensity is None: line15 = '#' line16 = '#' # process the phase and write out coef = ZYGO_PHASE_RES_FACTORS[int(high_phase_res)] encoded_phase = phase * (coef / wavelength / wavelength / 0.5) encoded_phase[m.isnan(encoded_phase)] = ZYGO_INVALID_PHASE encoded_phase = m.flipud(encoded_phase.astype(m.int64)) encoded_phase = encoded_phase.flatten() npts = encoded_phase.shape[0] fits_by_ten = npts // 10 boundary = 10 * fits_by_ten # create an in-memory buffer and write out the phase to it s = StringIO() s.write(header) s.write('#\n#\n') m.savetxt(s, encoded_phase[:boundary].reshape(-1, 10), fmt='%d', delimiter=' ', newline=' \n') tail = ' '.join((str(d) for d in encoded_phase[boundary:])) s.write(tail) s.write('\n#\n') s.seek(0) if not isinstance(file, IOBase): with open(file, 'w') as fd: shutil.copyfileobj(s, fd) else: shutil.copyfileobj(s, fd)