Example #1
0
    def test_get_last_line_counts(self, line_info_widget, allowed_species,
                                  filter_mode, group_mode):
        """
        Test for get_last_line_counts() method.

        Since this method depends on get_species_interactions() so we need to
        make sure that we select only allowed species i.e. present within the
        wavelength range selected by the get_species_interactions(), which is
        being done here by allowed_species fixture.
        """
        if allowed_species is None:
            last_line_counts_df = line_info_widget.get_last_line_counts(
                None, filter_mode, group_mode)
            # Dataframe contains all falsy values (proxy for empty)
            assert last_line_counts_df.all(axis=None) == False
            return

        for selected_species in allowed_species:
            last_line_counts_df = line_info_widget.get_last_line_counts(
                selected_species, filter_mode, group_mode)

            last_lines_in = (
                line_info_widget.line_interaction_analysis[filter_mode].
                last_line_in.xs(
                    key=species_string_to_tuple(selected_species),
                    level=["atomic_number", "ion_number"],
                ).reset_index())

            last_lines_out = (
                line_info_widget.line_interaction_analysis[filter_mode].
                last_line_out.xs(
                    key=species_string_to_tuple(selected_species),
                    level=["atomic_number", "ion_number"],
                ).reset_index())

            if group_mode == "exc":
                expected_df_length = last_lines_in.groupby("line_id").ngroups
            elif group_mode == "de-exc":
                expected_df_length = last_lines_out.groupby("line_id").ngroups
            elif group_mode == "both":
                expected_df_length = last_lines_in.groupby(
                    ["line_id", last_lines_out["line_id"]]).ngroups

            # Test shape of the dataframe
            assert last_line_counts_df.shape == (expected_df_length, 1)
Example #2
0
def test_species_string_to_tuple(species_string, species_tuple):
    assert species_string_to_tuple(species_string) == species_tuple

    with pytest.raises(MalformedSpeciesError):
        species_string_to_tuple('II')

    with pytest.raises(MalformedSpeciesError):
        species_string_to_tuple('He Si')

    with pytest.raises(ValueError):
        species_string_to_tuple('He IX')
Example #3
0
def test_species_string_to_tuple(species_string, species_tuple):
    assert species_string_to_tuple(species_string) == species_tuple

    with pytest.raises(MalformedSpeciesError):
        species_string_to_tuple('II')

    with pytest.raises(MalformedSpeciesError):
        species_string_to_tuple('He Si')

    with pytest.raises(ValueError):
        species_string_to_tuple('He IX')
