diff --git a/docs/plasma_doc/plasma_plots/lte_ionization_balance.py b/docs/plasma_doc/plasma_plots/lte_ionization_balance.py index 7d117dd3181..81f713a70e1 100644 --- a/docs/plasma_doc/plasma_plots/lte_ionization_balance.py +++ b/docs/plasma_doc/plasma_plots/lte_ionization_balance.py @@ -2,7 +2,7 @@ from tardis import atomic, plasma, model_radial_oned atom_data = atomic.AtomData.from_hdf5() -number_density = model_radial_oned.calculate_atom_number_densities(atom_data, {'Si':1}, density=1e-13) +number_density = model_radial_oned.calculate_atom_number_densities(atom_data, {'Si': 1}, density=1e-13) atom_data.prepare_atom_data(set(number_density.index.values.astype(int)), max_ion_number=3) lte_plasma = plasma.LTEPlasma(number_density, atom_data, max_ion_number=3) siI = [] @@ -13,7 +13,7 @@ for t_rad in t_rads: lte_plasma.update_radiationfield(t_rad) si_number_density = number_density.get_value(14, 'number_density') - ion_density = lte_plasma.ion_number_density / si_number_density + ion_density = lte_plasma.ion_populations / si_number_density siI.append(ion_density.get_value((14, 0))) siII.append(ion_density.get_value((14, 1))) siIII.append(ion_density.get_value((14, 2))) diff --git a/docs/plasma_doc/plasma_plots/nebular_ionization_balance_w.5.py b/docs/plasma_doc/plasma_plots/nebular_ionization_balance_w.5.py index a58cb17e2b9..aa3216d4e02 100644 --- a/docs/plasma_doc/plasma_plots/nebular_ionization_balance_w.5.py +++ b/docs/plasma_doc/plasma_plots/nebular_ionization_balance_w.5.py @@ -6,7 +6,7 @@ atom_fname = os.path.expanduser('~/.tardis/kurucz_atom.h5') atom_data = atomic.AtomData.from_hdf5(atom_fname, use_zeta_data=True) -number_density = model_radial_oned.calculate_atom_number_densities(atom_data, {'Si':1}, density=1e-13) +number_density = model_radial_oned.calculate_atom_number_densities(atom_data, {'Si': 1}, density=1e-13) atom_data.prepare_atom_data(set(number_density.index.values.astype(int)), max_ion_number=3) nebular_plasma = plasma.NebularPlasma(number_density, atom_data, max_ion_number=3) siI = [] @@ -17,7 +17,7 @@ for t_rad in t_rads: nebular_plasma.update_radiationfield(t_rad, w=.5) si_number_density = number_density.get_value(14, 'number_density') - ion_density = nebular_plasma.ion_number_density / si_number_density + ion_density = nebular_plasma.ion_populations / si_number_density siI.append(ion_density.get_value((14, 0))) siII.append(ion_density.get_value((14, 1))) siIII.append(ion_density.get_value((14, 2))) diff --git a/docs/plasma_doc/plasma_plots/nebular_ionization_balance_w1.py b/docs/plasma_doc/plasma_plots/nebular_ionization_balance_w1.py index de6fcacac33..3d30cc88688 100644 --- a/docs/plasma_doc/plasma_plots/nebular_ionization_balance_w1.py +++ b/docs/plasma_doc/plasma_plots/nebular_ionization_balance_w1.py @@ -6,7 +6,7 @@ atom_fname = os.path.expanduser('~/.tardis/kurucz_atom.h5') atom_data = atomic.AtomData.from_hdf5(atom_fname, use_zeta_data=True) -number_density = model_radial_oned.calculate_atom_number_densities(atom_data, {'Si':1}, density=1e-13) +number_density = model_radial_oned.calculate_atom_number_densities(atom_data, {'Si': 1}, density=1e-13) atom_data.prepare_atom_data(set(number_density.index.values.astype(int)), max_ion_number=3) nebular_plasma = plasma.NebularPlasma(number_density, atom_data, max_ion_number=3) siI = [] @@ -17,7 +17,7 @@ for t_rad in t_rads: nebular_plasma.update_radiationfield(t_rad, w=1.) si_number_density = number_density.get_value(14, 'number_density') - ion_density = nebular_plasma.ion_number_density / si_number_density + ion_density = nebular_plasma.ion_populations / si_number_density siI.append(ion_density.get_value((14, 0))) siII.append(ion_density.get_value((14, 1))) siIII.append(ion_density.get_value((14, 2))) diff --git a/tardis/atomic.py b/tardis/atomic.py index dcdfbd0e74a..907c4c61216 100644 --- a/tardis/atomic.py +++ b/tardis/atomic.py @@ -7,6 +7,8 @@ import os import h5py +import pdb + from astropy import table, units, constants from collections import OrderedDict @@ -525,52 +527,85 @@ def prepare_atom_data(self, selected_atomic_numbers, line_interaction_type='scat self.macro_atom_data['destination_level_idx'] = (np.ones(len(self.macro_atom_data)) * -1).astype( np.int64) - #Setting NLTE species - self.set_nlte_mask(nlte_species) - - - def set_nlte_mask(self, nlte_species): - - logger.debug('Setting NLTE Species Mask for %s' % nlte_species) - self.nlte_mask = np.zeros(self.levels.shape[0]).astype(bool) - - for species in nlte_species: - current_mask = (self.levels.index.get_level_values(0) == species[0]) & \ - (self.levels.index.get_level_values(1) == species[1]) - - self.nlte_mask |= current_mask - - def prepare_nlte_indices(self, nlte_species): - pass - - def get_collision_coefficients(self, atomic_number, ion_number, level_number_lower, level_number_upper, t_electron): - if self.has_collision_data: - try: - C_lus = self.collision_data.ix[ - (atomic_number, ion_number, level_number_lower, level_number_upper)].values[1:] - C_lu = np.interp(t_electron, self.collision_data_temperatures, C_lus) - C_ul = C_lu * self.collision_data.ix[ - (atomic_number, ion_number, level_number_lower, level_number_upper)].values[0] - - except pd.core.indexing.IndexingError: - C_lu = 0 - C_ul = 0 - logger.debug('Could not find collision data for atom=%d ion=%d lvl_lower=%d lvl_upper=%d', - atomic_number, ion_number, level_number_lower, level_number_upper) - else: - logger.debug('Found collision data for atom=%d ion=%d lvl_lower=%d lvl_upper=%d', - atomic_number, ion_number, level_number_lower, level_number_upper) - - return C_lu, C_ul + self.nlte_data = NLTEData(self, nlte_species) - else: - return 0., 0. def __repr__(self): return "" % \ (self.uuid1, self.md5, self.lines_data.atomic_number.count(), self.levels_data.energy.count()) +class NLTEData(object): + def __init__(self, atom_data, nlte_species): + logger.info('Preparing the NLTE data') + self.atom_data = atom_data + self.lines = atom_data.lines.reset_index() + self.nlte_species = nlte_species + self._init_indices() + self._create_nlte_mask() + if atom_data.has_collision_data: + self._create_collision_coefficient_matrix() + + + def _init_indices(self): + self.lines_idx = {} + self.lines_level_number_lower = {} + self.lines_level_number_upper = {} + self.A_uls = {} + self.B_uls = {} + self.B_lus = {} + + for species in self.nlte_species: + lines_idx = np.where((self.lines.atomic_number == species[0]) & + (self.lines.ion_number == species[1])) + self.lines_idx[species] = lines_idx + self.lines_level_number_lower[species] = self.lines.level_number_lower.values[lines_idx].astype(int) + self.lines_level_number_upper[species] = self.lines.level_number_upper.values[lines_idx].astype(int) + + self.A_uls[species] = self.atom_data.lines.A_ul.values[lines_idx] + self.B_uls[species] = self.atom_data.lines.B_ul.values[lines_idx] + self.B_lus[species] = self.atom_data.lines.B_lu.values[lines_idx] + + def _create_nlte_mask(self): + self.nlte_mask = np.zeros(self.atom_data.levels.energy.count()).astype(bool) + + for species in self.nlte_species: + current_mask = (self.atom_data.levels.index.get_level_values(0) == species[0]) & \ + (self.atom_data.levels.index.get_level_values(1) == species[1]) + + self.nlte_mask |= current_mask + def _create_collision_coefficient_matrix(self): + self.C_ul_interpolator = {} + self.delta_E_matrices = {} + self.g_ratio_matrices = {} + collision_group = self.atom_data.collision_data.groupby(level=['atomic_number', 'ion_number']) + for species in self.nlte_species: + no_of_levels = self.atom_data.levels.ix[species].energy.count() + C_ul_matrix = np.zeros((no_of_levels, no_of_levels, len(self.atom_data.collision_data_temperatures))) + delta_E_matrix = np.zeros((no_of_levels, no_of_levels)) + g_ratio_matrix = np.zeros((no_of_levels, no_of_levels)) + + for (atomic_number, ion_number, level_number_lower, level_number_upper), line in \ + collision_group.get_group(species).iterrows(): + C_ul_matrix[level_number_lower, level_number_upper, :] = line.values[2:] + delta_E_matrix[level_number_lower, level_number_upper] = line['delta_e'] + #TODO TARDISATOMIC fix change the g_ratio to be the otherway round - I flip them now here. + g_ratio_matrix[level_number_lower, level_number_upper] = line['g_ratio'] + self.C_ul_interpolator[species] = interpolate.interp1d(self.atom_data.collision_data_temperatures, + C_ul_matrix) + self.delta_E_matrices[species] = delta_E_matrix + + self.g_ratio_matrices[species] = g_ratio_matrix + + + def get_collision_matrix(self, species, t_electron): + c_ul_matrix = self.C_ul_interpolator[species](t_electron) + + c_ul_matrix[np.isnan(c_ul_matrix)] = 0.0 + #TODO in tardisatomic the g_ratio is the other way round - here I'll flip it in prepare_collision matrix + c_lu_matrix = c_ul_matrix * np.exp(-self.delta_E_matrices[species] / t_electron) * self.g_ratio_matrices[ + species] + return c_ul_matrix + c_lu_matrix.transpose() diff --git a/tardis/config_reader.py b/tardis/config_reader.py index b1576ad5bcb..eefc2856146 100644 --- a/tardis/config_reader.py +++ b/tardis/config_reader.py @@ -519,7 +519,13 @@ def from_yaml(cls, fname, args=None): logger.warn('"w_epsilon" not specified in plasma section - setting it to 1e-10') config_dict['w_epsilon'] = 1e-10 - ##### spectrum section ###### + ##### NLTE Section ##### + + config_dict['nlte_options'] = yaml_dict.pop('nlte', {}) + + + + ##### spectrum section ###### spectrum_section = yaml_dict.pop('spectrum') spectrum_start = parse2quantity(spectrum_section['start']).to('angstrom', units.spectral()) spectrum_end = parse2quantity(spectrum_section['end']).to('angstrom', units.spectral()) diff --git a/tardis/data/synpp_default.yaml b/tardis/data/synpp_default.yaml new file mode 100644 index 00000000000..e705d7122fe --- /dev/null +++ b/tardis/data/synpp_default.yaml @@ -0,0 +1,37 @@ +#- +#- Need to document... +#- +--- +output : + min_wl : 100.0 # min. wavelength in AA + max_wl : 10000.0 # max. wavelength in AA + wl_step : 5.0 # wavelength spacing in AA +grid : + bin_width : 0.3 # opacity bin size in kkm/s + v_size : 20 # size of line-forming region grid + v_outer_max : 30.0 # fastest ejecta velocity in kkm/s +opacity : + line_dir : /Users/wkerzend/software/synapps-linedb/es-data/lines # path to atomic line data + ref_file : /Users/wkerzend/software/synapps-linedb/es-data/refs.dat # path to ref. line data + form : exp # parameterization (only exp for now) + v_ref : 10.0 # reference velocity for parameterization + log_tau_min : -5.0 # opacity threshold +source : + mu_size : 10 # number of angles for source integration +spectrum : + p_size : 60 # number of phot. impact parameters for spectrum + flatten : No # divide out continuum or not +setups : + - a0 : 1.0 # constant term + a1 : 0.0 # linear warp term + a2 : 0.0 # quadratic warp term + v_phot : 10.0 # velocity at photosphere (kkm/s) + v_outer : 30.0 # outer velocity of line forming region (kkm/s) + t_phot : 10.0 # blackbody photosphere temperature (kK) + ions : [ 1400, 1401, 1402, 1403 ] # ions (100*Z+I, I=0 is neutral) + active : [ Yes, Yes, Yes, Yes ] # actually use the ion or not + log_tau : [-99, -99 , -99, -99] # ref. line opacity at v_ref + v_min : [ 10.0, 10.0, 10.0, 10.0 ] # lower cutoff (kkm/s) + v_max : [ 30.0, 30.0, 30.0, 30.0 ] # upper cutoff (kkm/s) + aux : [ 1e300 ,1e300, 1e300, 1e300 ] # e-folding for exp form + temp : [ 10.0, 10.0, 10.0, 10.0 ] # Boltzmann exc. temp. (kK) diff --git a/tardis/model_radial_oned.py b/tardis/model_radial_oned.py index 6d5eafe9356..d4bf4fab0f8 100644 --- a/tardis/model_radial_oned.py +++ b/tardis/model_radial_oned.py @@ -22,7 +22,7 @@ w_estimator_constant = (c ** 2 / (2 * h)) * (15 / np.pi ** 4) * (h / kb) ** 4 / (4 * np.pi) -synpp_default_yaml = os.path.join(os.path.dirname(__file__), 'data', 'synpp_default.yaml') +synpp_default_yaml_fname = os.path.join(os.path.dirname(__file__), 'data', 'synpp_default.yaml') class Radial1DModel(object): @@ -120,13 +120,13 @@ def __init__(self, tardis_config): self.plasma_type = tardis_config.plasma_type self.radiative_rates_type = tardis_config.radiative_rates_type if self.plasma_type == 'lte': - self.plasma_class = plasma.LTEPlasma - if tardis_config.ws is not None: + plasma_class = plasma.LTEPlasma + if hasattr(tardis_config, 'ws') and tardis_config.ws is not None: raise ValueError( "the dilution factor W ('ws') can only be specified when selecting plasma_type='nebular'") elif self.plasma_type == 'nebular': - self.plasma_class = plasma.NebularPlasma + plasma_class = plasma.NebularPlasma if not self.atom_data.has_zeta_data: raise ValueError("Requiring Recombination coefficients Zeta for 'nebular' plasma_type") else: @@ -146,7 +146,10 @@ def __init__(self, tardis_config): #setting dilution factors - self.ws = 0.5 * (1 - np.sqrt(1 - self.r_inner[0] ** 2 / self.r_middle ** 2)) + if self.plasma_type == 'lte': + self.ws = np.ones_like(self.r_middle) + else: + self.ws = 0.5 * (1 - np.sqrt(1 - self.r_inner[0] ** 2 / self.r_middle ** 2)) #initializing temperatures @@ -156,7 +159,7 @@ def __init__(self, tardis_config): assert len(tardis_config.initial_t_rad) == self.no_of_shells self.t_rads = np.array(tardis_config.initial_t_rad, dtype=np.float64) - self.initialize_plasmas() + self.initialize_plasmas(plasma_class) @property @@ -181,7 +184,8 @@ def line_interaction_type(self, value): self._line_interaction_type = value #final preparation for atom_data object - currently building data self.atom_data.prepare_atom_data(self.selected_atomic_numbers, - line_interaction_type=self.line_interaction_type, max_ion_number=None) + line_interaction_type=self.line_interaction_type, max_ion_number=None, + nlte_species=self.tardis_config.nlte_species) @property @@ -203,54 +207,40 @@ def create_packets(self): no_of_packets = self.current_no_of_packets self.packet_src.create_packets(no_of_packets, self.t_inner) - def initialize_plasmas(self): + def initialize_plasmas(self, plasma_class): self.plasmas = [] self.tau_sobolevs = np.zeros((self.no_of_shells, len(self.atom_data.lines))) self.line_list_nu = self.atom_data.lines['nu'] if self.line_interaction_id in (1, 2): + if self.line_interaction_id == 1: + logger.info('Downbranch selected - creating transition probabilities') + else: + logger.info('Macroatom selected - creating transition probabilties') self.transition_probabilities = [] + else: + logger.info('Scattering selected - no transition probabilities created') - if self.plasma_type == 'lte': - for i, ((tmp_index, current_abundances), current_t_rad) in \ - enumerate(zip(self.number_densities.iterrows(), self.t_rads)): - current_plasma = self.plasma_class(current_abundances, self.atom_data, self.time_explosion, - nlte_species=self.tardis_config.nlte_species, zone_id=i) - logger.debug('Initializing Shell %d Plasma with T=%.3f' % (i, current_t_rad)) - if self.radiative_rates_type in ('lte', 'detailed'): - j_blues = plasma.intensity_black_body(self.atom_data.lines.nu.values, current_t_rad) - current_plasma.set_j_blues(j_blues) - else: - raise ValueError('For the current plasma_type (%s) the radiative_rates_type can only' - ' be "lte" or "detailed"' % (self.plasma_type)) + for i, ((tmp_index, number_density), current_t_rad, current_w) in \ + enumerate(zip(self.number_densities.iterrows(), self.t_rads, self.ws)): - current_plasma.set_j_blues(j_blues) - current_plasma.update_radiationfield(current_t_rad) - self.tau_sobolevs[i] = current_plasma.tau_sobolevs + logger.debug('Initializing Shell %d Plasma with T=%.3f W=%.4f' % (i, current_t_rad, current_w)) + if self.radiative_rates_type in ('lte',): + j_blues = plasma.intensity_black_body(self.atom_data.lines.nu.values, current_t_rad) + elif self.radiative_rates_type in ('nebular', 'detailed'): + j_blues = current_w * plasma.intensity_black_body(self.atom_data.lines.nu.values, current_t_rad) + else: + raise ValueError('For the current plasma_type (%s) the radiative_rates_type can only' + ' be "lte" or "detailed" or "nebular"' % (self.plasma_type)) - self.plasmas.append(current_plasma) + current_plasma = plasma_class(t_rad=current_t_rad, w=current_w, number_density=number_density, + atom_data=self.atom_data, time_explosion=self.time_explosion, + nlte_species=self.tardis_config.nlte_species, + nlte_options=self.tardis_config.nlte_options, zone_id=i, j_blues=j_blues) + self.tau_sobolevs[i] = current_plasma.tau_sobolevs - elif self.plasma_type == 'nebular': - for i, ((tmp_index, current_abundances), current_t_rad, current_w) in \ - enumerate(zip(self.number_densities.iterrows(), self.t_rads, self.ws)): - current_plasma = self.plasma_class(current_abundances, self.atom_data, self.time_explosion, - nlte_species=self.tardis_config.nlte_species, zone_id=i) - logger.debug('Initializing Shell %d Plasma with T=%.3f W=%.4f' % (i, current_t_rad, current_w)) - if self.radiative_rates_type in ('lte',): - j_blues = plasma.intensity_black_body(self.atom_data.lines.nu.values, current_t_rad) - elif self.radiative_rates_type in ('nebular', 'detailed'): - j_blues = current_w * plasma.intensity_black_body(self.atom_data.lines.nu.values, current_t_rad) - else: - raise ValueError('For the current plasma_type (%s) the radiative_rates_type can only' - ' be "lte" or "detailed" or "nebular"' % (self.plasma_type)) - - current_plasma.set_j_blues(j_blues) - current_plasma.update_radiationfield(current_t_rad, current_w) - - self.tau_sobolevs[i] = current_plasma.tau_sobolevs - - self.plasmas.append(current_plasma) + self.plasmas.append(current_plasma) self.tau_sobolevs = np.array(self.tau_sobolevs, dtype=float) self.j_blues = np.zeros_like(self.tau_sobolevs) @@ -264,7 +254,7 @@ def calculate_transition_probabilities(self): self.transition_probabilities = [] for current_plasma in self.plasmas: - self.transition_probabilities.append(current_plasma.update_macro_atom().values) + self.transition_probabilities.append(current_plasma.calculate_transition_probabilities().values) self.transition_probabilities = np.array(self.transition_probabilities, dtype=np.float64) @@ -307,40 +297,24 @@ def normalize_j_blues(self): i].t_rad) def update_plasmas(self): - if self.plasma_type == 'lte': - for i, (current_plasma, new_trad) in enumerate(zip(self.plasmas, self.t_rads)): - logger.debug('Updating Shell %d Plasma with T=%.3f' % (i, new_trad)) - if self.radiative_rates_type == 'lte': - j_blues = plasma.intensity_black_body(self.atom_data.lines.nu.values, new_trad) - elif self.radiative_rates_type == 'detailed': - j_blues = self.j_blues[i] - else: - raise ValueError('For the current plasma_type (%s) the radiative_rates_type can only' - ' be "lte" or "detailed"' % (self.plasma_type)) - - current_plasma.set_j_blues(j_blues) - - current_plasma.update_radiationfield(new_trad) - self.tau_sobolevs[i] = current_plasma.tau_sobolevs - - elif self.plasma_type == 'nebular': - for i, (current_plasma, new_trad, new_ws) in enumerate(zip(self.plasmas, self.t_rads, self.ws)): - logger.debug('Updating Shell %d Plasma with T=%.3f W=%.4f' % (i, new_trad, new_ws)) - if self.radiative_rates_type == 'lte': - j_blues = plasma.intensity_black_body(self.atom_data.lines.nu.values, new_trad) - elif self.radiative_rates_type == 'nebular': - j_blues = new_ws * plasma.intensity_black_body(self.atom_data.lines.nu.values, new_trad) - - elif self.radiative_rates_type == 'detailed': - j_blues = self.j_blues[i] - else: - raise ValueError('For the current plasma_type (%s) the radiative_rates_type can only' - ' be "lte" or "detailed" or "nebular"' % (self.plasma_type)) - - current_plasma.set_j_blues(j_blues) + for i, (current_plasma, new_trad, new_ws) in enumerate(zip(self.plasmas, self.t_rads, self.ws)): + logger.debug('Updating Shell %d Plasma with T=%.3f W=%.4f' % (i, new_trad, new_ws)) + if self.radiative_rates_type == 'lte': + j_blues = plasma.intensity_black_body(self.atom_data.lines.nu.values, new_trad) + elif self.radiative_rates_type == 'nebular': + j_blues = new_ws * plasma.intensity_black_body(self.atom_data.lines.nu.values, new_trad) + + elif self.radiative_rates_type == 'detailed': + j_blues = self.j_blues[i] + else: + raise ValueError('For the current plasma_type (%s) the radiative_rates_type can only' + ' be "lte" or "detailed" or "nebular"' % (self.plasma_type)) - current_plasma.update_radiationfield(new_trad, new_ws) - self.tau_sobolevs[i] = current_plasma.tau_sobolevs + current_plasma.set_j_blues(j_blues) + if self.plasma_type == 'lte': + new_ws = 1.0 + current_plasma.update_radiationfield(new_trad, w=new_ws) + self.tau_sobolevs[i] = current_plasma.tau_sobolevs if self.line_interaction_id in (1, 2): self.calculate_transition_probabilities() @@ -446,6 +420,7 @@ def update_radiationfield(self, update_mode='dampened', damping_constant=0.5, lo def create_synpp_yaml(self, fname, lines_db=None): + logger.warning('Currently only works with Si and a special setup') if not self.atom_data.has_synpp_refs: raise ValueError( 'The current atom dataset does not contain the necesarry reference files (please contact the authors)') @@ -461,14 +436,21 @@ def create_synpp_yaml(self, fname, lines_db=None): relevant_synpp_refs = self.atom_data.synpp_refs[self.atom_data.synpp_refs['ref_log_tau'] > -50] - yaml_reference = yaml.load(file(synpp_default_yaml)) + yaml_reference = yaml.load(file(synpp_default_yaml_fname)) if lines_db is not None: yaml_reference['opacity']['line_dir'] = os.path.join(lines_db, 'lines') yaml_reference['opacity']['line_dir'] = os.path.join(lines_db, 'refs.dat') - yaml_setup = yaml_reference['setups'][0] + yaml_reference['output']['min_wl'] = float(self.spec_angstrom.min()) + yaml_reference['output']['max_wl'] = float(self.spec_angstrom.max()) + + yaml_reference['opacity']['v_ref'] = float(self.v_inner[0] / 1e8) + yaml_reference['grid']['v_outer_max'] = float(self.v_outer[-1] / 1e8) + #pdb.set_trace() + + yaml_setup = yaml_reference['setups'][0] yaml_setup['ions'] = [] yaml_setup['log_tau'] = [] yaml_setup['active'] = [] @@ -486,7 +468,8 @@ def create_synpp_yaml(self, fname, lines_db=None): yaml_setup['v_max'].append(yaml_reference['grid']['v_outer_max']) yaml_setup['aux'].append(1e200) - yaml.dump(yaml_reference, file(fname, 'w')) + yaml.dump(yaml_reference, stream=file(fname, 'w'), explicit_start=True) + def plot_spectrum(self, ax=None, mode='wavelength', virtual=True): if ax is None: @@ -507,6 +490,10 @@ def plot_spectrum(self, ax=None, mode='wavelength', virtual=True): ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) + def save_spectrum(self, prefix): + np.savetxt(prefix + '_virtual_spec.dat', zip(self.spec_angstrom, self.spec_virtual_flux_angstrom)) + np.savetxt(prefix + '_spec.dat', zip(self.spec_angstrom, self.spec_flux_angstrom)) + class ModelHistory(object): """ @@ -516,25 +503,34 @@ class ModelHistory(object): def __init__(self, tardis_config): self.t_rads = pd.DataFrame(index=np.arange(tardis_config.no_of_shells)) self.ws = pd.DataFrame(index=np.arange(tardis_config.no_of_shells)) + self.electron_density = pd.DataFrame(index=np.arange(tardis_config.no_of_shells)) self.level_populations = {} self.j_blues = {} + self.tau_sobolevs = {} def store_all(self, radial1d_mdl, iteration): - self.t_rads['iter%d' % iteration] = radial1d_mdl.t_rads - self.ws['iter%d' % iteration] = radial1d_mdl.ws + self.t_rads['iter%03d' % iteration] = radial1d_mdl.t_rads + self.ws['iter%03d' % iteration] = radial1d_mdl.ws + self.electron_density['iter%03d' % iteration] = radial1d_mdl.electron_density + #current_ion_populations = pd.DataFrame(index=radial1d_mdl.atom_data.) current_level_populations = pd.DataFrame(index=radial1d_mdl.atom_data.levels.index) current_j_blues = pd.DataFrame(index=radial1d_mdl.atom_data.lines.index) + current_tau_sobolevs = pd.DataFrame(index=radial1d_mdl.atom_data.lines.index) for i, plasma in enumerate(radial1d_mdl.plasmas): current_level_populations[i] = plasma.level_populations current_j_blues[i] = plasma.j_blues + current_tau_sobolevs[i] = plasma.tau_sobolevs + + self.level_populations['iter%03d' % iteration] = current_level_populations.copy() + self.j_blues['iter%03d' % iteration] = current_j_blues.copy() + self.tau_sobolevs['iter%03d' % iteration] = current_tau_sobolevs.copy() - self.level_populations['iter%d' % iteration] = current_level_populations.copy() - self.j_blues['iter%d' % iteration] = current_j_blues.copy() def finalize(self): self.level_populations = pd.Panel.from_dict(self.level_populations) self.j_blues = pd.Panel.from_dict(self.j_blues) + self.tau_sobolevs = pd.Panel.from_dict(self.tau_sobolevs) diff --git a/tardis/montecarlo_multizone.pyx b/tardis/montecarlo_multizone.pyx index 40cee55d57d..f0b7ecb3256 100644 --- a/tardis/montecarlo_multizone.pyx +++ b/tardis/montecarlo_multizone.pyx @@ -374,7 +374,7 @@ cdef float_type_t move_packet(float_type_t*r, cdef void increment_j_blue_estimator(int_type_t*current_line_id, float_type_t*current_nu, float_type_t*current_energy, float_type_t*mu, float_type_t*r, float_type_t d_line, int_type_t j_blue_idx, StorageModel storage): - cdef float_type_t comov_energy, r_interaction, mu_interaction, distance, doppler_factor + cdef float_type_t comov_energy, comov_nu, r_interaction, mu_interaction, distance, doppler_factor distance = d_line @@ -384,6 +384,8 @@ cdef void increment_j_blue_estimator(int_type_t*current_line_id, float_type_t*cu doppler_factor = (1 - (mu_interaction * r_interaction * storage.inverse_time_explosion * inverse_c)) comov_energy = current_energy[0] * doppler_factor + comov_nu = current_nu[0] * doppler_factor + storage.line_lists_j_blues[j_blue_idx] += (comov_energy / current_nu[0]) #print "incrementing j_blues = %g" % storage.line_lists_j_blues[j_blue_idx] @@ -522,6 +524,7 @@ def montecarlo_radial1d(model, int_type_t virtual_packet_flag=0): #linelists current_line_id = binary_search(storage.line_list_nu, comov_current_nu, 0, storage.no_of_lines) + if current_line_id == storage.no_of_lines: #setting flag that the packet is off the red end of the line list last_line = 1 diff --git a/tardis/plasma.py b/tardis/plasma.py index 450451f9547..a3a6f4fee31 100644 --- a/tardis/plasma.py +++ b/tardis/plasma.py @@ -1,20 +1,18 @@ #Calculations of the Plasma conditions -#import constants + import numpy as np import logging -from astropy import table, units, constants -from collections import OrderedDict - -from pandas import DataFrame, Series, Index, lib as pdlib +from astropy import constants import pandas as pd - import macro_atom +import pdb +from .config_reader import reformat_element_symbol logger = logging.getLogger(__name__) -import pdb -#Bnu = lambda nu, t: (2 * constants.h * nu ** 3 / constants.c ** 2) * np.exp( -# 1 / ((constants.h * nu) / (constants.kb * t))) + + + #Defining soboleve constant @@ -65,71 +63,113 @@ class BasePlasma(object): maximum used ionization of atom used in the calculation (inclusive the number) """ - #TODO make density a astropy.quantity - def __init__(self, abundances, atom_data, time_explosion, density_unit='g/cm^3', max_ion_number=None, - use_macro_atom=False, zone_id=None): - self.atom_data = atom_data - # self.selected_atoms = self.atom_data.selected_atoms - # self.selected_atomic_numbers = self.atom_data.selected_atomic_numbers - self.abundances = abundances - self.initialize = True - self.zone_id = zone_id - self.time_explosion = time_explosion + @classmethod + def from_abundance(cls, t_rad, w, abundance, density, atom_data, time_explosion, j_blues=None, t_electron=None, + use_macro_atom=False, nlte_species=[], nlte_options={}, zone_id=None, saha_treatment='lte'): + """ + :param cls: + :param t_rad: + :param w: + :param abundance: + :param density: + :param atom_data: + :param time_explosion: + :param t_electron: + :param use_macro_atom: + :param zone_id: + :param nlte_species: + :return: + """ - def validate_atom_data(self): - required_attributes = ['lines', 'levels'] - for attribute in required_attributes: - if not hasattr(self.atom_data, attribute): - raise ValueError('AtomData incomplete missing') + number_density = pd.Series(index=np.arange(1, 120)) + for symbol in abundance: + element_symbol = reformat_element_symbol(symbol) + if element_symbol not in atom_data.symbol2atomic_number: + raise ValueError('Element %s provided in config unknown' % element_symbol) + z = atom_data.symbol2atomic_number[element_symbol] - def update_radiationfield(self, t_rad): - """ - This functions updates the radiation temperature `t_rad` and calculates the beta_rad - Parameters - ---------- - t_rad : float + number_density.ix[z] = abundance[symbol] + number_density = number_density[~number_density.isnull()] - """ - self.t_rad = t_rad - self.beta_rad = 1 / (constants.k_B.cgs.value * t_rad) + abundance_sum = number_density.sum() + if abs(abundance_sum - 1.) > 0.02: + logger.warning('Abundances do not sum up to 1 (%g)- normalizing', abundance_sum) -class LTEPlasma(BasePlasma): - """ - Model for BasePlasma using a local thermodynamic equilibrium approximation. + number_density /= abundance_sum - Parameters - ---------- + number_density *= density + number_density /= atom_data.atom_data.mass[number_density.index] - abundances : `~dict` - A dictionary with the abundances for each element + return cls(t_rad=t_rad, w=w, number_density=number_density, atom_data=atom_data, j_blues=j_blues, + time_explosion=time_explosion, t_electron=t_electron, use_macro_atom=use_macro_atom, zone_id=zone_id, + nlte_species=nlte_species, nlte_options=nlte_options, saha_treatment=saha_treatment) - t_rad : `~float` - Temperature in Kelvin for the plasma - density : `float` - density in g/cm^3 + def __init__(self, t_rad, w, number_density, atom_data, time_explosion, j_blues=None, t_electron=None, + use_macro_atom=False, nlte_species=[], nlte_options={}, zone_id=None, saha_treatment='lte'): + self.number_density = number_density + self.electron_density = self.number_density.sum() - .. warning:: - Instead of g/cm^ will later use the keyword `density_unit` as unit + if saha_treatment == 'lte': + self.calculate_saha = self.calculate_saha_lte + elif saha_treatment == 'nebular': + self.calculate_saha = self.calculate_saha_nebular + else: + raise ValueError('keyword "saha_treatment" can only be "lte" or "nebular" - %s chosen' % saha_treatment) - atom_data : `~tardis.atomic.AtomData`-object + self.atom_data = atom_data + self.initialize = True + self.t_rad = t_rad + self.w = w + self.t_electron = t_electron - """ + self.set_j_blues(j_blues) - def __init__(self, abundances, atom_data, time_explosion, max_ion_number=None, - use_macro_atom=False, nlte_species=[], zone_id=None): - BasePlasma.__init__(self, abundances, atom_data, time_explosion, - max_ion_number=max_ion_number, use_macro_atom=use_macro_atom, zone_id=zone_id) + self.time_explosion = time_explosion - self.ion_number_density = None self.nlte_species = nlte_species + self.nlte_options = nlte_options + self.zone_id = zone_id + + self.update_radiationfield(self.t_rad, self.w) + #Properties - def update_radiationfield(self, t_rad, t_electron=None, n_e_convergence_threshold=0.05): + @property + def t_rad(self): + return self._t_rad + + @t_rad.setter + def t_rad(self, value): + self._t_rad = value + self.beta_rad = 1 / (constants.k_B.cgs.value * self._t_rad) + self.ge = ((2 * np.pi * constants.m_e.cgs.value / self.beta_rad) / (constants.h.cgs.value ** 2)) ** 1.5 + + @property + def t_electron(self): + if self._t_electron is None: + return self.t_rad * self.link_t_rad_electron + else: + return self._t_electron + + @t_electron.setter + def t_electron(self, value): + if value is None: + self.link_t_rad_electron = 0.9 + self._t_electron = None + else: + self._t_electron = value + + self.beta_electron = 1 / (constants.k_B.cgs.value * self.t_electron) + + + #Functions + + def update_radiationfield(self, t_rad, w, n_e_convergence_threshold=0.05): """ This functions updates the radiation temperature `t_rad` and calculates the beta_rad Parameters. Then calculating :math:`g_e=\\left(\\frac{2 \\pi m_e k_\\textrm{B}T}{h^2}\\right)^{3/2}`. @@ -145,50 +185,47 @@ def update_radiationfield(self, t_rad, t_electron=None, n_e_convergence_threshol ionization balance. """ - BasePlasma.update_radiationfield(self, t_rad) - if t_electron is None: - self.t_electron = t_rad * 0.9 - else: - self.t_electron = t_electron + self.t_rad = t_rad + self.w = w + + self.calculate_partition_functions() - self.calculate_partition_functions(initialize=self.initialize) - self.ge = ((2 * np.pi * constants.m_e.cgs.value / self.beta_rad) / (constants.h.cgs.value ** 2)) ** 1.5 #Calculate the Saha ionization balance fractions phis = self.calculate_saha() #initialize electron density with the sum of number densities - electron_density = self.abundances.sum() - n_e_iterations = 0 while True: - self.calculate_ionization_balance(phis, electron_density) - ion_numbers = np.array([item[1] for item in self.ion_number_density.index]) - new_electron_density = np.sum(self.ion_number_density.values * ion_numbers) + self.calculate_ion_populations(phis) + ion_numbers = np.array([item[1] for item in self.ion_populations.index]) + new_electron_density = np.sum(self.ion_populations.values * ion_numbers) n_e_iterations += 1 - if abs(new_electron_density - electron_density) / electron_density < n_e_convergence_threshold: break - electron_density = 0.5 * (new_electron_density + electron_density) + if abs( + new_electron_density - self.electron_density) / self.electron_density < n_e_convergence_threshold: break + self.electron_density = 0.5 * (new_electron_density + self.electron_density) self.electron_density = new_electron_density logger.debug('Took %d iterations to converge on electron density' % n_e_iterations) self.calculate_level_populations() self.calculate_tau_sobolev() - self.calculate_nlte_level_populations() + if self.nlte_species != []: + self.calculate_nlte_level_populations() if self.initialize: self.initialize = False - def set_j_blues(self, j_blues=None): - if j_blues is None: - self.j_blues = intensity_black_body(self.atom_data.lines['nu'].values, self.t_rad) - else: - self.j_blues = j_blues + def validate_atom_data(self): + required_attributes = ['lines', 'levels'] + for attribute in required_attributes: + if not hasattr(self.atom_data, attribute): + raise ValueError('AtomData incomplete missing') - def calculate_partition_functions(self, initialize=False): + def calculate_partition_functions(self): """ Calculate partition functions for the ions using the following formula, where :math:`i` is the atomic_number, :math:`j` is the ion_number and :math:`k` is the level number. @@ -222,8 +259,8 @@ def group_calculate_partition_function(group): self.partition_functions = self.atom_data.levels.groupby(level=['atomic_number', 'ion_number']).apply( group_calculate_partition_function) - self.atom_data.atom_ion_index = Series(np.arange(len(self.partition_functions)), - self.partition_functions.index) + self.atom_data.atom_ion_index = pd.Series(np.arange(len(self.partition_functions)), + self.partition_functions.index) self.atom_data.levels_index2atom_ion_index = self.atom_data.atom_ion_index.ix[ self.atom_data.levels.index.droplevel(2)].values else: @@ -239,8 +276,7 @@ def group_calculate_partition_function(group): else: self.partition_functions.ix[species] = np.sum(group['g'] * np.exp(-group['energy'] * self.beta_rad)) - - def calculate_saha(self): + def calculate_saha_lte(self): """ Calculating the ionization equilibrium using the Saha equation, where i is atomic number, j is the ion_number, :math:`n_e` is the electron density, :math:`Z_{i, j}` are the partition functions @@ -260,154 +296,183 @@ def calculate_phis(group): phis = self.partition_functions.groupby(level='atomic_number').apply(calculate_phis) - phis = Series(phis.values, phis.index.droplevel(0)) + phis = pd.Series(phis.values, phis.index.droplevel(0)) phis *= self.ge * np.exp(-self.beta_rad * self.atom_data.ionization_data.ix[phis.index]['ionization_energy']) return phis - - def calculate_ionization_balance(self, phis, electron_density): + def calculate_saha_nebular(self): """ - Calculate the ionization balance + Calculating the ionization equilibrium using the Saha equation, where i is atomic number, + j is the ion_number, :math:`n_e` is the electron density, :math:`Z_{i, j}` are the partition functions + and :math:`\chi` is the ionization energy. For the `NebularPlasma` we first calculate the + ionization balance assuming LTE conditions (:math:`\\Phi_{i, j}(\\textrm{LTE})`) and use factors to more accurately + describe the plasma. The two important factors are :math:`\\zeta` - a correction factor to take into account + ionizations from excited states. The second factor is :math:`\\delta` , adjusting the ionization balance for the fact that + there's more line blanketing in the blue. - .. math:: - N(X) = N_1 + N_2 + N_3 + \\dots + The :math:`\\zeta` factor for different temperatures is read in to the `~tardis.atomic.NebularAtomData` and then + interpolated for the current temperature. - N(X) = (N_2/N_1) \\times N_1 + (N3/N2) \\times (N_2/N_1) \\times N_1 + \\dots + The :math:`\\delta` factor is calculated with :method:`calculate_radiation_field_correction`. - N(X) = N_1(1 + N_2/N_1 + (N_3/N_2) \\times (N_2/N_1) + \\dots + Finally the ionization balance is adjusted (as equation 14 in :cite:`1993A&A...279..447M`): + + .. math:: - N(X) = N_1(1+ \\Phi_{i,j}/N_e + \\Phi_{i, j}/N_e \\times \\Phi_{i, j+1}/N_e + \\dots) + \\Phi_{i,j} =& \\frac{N_{i, j+1} n_e}{N_{i, j}} \\\\ + + \\Phi_{i, j} =& W \\times[\\delta \\zeta + W ( 1 - \\zeta)] \\left(\\frac{T_\\textrm{e}}{T_\\textrm{R}}\\right)^{1/2} + \\Phi_{i, j}(\\textrm{LTE}) """ - if self.ion_number_density is None: - self.ion_number_density = pd.Series(index=self.partition_functions.index.copy()) - self.cleaned_levels = pd.Series(index=self.partition_functions.index.copy()) + phis = super(NebularPlasma, self).calculate_saha() - for atomic_number, groups in phis.groupby(level='atomic_number'): - current_phis = groups.values / electron_density - phis_product = np.cumproduct(current_phis) + delta = self.calculate_radiation_field_correction() - neutral_atom_density = self.abundances.ix[atomic_number] / (1 + np.sum(phis_product)) - ion_densities = [neutral_atom_density] + list(neutral_atom_density * phis_product) + zeta = pd.Series(index=phis.index) + + for idx in zeta.index: + try: + current_zeta = self.atom_data.zeta_data[idx](self.t_rad) + except KeyError: + current_zeta = 1.0 - self.ion_number_density.ix[atomic_number] = ion_densities + zeta.ix[idx] = current_zeta + phis *= self.w * (delta.ix[phis.index] * zeta + self.w * (1 - zeta)) * \ + (self.t_electron / self.t_rad) ** .5 - def calculate_level_populations(self): + return phis + + def calculate_radiation_field_correction(self, departure_coefficient=None, + chi_threshold_species=(20, 1)): """ - Calculate the level populations and storing in self.level_populations table. - :math:`N` denotes the ion number density calculated with `calculate_ionization_balance`, i is the atomic number, - j is the ion number and k is the level number. + Calculating radiation field correction factors according to Mazzali & Lucy 1993 (:cite:`1993A&A...279..447M`; henceforth ML93) + + + In ML93 the radiation field correction factor is denoted as :math:`\\delta` and is calculated in Formula 15 & 20 + + The radiation correction factor changes according to a ionization energy threshold :math:`\\chi_\\textrm{T}` + and the species ionization threshold (from the ground state) :math:`\\chi_0`. + + For :math:`\\chi_\\textrm{T} \\ge \\chi_0` .. math:: - \\frac{g_k}{Z_{i,j}} \\times N_{i, j} \\times e^{-\\beta_\\textrm{rad} \\times E_k} + \\delta = \\frac{T_\\textrm{e}}{b_1 W T_\\textrm{R}} \\exp(\\frac{\\chi_\\textrm{T}}{k T_\\textrm{R}} - + \\frac{\\chi_0}{k T_\\textrm{e}}) - """ + For :math:`\\chi_\\textrm{T} < \\chi_0` - Z = self.partition_functions.values[self.atom_data.levels_index2atom_ion_index] + .. math:: + \\delta = 1 - \\exp(\\frac{\\chi_\\textrm{T}}{k T_\\textrm{R}} - \\frac{\\chi_0}{k T_\\textrm{R}}) + \\frac{T_\\textrm{e}}{b_1 W T_\\textrm{R}} \\exp(\\frac{\\chi_\\textrm{T}}{k T_\\textrm{R}} - + \\frac{\\chi_0}{k T_\\textrm{e}}), - ion_number_density = self.ion_number_density.values[self.atom_data.levels_index2atom_ion_index] + where :math:`T_\\textrm{R}` is the radiation field Temperature, :math:`T_\\textrm{e}` is the electron temperature and W is the + dilution factor. - levels_g = self.atom_data.levels['g'].values - levels_energy = self.atom_data.levels['energy'].values - level_populations = (levels_g / Z) * ion_number_density * np.exp(-self.beta_rad * levels_energy) + Parameters + ---------- + phi_table : `~astropy.table.Table` + a table containing the field 'atomic_number', 'ion_number', 'phi' - if self.initialize: - self.level_populations = Series(level_populations, index=self.atom_data.levels.index) + departure_coefficient : `~float` or `~None`, optional + departure coefficient (:math:`b_1` in ML93) For the default (`None`) it is set to 1/W. - else: - level_populations = Series(level_populations, index=self.atom_data.levels.index) - self.level_populations.update(level_populations[~self.atom_data.nlte_mask]) + chi_threshold_species : `~tuple`, optional + This describes which ionization energy to use for the threshold. Default is Calcium II + (1044 Angstrom; useful for Type Ia) + For Type II supernovae use Lyman break (912 Angstrom) or (1,1) as the tuple - def calculate_nlte_level_populations(self): - """ - Calculating the NLTE level populations for specific ions + Returns + ------- + + This function adds a field 'delta' to the phi table given to the function """ + #factor delta ML 1993 + if departure_coefficient is None: + departure_coefficient = 1 / float(self.w) - if not hasattr(self, 'beta_sobolevs'): - self.beta_sobolevs = np.zeros_like(self.atom_data.lines['nu'].values) + chi_threshold = self.atom_data.ionization_data['ionization_energy'].ix[chi_threshold_species] - macro_atom.calculate_beta_sobolev(self.tau_sobolevs, self.beta_sobolevs) + radiation_field_correction = (self.t_electron / (departure_coefficient * self.w * self.t_rad)) * \ + np.exp(self.beta_rad * chi_threshold - self.beta_electron * + self.atom_data.ionization_data['ionization_energy']) - for species in self.nlte_species: - logger.info('Calculating rates for species %s', species) - number_of_levels = self.level_populations.ix[species].size - rates_matrix = np.zeros((number_of_levels, number_of_levels), dtype=np.float64) + less_than_chi_threshold = self.atom_data.ionization_data['ionization_energy'] < chi_threshold - for i, (line_id, line) in enumerate(self.atom_data.lines.iterrows()): - atomic_number = int(line['atomic_number']) - ion_number = int(line['ion_number']) - if (atomic_number, ion_number) != species: - continue + radiation_field_correction[less_than_chi_threshold] += 1 - \ + np.exp(self.beta_rad * chi_threshold - self.beta_rad * + self.atom_data.ionization_data[ + less_than_chi_threshold]['ionization_energy']) - level_number_lower = int(line['level_number_lower']) - level_number_upper = int(line['level_number_upper']) + return radiation_field_correction - n_lower = self.level_populations.ix[atomic_number, ion_number, level_number_lower] - n_upper = self.level_populations.ix[atomic_number, ion_number, level_number_upper] + def calculate_ion_populations(self, phis): + """ + Calculate the ionization balance - cur_beta_sobolev = self.beta_sobolevs[i] + .. math:: + N(X) = N_1 + N_2 + N_3 + \\dots - stimulated_emission_term = (1 - (n_upper * line['B_ul']) / (n_lower * line['B_lu'])) + N(X) = (N_2/N_1) \\times N_1 + (N3/N2) \\times (N_2/N_1) \\times N_1 + \\dots - if stimulated_emission_term < 0: - logger.warn('Stimulated emission term less than 0 %g n_upper=%g n_lower=%g (zone_id=%s)', - stimulated_emission_term, n_upper, n_lower, self.zone_id) + N(X) = N_1(1 + N_2/N_1 + (N_3/N_2) \\times (N_2/N_1) + \\dots - C_lu, C_ul = self.atom_data.get_collision_coefficients(atomic_number, ion_number, level_number_lower, - level_number_upper, self.t_electron) - r_lu = line['B_lu'] * cur_beta_sobolev * self.j_blues[ - i] * stimulated_emission_term + C_lu * self.electron_density - r_ul = line['A_ul'] * cur_beta_sobolev + C_ul * self.electron_density + N(X) = N_1(1+ \\Phi_{i,j}/N_e + \\Phi_{i, j}/N_e \\times \\Phi_{i, j+1}/N_e + \\dots) - rates_matrix[level_number_upper, level_number_lower] = r_lu - rates_matrix[level_number_lower, level_number_upper] = r_ul - rates_matrix[level_number_lower, level_number_lower] -= r_lu - rates_matrix[level_number_upper, level_number_upper] -= r_ul + """ + #TODO see if self.ion_populations is None is needed (first class should be enough) + if not hasattr(self, 'ion_populations') or self.ion_populations is None: + self.ion_populations = pd.Series(index=self.partition_functions.index.copy()) + self.cleaned_levels = pd.Series(index=self.partition_functions.index.copy()) - rates_matrix[0] = 1.0 - x = np.zeros(rates_matrix.shape[0]) - x[0] = 1.0 - self.level_populations.ix[species] = np.linalg.solve(rates_matrix, x) * self.ion_number_density.ix[species] + for atomic_number, groups in phis.groupby(level='atomic_number'): + current_phis = groups.values / self.electron_density + phis_product = np.cumproduct(current_phis) + + neutral_atom_density = self.number_density.ix[atomic_number] / (1 + np.sum(phis_product)) + ion_densities = [neutral_atom_density] + list(neutral_atom_density * phis_product) + + self.ion_populations.ix[atomic_number] = ion_densities + + def calculate_level_populations(self): + """ + Calculate the level populations and putting them in the column 'number-density' of the self.levels table. + :math:`N` denotes the ion number density calculated with `calculate_ionization_balance`, i is the atomic number, + j is the ion number and k is the level number. For non-metastable levels we add the dilution factor (W) to the calculation. + + .. math:: + + N_{i, j, k}(\\textrm{metastable}) &= \\frac{g_k}{Z_{i, j}}\\times N_{i, j} \\times e^{-\\beta_\\textrm{rad} E_k} \\\\ + N_{i, j, k}(\\textrm{not metastable}) &= W\\frac{g_k}{Z_{i, j}}\\times N_{i, j} \\times e^{-\\beta_\\textrm{rad} E_k} \\\\ - #Cleaning Level populations - self.cleaned_levels.ix[species] = 0 - self.lowest_cleaned_level = 100000 - for i in xrange(1, number_of_levels): - n_upper = self.level_populations.ix[species][i] - n_lower = self.level_populations.ix[species][i - 1] + This function updates the 'number_density' column on the levels table (or adds it if non-existing) + """ + Z = self.partition_functions.values[self.atom_data.levels_index2atom_ion_index] - g_upper = float(self.atom_data.levels.ix[species]['g'][i]) - g_lower = float(self.atom_data.levels.ix[species]['g'][i - 1]) + ion_number_density = self.ion_populations.values[self.atom_data.levels_index2atom_ion_index] - current_stim_ems = (n_upper / n_lower) * (g_lower / g_upper) + levels_g = self.atom_data.levels['g'].values + levels_energy = self.atom_data.levels['energy'].values + level_populations = (levels_g / Z) * ion_number_density * np.exp(-self.beta_rad * levels_energy) - if current_stim_ems > 1.: - self.level_populations.ix[species[0], species[1], i] = (1 - 1e-12) * (g_upper / g_lower) * n_lower - self.cleaned_levels.ix[species] += 1 - self.lowest_cleaned_level = min(i, self.lowest_cleaned_level) - #### After cleaning check if the normalization is good: - if abs((self.level_populations.ix[species].sum() / self.ion_number_density.ix[species] - 1)) > 0.02: - logger.warn("NLTE populations (after cleaning) does not sum up to 1 within 2 percent " - "(%.2f / 1.0 - zone id = %s, lowest_cleaned_level=%d)", - ((1 - self.level_populations.ix[species].sum() / self.ion_number_density.ix[species])), - self.zone_id, self.lowest_cleaned_level) + #only change between lte plasma and nebular + level_populations[~self.atom_data.levels['metastable']] *= self.w - logger.debug('Number of cleaned levels %d of %d (zone id =%s)', self.cleaned_levels.ix[species], - self.level_populations.ix[species].count(), self.zone_id) + if self.initialize: + self.level_populations = pd.Series(level_populations, index=self.atom_data.levels.index) - if float(self.cleaned_levels.ix[species]) / self.level_populations.ix[species].count() > 0.5: - logger.warn('Number of cleaned levels very high %d of %d (zone id=%s, , lowest_cleaned_level=%d)', - self.cleaned_levels.ix[species], - self.level_populations.ix[species].count(), self.zone_id, self.lowest_cleaned_level) + else: + level_populations = pd.Series(level_populations, index=self.atom_data.levels.index) + self.level_populations.update(level_populations[~self.atom_data.nlte_data.nlte_mask]) def calculate_tau_sobolev(self): @@ -436,31 +501,79 @@ def calculate_tau_sobolev(self): n_lower = self.level_populations.values[self.atom_data.lines_lower2level_idx] self.tau_sobolevs = sobolev_coefficient * f_lu * wavelength * self.time_explosion * n_lower - def calculate_bound_free(self): + def calculate_nlte_level_populations(self): """ - :return: + Calculating the NLTE level populations for specific ions + """ - nu_bins = range(1000, 10000, 1000) #TODO: get the binning from the input file. - try: - bf = np.zeros(len(self.atom_data.levels), len(self.atom_data.selected_atomic_numbers), len(nu_bins)) - except AttributeError: - logger.critical("Err creating the bf array.") - phis = self.calculate_saha() - nnlevel = self.level_populations - for nu in nu_bins: - for i, (level_id, level) in enumerate(self.atom_data.levels.iterrows()): - atomic_number = level.name[0] - ion_number = level.name[1] - level_number = level.name[2] - sigma_bf_th = self.atom_data.ion_cx_th.ix[atomic_number, ion_number, level_number] - phi = phis.ix[atomic_number, ion_number] + if not hasattr(self, 'beta_sobolevs'): + self.beta_sobolevs = np.zeros_like(self.atom_data.lines['nu'].values) + + macro_atom.calculate_beta_sobolev(self.tau_sobolevs, self.beta_sobolevs) + + if self.nlte_options.get('coronal_approximation', False): + beta_sobolevs = np.ones_like(self.beta_sobolevs) + j_blues = np.zeros_like(self.j_blues) + else: + beta_sobolevs = self.beta_sobolevs + j_blues = self.j_blues + + if self.nlte_options.get('classical_nebular', False): + print "setting classical nebular = True" + beta_sobolevs[:] = 1.0 + + for species in self.nlte_species: + logger.info('Calculating rates for species %s', species) + number_of_levels = self.level_populations.ix[species].size + + level_populations = self.level_populations.ix[species].values + lnl = self.atom_data.nlte_data.lines_level_number_lower[species] + lnu = self.atom_data.nlte_data.lines_level_number_upper[species] + + lines_index = self.atom_data.nlte_data.lines_idx[species] + A_uls = self.atom_data.nlte_data.A_uls[species] + B_uls = self.atom_data.nlte_data.B_uls[species] + B_lus = self.atom_data.nlte_data.B_lus[species] + + r_lu_index = lnu * number_of_levels + lnl + r_ul_index = lnl * number_of_levels + lnu + + r_ul_matrix = np.zeros((number_of_levels, number_of_levels), dtype=np.float64) + r_ul_matrix.ravel()[r_ul_index] = A_uls + r_ul_matrix.ravel()[r_ul_index] *= beta_sobolevs[lines_index] + + stimulated_emission_matrix = np.zeros_like(r_ul_matrix) + stimulated_emission_matrix.ravel()[r_lu_index] = 1 - ((level_populations[lnu] * B_uls) / ( + level_populations[lnl] * B_lus)) + + stimulated_emission_matrix[stimulated_emission_matrix < 0.] = 0.0 + r_lu_matrix = np.zeros_like(r_ul_matrix) + r_lu_matrix.ravel()[r_lu_index] = B_lus * j_blues[lines_index] * beta_sobolevs[lines_index] + r_lu_matrix *= stimulated_emission_matrix - def update_macro_atom(self): + collision_matrix = self.atom_data.nlte_data.get_collision_matrix(species, + self.t_electron) * self.electron_density + + rates_matrix = r_lu_matrix + r_ul_matrix + collision_matrix + + for i in xrange(number_of_levels): + rates_matrix[i, i] = -np.sum(rates_matrix[:, i]) + + rates_matrix[0] = 1.0 + + x = np.zeros(rates_matrix.shape[0]) + x[0] = 1.0 + relative_level_populations = np.linalg.solve(rates_matrix, x) + + self.level_populations.ix[species] = relative_level_populations * self.ion_populations.ix[species] + + return + + def calculate_transition_probabilities(self): """ Updating the Macro Atom computations - """ macro_tau_sobolevs = self.tau_sobolevs[self.atom_data.macro_atom_data['lines_idx'].values.astype(int)] @@ -487,10 +600,36 @@ def update_macro_atom(self): return transition_probabilities + def set_j_blues(self, j_blues=None): + if j_blues is None: + self.j_blues = self.w * intensity_black_body(self.atom_data.lines['nu'].values, self.t_rad) + else: + self.j_blues = j_blues + + def calculate_bound_free(self): + """ + :return: + """ + nu_bins = range(1000, 10000, 1000) #TODO: get the binning from the input file. + try: + bf = np.zeros(len(self.atom_data.levels), len(self.atom_data.selected_atomic_numbers), len(nu_bins)) + except AttributeError: + logger.critical("Err creating the bf array.") + + phis = self.calculate_saha() + nnlevel = self.level_populations + for nu in nu_bins: + for i, (level_id, level) in enumerate(self.atom_data.levels.iterrows()): + atomic_number = level.name[0] + ion_number = level.name[1] + level_number = level.name[2] + sigma_bf_th = self.atom_data.ion_cx_th.ix[atomic_number, ion_number, level_number] + phi = phis.ix[atomic_number, ion_number] + -class NebularPlasma(LTEPlasma): +class LTEPlasma(BasePlasma): """ - Model for BasePlasma using the Nebular approximation + Model for BasePlasma using a local thermodynamic equilibrium approximation. Parameters ---------- @@ -509,262 +648,66 @@ class NebularPlasma(LTEPlasma): atom_data : `~tardis.atomic.AtomData`-object - t_electron : `~float`, or `None` - the electron temperature. if set to `None` we assume the electron temperature is 0.9 * radiation temperature - """ - def __init__(self, abundances, atom_data, time_explosion, nlte_species=[], t_electron=None, density_unit='g/cm^3', - max_ion_number=None, - use_macro_atom=False, zone_id=None): - BasePlasma.__init__(self, abundances, atom_data, time_explosion=time_explosion, density_unit=density_unit, - max_ion_number=max_ion_number, - use_macro_atom=use_macro_atom, zone_id=zone_id) - - self.ion_number_density = None - self.nlte_species = nlte_species - - - def update_radiationfield(self, t_rad, w, t_electron=None, n_e_convergence_threshold=0.05): - BasePlasma.update_radiationfield(self, t_rad) - - self.w = w - - if t_electron is None: - self.t_electron = 0.9 * self.t_rad - - self.beta_electron = 1 / (self.t_electron * constants.k_B.cgs.value) - - self.calculate_partition_functions() - - self.ge = ((2 * np.pi * constants.m_e.cgs.value / self.beta_rad) / (constants.h.cgs.value ** 2)) ** 1.5 - #Calculate the Saha ionization balance fractions - phis = self.calculate_saha() - - #initialize electron density with the sum of number densities - electron_density = self.abundances.sum() - - n_e_iterations = 0 - - while True: - self.calculate_ionization_balance(phis, electron_density) - ion_numbers = np.array([item[1] for item in self.ion_number_density.index]) - new_electron_density = np.sum(self.ion_number_density.values * ion_numbers) - n_e_iterations += 1 - if abs(new_electron_density - electron_density) / electron_density < n_e_convergence_threshold: break - electron_density = 0.5 * (new_electron_density + electron_density) - - self.electron_density = new_electron_density - logger.debug('Took %d iterations to converge on electron density' % n_e_iterations) - - self.calculate_level_populations() - self.calculate_tau_sobolev() - self.calculate_nlte_level_populations() - - if self.initialize: - self.initialize = False - - def set_j_blues(self, j_blues=None): - if j_blues is None: - self.j_blues = self.w * intensity_black_body(self.atom_data.lines['nu'].values, self.t_rad) - else: - self.j_blues = j_blues - - def calculate_partition_functions(self): - """ - Calculate partition functions for the ions using the following formula, where - :math:`i` is the atomic_number, :math:`j` is the ion_number and :math:`k` is the level number. - - .. math:: - Z_{i,j} = \\underbrace{\\sum_{k=0}^{max(k)_{i,j}} g_k \\times e^{-E_k / (k_\\textrm{b} T)}}_\\textrm{metastable levels} + - \\underbrace{W\\times\\sum_{k=0}^{max(k)_{i,j}} g_k \\times e^{-E_k / (k_\\textrm{b} T)}}_\\textrm{non-metastable levels} - - - - Returns - ------- - - partition_functions : `~astropy.table.Table` - with fields atomic_number, ion_number, partition_function - - """ - - def group_calculate_partition_function(group): - metastable = group['metastable'] - meta_z = np.sum(group['g'][metastable] * np.exp(-group['energy'][metastable] * self.beta_rad)) - non_meta_z = np.sum(group['g'][~metastable] * np.exp(-group['energy'][~metastable] * self.beta_rad)) - return meta_z + self.w * non_meta_z - - - if self.initialize: - logger.debug('Initializing the partition functions and indices') - - self.partition_functions = self.atom_data.levels.groupby(level=['atomic_number', 'ion_number']).apply( - group_calculate_partition_function) - - self.atom_data.atom_ion_index = Series(np.arange(len(self.partition_functions)), - self.partition_functions.index) - self.atom_data.levels_index2atom_ion_index = self.atom_data.atom_ion_index.ix[ - self.atom_data.levels.index.droplevel(2)].values - else: - if not hasattr(self, 'partition_functions'): - raise ValueError("Called calculate partition_functions without initializing at least once") - - for species, group in self.atom_data.levels.groupby(level=['atomic_number', 'ion_number']): - if species in self.nlte_species: - ground_level_population = self.level_populations[species][0] - self.partition_functions.ix[species] = self.atom_data.levels.ix[species]['g'][0] * \ - np.sum(self.level_populations[ - species].values / ground_level_population) - else: - self.partition_functions.ix[species] = np.sum(group['g'] * np.exp(-group['energy'] * self.beta_rad)) + @classmethod + def from_abundance(cls, t_rad, abundance, density, atom_data, time_explosion, j_blues=None, t_electron=None, + use_macro_atom=False, nlte_species=[], nlte_options={}, zone_id=None): + return super(LTEPlasma, cls).from_abundance(t_rad, 1., abundance, density, atom_data, time_explosion, + j_blues=j_blues, t_electron=t_electron, + use_macro_atom=use_macro_atom, nlte_species=nlte_species, + nlte_options=nlte_options, zone_id=zone_id) + def __init__(self, t_rad, number_density, atom_data, time_explosion, w=1., j_blues=None, t_electron=None, + use_macro_atom=False, + nlte_species=[], nlte_options=None, zone_id=None, saha_treatment='lte'): + super(LTEPlasma, self).__init__(t_rad, w, number_density, atom_data, time_explosion, j_blues=j_blues, + t_electron=t_electron, use_macro_atom=use_macro_atom, nlte_species=nlte_species, + nlte_options=nlte_options, zone_id=zone_id, saha_treatment=saha_treatment) - def calculate_saha(self): - """ - Calculating the ionization equilibrium using the Saha equation, where i is atomic number, - j is the ion_number, :math:`n_e` is the electron density, :math:`Z_{i, j}` are the partition functions - and :math:`\chi` is the ionization energy. For the `NebularPlasma` we first calculate the - ionization balance assuming LTE conditions (:math:`\\Phi_{i, j}(\\textrm{LTE})`) and use factors to more accurately - describe the plasma. The two important factors are :math:`\\zeta` - a correction factor to take into account - ionizations from excited states. The second factor is :math:`\\delta` , adjusting the ionization balance for the fact that - there's more line blanketing in the blue. - The :math:`\\zeta` factor for different temperatures is read in to the `~tardis.atomic.NebularAtomData` and then - interpolated for the current temperature. - - The :math:`\\delta` factor is calculated with :method:`calculate_radiation_field_correction`. - - Finally the ionization balance is adjusted (as equation 14 in :cite:`1993A&A...279..447M`): - - .. math:: - - - \\Phi_{i,j} =& \\frac{N_{i, j+1} n_e}{N_{i, j}} \\\\ - - \\Phi_{i, j} =& W \\times[\\delta \\zeta + W ( 1 - \\zeta)] \\left(\\frac{T_\\textrm{e}}{T_\\textrm{R}}\\right)^{1/2} - \\Phi_{i, j}(\\textrm{LTE}) - - """ - - phis = super(NebularPlasma, self).calculate_saha() - - delta = self.calculate_radiation_field_correction() - - zeta = Series(index=phis.index) - - for idx in zeta.index: - try: - current_zeta = self.atom_data.zeta_data[idx](self.t_rad) - except KeyError: - current_zeta = 1.0 - - zeta.ix[idx] = current_zeta - - phis *= self.w * (delta.ix[phis.index] * zeta + self.w * (1 - zeta)) * \ - (self.t_electron / self.t_rad) ** .5 - - return phis - - - def calculate_radiation_field_correction(self, departure_coefficient=None, - chi_threshold_species=(20, 1)): - """ - Calculating radiation field correction factors according to Mazzali & Lucy 1993 (:cite:`1993A&A...279..447M`; henceforth ML93) - - - In ML93 the radiation field correction factor is denoted as :math:`\\delta` and is calculated in Formula 15 & 20 - - The radiation correction factor changes according to a ionization energy threshold :math:`\\chi_\\textrm{T}` - and the species ionization threshold (from the ground state) :math:`\\chi_0`. - - For :math:`\\chi_\\textrm{T} \\ge \\chi_0` - - .. math:: - \\delta = \\frac{T_\\textrm{e}}{b_1 W T_\\textrm{R}} \\exp(\\frac{\\chi_\\textrm{T}}{k T_\\textrm{R}} - - \\frac{\\chi_0}{k T_\\textrm{e}}) - - For :math:`\\chi_\\textrm{T} < \\chi_0` - - .. math:: - \\delta = 1 - \\exp(\\frac{\\chi_\\textrm{T}}{k T_\\textrm{R}} - \\frac{\\chi_0}{k T_\\textrm{R}}) + \\frac{T_\\textrm{e}}{b_1 W T_\\textrm{R}} \\exp(\\frac{\\chi_\\textrm{T}}{k T_\\textrm{R}} - - \\frac{\\chi_0}{k T_\\textrm{e}}), - - where :math:`T_\\textrm{R}` is the radiation field Temperature, :math:`T_\\textrm{e}` is the electron temperature and W is the - dilution factor. - - Parameters - ---------- - phi_table : `~astropy.table.Table` - a table containing the field 'atomic_number', 'ion_number', 'phi' - - departure_coefficient : `~float` or `~None`, optional - departure coefficient (:math:`b_1` in ML93) For the default (`None`) it is set to 1/W. - - chi_threshold_species : `~tuple`, optional - This describes which ionization energy to use for the threshold. Default is Calcium II - (1044 Angstrom; useful for Type Ia) - For Type II supernovae use Lyman break (912 Angstrom) or (1,1) as the tuple - - Returns - ------- - - This function adds a field 'delta' to the phi table given to the function +class NebularPlasma(BasePlasma): + """ + Model for BasePlasma using the Nebular approximation - """ - #factor delta ML 1993 - if departure_coefficient is None: - departure_coefficient = 1 / float(self.w) + Parameters + ---------- - chi_threshold = self.atom_data.ionization_data['ionization_energy'].ix[chi_threshold_species] + abundances : `~dict` + A dictionary with the abundances for each element - radiation_field_correction = (self.t_electron / (departure_coefficient * self.w * self.t_rad)) * \ - np.exp(self.beta_rad * chi_threshold - self.beta_electron * - self.atom_data.ionization_data['ionization_energy']) + t_rad : `~float` + Temperature in Kelvin for the plasma - less_than_chi_threshold = self.atom_data.ionization_data['ionization_energy'] < chi_threshold + density : `float` + density in g/cm^3 - radiation_field_correction[less_than_chi_threshold] += 1 - \ - np.exp(self.beta_rad * chi_threshold - self.beta_rad * - self.atom_data.ionization_data[ - less_than_chi_threshold]['ionization_energy']) + .. warning:: + Instead of g/cm^ will later use the keyword `density_unit` as unit - return radiation_field_correction + atom_data : `~tardis.atomic.AtomData`-object + t_electron : `~float`, or `None` + the electron temperature. if set to `None` we assume the electron temperature is 0.9 * radiation temperature - def calculate_level_populations(self): - """ - Calculate the level populations and putting them in the column 'number-density' of the self.levels table. - :math:`N` denotes the ion number density calculated with `calculate_ionization_balance`, i is the atomic number, - j is the ion number and k is the level number. For non-metastable levels we add the dilution factor (W) to the calculation. + """ - .. math:: + def __init__(self, t_rad, w, number_density, atom_data, time_explosion, j_blues=None, t_electron=None, + use_macro_atom=False, + nlte_species=[], nlte_options=None, zone_id=None, saha_treatment='lte'): + super(NebularPlasma, self).__init__(t_rad, w, number_density, atom_data, time_explosion, j_blues=j_blues, + t_electron=t_electron, use_macro_atom=use_macro_atom, + nlte_species=nlte_species, nlte_options=nlte_options, zone_id=zone_id, + saha_treatment=saha_treatment) - N_{i, j, k}(\\textrm{metastable}) &= \\frac{g_k}{Z_{i, j}}\\times N_{i, j} \\times e^{-\\beta_\\textrm{rad} E_k} \\\\ - N_{i, j, k}(\\textrm{not metastable}) &= W\\frac{g_k}{Z_{i, j}}\\times N_{i, j} \\times e^{-\\beta_\\textrm{rad} E_k} \\\\ - This function updates the 'number_density' column on the levels table (or adds it if non-existing) - """ - Z = self.partition_functions.values[self.atom_data.levels_index2atom_ion_index] - ion_number_density = self.ion_number_density.values[self.atom_data.levels_index2atom_ion_index] - levels_g = self.atom_data.levels['g'].values - levels_energy = self.atom_data.levels['energy'].values - level_populations = (levels_g / Z) * ion_number_density * np.exp(-self.beta_rad * levels_energy) - #only change between lte plasma and nebular - level_populations[~self.atom_data.levels['metastable']] *= self.w - self.level_populations = Series(level_populations, index=self.atom_data.levels.index) - if self.initialize: - self.level_populations = Series(level_populations, index=self.atom_data.levels.index) - else: - level_populations = Series(level_populations, index=self.atom_data.levels.index) - self.level_populations.update(level_populations[~self.atom_data.nlte_mask])