Ejemplo n.º 1
0
    def __init__(self,
                 ions=None,
                 continuum=None,
                 defaults=None,
                 z=None,
                 auto_fit=True,
                 velocity_convention='relativistic',
                 output='flux',
                 fitter=None,
                 with_rejection=False,
                 fitter_args=None,
                 *args,
                 **kwargs):
        super().__init__(*args, **kwargs)

        self._ions = ions or []
        self._continuum = continuum
        self._defaults = defaults or {}
        self._redshift_model = RedshiftScaleFactor(z=z or 0)
        self._model_result = None
        self._auto_fit = auto_fit
        self._output = output
        self._velocity_convention = velocity_convention
        self._fitter_args = fitter_args or {}
        self._fitter = fitter or CurveFitter()
        self._with_rejection = with_rejection
Ejemplo n.º 2
0
 def test_redshift(self):
     sp = SourceSpectrum(
         GaussianFlux1D, total_flux=(1 * units.FLAM), mean=5000, fwhm=10)
     sp.z = 1.3
     m = RedshiftScaleFactor(z=1.3)
     w_step25_z0 = [4978.76695499, 4989.3834775, 5000, 5010.6165225]
     np.testing.assert_allclose(sp.waveset.value[::25], m(w_step25_z0))
Ejemplo n.º 3
0
 def test_redshift(self):
     tf_unit = u.erg / (u.cm * u.cm * u.s)
     sp = SourceSpectrum(GaussianFlux1D,
                         total_flux=(1 * tf_unit),
                         mean=5000,
                         fwhm=10)
     sp.z = 1.3
     m = RedshiftScaleFactor(z=1.3)
     w_step25_z0 = [4978.76695499, 4989.3834775, 5000, 5010.6165225] * u.AA
     assert_quantity_allclose(sp.waveset[::25], m(w_step25_z0))
Ejemplo n.º 4
0
 def test_redshift(self):
     x0 = 5000
     totflux = units.convert_flux(x0, 1 * units.FLAM, units.PHOTLAM)
     sp = SourceSpectrum(GaussianFlux1D,
                         total_flux=totflux,
                         mean=x0,
                         fwhm=10)
     sp.z = 1.3
     m = RedshiftScaleFactor(z=1.3)
     w_step25_z0 = [
         4976.64365049, 4987.260173, 4997.8766955, 5008.493218,
         5019.10974051
     ]
     np.testing.assert_allclose(sp.waveset.value[::25], m(w_step25_z0))
