示例#1
0
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)
示例#2
0
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)
示例#3
0
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)
示例#4
0
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)
示例#5
0
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
示例#6
0
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)