Example #4
0
def assemble_plasma(config, model, atom_data=None):
    """
    Create a BasePlasma instance from a Configuration object
    and a Radial1DModel.

    Parameters
    ----------
    config : io.config_reader.Configuration
    model : model.Radial1DModel
    atom_data : atomic.AtomData
        If None, an attempt will be made to read the atomic data
        from config.

    Returns
    -------
    : plasma.BasePlasma

    """
    # Convert the nlte species list to a proper format.
    nlte_species = [
        species_string_to_tuple(s) for s in config.plasma.nlte.species
    ]

    # Convert the continuum interaction species list to a proper format.
    continuum_interaction_species = [
        species_string_to_tuple(s)
        for s in config.plasma.continuum_interaction.species
    ]
    continuum_interaction_species = pd.MultiIndex.from_tuples(
        continuum_interaction_species, names=["atomic_number", "ion_number"])

    if atom_data is None:
        if "atom_data" in config:
            if os.path.isabs(config.atom_data):
                atom_data_fname = config.atom_data
            else:
                atom_data_fname = os.path.join(config.config_dirname,
                                               config.atom_data)
        else:
            raise ValueError("No atom_data option found in the configuration.")

        logger.info("Reading Atomic Data from %s", atom_data_fname)

        try:
            atom_data = AtomData.from_hdf(atom_data_fname)
        except TypeError as e:
            print(
                e,
                "Error might be from the use of an old-format of the atomic database, \n"
                "please see https://github.com/tardis-sn/tardis-refdata/tree/master/atom_data"
                " for the most recent version.",
            )
            raise

    atom_data.prepare_atom_data(
        model.abundance.index,
        line_interaction_type=config.plasma.line_interaction_type,
        nlte_species=nlte_species,
    )

    # Check if continuum interaction species are in selected_atoms
    continuum_atoms = continuum_interaction_species.get_level_values(
        "atomic_number")
    continuum_atoms_in_selected_atoms = np.all(
        continuum_atoms.isin(atom_data.selected_atomic_numbers))
    if not continuum_atoms_in_selected_atoms:
        raise PlasmaConfigError("Not all continuum interaction species "
                                "belong to atoms that have been specified "
                                "in the configuration.")

    kwargs = dict(
        t_rad=model.t_radiative,
        abundance=model.abundance,
        density=model.density,
        atomic_data=atom_data,
        time_explosion=model.time_explosion,
        w=model.dilution_factor,
        link_t_rad_t_electron=0.9,
        continuum_interaction_species=continuum_interaction_species,
    )

    plasma_modules = basic_inputs + basic_properties
    property_kwargs = {}
    if config.plasma.continuum_interaction.species:
        line_interaction_type = config.plasma.line_interaction_type
        if line_interaction_type != "macroatom":
            raise PlasmaConfigError(
                "Continuum interactions require line_interaction_type "
                "macroatom (instead of {}).".format(line_interaction_type))

        plasma_modules += continuum_interaction_properties
        plasma_modules += continuum_interaction_inputs

        if config.plasma.continuum_interaction.enable_adiabatic_cooling:
            plasma_modules += adiabatic_cooling_properties

        if config.plasma.continuum_interaction.enable_two_photon_decay:
            plasma_modules += two_photon_properties

        transition_probabilities_outputs = [
            plasma_property.transition_probabilities_outputs
            for plasma_property in plasma_modules
            if issubclass(plasma_property, TransitionProbabilitiesProperty)
        ]
        transition_probabilities_outputs = [
            item for sublist in transition_probabilities_outputs
            for item in sublist
        ]

        property_kwargs[MarkovChainTransProbsCollector] = {
            "inputs": transition_probabilities_outputs
        }

        kwargs.update(
            gamma_estimator=None,
            bf_heating_coeff_estimator=None,
            alpha_stim_estimator=None,
            volume=model.volume,
            r_inner=model.r_inner,
            t_inner=model.t_inner,
        )
    if config.plasma.radiative_rates_type == "blackbody":
        plasma_modules.append(JBluesBlackBody)
    elif config.plasma.radiative_rates_type == "dilute-blackbody":
        plasma_modules.append(JBluesDiluteBlackBody)
    elif config.plasma.radiative_rates_type == "detailed":
        plasma_modules += detailed_j_blues_properties + detailed_j_blues_inputs
        kwargs.update(
            r_inner=model.r_inner,
            t_inner=model.t_inner,
            volume=model.volume,
            j_blue_estimator=None,
        )
        property_kwargs[JBluesDetailed] = {
            "w_epsilon": config.plasma.w_epsilon
        }
    else:
        raise ValueError(
            f"radiative_rates_type type unknown - {config.plasma.radiative_rates_type}"
        )

    if config.plasma.excitation == "lte":
        plasma_modules += lte_excitation_properties
    elif config.plasma.excitation == "dilute-lte":
        plasma_modules += dilute_lte_excitation_properties

    if config.plasma.ionization == "lte":
        plasma_modules += lte_ionization_properties
    elif config.plasma.ionization == "nebular":
        plasma_modules += nebular_ionization_properties

    if nlte_species:
        plasma_modules += nlte_properties
        nlte_conf = config.plasma.nlte
        plasma_modules.append(LevelBoltzmannFactorNLTE.from_config(nlte_conf))
        property_kwargs[StimulatedEmissionFactor] = dict(
            nlte_species=nlte_species)
    else:
        plasma_modules += non_nlte_properties

    if config.plasma.line_interaction_type in ("downbranch", "macroatom"):
        plasma_modules += macro_atom_properties

    if "delta_treatment" in config.plasma:
        property_kwargs[RadiationFieldCorrection] = dict(
            delta_treatment=config.plasma.delta_treatment)

    if config.plasma.helium_treatment == "recomb-nlte":
        plasma_modules += helium_nlte_properties
    elif config.plasma.helium_treatment == "numerical-nlte":
        plasma_modules += helium_numerical_nlte_properties
        # TODO: See issue #633
        if config.plasma.heating_rate_data_file in ["none", None]:
            raise PlasmaConfigError("Heating rate data file not specified")
        else:
            property_kwargs[HeliumNumericalNLTE] = dict(
                heating_rate_data_file=config.plasma.heating_rate_data_file)
    else:
        plasma_modules += helium_lte_properties

    if model._electron_densities:
        electron_densities = pd.Series(model._electron_densities.cgs.value)
        if config.plasma.helium_treatment == "numerical-nlte":
            property_kwargs[IonNumberDensityHeNLTE] = dict(
                electron_densities=electron_densities)
        else:
            property_kwargs[IonNumberDensity] = dict(
                electron_densities=electron_densities)

    kwargs["helium_treatment"] = config.plasma.helium_treatment

    plasma = BasePlasma(
        plasma_properties=plasma_modules,
        property_kwargs=property_kwargs,
        **kwargs,
    )

    return plasma