Ejemplo n.º 5
0
    def __new__(cls,
                lines=None,
                continuum=None,
                z=None,
                lsf=None,
                output=None,
                velocity_convention=None,
                rest_wavelength=None,
                copy=False,
                input_redshift=None,
                **kwargs):
        # If the cls already contains parameter attributes, assume that this is
        # being called as part of a copy operation and return the class as-is.
        if (lines is None and continuum is None and z is None
                and output is None and velocity_convention is None
                and rest_wavelength is None):
            return super().__new__(cls)

        output = output or 'optical_depth'
        velocity_convention = velocity_convention or 'relativistic'
        rest_wavelength = rest_wavelength or u.Quantity(0, 'Angstrom')
        z = z or 0
        input_redshift = input_redshift or 0

        # If no continuum is provided, or the continuum provided is not a
        # model, use a constant model to represent the continuum.
        if continuum is not None:
            if not issubclass(type(continuum), FittableModel):
                if isinstance(continuum, (float, int)):
                    continuum = Const1D(amplitude=continuum,
                                        fixed={'amplitude': True})
                else:
                    raise ValueError(
                        "Continuum must be a number or `FittableModel`.")
            else:
                # If the continuum model is an astropy model, ensure that it
                # can handle inputs with units or wrap otherwise.
                if not continuum._supports_unit_fitting:
                    continuum = _wrap_unitless_model(continuum)
        else:
            continuum = Const1D(amplitude=0)

        if output not in ('flux', 'flux_decrement', 'optical_depth'):
            raise ValueError("Parameter 'output' must be one of 'flux', "
                             "'flux_decrement', 'optical_depth'.")

        # Parse the lines argument which can be a list, a quantity, or a string
        _lines = []

        if isinstance(lines, Fittable1DModel):
            _lines.append(lines)
        elif isinstance(lines, str):
            _lines.append(OpticalDepth1D(name=lines))
        elif isinstance(lines, list):
            for line in lines:
                if isinstance(line, str):
                    _lines.append(OpticalDepth1D(name=line))
                elif isinstance(line, u.Quantity):
                    _lines.append(OpticalDepth1D(lambda_0=line))
                elif isinstance(line, Fittable1DModel):
                    _lines.append(line)

        # Parse the lsf information, if provided
        if lsf is not None and not isinstance(lsf, LSFModel):
            if isinstance(lsf, Kernel1D):
                lsf = LSFModel(kernel=lsf)
            elif isinstance(lsf, str):
                if lsf == 'cos':
                    lsf = COSLSFModel()
                elif lsf == 'gaussian':
                    lsf = GaussianLSFModel(kwargs.pop('stddev', 1))
            else:
                raise ValueError("Kernel must be of type 'LSFModel', or "
                                 "'Kernel1D'; or a string with value 'cos' "
                                 "or 'gaussian'.")

        # Compose the line-based compound model taking into consideration
        # the redshift, continuum, and dispersion conversions.
        rs = RedshiftScaleFactor(z, fixed={'z': True}, name="redshift").inverse

        irs = RedshiftScaleFactor(input_redshift,
                                  fixed={'z': True},
                                  name="input_redshift")

        if lines is not None and len(_lines) > 0:
            ln = np.sum(_lines)

            if output == 'flux_decrement':
                compound_model = rs | (
                    (ln | FluxDecrementConvert()) + continuum)
            elif output == 'flux':
                compound_model = rs | ((ln | FluxConvert()) + continuum)
            else:
                compound_model = rs | (ln + continuum)
        else:
            compound_model = rs | continuum

        # Check for any lsf kernels that have been added
        if lsf is not None:
            compound_model |= lsf

        # Model parameter members are setup in the model's compound meta class.
        # After we've attached the parameters to this fittable model, call the
        # __new__ and __init__ meta methods again to ensure proper creation.
        members = {}
        members.update(cls.__dict__)

        # Delete all previous parameter definitions living on the class
        for k, v in members.items():
            if isinstance(v, Parameter):
                delattr(cls, k)

        # Create a dictionary to pass as the parameter unit definitions to the
        # new class. This ensures fitters know this model supports units.
        data_units = OrderedDict()

        # Attach all of the compound model parameters to this model
        for param_name in compound_model.param_names:
            param = getattr(compound_model, param_name)
            members[param_name] = param.copy(fixed=param.fixed)
            data_units[param_name] = param.unit

        members['_parameter_units_for_data_units'] = lambda *pufdu: data_units

        new_cls = type('Spectral{}'.format(compound_model.__name__), (cls, ),
                       members)

        # Ensure that the class is recorded in the global scope so that the
        # serialization done can access the stored class definitions.
        new_cls.__module__ = '__main__'
        # globals()[new_cls.__name__] = new_cls
        globals()[compound_model.__name__] = compound_model.__class__

        instance = super().__new__(new_cls)

        # Define the instance-level parameters
        setattr(instance, '_continuum', continuum)
        setattr(instance, '_compound_model', compound_model)
        setattr(instance, '_velocity_convention', velocity_convention)
        setattr(instance, '_rest_wavelength', rest_wavelength)
        setattr(instance, '_output', output)

        return instance
