def to_dict(self): """Represents object as dictionary with JSON-accepted datatypes Returns ------- obj_dict : dict """ obj_dict = {'class': str(self.__class__), 'type': 'empiricalbase', 'name': self.name, 'phase': self.phase, 'elements': self.elements, 'notes': self.notes, 'smiles': self.smiles, } try: obj_dict['model'] = self.model.to_dict() except AttributeError: obj_dict['model'] = self.model if _is_iterable(self.misc_models): obj_dict['misc_models'] = \ [misc_model.to_dict() for misc_model in self.misc_models] else: obj_dict['misc_models'] = self.misc_models return obj_dict
def to_dict(self): """Represents object as dictionary with JSON-accepted datatypes Returns ------- obj_dict : dict """ obj_dict = { 'class': str(self.__class__), 'name': self.name, 'trans_model': self.trans_model.to_dict(), 'vib_model': self.vib_model.to_dict(), 'rot_model': self.rot_model.to_dict(), 'elec_model': self.elec_model.to_dict(), 'nucl_model': self.nucl_model.to_dict(), 'smiles': self.smiles, 'notes': self.notes } if _is_iterable(self.misc_models): obj_dict['misc_models'] = \ [mix_model.to_dict() for mix_model in self.misc_models] else: obj_dict['misc_models'] = self.misc_models try: obj_dict['references'] = self.references.to_dict() except AttributeError: obj_dict['references'] = self.references return obj_dict
def __init__(self, name=None, phase=None, elements=None, statmech_model=None, references=None, misc_models=None, smiles=None, notes=None, **kwargs): self.name = name self.phase = phase self.elements = elements self.smiles = smiles self.notes = notes # Assign self.statmech_model if inspect.isclass(statmech_model): # If you're passing a class. Note that the required # arguments will be guessed. self.statmech_model = statmech_model(**kwargs) else: # If it's an object that has already been initialized self.statmech_model = statmech_model # Assign mixing models # TODO Mixing models can not be initialized by passing the class # because all the models will have the same attributes. Figure out a # way to pass them. Perhaps have a dictionary that contains the # attributes separated by species if not _is_iterable(misc_models) and misc_models is not None: misc_models = [misc_models] self.misc_models = misc_models
def test_is_iterable(self): self.assertTrue(pmutt._is_iterable(list())) self.assertTrue(pmutt._is_iterable(tuple())) self.assertTrue(pmutt._is_iterable(set())) self.assertTrue(pmutt._is_iterable(dict())) self.assertFalse(pmutt._is_iterable('')) self.assertFalse(pmutt._is_iterable(1)) self.assertFalse(pmutt._is_iterable(1.)) self.assertFalse(pmutt._is_iterable(None))
def __init__(self, name=None, trans_model=EmptyMode(), vib_model=EmptyMode(), rot_model=EmptyMode(), elec_model=EmptyMode(), nucl_model=EmptyMode(), misc_models=None, elements=None, references=None, smiles=None, notes=None, **kwargs): self.name = name self.smiles = smiles self.notes = notes self.elements = elements self.references = references # Translational modes if inspect.isclass(trans_model): self.trans_model = _pass_expected_arguments(trans_model, **kwargs) else: self.trans_model = trans_model # Vibrational modes if inspect.isclass(vib_model): self.vib_model = _pass_expected_arguments(vib_model, **kwargs) else: self.vib_model = vib_model # Rotational modes if inspect.isclass(rot_model): self.rot_model = _pass_expected_arguments(rot_model, **kwargs) else: self.rot_model = rot_model # Electronic modes if inspect.isclass(elec_model): self.elec_model = _pass_expected_arguments(elec_model, **kwargs) else: self.elec_model = elec_model # Nuclear modes if inspect.isclass(nucl_model): self.nucl_model = _pass_expected_arguments(nucl_model, **kwargs) else: self.nucl_model = nucl_model # Assign misc models # TODO misc models can not be initialized by passing the class # because all the models will have the same attributes. Figure out # a way to pass them. Perhaps have a dictionary that contains the # attributes separated by species if not _is_iterable(misc_models) and misc_models is not None: misc_models = [misc_models] self.misc_models = misc_models
def _get_target_sets(target, species_delimiter='+'): target_sets = [] if target is None: target_sets.append(set()) else: if not _is_iterable(target): target = [target] for reaction_str in target: target_names, target_stoich = _parse_reaction_state( reaction_str=reaction_str, species_delimiter=species_delimiter) target_set = state_str_to_set(species_names=target_names, stoich=target_stoich) target_sets.append(target_set) return target_sets
def get_CpoR(self, T, raise_error=True, raise_warning=True, **kwargs): """Calculate the dimensionless heat capacity Parameters ---------- T : float or (N,) `numpy.ndarray`_ Temperature(s) in K raise_error : bool, optional If True, raises an error if any of the modes do not have the quantity of interest. Default is True raise_warning : bool, optional Only relevant if raise_error is False. Raises a warning if any of the modes do not have the quantity of interest. Default is True kwargs : key-word arguments Arguments to calculate mixture model properties, if any Returns ------- CpoR : float or (N,) `numpy.ndarray`_ Dimensionless heat capacity .. _`numpy.ndarray`: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html """ # Convert T to 1D numpy format if not _is_iterable(T): T = [T] T = np.array(T) self._check_T(T) # Calculate pure properties CpoR = get_shomate_CpoR(a=self.a, T=T, units=self.units) # Calculate mixing properties for T_i in T: CpoR_mix = _get_mix_quantity(misc_models=self.misc_models, method_name='get_CpoR', raise_error=raise_error, raise_warning=raise_warning, default_value=0., T=T_i, **kwargs) # Add mixing quantity in appropriate format CpoR = CpoR + CpoR_mix if len(T) == 1: CpoR = CpoR.item(0) return CpoR
def _shomate_CpoR(T, A, B, C, D, E, units): """ Helper function to fit Shomate heat capacity. Paramters --------- T - float Temperature in K A, B, C, D, E - float Shomate parameters Returns ------- CpoR - float Dimensionless heat capacity """ a = np.array([A, B, C, D, E, 0., 0., 0.]) if not _is_iterable(T): T = [T] T = np.array(T) return get_shomate_CpoR(a=a, T=T, units=units)
def write_yaml(reactor_type=None, mode=None, nodes=None, V=None, T=None, P=None, A=None, L=None, cat_abyv=None, flow_rate=None, residence_time=None, mass_flow_rate=None, end_time=None, transient=None, stepping=None, init_step=None, step_size=None, atol=None, rtol=None, full_SA=None, reactions_SA=None, species_SA=None, phases=None, reactor=None, inlet_gas=None, multi_T=None, multi_P=None, multi_flow_rate=None, output_format=None, solver=None, simulation=None, multi_input=None, misc=None, units=None, filename=None, yaml_options={ 'default_flow_style': False, 'indent': 4 }, newline='\n'): """Writes the reactor options in a YAML file for OpenMKM. Parameters ---------- reactor_type : str Type of reactor. Supported options include: - pfr - pfr_0d - cstr - batch Value written to ``reactor.type``. mode : str Operation of reactor. Supported options include: - Isothermal - Adiabatic Value written to ``reactor.mode``. nodes : int Number of nodes to use if ``reactor_type`` is 'pfr_0d'. Value written to ``reactor.nodes`` V : float or str Volume of reactor. Value written to ``reactor.volume``. Units of length^3. See Notes section regarding unit specification. T : float Temperature (in K) of reactor. Value written to ``reactor.temperature``. P : float or str Pressure of reactor. Value written to ``reactor.pressure``. Units of pressure. See Notes section regarding unit specification. A : float or str Surface area of reactor. Value written to ``reactor.area``. Units of length^2. See Notes section regarding unit specification. L : float or str Length of reactor. Value written to ``reactor.length``. Units of length. See Notes section regarding unit specification. cat_abyv : float or str Catalyst surface area to volume ratio. Value written to ``reactor.cat_abyv``. Units of 1/length. See Notes section regarding unit specification. flow_rate : float or str Volumetric flow rate of inlet. Value written to ``inlet_gas.flow_rate``. Units of length^3/time. See Notes section regarding unit specification. residence_time : float or str Residence time of reactor. Value written to ``inlet_gas.residence_time``. Not required if ``flow_rate`` or ``mass_flow_rate`` already specified. Units of time. See Notes section regarding unit specification. mass_flow_rate : float or str Mass flow rate of inlet. Value written to ``inlet_gas.mass_flow_rate``. Units of mass^3/time. See Notes section regarding unit specification. end_time : float or str Reactor simulation time. For continuous reactors, the system is assumed to reach steady state by this time. Value written to ``simulation.end_time``. Units of time. See Notes section regarding unit specification. transient : bool If True, transient operation results are saved. Otherwise, transient files are left blank. Value written to ``simulation.transient``. stepping : str Time steps taken to simulate reactor. Supported options include: - logarithmic - regular Value written to ``simulation.stepping``. init_step : float or str Initial step to take. Value written to ``simulation.init_step``. step_size : float or str If ``stepping`` is logarithmic, represents the ratio between the next step and the current step. If ``stepping`` is regular, represents the time between the next step and the current step in units of time. Value written to simulation.step_size. See Notes section regarding unit specification. atol : float Absolute tolerance for solver. Value written to ``simulation.solver.atol``. rtol : float Relative tolerance for solver. Value written to ``simulation.solver.rtol``. full_SA : bool If True, OpenMKM will do a full sensitivity analysis using the Fisher Information Matrix (FIM). Value written to ``simulation.sensitivity.full``. reactions_SA : list of str or list of :class:`~pmutt.omkm.reaction.SurfaceReaction` obj List of reactions to perturb using local sensitivity analysis (LSA). If a list of :class:`~pmutt.omkm.reaction.SurfaceReaction` is given, the ``id`` attribute will be used. species_SA : list of str or list of :class:`~pmutt.empirical.EmpiricalBase` obj List of species to perturb using local sensitivity analysis (LSA). If a list of :class:`~pmutt.empirical.EmpiricalBase` is given, the ``name`` attribute will be used. phases : list of ``Phase`` objects Phases present in reactor. Each phase should have the ``name`` and ``initial_state`` attribute. reactor : dict Generic dictionary for reactor to specify values not supported by ``write_yaml``. inlet_gas : dict Generic dictionary for inlet_gas to specify values not supported by ``write_yaml``. multi_T : list of float Multiple temperatures (in K) of reactor. Value written to ``simulation.multi_input.temperature``. multi_P : list of float/str Multiple pressures of reactor. Value written to ``simulation.multi_input.pressure``. Units of pressure. See Notes section regarding unit specification. multi_flow_rate : list of float/str Multiple flow rates to run the model. Value written to ``simulation.multi_input.flow_rate``. Units of length3/time. See Notes section regarding unit specification. output_format : str Format for output files. Supported options include: - CSV - DAT Value written to ``simulation.output_format``. misc : dict Generic dictionary for any parameter specified at the top level. solver : dict Generic dictionary for solver to specify values not supported by ``write_yaml``. simulation : dict Generic dictionary for simultaion to specify values not supported by ``write_yaml``. multi_input : dict Generic dictionary for multi_input to specify values not supported by ``write_yaml``. units : dict or :class:`~pmutt.omkm.units.Unit` object, optional Units used for file. If a dict is inputted, the key is the quantity and the value is the unit. If not specified, uses the default units of :class:`~pmutt.omkm.units.Unit`. filename: str, optional Filename for the input.cti file. If not specified, returns file as str. yaml_options : dict Options to pass when converting the parameters to YAML format. See `PyYAML documentation`_ for ``dump`` for available options. Returns ------- lines_out : str If ``filename`` is None, CTI file is returned Notes ----- **Units** If ``units`` is not specified, all values in file are assumed to be SI units. If ``units`` is specified, all values inputted are assumed to be in the units specified by ``units``. If a particular unit set is desired for a value, a str can be inputted with the form quantity="<<value>> <<desired units>>" where ``value`` is float-like and ``desired units`` is a string. For example, flow_rate="1 cm3/s". See https://vlachosgroup.github.io/openmkm/input for the most up-to-date supported values. **Generic Dictionaries** Also, values in generic dictionaries (i.e. ``reactor``, ``inlet_gas``, ``simulation``) will ben written preferentially over arguments passed. i.e. The value in ``reactor['temperature']`` will be written instead of ``T``. .. _`PyYAML Documentation`: https://pyyaml.org/wiki/PyYAMLDocumentation """ lines = [ _get_file_timestamp(comment_char='# '), '# See documentation for OpenMKM YAML file here:', '# https://vlachosgroup.github.io/openmkm/input' ] '''Initialize units''' if isinstance(units, dict): units = Units(**units) '''Organize reactor parameters''' if reactor is None: reactor = {} reactor_params = [ _Param('type', reactor_type, None), _Param('mode', mode, None), _Param('nodes', nodes, None), _Param('volume', V, '_length3'), _Param('area', A, '_length2'), _Param('length', L, '_length'), _Param('cat_abyv', cat_abyv, '/_length') ] # Process temperature if T is not None: reactor_params.append(_Param('temperature', T, None)) elif multi_T is not None: reactor_params.append(_Param('temperature', multi_T[0], None)) # Process pressure if P is not None: reactor_params.append(_Param('pressure', P, '_pressure')) elif multi_P is not None: reactor_params.append(_Param('pressure', multi_P[0], '_pressure')) for parameter in reactor_params: _assign_yaml_val(parameter, reactor, units) if inlet_gas is None: inlet_gas = {} inlet_gas_params = [ _Param('residence_time', residence_time, '_time'), _Param('mass_flow_rate', mass_flow_rate, '_mass/_time') ] # Process flow rate if flow_rate is not None: inlet_gas_params.append( _Param('flow_rate', flow_rate, '_length3/_time')) elif multi_flow_rate is not None: inlet_gas_params.append( _Param('flow_rate', multi_flow_rate[0], '_length3/_time')) '''Process inlet gas parameters''' for parameter in inlet_gas_params: _assign_yaml_val(parameter, inlet_gas, units) '''Organize solver parameters''' if solver is None: solver = {} solver_params = [_Param('atol', atol, None), _Param('rtol', rtol, None)] for parameter in solver_params: _assign_yaml_val(parameter, solver, units) '''Organize multi parameters''' if multi_input is None: multi_input = {} # Ensure multi quantities are in correct type if _is_iterable(multi_T): multi_T = list(multi_T) if _is_iterable(multi_P): multi_P = list(multi_P) if _is_iterable(multi_flow_rate): multi_flow_rate = list(multi_flow_rate) multi_input_params = [ _Param('temperature', multi_T, None), _Param('pressure', multi_P, '_pressure'), _Param('flow_rate', multi_flow_rate, '_length3/_time') ] for parameter in multi_input_params: _assign_yaml_val(parameter, multi_input, units) '''Organize sensitivity parameters''' sensitivity = {} if reactions_SA is None: reactions_SA_names = None else: reactions_SA_names = [] for reaction in reactions_SA: if isinstance(reaction, str): name = reaction else: try: name = reaction.id except AttributeError: err_msg = ('Unable to write reaction id to reactor YAML ' 'file because object is invalid type ({}). ' 'Expected str or ' 'pmutt.omkm.reaction.SurfaceReaction type with ' 'id attribute assigned.' ''.format(type(reaction))) raise TypeError(err_msg) reactions_SA_names.append(name) if species_SA is None: species_SA_names = None else: species_SA_names = [] for ind_species in species_SA: if isinstance(ind_species, str): name = ind_species else: try: name = ind_species.name except AttributeError: err_msg = ('Unable to write species name to reactor YAML ' 'file because object is invalid type ({}). ' 'Expected str or ' 'pmutt.empirical.EmpiricalBase type with ' 'name attribute assigned.' ''.format(type(ind_species))) raise TypeError(err_msg) species_SA_names.append(name) sensitivity_params = (_Param('full', full_SA, None), _Param('reactions', reactions_SA_names, None), _Param('species', species_SA_names, None)) for parameter in sensitivity_params: _assign_yaml_val(parameter, sensitivity, units) '''Organize simulation parameters''' if simulation is None: simulation = {} simulation_params = (_Param('end_time', end_time, '_time'), _Param('transient', transient, None), _Param('stepping', stepping, None), _Param('step_size', step_size, None), _Param('init_step', init_step, None), _Param('output_format', output_format, None)) for parameter in simulation_params: _assign_yaml_val(parameter, simulation, units) if len(solver) > 0: _assign_yaml_val(_Param('solver', solver, None), simulation, units) if len(multi_input) > 0: _assign_yaml_val(_Param('multi_input', multi_input, None), simulation, units) if len(sensitivity) > 0: _assign_yaml_val(_Param('sensitivity', sensitivity, None), simulation, units) '''Organize phase parameters''' if phases is not None: if isinstance(phases, dict): phases_dict = phases.copy() elif isinstance(phases, list): phases_dict = {} for phase in phases: phase_info = {'name': phase.name} # Assign intial state if available if phase.initial_state is not None: initial_state_str = '"' for species, mole_frac in phase.initial_state.items(): initial_state_str += '{}:{}, '.format( species, mole_frac) initial_state_str = '{}"'.format(initial_state_str[:-2]) phase_info['initial_state'] = initial_state_str # Assign phase type if isinstance(phase, IdealGas): phase_type = 'gas' elif isinstance(phase, StoichSolid): phase_type = 'bulk' elif isinstance(phase, InteractingInterface): phase_type = 'surfaces' try: phases_dict[phase_type].append(phase_info) except KeyError: phases_dict[phase_type] = [phase_info] # If only one entry for phase type, reassign it. for phase_type, phases in phases_dict.items(): if len(phases) == 1 and phase_type != 'surfaces': phases_dict[phase_type] = phases[0] '''Assign misc values''' if misc is None: misc = {} yaml_dict = misc.copy() '''Assign values to overall YAML dict''' headers = (('reactor', reactor), ('inlet_gas', inlet_gas), ('simulation', simulation), ('phases', phases_dict)) for header in headers: if len(header[1]) > 0: yaml_dict[header[0]] = header[1] '''Convert dictionary to YAML str''' yaml_str = yaml.dump(yaml_dict, **yaml_options) # Remove redundant quotes yaml_str = yaml_str.replace('\'', '') lines.append(yaml_str) '''Write to file''' lines_out = '\n'.join(lines) if filename is not None: with open(filename, 'w', newline=newline) as f_ptr: f_ptr.write(lines_out) else: # Or return as string return lines_out
def plot_coordinate_diagram(self, source, target, method_name, units=None, cutoff=None, max_energy_span=None, max_paths=None, pathway_numbers=None, min_x_spacing=1., x_width=0.5, x_scale_TS=0.5, y_scale_TS=0.5, x_label_offset=-0.1, y_label_offset=0.1, species_delimiter='+', viewer='matplotlib', show_state_table=True, show_state_labels=True, table_font_size=None, table_width_ratio=[3, 1], show_energy_span=False, energy_span_format='.2f', colors=None, **kwargs): """Plots the reaction coordinate diagram Parameters ---------- source : str Initial state as string. All pathways will start here. target : str or list of str Final state as string. All pathways will end here method_name : str Method to evaluate property of the states. Examples include: 'get_H', 'get_HoRT', 'get_G', 'get_GoRT' units : str, optional Units to use to evaluate method_name. Must be specified if the `method_name` returns a dimensional property cutoff : int, optional Maximum number of states in the pathway. If not specified, all pathways are shown max_energy_span : float, optional If specified, pathways with larger energy spans are eliminated max_paths : int, optional If specified, the number of pathways plotted are limited pathway_numbers : list of int, optional If specified, only certain pathways are plotted min_x_spacing : float, optional Minimum spacing between states. Default is 1. x_width : float, optional Spacing of stable states. Default is 0.5 x_scale_TS : float, optional Value between 0 and 1 that controls curvature of transition state peaks. Higher values produce sharper peaks. Default is 0.5 y_scale_TS : float, optional Value between 0 and 1 that controls curvature of transition state peaks. Higher values produce sharper peaks. Default is 0.5 x_label_offset : float, optional Horizontal value to offset TS_label from the TS position. This value scales with the difference between major ticks. Negative values will shift the label leftward. Default is -0.1 y_label_offset : float, optional Vertical value to offset TS_label from the TS position. This value scales with the difference between major ticks. Negative values will shift the label downwards. Default is 0.1 species_delimiter : str, optional Delimiter that separate species for target and source. Leading and trailing spaces will be trimmed. Default is '+' viewer : str, optional Visualization package to use. Currently, the accepted options are: - 'matplotlib' (default) - 'pygal' show_state_table : bool, optional Only applies if `viewer` = 'matplotlib'. If True, a table of the states is printed with the diagram. Default is True show_state_labels : bool, optional Only applies if `viewer` = 'matplotlib'. If True, numbers are added to the states which correspond to the entries in the table. Default is True table_font_size : int, optional Only applies if `viewer` = 'matplotlib'. Controls the text font size. If not specified, font rescales with figure size table_width_ratio : list of int, optional Only applies if `viewer` = 'matplotlib'. Controls the relative size of diagram to table. i.e. [2, 1] will make the diagram width twice as large as the table. Default is [3, 1] show_energy_span : bool, optional If True, adds energy span value to legend. Default is True energy_span_format : str, optional String format for energy span in legend. Default is 2 floating decimal points colors : list of str, optional Colors to use for reaction plots kwargs: keyword arguments Extra arguments that will be fed to evaluate reaction states Returns ------- figure : `matplotlib.figure.Figure`_ Figure axes : tuple of `matplotlib.axes.Axes.axis`_ Axes of the plot. Raises ------ ValueError : Raised when `viewer` is not supported. """ # Get the y axis value for the axis label y_title = method_name.replace('get_', '') if units is not None: y_title = '{} ({})'.format(y_title, units) # Initialize plot using appropriate viewer if viewer == 'matplotlib': # Split graph into two axes if including the table if show_state_table: fig, axes = plt.subplots( ncols=2, gridspec_kw={'width_ratios': table_width_ratio}) else: fig, axes = plt.subplots() axes = [axes] elif viewer == 'pygal': # Use the tooltip x value to indicate what the y value indicates x_value_formatter = lambda x: y_title # Edit style sheet to have the same color for points and colors style = pygal.style.DefaultStyle new_colors = [] if colors is None: colors = style.colors for color in colors: new_colors.extend([color] * 2) style.colors = new_colors # Initialize the graph graph = pygal.XY(x_title='Reaction Coordinate', y_title=y_title, pretty_print=True, show_y_guides=False, show_x_guides=False, show_x_labels=False, x_value_formatter=x_value_formatter, style=style, truncate_legend=-1) else: err_msg = ( 'Viewer {} not supported. Type help(pmutt.reaction.' 'network.Network) for supported options.'.format(viewer)) raise ValueError(err_msg) # If the pathway to plot was specified as an integer, convert to a list if pathway_numbers is not None and not _is_iterable(pathway_numbers): pathway_numbers = [pathway_numbers] # Encode inital and final node source_names, source_stoich = _parse_reaction_state(source) source_set = state_str_to_set(source_names, source_stoich) target_sets = _get_target_sets(target=target, species_delimiter=species_delimiter) # Get all the pathways and associated data for sorting paths = list(path for path in nx.all_simple_paths( self.graph, source=source_set, target=target_sets, cutoff=cutoff)) path_lens = list(len(path) for path in paths) energy_spans = list( self.get_E_span(path, units, **kwargs) for path in paths) # Get n paths with smallest energy span if max_paths is not None: paths_data = [ (span, path_len, path) for span, path_len, path in zip(energy_spans, path_lens, paths) ] paths_data_reduced = heapq.nsmallest(max_paths, paths_data) energy_spans, path_lens, paths = map(list, zip(*paths_data_reduced)) # Remove pathways greater than the limit if any if max_energy_span is not None: for i in range(len(paths) - 1, -1, -1): if energy_spans[i] > max_energy_span: del paths[i], path_lens[i], energy_spans[i] # Sort in descending order of path length _, paths_sorted, energy_spans_sorted = zip( *sorted(zip(path_lens, paths, energy_spans), reverse=True)) # Determine x values for each state x_vals = {} for path in paths_sorted: x_spacing = min_x_spacing # Find duplicates duplicates = tuple(state for state in path if state in x_vals) # If there are no duplicates, assign all the states to values and # move to next path if len(duplicates) == 0: for i, state in enumerate(path): x_vals[state] = i * x_spacing continue # If there is only one duplicate if len(duplicates) == 1: # If it is the last index, then assign to the length of the # reaction. if duplicates[0] == path[-1]: x_vals[duplicates[0]] = i * x_spacing * ( np.max(path_lens) - 1) else: i = path.index(duplicates[0]) prev_state = path[i - 1] next_state = path[i + 1] x_vals[duplicates[0]] = np.mean( [x_vals[prev_state], x_vals[next_state]]) continue # Skip this path if all the states are duplicates if len(duplicates) == len(path): continue # Check adjacent duplicates for intermedient elements for duplicate_i, duplicate_j in zip(duplicates, duplicates[1:]): i = path.index(duplicate_i) j = path.index(duplicate_j) # Skip if adjacent duplicates are also adjacent in reaction path if j - i == 1: continue # Calculate spacing x_spacing = (x_vals[duplicate_j] - x_vals[duplicate_i]) / (j - i) # Assign x positions for new states x_initial = x_vals[duplicate_i] for l, k in enumerate(range(i + 1, j), start=1): x_vals[path[k]] = x_spacing * l + x_initial # Sort ascending order by energy span energy_spans_sorted, paths_sorted = zip( *sorted(zip(energy_spans_sorted, paths_sorted))) # Assign x, y values for plot labels_list = [] labels_set = set() y_states = {} n_paths = len(paths_sorted) for i, (path, energy_span) in enumerate(zip(paths_sorted, energy_spans_sorted), start=1): # If pathway_numbers set, skips pathways not specified if pathway_numbers is not None and i not in pathway_numbers: continue # Initialize x, y points for continuous line x_plot = [] y_plot = [] # Initialize x, y points for interactive points (when viewer is # pygal) x_points = [] y_points = [] # Generate legend for trend if show_energy_span: if units is None: units_str = '' else: units_str = units path_name = 'Pathway {:>3} ({:%s} {})' % energy_span_format path_name = path_name.format(i, energy_span, units_str) else: path_name = 'Pathway {:>3}'.format(i) point_name = 'Points {:>4}'.format(i) for j, state in enumerate(path): # If unique state found, add it to the label set if state not in labels_set: labels_list.append(state) labels_set.add(state) # Get x and y value x_state = x_vals[state] species = self.graph.nodes[state]['species'] stoich = self.graph.nodes[state]['stoich'] y_val = get_state_quantity(species=species, stoich=stoich, method_name=method_name, units=units, **kwargs) # Subtract the initial state's energy if j == 0: y_ref = y_val y_state = y_val - y_ref y_states[state] = y_state # Generate continuous points for plot if self.graph.nodes[state]['is_transition_state']: # Calculate product properties for y interpolation products = self.graph.nodes[path[j + 1]]['species'] prod_stoich = self.graph.nodes[path[j + 1]]['stoich'] y_prod = get_state_quantity(species=products, stoich=prod_stoich, method_name=method_name, units=units, **kwargs) - y_ref # Fit spline delta_x = x_state - x_plot[-1] delta_y = y_state - y_plot[-1] x_fit = np.array([ x_plot[-1], x_state - delta_x * x_scale_TS, x_state, x_state + delta_x * x_scale_TS, x_state + delta_x ]) y_fit = np.array([ y_plot[-1], y_state - delta_y * y_scale_TS, y_state, (y_state - y_prod) * y_scale_TS + y_prod, y_prod ]) tck = interpolate.splrep(x_fit, y_fit, k=2) # Calculate new x and y points from spline fit x_spline = np.linspace(x_state - delta_x, x_state + delta_x, 100) y_spline = interpolate.splev(x_spline, tck) # Get x value corresponding to peak for pygal max_i = np.argmax(y_spline) x_points.append(x_spline[max_i]) y_points.append(y_spline[max_i]) # Add new data to the appropriate lists x_plot.extend(x_spline) y_plot.extend(y_spline) else: # For intermediates, use a straight line x_plot.extend([ x_state - x_width / 2., x_state, x_state + x_width / 2. ]) y_plot.extend([y_state, y_state, y_state]) x_points.append(x_state) y_points.append(y_state) # Add data to plot if viewer == 'matplotlib': axes[0].plot(x_plot, y_plot, label=path_name, zorder=n_paths - i) elif viewer == 'pygal': # Add line line_data = [{ 'value': (x, y), } for x, y in zip(x_plot, y_plot)] graph.add(path_name, line_data, show_dots=False) # Add interactive points point_data = [{ 'value': (x, y), 'label': self.graph.nodes[state]['name'], } for x, y, state in zip(x_points, y_points, path)] graph.add(point_name, point_data, stroke=False) if viewer == 'matplotlib': # Add other misc labels axes[0].legend() axes[0].set_ylabel(y_title) axes[0].set_xlabel('Reaction coordinate') axes[0].tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False) # Add state labels if show_state_labels: for i, label in enumerate(labels_list, start=1): axes[0].text(x=x_vals[label] + x_label_offset, y=y_states[label] + y_label_offset, s='{:^}'.format(i)) # Add table if show_state_table: axes[1].axis('off') # Setting up table info columns = ('State', ) rows = range(1, len(labels_list) + 1) cellText = tuple([self.graph.nodes[state]['name']] for state in labels_list) # Adding table table = axes[1].table(cellText=cellText, colLabels=columns, rowLabels=rows, loc='center') # Adjust font size if table_font_size is not None: table.auto_set_font_size(False) table.set_fontsize(table_font_size) return fig, axes else: return graph
def from_model(cls, model, name=None, T_low=None, T_high=None, elements=None, n_T=50, units='J/mol/K', **kwargs): """Calculates the NASA polynomials using the model passed Parameters ---------- model : Model object or class Model to generate data. Must contain the methods `get_CpoR`, `get_HoRT` and `get_SoR` name : str, optional Name of the species. If not passed, `model.name` will be used. T_low : float, optional Lower limit temerature in K. If not passed, `model.T_low` will be used. T_high : float, optional Higher limit temperature in K. If not passed, `model.T_high` will be used. elements : dict, optional Composition of the species. If not passed, `model.elements` will be used. Keys of dictionary are elements, values are stoichiometric values in a formula unit. e.g. CH3OH can be represented as: {'C': 1, 'H': 4, 'O': 1,}. n_T : int, optional Number of data points between `T_low` and `T_high` for fitting heat capacity. Default is 50. units : str, optional Units used to fit the Shomate polynomial. Units should be supported by :class:`~pmutt.constants.R` (e.g. J/mol/K, cal/mol/K, eV/K). Default is J/mol/K. kwargs : keyword arguments Used to initalize model if a class is passed. Returns ------- Shomate : Shomate object Shomate object with polynomial terms fitted to data. """ # Initialize the model object if inspect.isclass(model): model = model(name=name, elements=elements, **kwargs) if name is None: try: name = model.name except AttributeError: err_msg = ('Name must either be passed to from_model directly ' 'or be an attribute of model.') raise AttributeError(err_msg) if T_low is None: try: T_low = model.T_low except AttributeError: err_msg = ('T_low must either be passed to from_model ' 'directly or be an attribute of model.') raise AttributeError(err_msg) if T_high is None: try: T_high = model.T_high except AttributeError: err_msg = ('T_high must either be passed to from_model ' 'directly or be an attribute of model.') raise AttributeError(err_msg) if elements is None: try: elements = model.elements except AttributeError: pass # Check if inputted T_low and T_high are outside model's T_low and # T_high range try: model.T_low except AttributeError: pass else: if T_low < model.T_low: warn_msg = ('Inputted T_low ({} K) is lower than model ' 'T_low ({} K). Fitted empirical object may not be ' 'valid.' ''.format(T_low, model.T_low)) warn(warn_msg, UserWarning) try: if T_high > model.T_high: warn_msg = ('Inputted T_high ({} K) is higher than model ' 'T_high ({} K). Fitted empirical object may not be ' 'valid.' ''.format(T_high, model.T_high)) warn(warn_msg, UserWarning) except AttributeError: pass # Generate heat capacity data T = np.linspace(T_low, T_high, n_T) try: CpoR = model.get_CpoR(T=T) except ValueError: CpoR = np.array([model.get_CpoR(T=T_i) for T_i in T]) else: if not _is_iterable(CpoR) or len(CpoR) != len(T): CpoR = np.array([model.get_CpoR(T=T_i) for T_i in T]) # Generate enthalpy and entropy data T_mean = (T_low + T_high) / 2. HoRT_ref = model.get_HoRT(T=T_mean) SoR_ref = model.get_SoR(T=T_mean) return cls.from_data(name=name, T=T, CpoR=CpoR, T_ref=T_mean, HoRT_ref=HoRT_ref, SoR_ref=SoR_ref, model=model, elements=elements, units=units, **kwargs)