Example #5
0
def assemble_plasma(config, model, atom_data=None):
    """
    Create a BasePlasma instance from a Configuration object
    and a Radial1DModel.

    Parameters
    ----------
    config: ~io.config_reader.Configuration
    model: ~model.Radial1DModel
    atom_data: ~atomic.AtomData
        If None, an attempt will be made to read the atomic data
        from config.

    Returns
    -------
    : ~plasma.BasePlasma

    """
    # Convert the nlte species list to a proper format.
    nlte_species = [species_string_to_tuple(s) for s in
                    config.plasma.nlte.species]

    if atom_data is None:
        if 'atom_data' in config:
            if os.path.isabs(config.atom_data):
                atom_data_fname = config.atom_data
            else:
                atom_data_fname = os.path.join(config.config_dirname,
                                               config.atom_data)
        else:
            raise ValueError('No atom_data option found in the configuration.')

        logger.info('Reading Atomic Data from %s', atom_data_fname)

        try:
            atom_data = atomic.AtomData.from_hdf(atom_data_fname)
        except TypeError as e:
            print (e, 'Error might be from the use of an old-format of the atomic database, \n'
                'please see https://github.com/tardis-sn/tardis-refdata/tree/master/atom_data'
                ',for the most recent version.')
            raise

    atom_data.prepare_atom_data(
        model.abundance.index,
        line_interaction_type=config.plasma.line_interaction_type,
        nlte_species=nlte_species)

    kwargs = dict(t_rad=model.t_radiative, abundance=model.abundance,
                  density=model.density, atomic_data=atom_data,
                  time_explosion=model.time_explosion,
                  w=model.dilution_factor, link_t_rad_t_electron=0.9)

    plasma_modules = basic_inputs + basic_properties
    property_kwargs = {}
    if config.plasma.radiative_rates_type == 'blackbody':
        plasma_modules.append(JBluesBlackBody)
    elif config.plasma.radiative_rates_type == 'dilute-blackbody':
        plasma_modules.append(JBluesDiluteBlackBody)
    elif config.plasma.radiative_rates_type == 'detailed':
        plasma_modules += detailed_j_blues_properties + detailed_j_blues_inputs
        kwargs.update(r_inner=model.r_inner,
                      t_inner=model.t_inner,
                      volume=model.volume,
                      j_blue_estimator=None)
        property_kwargs[JBluesDetailed] = {'w_epsilon': config.plasma.w_epsilon}
    else:
        raise ValueError('radiative_rates_type type unknown - %s',
                         config.plasma.radiative_rates_type)

    if config.plasma.excitation == 'lte':
        plasma_modules += lte_excitation_properties
    elif config.plasma.excitation == 'dilute-lte':
        plasma_modules += dilute_lte_excitation_properties

    if config.plasma.ionization == 'lte':
        plasma_modules += lte_ionization_properties
    elif config.plasma.ionization == 'nebular':
        plasma_modules += nebular_ionization_properties

    if nlte_species:
        plasma_modules += nlte_properties
        nlte_conf = config.plasma.nlte
        plasma_modules.append(
                LevelBoltzmannFactorNLTE.from_config(nlte_conf)
                )
        property_kwargs[StimulatedEmissionFactor] = dict(
            nlte_species=nlte_species)
    else:
        plasma_modules += non_nlte_properties

    if config.plasma.line_interaction_type in ('downbranch', 'macroatom'):
        plasma_modules += macro_atom_properties

    if 'delta_treatment' in config.plasma:
        property_kwargs[RadiationFieldCorrection] = dict(
            delta_treatment=config.plasma.delta_treatment)

    if config.plasma.helium_treatment == 'recomb-nlte':
        plasma_modules += helium_nlte_properties
    elif config.plasma.helium_treatment == 'numerical-nlte':
        plasma_modules += helium_numerical_nlte_properties
        # TODO: See issue #633
        if config.plasma.heating_rate_data_file in ['none', None]:
            raise PlasmaConfigError('Heating rate data file not specified')
        else:
            property_kwargs[HeliumNumericalNLTE] = dict(
                heating_rate_data_file=config.plasma.heating_rate_data_file)
    else:
        plasma_modules += helium_lte_properties

    if model._electron_densities:
        electron_densities = pd.Series(model._electron_densities.cgs.value)
        if config.plasma.helium_treatment == 'numerical-nlte':
            property_kwargs[IonNumberDensityHeNLTE] = dict(
                electron_densities=electron_densities)
        else:
            property_kwargs[IonNumberDensity] = dict(
                electron_densities=electron_densities)

    kwargs['helium_treatment'] = config.plasma.helium_treatment

    plasma = BasePlasma(plasma_properties=plasma_modules,
                        property_kwargs=property_kwargs, **kwargs)

    return plasma
    def generate_plot(
        self,
        ax=None,
        cmap=cm.jet,
        bins=None,
        xlim=None,
        ylim=None,
        nelements=None,
        twinx=False,
        species_list=None,
    ):
        """Generate the actual "Kromer" plot
        Parameters
        ----------
        ax : matplotlib.axes or None
            axes object into which the emission part of the Kromer plot should
            be plotted; if None, a new one is generated (default None)
        cmap : matplotlib.cm.ListedColormap or None
            color map object used for the illustration of the different atomic
            contributions (default matplotlib.cm.jet)
        bins : np.ndarray or None
            array of the wavelength bins used for the illustration of the
            atomic contributions; if None, the same binning as for the stored
            virtual spectrum is used (default None)
        xlim : tuple or array-like or None
            wavelength limits for the display; if None, the x-axis is
            automatically scaled (default None)
        ylim : tuple or array-like or None
            flux limits for the display; if None, the y-axis is automatically
            scaled (default None)
        nelements: int or None
            number of elements that should be included in the Kromer plots.
            The top nelements are determined based on those with the most packet
            interactions
        twinx : boolean
            determines where the absorption part of the Kromer plot is placed,
            if True, the absorption part is attached at the top of the main
            axes box, otherwise it is placed below the emission part (default
            False)
        species_list: list of strings or None
            list of strings containing the names of species that should be included in the Kromer plots,
            e.g. ['Si II', 'Ca II']
        Returns
        -------
        fig : matplotlib.figure
            figure instance containing the plot
        """
        self._ax = None
        self._pax = None

        self._cmap = cmap
        self._ax = ax
        self._ylim = ylim
        self._twinx = twinx

        # the species list can contain either a specific element, a specific
        # ion, a range of ions, or any combination of these if the list
        # contains a range of ions, separate each one into a new entry in the
        # species list
        full_species_list = []
        if species_list is not None:
            for species in species_list:
                # check if a hyphen is present. If it is, then it indicates a
                # range of ions. Add each ion in that range to the list
                if "-" in species:
                    element = species.split(" ")[0]
                    first_ion_numeral = roman_to_int(
                        species.split(" ")[-1].split("-")[0])
                    second_ion_numeral = roman_to_int(
                        species.split(" ")[-1].split("-")[-1])
                    for i in np.arange(first_ion_numeral,
                                       second_ion_numeral + 1):
                        full_species_list.append(element + " " +
                                                 int_to_roman(i))
                else:
                    full_species_list.append(species)
            self._species_list = full_species_list
        else:
            self._species_list = None

        if xlim is None:
            self._xlim = [
                np.min(self.mdl.spectrum_wave).value,
                np.max(self.mdl.spectrum_wave).value,
            ]
        else:
            self._xlim = xlim

        if bins is None:
            self._bins = self.mdl.spectrum_wave[::-1]
        else:
            self._bins = bins

        # get the elements/species to be included in the plot
        self._elements_in_kromer_plot = self.line_info

        # if no nelements and no species list is specified, then the number of
        # elements to be included in the colourbar is determined from the list
        # of unique elements that appear in the model
        if nelements is None and species_list is None:
            self._nelements = len(
                np.unique(self.line_in_and_out_infos_within_xlims.
                          atomic_number.values))
        elif nelements is None and species_list is not None:
            # if species_list has been specified, then the number of elements
            # to be included is set to the length of that list
            self._nelements = len(self._species_list)
        else:
            # if nelements has been specified, then the number of elements to
            # be included is set to the length of that list
            self._nelements = nelements

        # if the length of self._elements_in_kromer_plot exceeds the requested
        # number of elements to be included in the colourbar, then this if
        # statement applies
        if self._species_list is not None:
            # if we have specified a species list then only take those species
            # that are requested
            mask = np.in1d(self._elements_in_kromer_plot[:, 0],
                           self.requested_species_ids)
            self._elements_in_kromer_plot = self._elements_in_kromer_plot[mask]
        elif len(self._elements_in_kromer_plot) > self._nelements:
            # if nelements is specified, then sort to find the top contributing
            # elements, pick the top nelements, and sort back by atomic number
            self._elements_in_kromer_plot = self._elements_in_kromer_plot[
                np.argsort(self._elements_in_kromer_plot[:, 1])[::-1]]
            self._elements_in_kromer_plot = self._elements_in_kromer_plot[:self
                                                                          .
                                                                          _nelements]
            self._elements_in_kromer_plot = self._elements_in_kromer_plot[
                np.argsort(self._elements_in_kromer_plot[:, 0])]
        else:
            # if the length of self._elements_in_kromer_plot is less than the
            # requested number of elements in the model, then this requested
            # length is updated to be the length of length of
            # self._elements_in_kromer_plot
            self._nelements = len(self._elements_in_kromer_plot)

        # this will reset nelements if species_list is turned on
        # it's possible to request a species that doesn't appear in the plot
        # this will ensure that species isn't counted when determining labels
        # and colours
        if self._species_list is not None:
            labels = []
            for species in self._species_list:
                if " " in species:
                    atomic_number = species_string_to_tuple(species)[0]
                    ion_number = species_string_to_tuple(species)[1]

                    species_id = atomic_number * 100 + ion_number
                    if species_id in self._elements_in_kromer_plot:
                        labels.append(species)
                else:
                    labels.append(species)
            self._nelements = len(labels)

        self._axes_handling_preparation()
        self._generate_emission_part()
        self._generate_photosphere_part()
        self._generate_and_add_colormap()
        self._generate_and_add_legend()
        self._paxes_handling_preparation()
        self._generate_absorption_part()
        self._axis_handling_label_rescale()

        return plt.gcf()
    def line_info(self):
        """produces list of elements to be included in the kromer plot"""
        # gets list of elements and number of emitted packets
        self.last_line_interaction_out_id = self.line_out_infos
        self.last_line_interaction_out_angstrom = self.line_out_nu.to(
            units.Angstrom, equivalencies=units.spectral())

        self.last_line_interaction_out_id[
            "emitted_wavelength"] = self.last_line_interaction_out_angstrom

        self.line_out_infos_within_xlims = self.last_line_interaction_out_id.loc[
            (self.last_line_interaction_out_id.emitted_wavelength >=
             self._xlim[0])
            & (self.last_line_interaction_out_id.emitted_wavelength <=
               self._xlim[1])]

        # gets list of elements and number of absorbed packets
        self.last_line_interaction_in_id = self.line_in_infos
        self.last_line_interaction_in_angstrom = self.line_in_nu.to(
            units.Angstrom, equivalencies=units.spectral())

        self.last_line_interaction_in_id[
            "emitted_wavelength"] = self.last_line_interaction_in_angstrom

        self.line_in_infos_within_xlims = self.last_line_interaction_in_id.loc[
            (self.last_line_interaction_in_id.emitted_wavelength >=
             self._xlim[0])
            & (self.last_line_interaction_in_id.emitted_wavelength <=
               self._xlim[1])]

        self.line_in_and_out_infos_within_xlims = pd.concat([
            self.line_in_infos_within_xlims, self.line_out_infos_within_xlims
        ])

        # this generates the 4-digit ID for all transitions in the model
        # (e.g. Fe III line --> 2602)
        self.line_in_and_out_infos_within_xlims["ion_id"] = (
            self.line_in_and_out_infos_within_xlims["atomic_number"] * 100 +
            self.line_in_and_out_infos_within_xlims["ion_number"])

        # this is a list that will hold which elements should all be in the
        # same colour. This is used if the user requests a mix of ions and
        # elements.
        self.keep_colour = []
        # this reads in the species specified by user and generates the 4-digit
        # ID keys for them
        if self._species_list is not None:
            # create a list of the ions ids requested by species_list
            requested_species_ids = []
            # check if there are any digits in the species list. If there are
            # then exit
            # species_list should only contain species in the Roman numeral
            # format, e.g. Si II, and each ion must contain a space
            if any(char.isdigit()
                   for char in " ".join(self._species_list)) == True:
                raise ValueError(
                    "All species must be in Roman numeral form, e.g. Si II")
            else:
                # go through each of the request species. Check whether it is
                # an element or ion (ions have spaces). If it is an element,
                # add all possible ions to the ions list. Otherwise just add
                # the requested ion
                for species in self._species_list:
                    if " " in species:
                        requested_species_ids.append([
                            species_string_to_tuple(species)[0] * 100 +
                            species_string_to_tuple(species)[1]
                        ])
                    else:
                        atomic_number = elements.loc[elements['chem_symbol'] ==
                                                     species.lower(),
                                                     'atomic_no'].values[0]
                        requested_species_ids.append([
                            atomic_number * 100 + i
                            for i in np.arange(atomic_number)
                        ])
                        self.keep_colour.append(atomic_number)
                self.requested_species_ids = [
                    species_id for list in requested_species_ids
                    for species_id in list
                ]

        # now we are getting the list of unique values for 'ion_id' if we would
        # like to use species. Otherwise we get unique atomic numbers
        if self._species_list is not None:
            self._elements_in_kromer_plot = np.c_[np.unique(
                self.line_in_and_out_infos_within_xlims.ion_id.values,
                return_counts=True,
            )]
        else:
            self._elements_in_kromer_plot = np.c_[np.unique(
                self.line_in_and_out_infos_within_xlims.atomic_number.values,
                return_counts=True,
            )]

        return self._elements_in_kromer_plot
