def set_jointplot(row, col, nrows, ncols, create=True, top=0, ratio=2): """Move to the plot, creating them if necessary. Parameters ---------- row : int The row number, starting from 0. col : int The column number, starting from 0. nrows : int The number of rows. ncols : int The number of columns. create : bool, optional If True then create the plots top : int The row that is set to the ratio height, numbered from 0. ratio : float The ratio of the height of row number top to the other rows. """ if (row < 0) or (row >= nrows): emsg = 'Invalid row number {} (nrows={})'.format(row, nrows) raise ArgumentErr(emsg) if (col < 0) or (col >= ncols): emsg = 'Invalid column number {} (ncols={})'.format(col, ncols) raise ArgumentErr(emsg) plotnum = row * ncols + col if create: # We only change the vertical spacing ratios = [1] * nrows ratios[top] = ratio gs = {'height_ratios': ratios} fig, axes = plt.subplots(nrows, ncols, sharex=True, num=1, gridspec_kw=gs) fig.subplots_adjust(hspace=0.05) # Change all but the bottom row. By setting sharex # this is probably unneeded as get_xticklabels is empty. # if axes.ndim == 2: axes = axes[:-1, :].flatten() else: axes = axes[:-1] plt.setp([a.get_xticklabels() for a in axes[:-1]], visible=False) try: ax = plt.gcf().axes[plotnum] except IndexError: emsg = "Unable to find a plot with row={} col={}".format(row, col) raise ArgumentErr(emsg) from None plt.sca(ax)
def make_ideal_rmf(e_min, e_max, offset=1, name='rmf'): """A simple in-memory representation of an ideal RMF. This RMF represents a 1-to-1 mapping from channel to energy bin (i.e. there's no blurring or secondary channels). Parameters ---------- e_min, e_max : array The energy ranges corresponding to the channels. The units are in keV and each bin has energ_hi > energ_lo. The arrays are assumed to be ordered, but it is not clear yet whether they have to be in ascending order. The sizes must match each other. This corresponds to the E_MIN and E_MAX columns of the EBOUNDS extension of the RMF file format. offset : int, optional The value of the first channel (corresponding to the TLMIN value of the F_CHAN column of the 'MATRIX' or 'SPECRESP MATRIX' block. It is expected to be 0 or 1, but the only restriction is that it is 0 or greater. name : str, optional The name to give to the RMF instance. Returns ------- rmf : sherpa.astro.data.DatAMRF The RMF. """ elo = np.asarray(e_min) ehi = np.asarray(e_max) if elo.size != ehi.size: raise DataErr('mismatch', 'e_min', 'e_max') detchans = elo.size if offset < 0: raise ArgumentErr('bad', 'offset', 'value can not be negative') # The "ideal" matrix is the identity matrix, which, in compressed # form, is an array of 1.0's (matrix) and an array of locations # giving the column where the element is 1 (fchan). It appears # that this uses 1 indexing. # dummy = np.ones(detchans, dtype=np.int16) matrix = np.ones(detchans, dtype=np.float32) fchan = np.arange(1, detchans + 1, dtype=np.int16) return DataRMF(name=name, detchans=detchans, energ_lo=elo, energ_hi=ehi, n_grp=dummy, n_chan=dummy, f_chan=fchan, matrix=matrix, offset=offset)
def make_arf(energ_lo, energ_hi, specresp=None, exposure=1.0, name='arf'): """A simple in-memory representation of an ARF. Parameters ---------- energ_lo, energ_hi : array The energy grid over which the ARF is defined. The units are keV and each bin has energ_hi > energ_lo. The arrays are assumed to be ordered, but it is not clear yet whether they have to be in ascending order. specresp : array or None, optional The spectral response (effective area) for each bin, in cm^2. If not given then a value of 1.0 per bin is used. exposure : number, optional The exposure time, in seconds. It must be positive. name : str, optional The name to give to the ARF instance. Returns ------- arf : sherpa.astro.data.DataARF The ARF. """ elo = np.asarray(energ_lo) ehi = np.asarray(energ_hi) if elo.size != ehi.size: raise DataErr('mismatch', 'energ_lo', 'energ_hi') if specresp is None: specresp = np.ones(elo.size, dtype=np.float32) else: specresp = np.asarray(specresp) if specresp.size != elo.size: raise DataErr('mismatch', 'energy grid', 'effarea') if exposure <= 0.0: raise ArgumentErr('bad', 'exposure', 'value must be positive') return DataARF(name=name, energ_lo=elo, energ_hi=ehi, specresp=specresp, exposure=exposure)
def sample_flux(fit, data, src, method=calc_energy_flux, correlated=False, num=1, lo=None, hi=None, numcores=None, samples=None, clip='hard'): """Calculate model fluxes from a sample of parameter values. Draw parameter values from a normal distribution and then calculate the model flux for each set of parameter values. The values are drawn from normal distributions, and the distributions can either be independent or have correlations between the parameters. .. versionchanged:: 4.12.2 The clip parameter was added and an extra column is added to the return to indicate if each row was clipped. Parameters ---------- fit : sherpa.fit.Fit instance The fit object. The src parameter is assumed to be a subset of the fit.model expression (to allow for calculating the flux of a model component), and the fit.data object matches the data object. The fit.model argument is expected to include instrumental models (for PHA data sets). These objects can represent simultaneous fits (e.g. sherpa.data.DataSimulFit and sherpa.models.model.SimulFitModel). data : sherpa.data.Data subclass The data object to use. This is not a DataSimulFit instance. src : sherpa.models.Arithmetic instance The source model (without instrument response for PHA data) that is used for calculating the flux. This is not a SimulFitModel instance. method : function, optional How to calculate the flux: assumed to be one of calc_energy_flux or calc_photon_flux correlated : bool, optional Are the parameter draws independent of each other? num : int, optional The number of iterations. lo : number or None, optional The lower edge of the dataspace range for the flux calculation. If None then the lower edge of the data grid is used. hi : number or None, optional The upper edge of the dataspace range for the flux calculation. If None then the upper edge of the data grid is used. numcores : int or None, optional Should the analysis be split across multiple CPU cores? When set to None all available cores are used. samples : 1D or 2D array, optional What are the errors on the parameters? If set to None then the covariance method is used to estimate the parameter errors. If given and correlated is True then samples must be a 2D array, and contain the covariance matrix for the free parameters in fit.model, and the matrix must be positive definite. If correlated is False then samples can either be sent the covariance matrix or a 1D array of the error values (i.e. the sigma of the normal distribution). If there are n free parameters then the 1D array has to have n elements and the 2D array n by n elements. clip : {'hard', 'soft', 'none'}, optional What clipping strategy should be applied to the sampled parameters. The default ('hard') is to fix values at their hard limits if they exceed them. A value of 'soft' uses the soft limits instead, and 'none' applies no clipping. The last column in the returned arrays indicates if the row had any clipped parameters (even when clip is set to 'none'). Returns ------- vals : 2D NumPy array The shape of samples is (num, nfree + 2), where nfree is the number of free parameters in fit.model. Each row contains one iteration, and the columns are the calculated flux, followed by the free parameters, and then a flag column indicating if the parameters were clipped (1) or not (0). See Also -------- calc_flux Notes ----- The ordering of the samples array, and the columns in the output, matches that of the free parameters in the fit.model expression. That is:: [p.fullname for p in fit.model.pars if not p.frozen] If src is a subset of the full source expression then samples, when not None, must still match the number of free parameters in the full source expression (that given by fit.model). """ if num <= 0: raise ArgumentErr('bad', 'num', 'must be a positive integer') # Ensure we have free parameters. Note that this uses the # 'src' model, not 'fit.model'. # npar = len(src.thawedpars) if npar == 0: raise FitErr('nothawedpar') # Check that src is a "subset" of fit.model. The current approach is # probably not sufficient to capture the requirements, where # ordering of the components is important, but is a start. # cpts_full = decompose(fit.model) for cpt in decompose(src): if cpt in cpts_full: continue raise ArgumentErr('bad', 'src', 'model contains term not in fit') mpar = len(fit.model.thawedpars) if npar > mpar: # This should not be possible given the above check, but leave in. raise ArgumentErr('bad', 'src', 'more free parameters than expected') # The argument to sample_flux should really be called scales and # not samples. # scales = samples if scales is None: samples, clipped = _sample_flux_get_samples(fit, src, correlated, num, clip=clip) else: samples, clipped = _sample_flux_get_samples_with_scales(fit, src, correlated, scales, num, clip=clip) # When a subset of the full model is used we need to know how # to select which rows in the samples array refer to the # parameters of interest. We could compare on fullname, # but is not sufficient to guarantee the match. # if npar < mpar: full_pars = dict( map(reversed, enumerate([p for p in fit.model.pars if not p.frozen]))) cols = [] for src_par in [p for p in src.pars if not p.frozen]: try: cols.append(full_pars[src_par]) except KeyError: # This should not be possible at this point but the # decompose check above may be insufficient. raise ArgumentErr( 'bad', 'src', 'unknown parameter "{}"'.format(src_par.fullname)) cols = numpy.asarray(cols) assert cols.size == npar, 'We have lost a parameter somewhere' else: cols = None # Need to append the clipped array (it would be nice to retain # the boolean nature of this). # vals = calc_flux(data, src, samples, method, lo, hi, numcores, subset=cols) return numpy.concatenate((vals, numpy.expand_dims(clipped, 1)), axis=1)
def _sample_flux_get_samples_with_scales(fit, src, correlated, scales, num, clip='hard'): """Return the parameter samples given the parameter scales. Parameters ---------- fit : sherpa.fit.Fit instance The fit instance. The fit.model expression is assumed to include any necessary response information. The number of free parameters in fit.model is mfree. src : sherpa.models.ArithmeticModel instance The model for which the flux is being calculated. This must be a subset of the fit.model expression, and should not include the response information. There must be at least one thawed parameter in this model. The number of free parameters in src is sfree. correlated : bool Are the parameters assumed to be correlated or not? If correlated is True then scales must be 2D. scales : 1D or 2D array The parameter scales. When 1D they are the gaussian sigma values for the parameter, and when a 2D array they are the covariance matrix. The scales parameter must match the number of parameters in fit (mfree) and not in src (sfree) when they are different. For 1D the size is mfree and for 2D it is mfree by mfree. num : int Tne number of samples to return. This must be 1 or greater. clip : {'hard', 'soft', 'none'}, optional What clipping strategy should be applied to the sampled parameters. The default ('hard') is to fix values at their hard limits if they exceed them. A value of 'soft' uses the soft limits instead, and 'none' applies no clipping. The last column in the returned arrays indicates if the row had any clipped parameters (even when clip is set to 'none'). Returns ------- samples, clipped : 2D NumPy array, 1D NumPy array The dimensions are num by mfree. The ordering of the parameter values in each row matches that of the free parameters in fit.model. The clipped array indicates whether a row had one or more clipped parameters. Raises ------ ArgumentErr If the scales argument contains invalid (e.g. None or IEEE non-finite values) values, or is the wrong shape. ModelErr If the scales argument has the wrong size (that is, it does not represent mfree parameter values). Notes ----- The support for src being a subset of the fit.model argument has not been tested for complex models, that is when fit.model is rmf(arf(source_model)) and src is a combination of components in source_model but not all the components of source_model. """ npar = len(src.thawedpars) mpar = len(fit.model.thawedpars) assert mpar >= npar scales = numpy.asarray(scales) # A None value will cause scales to have a dtype of object, # which is not supported by isfinite, so check for this # first. # # Numpy circa 1.11 raises a FutureWarning with 'if None in scales:' # about this changing to element-wise comparison (which is what # we want). To avoid this warning I use the suggestion from # https://github.com/numpy/numpy/issues/1608#issuecomment-9618150 # if numpy.equal(None, scales).any(): raise ArgumentErr('bad', 'scales', 'must not contain None values') # We require that scales only has finite values in it. # The underlying sample routines are assumed to check other # constraints, or deal with negative values (for the 1D case # uncorrelated case the absolute value is used). # if not numpy.isfinite(scales).all(): raise ArgumentErr('bad', 'scales', 'must only contain finite values') if scales.ndim == 2 and (scales.shape[0] != scales.shape[1]): raise ArgumentErr('bad', 'scales', 'scales must be square when 2D') # Ensure the scales array matches the correlated parameter: # - when True it must be the covariance matrix (2D) # - when False it can be either be a 1D array of sigmas or # the covariance matrix, which we convert to an array of # sigmas # if correlated: if scales.ndim != 2: raise ArgumentErr('bad', 'scales', 'when correlated=True, scales must be 2D') elif scales.ndim == 2: # convert from covariance matrix scales = numpy.sqrt(scales.diagonal()) elif scales.ndim != 1: raise ArgumentErr('bad', 'scales', 'when correlated=False, scales must be 1D or 2D') # At this point either 1D or 2D square array. Now to check the # number of elements. # if scales.shape[0] != mpar: raise ModelErr('numthawed', mpar, scales.shape[0]) if correlated: sampler = NormalParameterSampleFromScaleMatrix() else: sampler = NormalParameterSampleFromScaleVector() samples = sampler.get_sample(fit, scales, num=num) clipped = sampler.clip(fit, samples, clip=clip) return samples, clipped
def renorm(id=None, cpt=None, bkg_id=None, names=None, limscale=1000.0): """Change the normalization of a model to match the data. The idea is to change the normalization to be a better match to the data, so that the search can be quicker. It can be considered to be like the `guess` command, but for the normalization. It is *only* intended to change the normalization to a value near the correct one; it *should not* be used for any sort of calculation without first doing a fit. It is also only going to give reasonable results for models where the predicted data of a model is linearly related to the normalization. Parameters ---------- id : None, int, or str The data set identifier to use. A value of ``None`` uses the default identifier. cpt If not ``None``, the model component to use. When ``None``, the full source expression for the data set is used. There is no check that the ``id`` argument matches the component (i.e. that the component is included in the source model for the data set) bkg_id : None, int If not None then change the normalization of the model to the given background dataset. names : None or array of str The parameter names that should be changed (a case-insensitive comparison is made, and the name does not include the model name). If ``None`` then the default set of ``['ampl', 'norm']`` is used. limscale : float The min and max range of the normalization is set to the calculated value divided and multiplied by ``limscale``. These limits will be modified to match the hard limits of the parameter if they exceed them. See Also -------- guess, ignore, notice, set_par Notes ----- The normalization is computed so that the predicted model counts matches the observed counts for the currently-noticed data range, as long as parameter names match the ``names`` argument (or ['ampl', 'norm'] if that is ``None``) and the parameter is not frozen. If no matches are found, then no changes are made. Otherwise, a scale factor is created by summing up the data counts and dividing this by the model sum over the currently-noticed range. This scale factor is divided by the number of matching parameters, and then the parameter values are multiplied by this value. If a model contains multiple parameters matching the contents of the ``names`` argument then each one will be changed by this routine. It is not intended for use with source expressions created with `set_full_model`, and may not work well with image models that use a PSF (one set with `set_psf`). Examples -------- Adjust the normalization of the gal component before fitting. >>> load_pha('src.pi') >>> subtract() >>> notice(0.5, 7) >>> set_source(xsphabs.galabs * xsapec.gal) >>> renorm() Change the normalization of a 2D model using the 'src' dataset. Only the ``src`` component is changed since the default value for the ``names`` parameter - that is ['ampl', 'norm'] - does not match the normalization parameter of the `const2d` model. >>> load_image('src', 'img.fits') >>> set_source('src', gauss2d.src + const2d.bgnd) >>> renorm('src') The names parameter is set so that both components are adjusted, and each component is assumed to contribute half the signal. >>> load_image(12, 'img.fits') >>> notice2d_id(12, 'srcfit.reg') >>> set_source(12, gauss2d.src12 + const2d.bgnd12) >>> renorm(12, names=['ampl', 'c0']) Change the minimum and maximum values of the normalization parameter to be the calculated value divided by and multiplied by 1e4 respectively (these changes are made to the soft limits). >>> renorm(limscale=1e4) """ if names is None: matches = ['ampl', 'norm'] elif names == []: raise ArgumentErr('bad', 'names argument', '[]') else: matches = [n.lower() for n in names] if bkg_id is None: d = ui.get_data(id=id) m = ui.get_model(id=id) else: d = ui.get_bkg(id=id, bkg_id=id) m = ui.get_bkg_model(id=id, bkg_id=bkg_id) if cpt is not None: # In this case the get_[bkg_]model call is not needed above, # but leave in as it at least ensures there's a model defined # for the data set. m = cpt pars = [p for p in m.pars if p.name.lower() in matches and not p.frozen] npars = len(pars) if npars == 0: wmsg = "no thawed parameters found matching: {}".format( ", ".join(matches)) warn(wmsg) return yd = d.get_dep(filter=True).sum() ym = d.eval_model_to_fit(m).sum() # argh; these are numpy floats, and they do not throw a # ZeroDivisionError, rather you get a RuntimeWarning message. # So explicitly convert to Python float. # try: scale = float(yd) / float(ym) / npars except ZeroDivisionError: error("model sum evaluated to 0; no re-scaling attempted") return for p in pars: newval = p.val * scale newmin = newval / limscale newmax = newval * limscale # Could do the limit/range checks and then call set_par, # but only do so if there's a problem. # try: ui.set_par(p, val=newval, min=newmin, max=newmax) except ParameterErr: # The following is not guaranteed to catch all cases; # e.g if the new value is outside the hard limits. # minflag = newmin < p.hard_min maxflag = newmax > p.hard_max if minflag: newmin = p.hard_min if maxflag: newmax = p.hard_max ui.set_par(p, val=newval, min=newmin, max=newmax) # provide informational message after changing the # parameter if minflag and maxflag: reason = "to hard min and max limits" elif minflag: reason = "to the hard minimum limit" elif maxflag: reason = "to the hard maximum limit" else: # this should be impossible reason = "for an unknown reason" info("Parameter {} is restricted ".format(p.fullname) + reason)