Ejemplo n.º 6
0
class LineFinder1D(Fittable2DModel):
    inputs = ('x', 'y')
    outputs = ('y', )

    @property
    def input_units_allow_dimensionless(self):
        return {'x': False, 'y': True}

    threshold = Parameter(default=0, fixed=True)
    min_distance = Parameter(default=10.0, min=1, fixed=True)

    def __init__(self,
                 ions=None,
                 continuum=None,
                 defaults=None,
                 z=None,
                 auto_fit=True,
                 velocity_convention='relativistic',
                 output='flux',
                 fitter=None,
                 with_rejection=False,
                 fitter_args=None,
                 *args,
                 **kwargs):
        super().__init__(*args, **kwargs)

        self._ions = ions or []
        self._continuum = continuum
        self._defaults = defaults or {}
        self._redshift_model = RedshiftScaleFactor(z=z or 0)
        self._model_result = None
        self._auto_fit = auto_fit
        self._output = output
        self._velocity_convention = velocity_convention
        self._fitter_args = fitter_args or {}
        self._fitter = fitter or CurveFitter()
        self._with_rejection = with_rejection

    @property
    def model_result(self):
        return self._model_result

    @property
    def fitter(self):
        return self._fitter

    def __call__(self, x, *args, auto_fit=None, **kwargs):
        if auto_fit is not None:
            self._auto_fit = auto_fit

        if x.unit.physical_type == 'speed' and len(self._ions) != 1:
            raise ReferenceError("The line finder will not be able to parse "
                                 "ion information in velocity space without "
                                 "being given explicit ion reference in the "
                                 "defaults dictionary.")

        super().__call__(x, *args, **kwargs)

        return self._model_result

    def evaluate(self, x, y, threshold, min_distance, *args, **kwargs):
        spec_mod = Spectral1D(continuum=self._continuum, output=self._output)

        if x.unit.physical_type in ('length', 'frequency'):
            x = self._redshift_model.inverse(x)

        # Generate the subset of the table for the ions chosen by the user
        sub_registry = line_registry

        if len(self._ions) > 0:
            # In this case, the user has provided a list of ions for their
            # spectrum. Create a subset of the line registry so that only
            # these ions will be searched when attempting to identify.
            sub_registry = line_registry.subset(self._ions)

        # Convert the min_distance from dispersion units to data elements.
        # Assumes uniform spacing.
        # min_ind = (np.abs(x.value - (x[0].value + min_distance))).argmin()

        # Find peaks
        regions = region_bounds(x,
                                y,
                                threshold=threshold,
                                min_distance=min_distance)
        lines = []

        for centroid, (mn_bnd,
                       mx_bnd), is_absorption, buried in regions.values():
            mn_bnd, mx_bnd = mn_bnd * x.unit, mx_bnd * x.unit
            sub_x, vel_mn_bnd, vel_mx_bnd = None, None, None

            line_kwargs = {}

            # For the case where the user has provided a list of ions with a
            # dispersion in wavelength or frequency, convert each ion to
            # velocity space individually to avoid making assumptions of their
            # kinematics.
            if x.unit.physical_type in ('length', 'frequency'):
                line = sub_registry.with_lambda(centroid)

                disp_equiv = u.spectral() + DOPPLER_CONVERT[
                    self._velocity_convention](line['wave'])

                with u.set_enabled_equivalencies(disp_equiv):
                    sub_x = u.Quantity(x, 'km/s')
                    vel_mn_bnd, vel_mx_bnd, vel_centroid = mn_bnd.to('km/s'), \
                                                           mx_bnd.to('km/s'), \
                                                           centroid.to('km/s')
            else:
                line = sub_registry.with_name(self._ions[0])

            line_kwargs.update({
                'name': line['name'],
                'lambda_0': line['wave'],
                'gamma': line['gamma'],
                'f_value': line['osc_str']
            })

            # Estimate the doppler b and column densities for this line.
            # For the parameter estimator to be accurate, the spectrum must be
            # continuum subtracted.
            v_dop, col_dens, nmn_bnd, nmx_bnd = parameter_estimator(
                centroid=centroid,
                bounds=(vel_mn_bnd or mn_bnd, vel_mx_bnd or mx_bnd),
                x=sub_x or x,
                y=spec_mod.continuum(sub_x or x) - y if is_absorption else y,
                ion_info=line_kwargs,
                buried=buried)

            if np.isinf(col_dens) or np.isnan(col_dens):
                continue

            estimate_kwargs = {
                'v_doppler': v_dop,
                'column_density': col_dens,
                'fixed': {},
                'bounds': {},
            }

            # Depending on the dispersion unit information, decide whether
            # the fitter should consider delta values in velocity or
            # wavelength/frequency space.
            if x.unit.physical_type in ('length', 'frequency'):
                estimate_kwargs['delta_lambda'] = centroid - line['wave']
                estimate_kwargs['fixed'].update({'delta_v': True})
                estimate_kwargs['bounds'].update({
                    'delta_lambda': (mn_bnd.value - line['wave'].value,
                                     mx_bnd.value - line['wave'].value)
                })
            else:
                # In velocity space, the centroid *should* be zero for any
                # line given that the rest wavelength is taken as its lamba_0
                # in conversions. Thus, the given centroid is akin to the
                # velocity offset.
                estimate_kwargs['delta_v'] = centroid
                estimate_kwargs['fixed'].update({'delta_lambda': True})
                estimate_kwargs['bounds'].update(
                    {'delta_v': (mn_bnd.value, mx_bnd.value)})

            line_kwargs.update(estimate_kwargs)
            line_kwargs.update(self._defaults.copy())

            line = OpticalDepth1D(**line_kwargs)
            lines.append(line)

        logging.debug(
            "Found %s possible lines (theshold=%s, min_distance=%s).",
            len(lines), threshold, min_distance)

        if len(lines) == 0:
            return np.zeros(x.shape)

        spec_mod = Spectral1D(lines,
                              continuum=self._continuum,
                              output=self._output,
                              z=self._redshift_model.z.value)

        if self._auto_fit:
            if issubclass(self._fitter.__class__, LevMarLSQFitter):
                if 'maxiter' not in self._fitter_args:
                    self._fitter_args['maxiter'] = 1000

            fit_spec_mod = self._fitter(spec_mod, self._redshift_model(x), y,
                                        **self._fitter_args)
        else:
            fit_spec_mod = spec_mod

        # The parameter values on the underlying compound model must also be
        # updated given the new fitted parameters on the Spectral1D instance.
        # FIXME: when fitting without using line finder, these values will not
        # be updated in the compound model.
        for pn in fit_spec_mod.param_names:
            pv = getattr(fit_spec_mod, pn)
            setattr(fit_spec_mod._compound_model, pn, pv)

        fit_spec_mod.line_regions = regions

        self._model_result = fit_spec_mod

        return fit_spec_mod(x)
