def plot_mtf_thrufocus(self, field_index, focus_range, numpts, freqs, fig=None, ax=None): focus, mtfs = self._make_mtf_thrufocus(field_index, focus_range, numpts) t = [] s = [] for mtf in mtfs: t.append(mtf.exact_polar(freqs, 0)) s.append(mtf.exact_polar(freqs, 90)) t, s = m.asarray(t), m.asarray(s) fig, ax = share_fig_ax(fig, ax) for idx, freq in enumerate(freqs): l, = ax.plot(focus, t[:, idx], lw=2, label=freq) ax.plot(focus, s[:, idx], lw=2, ls='--', c=l.get_color()) ax.legend(title=r'$\nu$ [cy/mm]') ax.set(xlim=(focus[0], focus[-1]), xlabel=r'Defocus [$\mu m$]', ylim=(0, 1), ylabel='MTF [Rel. 1.0]', title='Through Focus MTF') return fig, ax
def read_trioptics_mtf(file, metadata=False): """Read MTF data from a Trioptics data file. Parameters ---------- file : `str` or path_like or file_like contents of a file, path_like to the file, or file object metadata : `bool` whether to also extract and return metadata Returns ------- `dict` dictionary with keys focus, wavelength, freq, tan, sag if metadata=True, also has keys in the return of `io.parse_trioptics_metadata`. """ data = read_file_stream_or_path(file) data = data[:len(data) // 10] # compile regex scanners to grab wavelength, focus, and frequency information # in addition to the T, S MTF data. # lastly, compile a scanner to cut the file after the end of the "MTF Sagittal" scanner focus_scanner = re.compile(r'Focus Position : (\-?\d+\.\d+) mm') data_scanner = re.compile(r'\r\n(\d+\.?\d?)=09\r\n(\d+\.\d+)=09') sag_scanner = re.compile( r'Measurement Table: MTF vs. Frequency \( Sagittal \)') blockend_scanner = re.compile(r' _____ =20') sagpos, cutoff = sag_scanner.search(data).end(), None for blockend in blockend_scanner.finditer(data): if blockend.end() > sagpos and cutoff is None: cutoff = blockend.end() # get focus and wavelength focus_pos = float(focus_scanner.search(data).group(1)) # simultaneously grab frequency and MTF result = data_scanner.findall(data[:cutoff]) freqs, mtfs = [], [] for dat in result: freqs.append(float(dat[0])) mtfs.append(dat[1]) breakpt = len(mtfs) // 2 t = m.asarray(mtfs[:breakpt], dtype=config.precision) s = m.asarray(mtfs[breakpt:], dtype=config.precision) freqs = tuple(freqs[:breakpt]) res = { 'focus': focus_pos, 'freq': freqs, 'tan': t, 'sag': s, } if metadata is True: return {**res, **parse_trioptics_metadata(data)} else: return res
def read_trioptics_mtf_vs_field(file, metadata=False): """Read tangential and sagittal MTF data from a Trioptics .mht file. Parameters ---------- file : `str` or path_like or file_like contents of a file, path_like to the file, or file object metadata : `bool` whether to also extract and return metadata Returns ------- `dict` dictionary with keys of freq, field, tan, sag """ data = read_file_stream_or_path(file) data = data[:len(data) // 10] # only search in a subset of the file for speed # compile a pattern that will search for the image heights in the file and extract fields_pattern = re.compile( f'MTF=09{os.linesep}(.*?){os.linesep}Legend=09', flags=re.DOTALL) fields = fields_pattern.findall(data)[0] # two copies, only need 1st # make a pattern that will search for and extract the tan and sag MTF data. The match will # return two copies; one for vs imght, one for vs angle. Only keep half the matches. tan_pattern = re.compile(r'Tan(.*?)=97', flags=re.DOTALL) sag_pattern = re.compile(r'Sag(.*?)=97', flags=re.DOTALL) tan, sag = tan_pattern.findall(data), sag_pattern.findall(data) endpt = len(tan) // 2 tan, sag = tan[:endpt], sag[:endpt] # now extract the freqs from the tan data freqs = m.asarray([float(s.split('(')[0][1:]) for s in tan]) # lastly, extract the floating point tan and sag data # also take fields, to the 4th decimal place (nearest .1um) # reformat T/S to 2D arrays with indices of (freq, field) tan = m.asarray([s.split('=09')[1:-1] for s in tan], dtype=config.precision) sag = m.asarray([s.split('=09')[1:-1] for s in sag], dtype=config.precision) fields = m.asarray(fields.split('=09')[0:-1], dtype=config.precision).round(4) res = { 'freq': freqs, 'field': fields, 'tan': tan, 'sag': sag, } if metadata is True: return {**res, **parse_trioptics_metadata(data)} else: return res
def read_trioptics_mtfvfvf(file, filename=None): """Read MTF vs Field vs Focus data from a Trioptics .txt dump. Parameters ---------- file : `str` or path_like or file_like file to read from, if string of file body, must provide filename filename : `str`, optional name of file; used to select tan/sag if file is given as contents Returns ------- `MTFvFvF` MTF vs Field vs Focus object """ if filename is None: with open(file, 'r') as fid: lines = fid.readlines() else: lines = file.splitlines() file = filename if str(file)[-7:-4] == 'Tan': azimuth = 'Tan' else: azimuth = 'Sag' imghts, objangs, focusposes, mtfs = [], [], [], [] for meta, data in zip(lines[0::2], lines[1::2]): # iterate 2 lines at a time metavalues = meta.split() imght, objang, focuspos, freqpitch = metavalues[1::2] mtf_raw = data.split()[1:] # first element is "MTF" mtf = m.asarray(mtf_raw, dtype=config.precision) imghts.append(imght) objangs.append(objang) focusposes.append(focuspos) mtfs.append(mtf) focuses = m.unique(m.asarray(focusposes, dtype=config.precision)) focuses = (focuses - m.mean(focuses)) * 1e3 imghts = m.unique(m.asarray(imghts, dtype=config.precision)) freqs = m.arange(len(mtfs[0]), dtype=config.precision) * float(freqpitch) data = m.swapaxes( m.asarray(mtfs).reshape(len(focuses), len(imghts), len(freqs)), 0, 1) return { 'data': data, 'focus': focuses, 'field': imghts, 'freq': freqs, 'azimuth': azimuth }
def barplot(self, orientation='h', buffer=1, zorder=3, fig=None, ax=None): """Create a barplot of coefficients and their names. Parameters ---------- orientation : `str`, {'h', 'v', 'horizontal', 'vertical'} orientation of the plot buffer : `float`, optional buffer to use around the left and right (or top and bottom) bars zorder : `int`, optional zorder of the bars. Use zorder > 3 to put bars in front of gridlines fig : `matplotlib.figure.Figure` Figure containing the plot ax : `matplotlib.axes.Axis` Axis containing the plot Returns ------- fig : `matplotlib.figure.Figure` Figure containing the plot ax : `matplotlib.axes.Axis` Axis containing the plot """ from matplotlib import pyplot as plt fig, ax = share_fig_ax(fig, ax) coefs = m.asarray(self.coefs) idxs = m.asarray(range(len(coefs))) + self.base names = self.names lab = f'{self.zaxis_label} [{self.phase_unit}]' lims = (idxs[0] - buffer, idxs[-1] + buffer) if orientation.lower() in ('h', 'horizontal'): vmin, vmax = coefs.min(), coefs.max() drange = vmax - vmin offset = drange * 0.01 ax.bar(idxs, self.coefs, zorder=zorder) plt.xticks(idxs, names, rotation=90) for i in idxs: ax.text(i, offset, str(i), ha='center') ax.set(ylabel=lab, xlim=lims) else: ax.barh(idxs, self.coefs, zorder=zorder) plt.yticks(idxs, names) for i in idxs: ax.text(0, i, str(i), ha='center') ax.set(xlabel=lab, ylim=lims) return fig, ax
def read_mtfmapper_sfr_single(file, pixel_pitch=None): """Read an MTF Mapper SFR (MTF) file generated by the -f flag with --single-roi. Notes ----- This reads a "raw_sfr_values.txt" file, not an "edge_sfr_values.txt" file. Parameters ---------- file : `str` or path_like or file_like contents of a file, path_like to the file, or file object pixel_pitch : `float` center-to-center pixel spacing, in microns Returns ------- `numpy.ndarray` spatial_frequencies `numpy.ndarray` mtf """ data = read_file_stream_or_path(file) floats = [float(d) for d in data.splitlines()[0].split(' ')[:-1]] edge_angle, *mtf = floats mtf = m.asarray(mtf) freqs = m.arange(len(mtf)) / 64 if pixel_pitch is not None: # convert cy/px to cy/mm freqs /= (pixel_pitch / 1e3) return freqs, mtf
def guarantee_array(variable): """Guarantee that a varaible is a numpy ndarray and supports -, *, +, and other operators. Parameters ---------- variable : `number` or `numpy.ndarray` variable to coalesce Returns ------- `object` an object that supports * / and other operations with ndarrays Raises ------ ValueError non-numeric type """ if type(variable) in [ float, m.ndarray, m.int32, m.int64, m.float32, m.float64, m.complex64, m.complex128 ]: return variable elif type(variable) is int: return float(variable) elif type(variable) is list: return m.asarray(variable) else: raise ValueError(f'variable is of invalid type {type(variable)}')
def encircled_energy(self, radius): """Compute the encircled energy of the PSF. Parameters ---------- radius : `float` or iterable radius or radii to evaluate encircled energy at Returns ------- encircled energy if radius is a float, returns a float, else returns a list. Notes ----- implementation of "Simplified Method for Calculating Encircled Energy," Baliga, J. V. and Cohn, B. D., doi: 10.1117/12.944334 """ from .otf import MTF if hasattr(radius, '__iter__'): # user wants multiple points # um to mm, cy/mm assumed in Fourier plane radius_is_array = True else: radius_is_array = False # compute MTF from the PSF if self._mtf is None: self._mtf = MTF.from_psf(self) nx, ny = m.meshgrid(self._mtf.unit_x, self._mtf.unit_y) self._nu_p = m.sqrt(nx**2 + ny**2) # this is meaninglessly small and will avoid division by 0 self._nu_p[self._nu_p == 0] = 1e-99 self._dnx, self._dny = ny[1, 0] - ny[0, 0], nx[0, 1] - nx[0, 0] if radius_is_array: out = [] for r in radius: if r not in self._ee: self._ee[r] = _encircled_energy_core( self._mtf.data, r / 1e3, self._nu_p, self._dnx, self._dny) out.append(self._ee[r]) return m.asarray(out) else: if radius not in self._ee: self._ee[radius] = _encircled_energy_core( self._mtf.data, radius / 1e3, self._nu_p, self._dnx, self._dny) return self._ee[radius]
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)
def top_n(self, n=5): """Identify the top n terms in the wavefront. Parameters ---------- n : `int`, optional identify the top n terms. Returns ------- `list` list of tuples (magnitude, index, term) """ coefs = m.asarray(self.coefs) coefs_work = abs(coefs) oidxs = m.arange(len(coefs), dtype=int) + self.base # "original indexes" idxs = m.argpartition(coefs_work, -n)[-n:] # argpartition does some magic to identify the top n (unsorted) idxs = idxs[m.argsort(coefs_work[idxs])[::-1]] # use argsort to sort them in ascending order and reverse big_terms = coefs[idxs] # finally, take the values from the big_idxs = oidxs[idxs] names = m.asarray(self.names, dtype=str)[big_idxs - self.base] return list(zip(big_terms, big_idxs, names))
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 fit(data, num_terms=16, rms_norm=False, round_at=6): ''' Fits a number of Zernike coefficients to provided data by minimizing the root sum square between each coefficient and the given data. The data should be uniformly sampled in an x,y grid. Args: data (`numpy.ndarray`): data to fit to. num_terms (`int`): number of terms to fit, fits terms 0~num_terms. rms_norm (`bool`): if true, normalize coefficients to unit RMS value. round_at (`int`): decimal place to round values at. Returns: numpy.ndarray: an array of coefficients matching the input data. ''' if num_terms > len(zernmap): raise ValueError(f'number of terms must be less than {len(zernmap)}') # precompute the valid indexes in the original data pts = m.isfinite(data) # set up an x/y rho/phi grid to evaluate Zernikes on x, y = m.linspace(-1, 1, data.shape[1]), m.linspace(-1, 1, data.shape[0]) xx, yy = m.meshgrid(x, y) rho = m.sqrt(xx**2 + yy**2)[pts].flatten() phi = m.arctan2(xx, yy)[pts].flatten() # compute each Zernike term zernikes = [] for i in range(num_terms): func = z.zernikes[zernmap[i]] base_zern = func(rho, phi) if rms_norm: base_zern *= func.norm zernikes.append(base_zern) zerns = m.asarray(zernikes).T # use least squares to compute the coefficients coefs = m.lstsq(zerns, data[pts].flatten(), rcond=None)[0] return coefs.round(round_at)
def from_trioptics_files(paths, azimuths, upsample=10, ret=('tan', 'sag')): """Convert a set of trioptics files to MTF FFD object(s). Parameters ---------- paths : path_like paths to trioptics files azimuths : iterable of `strs` azimuths, one per path ret : tuple, optional strings representing outputs, {'tan', 'sag'} are the only currently implemented options Returns ------- `MTFFFD` MTF FFD object Raises ------ NotImplemented return option is not available """ azimuths = m.radians(m.asarray(azimuths, dtype=m.float64)) freqs, ts, ss = [], [], [] for path, angle in zip(paths, azimuths): d = read_trioptics_mtf_vs_field(path) imght, freq, t, s = d['field'], d['freq'], d['tan'], d['sag'] freqs.append(freq) ts.append(t) ss.append(s) xx, yy, tan, sag = radial_mtf_to_mtfffd_data(ts, ss, imght, azimuths, upsample=10) if ret == ('tan', 'sag'): return MTFFFD(tan, xx, yy, freq), MTFFFD(sag, xx, yy, freq) else: raise NotImplementedError('other returns not implemented')
def uniform_cart_to_polar(x, y, data): """Interpolate data uniformly sampled in cartesian coordinates to polar coordinates. Parameters ---------- x : `numpy.ndarray` sorted 1D array of x sample pts y : `numpy.ndarray` sorted 1D array of y sample pts data : `numpy.ndarray` data sampled over the (x,y) coordinates Returns ------- rho : `numpy.ndarray` samples for interpolated values phi : `numpy.ndarray` samples for interpolated values f(rho,phi) : `numpy.ndarray` data uniformly sampled in (rho,phi) """ # create a set of polar coordinates to interpolate onto xmin, xmax = min(x), max(x) ymin, ymax = min(y), max(y) _max = max(abs(m.asarray([xmin, xmax, ymin, ymax]))) rho = m.linspace(0, _max, len(x)) phi = m.linspace(0, 2 * m.pi, len(y)) rv, pv = m.meshgrid(rho, phi) # map points to x, y and make a grid for the original samples xv, yv = polar_to_cart(rv, pv) # interpolate the function onto the new points f = interpolate.RegularGridInterpolator((y, x), data, bounds_error=False, fill_value=0) return rho, phi, f((yv, xv), method='linear')
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)
def __init__(self, *args, **kwargs): """Initialize a new Zernike instance.""" if args is not None: if len(args) is 0: self.coefs = m.zeros(len(self._map), dtype=config.precision) else: self.coefs = m.asarray([*args[0]], dtype=config.precision) self.normalize = False pass_args = {} self.base = config.zernike_base try: bb = kwargs['base'] if bb > 1: raise ValueError('It violates convention to use a base greater than 1.') elif bb < 0: raise ValueError('It is nonsensical to use a negative base.') self.base = bb except KeyError: # user did not specify base pass if kwargs is not None: for key, value in kwargs.items(): if key[0].lower() == 'z': idx = int(key[1:]) # strip 'Z' from index self.coefs[idx - self.base] = value elif key in ('norm'): self.normalize = value elif key.lower() == 'base': self.base = value else: pass_args[key] = value super().__init__(**pass_args)
def generate_vertices(num_sides, radius=1): """Generate a list of vertices for a convex regular polygon with the given number of sides and radius. Parameters ---------- num_sides : `int` number of sides to the polygon radius : `float` radius of the polygon Returns ------- `numpy.ndarray` array with first column X points, second column Y points """ angle = 2 * m.pi / num_sides pts = [] for point in range(num_sides): x = radius * m.sin(point * angle) y = radius * m.cos(point * angle) pts.append((int(x), int(y))) return m.asarray(pts)
def generate_mask(vertices, num_samples=128): """Create a filled convex polygon mask based on the given vertices. Parameters ---------- vertices : `iterable` ensemble of vertice (x,y) coordinates, in array units num_samples : `int` number of points in the output array along each dimension Returns ------- `numpy.ndarray` polygon mask """ vertices = m.asarray(vertices) unit = m.arange(num_samples) xxyy = m.stack(m.meshgrid(unit, unit), axis=2) # use delaunay to fill from the vertices and produce a mask triangles = Delaunay(vertices, qhull_options='Qj Qf') mask = ~(triangles.find_simplex(xxyy) < 0) return mask
def zernikefit(data, x=None, y=None, rho=None, phi=None, terms=16, norm=False, residual=False, round_at=6, map_='fringe'): """Fits a number of Zernike coefficients to provided data. Works by minimizing the mean square error between each coefficient and the given data. The data should be uniformly sampled in an x,y grid. Parameters ---------- data : `numpy.ndarray` data to fit to. x : `numpy.ndarray`, optional x coordinates, same shape as data y : `numpy.ndarray`, optional y coordinates, same shape as data rho : `numpy.ndarray`, optional radial coordinates, same shape as data phi : `numpy.ndarray`, optional azimuthal coordinates, same shape as data terms : `int`, optional number of terms to fit, fits terms 0~terms norm : `bool`, optional if True, normalize coefficients to unit RMS value residual : `bool`, optional if True, return a tuple of (coefficients, residual) round_at : `int`, optional decimal place to round values at. map_ : `str`, optional, {'fringe', 'noll'} which ordering of Zernikes to use Returns ------- coefficients : `numpy.ndarray` an array of coefficients matching the input data. residual : `float` RMS error between the input data and the fit. Raises ------ ValueError too many terms requested. """ map_ = maps[map_] if terms > len(fringemap): raise ValueError(f'number of terms must be less than {len(fringemap)}') data = data.T # transpose to mimic transpose of zernikes # precompute the valid indexes in the original data pts = m.isfinite(data) if x is None and rho is None: # set up an x/y rho/phi grid to evaluate Zernikes on rho, phi = make_rho_phi_grid(*reversed(data.shape)) rho = rho[pts].flatten() phi = phi[pts].flatten() elif rho is None: rho, phi = cart_to_polar(x, y) rho, phi = rho[pts].flatten(), phi[pts].flatten() # compute each Zernike term zerns_raw = [] for i in range(terms): func = zernikes[map_[i]] base_zern = func(rho, phi) if norm: base_zern *= func.norm zerns_raw.append(base_zern) zerns = m.asarray(zerns_raw).T # use least squares to compute the coefficients meas_pts = data[pts].flatten() coefs = m.lstsq(zerns, meas_pts, rcond=None)[0] if round_at is not None: coefs = coefs.round(round_at) if residual is True: components = [] for zern, coef in zip(zerns_raw, coefs): components.append(coef * zern) _fit = m.asarray(components) _fit = _fit.sum(axis=0) rmserr = rms(data[pts].flatten() - _fit) return coefs, rmserr else: return coefs
def names(self): """Names of the terms in self.""" # need to call through class variable to avoid insertion of self as arg idxs = m.asarray(range(len(self.coefs))) + self.base return [self.__class__._namer(i, base=self.base) for i in idxs] # NOQA
def radial_mtf_to_mtfffd_data(tan, sag, imagehts, azimuths, upsample): """Take radial MTF data and map it to inputs to the MTFFFD constructor. Performs upsampling/interpolation in cartesian coordinates Parameters ---------- tan : `np.ndarray` tangential data sag : `np.ndarray` sagittal data imagehts : `np.ndarray` array of image heights azimuths : iterable azimuths corresponding to the first dimension of the tan/sag arrays upsample : `float` upsampling factor Returns ------- out_x : `np.ndarray` x coordinates of the output data out_y : `np.ndarray` y coordinates of the output data tan : `np.ndarray` tangential data sag : `np.ndarray` sagittal data """ azimuths = m.asarray(azimuths) imagehts = m.asarray(imagehts) if imagehts[0] > imagehts[-1]: # distortion profiled, values "reversed" # just flip imagehts, since spacing matters and not exact values imagehts = imagehts[::-1] amin, amax = min(azimuths), max(azimuths) imin, imax = min(imagehts), max(imagehts) aq = m.linspace(amin, amax, int(len(azimuths) * upsample)) iq = m.linspace(imin, imax, int( len(imagehts) * 4)) # hard-code 4x linear upsample, change later aa, ii = m.meshgrid(aq, iq, indexing='ij') # for each frequency, build an interpolating function and upsample up_t = m.empty((len(aq), tan.shape[1], len(iq))) up_s = m.empty((len(aq), sag.shape[1], len(iq))) for idx in range(tan.shape[1]): t, s = tan[:, idx, :], sag[:, idx, :] interpft = RGI((azimuths, imagehts), t, method='linear') interpfs = RGI((azimuths, imagehts), s, method='linear') up_t[:, idx, :] = interpft((aa, ii)) up_s[:, idx, :] = interpfs((aa, ii)) # compute the locations of the samples on a cartesian grid xd, yd = m.outer(m.cos(m.radians(aq)), iq), m.outer(m.sin(m.radians(aq)), iq) samples = m.stack([xd.ravel(), yd.ravel()], axis=1) # for the output cartesian grid, figure out the x-y coverage and build a regular grid absamin = min(abs(azimuths)) closest_to_90 = azimuths[m.argmin(azimuths - 90)] xfctr = m.cos(m.radians(absamin)) yfctr = m.cos(m.radians(closest_to_90)) xmin, xmax = imin * xfctr, imax * xfctr ymin, ymax = imin * yfctr, imax * yfctr xq, yq = m.linspace(xmin, xmax, len(iq)), m.linspace(ymin, ymax, len(iq)) xx, yy = m.meshgrid(xq, yq) outt, outs = [], [] # for each frequency, interpolate onto the cartesian grid for idx in range(up_t.shape[1]): datt = griddata(samples, up_t[:, idx, :].ravel(), (xx, yy), method='linear') dats = griddata(samples, up_s[:, idx, :].ravel(), (xx, yy), method='linear') outt.append(datt.reshape(xx.shape)) outs.append(dats.reshape(xx.shape)) outt, outs = m.rollaxis(m.asarray(outt), 0, 3), m.rollaxis(m.asarray(outs), 0, 3) return xq, yq, outt, outs