class BigStarBasis(StarBasis): def __init__(self, libname='', verbose=False, log_interp=True, n_neighbors=0, driver=None, in_memory=False, use_params=None, strictness=0.0, **kwargs): """An object which holds the stellar spectral library, performs interpolations of that library, and has methods to return attenuated, normalized, smoothed stellar spoectra. This object is set up to work with large grids, so the models file is kept open for acces from disk. scikits-learn kd-trees are required for model access. Ideally the grid should be regular (though the spacings need not be equal along a given dimension). :param libname: Path to the hdf5 file to use for the spectral library. Must have "ckc" or "ykc" in the filename (to specify which kind of loader to use) :param n_neighbors: (default:0) Number of nearest neighbors to use when requested parameters are outside the convex hull of the library prameters. If ``0`` then a ValueError is raised instead of the nearest spectrum. :param verbose: If True, print information about the parameters used when a point is outside the convex hull :param log_interp: (default: True) Interpolate in log(flux) instead of flux. :param in_memory: (default: False) Switch to determine whether the grid is loaded in memory or read from disk each time a model is constructed (like you'd want for very large grids). :param use_params: Sequence of strings. If given, only use the listed parameters (which must be present in the `_libparams` structure) to build the grid and construct spectra. Otherwise all fields of `_libparams` will be used. :param strictness: (default: 0.0) Float from 0.0 to 1.0 that gives the fraction of a unit hypercube that is required for a parameter position to be accepted. That is, if the weights of the enclosing vertices sum to less than this number, raise an error. """ self.verbose = verbose self.logarithmic = log_interp self._libname = libname self.n_neighbors = n_neighbors self._in_memory = in_memory self._strictness = strictness self.load_lib(libname, driver=driver) # Do some important bookkeeping if use_params is None: self.stellar_pars = self._libparams.dtype.names else: self.stellar_pars = tuple(use_params) self.ndim = len(self.stellar_pars) self.lib_as_grid() self.params = {} def load_lib(self, libname='', driver=None): """Read a ykc library which has been preconvolved to be close to your data resolution. This library should be stored as an HDF5 file, with the datasets ``wavelengths``, ``parameters`` and ``spectra``. These are ndarrays of shape (nwave,), (nmodels,), and (nmodels, nwave) respecitvely. The ``parameters`` array is a structured array. The h5 file object is left open so that spectra can be accessed from disk. """ import h5py f = h5py.File(libname, "r", driver=driver) self._wave = np.array(f['wavelengths']) self._libparams = np.array(f['parameters']) if self._in_memory: self._spectra = np.array(f['spectra']) f.close() else: self._spectra = f['spectra'] def get_star_spectrum(self, **kwargs): """Given stellar parameters, obtain an interpolated spectrum at those parameters. :param **kwargs: Keyword arguments must include values for the ``stellar_pars`` parameters that are stored in ``_libparams``. :returns wave: The wavelengths at which the spectrum is defined. :returns spec: The spectrum interpolated to the requested parameters :returns unc: The uncertainty spectrum, where the uncertainty is due to interpolation error. Curently unimplemented (i.e. it is a None type object) """ inds, wghts = self.weights(**kwargs) if self.logarithmic: spec = np.exp(np.dot(wghts, np.log(self._spectra[inds, :]))) else: spec = np.dot(wghts, self._spectra[inds, :]) spec_unc = None return self._wave, spec, spec_unc def weights(self, **params): inds = self.knearest_inds(**params) wghts = self.linear_weights(inds, **params) if wghts.sum() <= self._strictness: raise ValueError("Something is wrong with the weights") good = wghts > 0 # if good.sum() < 2**self.ndim: # raise ValueError("Did not find all vertices of the hypercube, " # "or there is no enclosing hypercube in the library.") inds = inds[good] wghts = wghts[good] wghts /= wghts.sum() return inds, wghts def lib_as_grid(self): """Convert the library parameters to pixel indices in each dimension, and build and store a KDTree for the pixel coordinates. """ # Get the unique gridpoints in each param self.gridpoints = {} for p in self.stellar_pars: self.gridpoints[p] = np.unique(self._libparams[p]) # Digitize the library parameters X = np.array([np.digitize(self._libparams[p], bins=self.gridpoints[p], right=True) for p in self.stellar_pars]) self.X = X.T # Build the KDTree self._kdt = KDTree(self.X) # , metric='euclidean') def params_to_grid(self, **targ): """Convert a set of parameters to grid pixel coordinates. :param targ: The target parameter location, as keyword arguments. The elements of ``stellar_pars`` must be present as keywords. :returns x: The target parameter location in pixel coordinates. """ # bin index inds = np.array([np.digitize([targ[p]], bins=self.gridpoints[p], right=False) - 1 for p in self.stellar_pars]) inds = np.squeeze(inds) # fractional index. Could use stored denominator to be slightly faster try: find = [(targ[p] - self.gridpoints[p][i]) / (self.gridpoints[p][i+1] - self.gridpoints[p][i]) for i, p in zip(inds, self.stellar_pars)] except(IndexError): pstring = "{0}: min={2} max={3} targ={1}\n" s = [pstring.format(p, targ[p], *self.gridpoints[p][[0, -1]]) for p in self.stellar_pars] raise ValueError("At least one parameter outside grid.\n{}".format(' '.join(s))) return inds + np.squeeze(find) def knearest_inds(self, **params): """Find all parameter ``vertices`` within a sphere of radius sqrt(ndim). The parameter values are converted to pixel coordinates before a search of the KDTree. :param params: Keyword arguments which must include keys corresponding to ``stellar_pars``, the parameters of the grid. :returns inds: The sorted indices of all vertices within sqrt(ndim) of the pixel coordinates, corresponding to **params. """ # Convert from physical space to grid index space xtarg = self.params_to_grid(**params) # Query the tree within radius sqrt(ndim) try: inds = self._kdt.query_radius(xtarg.reshape(1, -1), r=np.sqrt(self.ndim)) except(AttributeError): inds = self._kdt.query_ball_point(xtarg.reshape(1, -1), np.sqrt(self.ndim)) return np.sort(inds[0]) def linear_weights(self, knearest, **params): """Use ND-linear interpolation over the knearest neighbors. :param knearest: The indices of the ``vertices`` for which to calculate weights. :param params: The target parameter location, as keyword arguments. :returns wght: The weight for each vertex, computed as the volume of the hypercube formed by the target parameter and each vertex. Vertices more than 1 away from the target in any dimension are given a weight of zero. """ xtarg = self.params_to_grid(**params) x = self.X[knearest, :] dx = xtarg - x # Fractional pixel weights wght = ((1 - dx) * (dx >= 0) + (1 + dx) * (dx < 0)) # set weights to zero if model is more than a pixel away wght *= (dx > -1) * (dx < 1) # compute hyperarea for each model and return return wght.prod(axis=-1) def triangle_weights(self, knearest, **params): """Triangulate the k-nearest models, then use the barycenter of the enclosing simplex to interpolate. """ inparams = np.array([params[p] for p in self.stellar_pars]) dtri = Delaunay(self.model_points[knearest, :]) triangle_ind = dtri.find_simplex(inparams) inds = dtri.simplices[triangle_ind, :] transform = dtri.transform[triangle_ind, :, :] Tinv = transform[:self.ndim, :] x_r = inparams - transform[self.ndim, :] bary = np.dot(Tinv, x_r) last = 1.0 - bary.sum() wghts = np.append(bary, last) oo = inds.argsort() return inds[oo], wghts[oo]
class BigStarBasis(StarBasis): def __init__(self, libname='', verbose=False, log_interp=True, n_neighbors=0, driver=None, in_memory=False, use_params=None, **kwargs): """An object which holds the stellar spectral library, performs interpolations of that library, and has methods to return attenuated, normalized, smoothed stellar spoectra. This object is set up to work with large grids, so the models file is kept open for acces from disk. scikits-learn kd-trees are required for model access. Ideally the grid should be regular (though the spacings need not be equal along a given dimension). :param libname: Path to the hdf5 file to use for the spectral library. Must have "ckc" or "ykc" in the filename (to specify which kind of loader to use) :param n_neighbors: (default:0) Number of nearest neighbors to use when requested parameters are outside the convex hull of the library prameters. If ``0`` then a ValueError is raised instead of the nearest spectrum. :param verbose: If True, print information about the parameters used when a point is outside the convex hull :param log_interp: (default: True) Interpolate in log(flux) instead of flux. :param in_memory: (default: False) Switch to determine whether the grid is loaded in memory or read from disk each time a model is constructed (like you'd want for very large grids). :param use_params: Sequence of strings. If given, only use the listed parameters (which must be present in the `_libparams` structure) to build the grid and construct spectra. Otherwise all fields of `_libparams` will be used. """ self.verbose = verbose self.logarithmic = log_interp self._libname = libname self.n_neighbors = n_neighbors self._in_memory = in_memory self.load_lib(libname, driver=driver) # Do some important bookkeeping if use_params is None: self.stellar_pars = self._libparams.dtype.names else: self.stellar_pars = tuple(use_params) self.ndim = len(self.stellar_pars) self.lib_as_grid() self.params = {} def load_lib(self, libname='', driver=None): """Read a ykc library which has been preconvolved to be close to your data resolution. This library should be stored as an HDF5 file, with the datasets ``wavelengths``, ``parameters`` and ``spectra``. These are ndarrays of shape (nwave,), (nmodels,), and (nmodels, nwave) respecitvely. The ``parameters`` array is a structured array. The h5 file object is left open so that spectra can be accessed from disk. """ import h5py f = h5py.File(libname, "r", driver=driver) self._wave = np.array(f['wavelengths']) self._libparams = np.array(f['parameters']) if self._in_memory: self._spectra = np.array(f['spectra']) f.close() else: self._spectra = f['spectra'] def get_star_spectrum(self, **kwargs): """Given stellar parameters, obtain an interpolated spectrum at those parameters. :param **kwargs: Keyword arguments must include values for the ``stellar_pars`` parameters that are stored in ``_libparams``. :returns wave: The wavelengths at which the spectrum is defined. :returns spec: The spectrum interpolated to the requested parameters :returns unc: The uncertainty spectrum, where the uncertainty is due to interpolation error. Curently unimplemented (i.e. it is a None type object) """ inds, wghts = self.weights(**kwargs) if self.logarithmic: spec = np.exp(np.dot(wghts, np.log(self._spectra[inds, :]))) else: spec = np.dot(wghts, self._spectra[inds, :]) spec_unc = None return self._wave, spec, spec_unc def weights(self, **params): inds = self.knearest_inds(**params) wghts = self.linear_weights(inds, **params) # if wghts.sum() < 1.0: # raise ValueError("Something is wrong with the weights") good = wghts > 0 # if good.sum() < 2**self.ndim: # raise ValueError("Did not find all vertices of the hypercube, " # "or there is no enclosing hypercube in the library.") inds = inds[good] wghts = wghts[good] wghts /= wghts.sum() return inds, wghts def lib_as_grid(self): """Convert the library parameters to pixel indices in each dimension, and build and store a KDTree for the pixel coordinates. """ # Get the unique gridpoints in each param self.gridpoints = {} for p in self.stellar_pars: self.gridpoints[p] = np.unique(self._libparams[p]) # Digitize the library parameters X = np.array([np.digitize(self._libparams[p], bins=self.gridpoints[p], right=True) for p in self.stellar_pars]) self.X = X.T # Build the KDTree self._kdt = KDTree(self.X) # , metric='euclidean') def params_to_grid(self, **targ): """Convert a set of parameters to grid pixel coordinates. :param targ: The target parameter location, as keyword arguments. The elements of ``stellar_pars`` must be present as keywords. :returns x: The target parameter location in pixel coordinates. """ # bin index inds = np.array([np.digitize([targ[p]], bins=self.gridpoints[p], right=False) - 1 for p in self.stellar_pars]) inds = np.squeeze(inds) # fractional index. Could use stored denominator to be slightly faster try: find = [(targ[p] - self.gridpoints[p][i]) / (self.gridpoints[p][i+1] - self.gridpoints[p][i]) for i, p in zip(inds, self.stellar_pars)] except(IndexError): pstring = "{0}: min={2} max={3} targ={1}\n" s = [pstring.format(p, targ[p], *self.gridpoints[p][[0, -1]]) for p in self.stellar_pars] raise ValueError("At least one parameter outside grid.\n{}".format(' '.join(s))) return inds + np.squeeze(find) def knearest_inds(self, **params): """Find all parameter ``vertices`` within a sphere of radius sqrt(ndim). The parameter values are converted to pixel coordinates before a search of the KDTree. :param params: Keyword arguments which must include keys corresponding to ``stellar_pars``, the parameters of the grid. :returns inds: The sorted indices of all vertices within sqrt(ndim) of the pixel coordinates, corresponding to **params. """ # Convert from physical space to grid index space xtarg = self.params_to_grid(**params) # Query the tree within radius sqrt(ndim) try: inds = self._kdt.query_radius(xtarg.reshape(1, -1), r=np.sqrt(self.ndim)) except(AttributeError): inds = self._kdt.query_ball_point(xtarg.reshape(1, -1), np.sqrt(self.ndim)) return np.sort(inds[0]) def linear_weights(self, knearest, **params): """Use ND-linear interpolation over the knearest neighbors. :param knearest: The indices of the ``vertices`` for which to calculate weights. :param params: The target parameter location, as keyword arguments. :returns wght: The weight for each vertex, computed as the volume of the hypercube formed by the target parameter and each vertex. Vertices more than 1 away from the target in any dimension are given a weight of zero. """ xtarg = self.params_to_grid(**params) x = self.X[knearest, :] dx = xtarg - x # Fractional pixel weights wght = ((1 - dx) * (dx >= 0) + (1 + dx) * (dx < 0)) # set weights to zero if model is more than a pixel away wght *= (dx > -1) * (dx < 1) # compute hyperarea for each model and return return wght.prod(axis=-1) def triangle_weights(self, knearest, **params): """Triangulate the k-nearest models, then use the barycenter of the enclosing simplex to interpolate. """ inparams = np.array([params[p] for p in self.stellar_pars]) dtri = Delaunay(self.model_points[knearest, :]) triangle_ind = dtri.find_simplex(inparams) inds = dtri.simplices[triangle_ind, :] transform = dtri.transform[triangle_ind, :, :] Tinv = transform[:self.ndim, :] x_r = inparams - transform[self.ndim, :] bary = np.dot(Tinv, x_r) last = 1.0 - bary.sum() wghts = np.append(bary, last) oo = inds.argsort() return inds[oo], wghts[oo]