Ejemplo n.º 7
0
class LineFinder1D(Fittable2DModel):
    """
    The line finder class used to discover ion profiles within spectral data.

    Parameters
    ----------
    ions : list
        The list of ions to consider when discovering centroids. Each found
        centroid will be assigned an ion from this list, or the entire
        ion database if no filter is provided.
    continuum : float, :class:`~astropy.modeling.fitting.Fittable1DModel`
        Either a value representing the continuum's constant value, or an
        astropy fittable model representing the continuum. Used in fitting and
        added to the final spectral model produced by the operation.
    defaults : dict
        Dictionary containing key-value pairs when the key is the parameter
        name accepted by the :class:`~Spectral1D` class. If a parameter is
        defined this way, the fitter will use it instead of the fitted
        parameter value.
    z : float
        The redshift applied to the spectral model. Default = 0.
    output : {'optical_depth', 'flux', 'flux_decrement'}
        The expected output when evaluating the model object. Default =
        'optical_depth'.
    velocity_convention : {'optical', 'radio', 'relativistic'}
        The velocity convention to use when converting between wavelength and
        velocity space dispersion values. Default = 'relativistic'.
    """
    inputs = ('x', 'y')
    outputs = ('y', )

    @property
    def input_units_allow_dimensionless(self):
        return {'x': False, 'y': True}

    threshold = Parameter(default=0, fixed=True)
    min_distance = Parameter(default=10.0, min=1, fixed=True)

    def __init__(self,
                 ions=None,
                 continuum=None,
                 defaults=None,
                 z=None,
                 auto_fit=True,
                 output='flux',
                 velocity_convention='relativistic',
                 fitter=None,
                 auto_reject=False,
                 fitter_args=None,
                 *args,
                 **kwargs):
        super().__init__(*args, **kwargs)

        self._ions = ions or []
        self._continuum = continuum
        self._defaults = defaults or {}
        self._redshift_model = RedshiftScaleFactor(z=z or 0)
        self._model_result = None
        self._auto_fit = auto_fit
        self._output = output
        self._velocity_convention = velocity_convention
        self._fitter_args = fitter_args or {}
        self._fitter = fitter or CurveFitter()
        self._auto_reject = auto_reject

    @property
    def model_result(self):
        return self._model_result

    @property
    def fitter(self):
        return self._fitter

    def __call__(self, x, *args, auto_fit=None, **kwargs):
        if auto_fit is not None:
            self._auto_fit = auto_fit

        if x.unit.physical_type == 'speed' and len(self._ions) != 1:
            raise ReferenceError("The line finder will not be able to parse "
                                 "ion information in velocity space without "
                                 "being given explicit ion reference in the "
                                 "defaults dictionary.")

        super().__call__(x, *args, **kwargs)

        return self._model_result

    def evaluate(self, x, y, threshold, min_distance, *args, **kwargs):
        spec_mod = Spectral1D(continuum=self._continuum, output=self._output)

        if x.unit.physical_type in ('length', 'frequency'):
            x = self._redshift_model.inverse(x)

        # Generate the subset of the table for the ions chosen by the user
        sub_registry = line_registry

        if len(self._ions) > 0:
            # In this case, the user has provided a list of ions for their
            # spectrum. Create a subset of the line registry so that only
            # these ions will be searched when attempting to identify.
            sub_registry = line_registry.subset(self._ions)

        # Convert the min_distance from dispersion units to data elements.
        # Assumes uniform spacing.
        # min_ind = (np.abs(x.value - (x[0].value + min_distance))).argmin()

        # Find peaks
        regions = region_bounds(x,
                                y,
                                threshold=threshold,
                                min_distance=min_distance)

        # First, generate profiles based on all the primary peaks
        prime_lines = self._discover_lines(
            x, y, spec_mod, sub_registry,
            {k: v
             for k, v in regions.items() if not v[3]}, threshold, min_distance)

        prime_mod = Spectral1D(prime_lines,
                               continuum=0,
                               output='optical_depth',
                               z=self._redshift_model.z.value)

        # Second, mask out the input data based on the found primary peaks,
        # then fit the buried lines.
        py = prime_mod(x)

        thresh_mask = np.greater(py / np.max(py), 0.0001)
        px, py = x[~thresh_mask], y[~thresh_mask]
        my = np.interp(x, px, py)

        tern_lines = self._discover_lines(
            x, my, spec_mod, sub_registry,
            {k: v
             for k, v in regions.items() if v[3]}, threshold, min_distance)

        # Generate the final spectral model
        spec_mod = Spectral1D(prime_lines + tern_lines,
                              continuum=self._continuum,
                              output=self._output,
                              z=self._redshift_model.z.value)

        if self._auto_fit:
            if issubclass(self._fitter.__class__, LevMarLSQFitter):
                if 'maxiter' not in self._fitter_args:
                    self._fitter_args['maxiter'] = 1000

            if self._auto_reject:
                fit_spec_mod, _, _, _ = spec_mod.rejection_criteria(
                    self._redshift_model(x),
                    y,
                    auto_fit=True,
                    fitter=self._fitter,
                    fitter_args=self._fitter_args)
            else:
                fit_spec_mod = self._fitter(spec_mod, self._redshift_model(x),
                                            y, **self._fitter_args)
        else:
            if self._auto_reject:
                fit_spec_mod, _, _, _ = spec_mod.rejection_criteria(
                    self._redshift_model(x), y, auto_fit=False)
            else:
                fit_spec_mod = spec_mod

        # The parameter values on the underlying compound model must also be
        # updated given the new fitted parameters on the Spectral1D instance.
        # FIXME: when fitting without using line finder, these values will not
        # be updated in the compound model.
        for pn in fit_spec_mod.param_names:
            pv = getattr(fit_spec_mod, pn)
            setattr(fit_spec_mod._compound_model, pn, pv)

        fit_spec_mod.line_regions = regions

        self._model_result = fit_spec_mod

        return fit_spec_mod(x)

    def _discover_lines(self, x, y, mod, registry, regions, threshold,
                        min_distance):
        lines = []

        # First, calculate all the primary line profiles
        for centroid, (mn_bnd,
                       mx_bnd), is_absorption, buried in regions.values():
            mn_bnd, mx_bnd = mn_bnd * x.unit, mx_bnd * x.unit
            sub_x, vel_mn_bnd, vel_mx_bnd = None, None, None

            line_kwargs = {}

            # For the case where the user has provided a list of ions with a
            # dispersion in wavelength or frequency, convert each ion to
            # velocity space individually to avoid making assumptions of their
            # kinematics.
            if x.unit.physical_type in ('length', 'frequency'):
                line = registry.with_lambda(centroid)

                disp_equiv = u.spectral() + DOPPLER_CONVERT[
                    self._velocity_convention](line['wave'])

                with u.set_enabled_equivalencies(disp_equiv):
                    sub_x = u.Quantity(x, 'km/s')
                    vel_mn_bnd, vel_mx_bnd, vel_centroid = mn_bnd.to('km/s'), \
                                                           mx_bnd.to('km/s'), \
                                                           centroid.to('km/s')
            else:
                line = registry.with_name(self._ions[0])

            line_kwargs.update({
                'name': line['name'],
                'lambda_0': line['wave'],
                'gamma': line['gamma'],
                'f_value': line['osc_str']
            })

            # Estimate the doppler b and column densities for this line.
            # For the parameter estimator to be accurate, the spectrum must be
            # continuum subtracted.
            centroid, v_dop, col_dens, nmn_bnd, nmx_bnd = parameter_estimator(
                centroid=centroid,
                bounds=(vel_mn_bnd or mn_bnd, vel_mx_bnd or mx_bnd),
                x=sub_x or x,
                y=mod.continuum(sub_x or x) - y if is_absorption else y,
                ion_info=line_kwargs,
                buried=buried)

            if np.isinf(col_dens) or np.isnan(col_dens):
                continue

            estimate_kwargs = {
                'v_doppler': v_dop,
                'column_density': col_dens,
                'fixed': {},
                'bounds': {},
            }

            # Depending on the dispersion unit information, decide whether
            # the fitter should consider delta values in velocity or
            # wavelength/frequency space.
            if x.unit.physical_type in ('length', 'frequency'):
                estimate_kwargs['delta_lambda'] = centroid - line['wave']
                estimate_kwargs['fixed'].update({'delta_v': True})
                estimate_kwargs['bounds'].update({
                    'delta_lambda': (mn_bnd.value - line['wave'].value,
                                     mx_bnd.value - line['wave'].value)
                })
            else:
                # In velocity space, the centroid *should* be zero for any
                # line given that the rest wavelength is taken as its lamba_0
                # in conversions. Thus, the given centroid is akin to the
                # velocity offset.
                estimate_kwargs['delta_v'] = centroid
                estimate_kwargs['fixed'].update({'delta_lambda': True})
                estimate_kwargs['bounds'].update(
                    {'delta_v': (mn_bnd.value, mx_bnd.value)})

            line_kwargs.update(estimate_kwargs)
            line_kwargs.update(self._defaults.copy())

            line = OpticalDepth1D(**line_kwargs)
            lines.append(line)

        logging.debug(
            "Found %s possible lines (theshold=%s, min_distance=%s).",
            len(lines), threshold, min_distance)

        return lines