Example #8
0
def assemble_plasma(config, model, atom_data=None):
    """
    Create a BasePlasma instance from a Configuration object
    and a Radial1DModel.

    Parameters
    ----------
    config: ~io.config_reader.Configuration
    model: ~model.Radial1DModel
    atom_data: ~atomic.AtomData
        If None, an attempt will be made to read the atomic data
        from config.

    Returns
    -------
    : ~plasma.BasePlasma

    """
    # Convert the nlte species list to a proper format.
    nlte_species = [
        species_string_to_tuple(s) for s in config.plasma.nlte.species
    ]

    if atom_data is None:
        if 'atom_data' in config:
            if os.path.isabs(config.atom_data):
                atom_data_fname = config.atom_data
            else:
                atom_data_fname = os.path.join(config.config_dirname,
                                               config.atom_data)
        else:
            raise ValueError('No atom_data option found in the configuration.')

        logger.info('Reading Atomic Data from %s', atom_data_fname)

        try:
            atom_data = atomic.AtomData.from_hdf(atom_data_fname)
        except TypeError as e:
            print(
                e,
                'Error might be from the use of an old-format of the atomic database, \n'
                'please see https://github.com/tardis-sn/tardis-refdata/tree/master/atom_data'
                ',for the most recent version.')
            raise

    atom_data.prepare_atom_data(
        model.abundance.index,
        line_interaction_type=config.plasma.line_interaction_type,
        nlte_species=nlte_species)

    kwargs = dict(t_rad=model.t_radiative,
                  abundance=model.abundance,
                  density=model.density,
                  atomic_data=atom_data,
                  time_explosion=model.time_explosion,
                  w=model.dilution_factor,
                  link_t_rad_t_electron=0.9)

    plasma_modules = basic_inputs + basic_properties
    property_kwargs = {}
    if config.plasma.radiative_rates_type == 'blackbody':
        plasma_modules.append(JBluesBlackBody)
    elif config.plasma.radiative_rates_type == 'dilute-blackbody':
        plasma_modules.append(JBluesDiluteBlackBody)
    elif config.plasma.radiative_rates_type == 'detailed':
        plasma_modules += detailed_j_blues_properties + detailed_j_blues_inputs
        kwargs.update(r_inner=model.r_inner,
                      t_inner=model.t_inner,
                      volume=model.volume,
                      j_blue_estimator=None)
        property_kwargs[JBluesDetailed] = {
            'w_epsilon': config.plasma.w_epsilon
        }
    else:
        raise ValueError('radiative_rates_type type unknown - %s',
                         config.plasma.radiative_rates_type)

    if config.plasma.excitation == 'lte':
        plasma_modules += lte_excitation_properties
    elif config.plasma.excitation == 'dilute-lte':
        plasma_modules += dilute_lte_excitation_properties

    if config.plasma.ionization == 'lte':
        plasma_modules += lte_ionization_properties
    elif config.plasma.ionization == 'nebular':
        plasma_modules += nebular_ionization_properties

    if nlte_species:
        plasma_modules += nlte_properties
        nlte_conf = config.plasma.nlte
        plasma_modules.append(LevelBoltzmannFactorNLTE.from_config(nlte_conf))
        property_kwargs[StimulatedEmissionFactor] = dict(
            nlte_species=nlte_species)
    else:
        plasma_modules += non_nlte_properties

    if config.plasma.line_interaction_type in ('downbranch', 'macroatom'):
        plasma_modules += macro_atom_properties

    if 'delta_treatment' in config.plasma:
        property_kwargs[RadiationFieldCorrection] = dict(
            delta_treatment=config.plasma.delta_treatment)

    if config.plasma.helium_treatment == 'recomb-nlte':
        plasma_modules += helium_nlte_properties
    elif config.plasma.helium_treatment == 'numerical-nlte':
        plasma_modules += helium_numerical_nlte_properties
        # TODO: See issue #633
        if config.plasma.heating_rate_data_file in ['none', None]:
            raise PlasmaConfigError('Heating rate data file not specified')
        else:
            property_kwargs[HeliumNumericalNLTE] = dict(
                heating_rate_data_file=config.plasma.heating_rate_data_file)
    else:
        plasma_modules += helium_lte_properties

    if model._electron_densities:
        electron_densities = pd.Series(model._electron_densities.cgs.value)
        if config.plasma.helium_treatment == 'numerical-nlte':
            property_kwargs[IonNumberDensityHeNLTE] = dict(
                electron_densities=electron_densities)
        else:
            property_kwargs[IonNumberDensity] = dict(
                electron_densities=electron_densities)

    kwargs['helium_treatment'] = config.plasma.helium_treatment

    plasma = BasePlasma(plasma_properties=plasma_modules,
                        property_kwargs=property_kwargs,
                        **kwargs)

    return plasma