class ArkaneSpecies(RMGObject):
    """
    A class for archiving an Arkane species including its statmech data into .yml files
    """
    def __init__(self, species=None, conformer=None, author='', level_of_theory='', model_chemistry='',
                 frequency_scale_factor=None, use_hindered_rotors=None, use_bond_corrections=None, atom_energies='',
                 chemkin_thermo_string='', smiles=None, adjacency_list=None, inchi=None, inchi_key=None, xyz=None,
                 molecular_weight=None, symmetry_number=None, transport_data=None, energy_transfer_model=None,
                 thermo=None, thermo_data=None, label=None, datetime=None, RMG_version=None, reactants=None,
                 products=None, reaction_label=None, is_ts=None):
        # reactants/products/reaction_label need to be in the init() to avoid error when loading a TS YAML file,
        # but we don't use them
        if species is None and conformer is None:
            # Expecting to get a species or a TS when generating the object within Arkane,
            # or a conformer when parsing from YAML.
            raise ValueError('No species (or TS) or conformer was passed to the ArkaneSpecies object')
        if conformer is not None:
            self.conformer = conformer
        if label is None and species is not None:
            self.label = species.label
        else:
            self.label = label
        self.author = author
        self.level_of_theory = level_of_theory
        self.model_chemistry = model_chemistry
        self.frequency_scale_factor = frequency_scale_factor
        self.use_hindered_rotors = use_hindered_rotors
        self.use_bond_corrections = use_bond_corrections
        self.atom_energies = atom_energies
        self.xyz = xyz
        self.molecular_weight = molecular_weight
        self.symmetry_number = symmetry_number
        self.is_ts = is_ts if is_ts is not None else isinstance(species, TransitionState)
        if not self.is_ts:
            self.chemkin_thermo_string = chemkin_thermo_string
            self.smiles = smiles
            self.adjacency_list = adjacency_list
            self.inchi = inchi
            self.inchi_key = inchi_key
            self.transport_data = transport_data
            self.energy_transfer_model = energy_transfer_model
            self.thermo = thermo
            self.thermo_data = thermo_data
        else:
            # initialize TS-related attributes
            self.imaginary_frequency = None
            self.reaction_label = ''
            self.reactants = list()
            self.products = list()
        if species is not None:
            self.update_species_attributes(species)
        self.RMG_version = RMG_version if RMG_version is not None else __version__
        self.datetime = datetime if datetime is not None else time.strftime("%Y-%m-%d %H:%M")

    def __repr__(self):
        """
        Return a string representation that can be used to reconstruct the object
        """
        result = '{0!r}'.format(self.__class__.__name__)
        result += '{'
        for key, value in self.as_dict().iteritems():
            if key != 'class':
                result += '{0!r}: {1!r}'.format(str(key), str(value))
        result += '}'
        return result

    def update_species_attributes(self, species=None):
        """
        Update the object with a new species/TS (while keeping non-species-dependent attributes unchanged)
        """
        if species is None:
            raise ValueError('No species was passed to ArkaneSpecies')
        self.label = species.label
        if isinstance(species, TransitionState):
            self.imaginary_frequency = species.frequency
            if species.conformer is not None:
                self.conformer = species.conformer
                self.xyz = self.update_xyz_string()
        elif species.molecule is not None and len(species.molecule) > 0:
            self.smiles = species.molecule[0].toSMILES()
            self.adjacency_list = species.molecule[0].toAdjacencyList()
            try:
                inchi = toInChI(species.molecule[0], backend='try-all', aug_level=0)
            except ValueError:
                inchi = ''
            try:
                inchi_key = toInChIKey(species.molecule[0], backend='try-all', aug_level=0)
            except ValueError:
                inchi_key = ''
            self.inchi = inchi
            self.inchi_key = inchi_key
            if species.conformer is not None:
                self.conformer = species.conformer
                self.xyz = self.update_xyz_string()
            self.molecular_weight = species.molecularWeight
            if species.symmetryNumber != -1:
                self.symmetry_number = species.symmetryNumber
            if species.transportData is not None:
                self.transport_data = species.transportData  # called `collisionModel` in Arkane
            if species.energyTransferModel is not None:
                self.energy_transfer_model = species.energyTransferModel
            if species.thermo is not None:
                self.thermo = species.thermo.as_dict()
                thermo_data = species.getThermoData()
                h298 = thermo_data.getEnthalpy(298) / 4184.
                s298 = thermo_data.getEntropy(298) / 4.184
                cp = dict()
                for t in [300,400,500,600,800,1000,1500,2000,2400]:
                    temp_str = '{0} K'.format(t)
                    cp[temp_str] = '{0:.2f}'.format(thermo_data.getHeatCapacity(t) / 4.184)
                self.thermo_data = {'H298': '{0:.2f} kcal/mol'.format(h298),
                                    'S298': '{0:.2f} cal/mol*K'.format(s298),
                                    'Cp (cal/mol*K)': cp}

    def update_xyz_string(self):
        if self.conformer is not None and self.conformer.number is not None:
            # generate the xyz-format string from the Conformer coordinates
            xyz_string = '{0}\n{1}'.format(len(self.conformer.number.value_si), self.label)
            for i, coorlist in enumerate(self.conformer.coordinates.value_si):
                for element in elementList:
                    if element.number == int(self.conformer.number.value_si[i]):
                        element_symbol = element.symbol
                        break
                else:
                    raise ValueError('Could not find element symbol corresponding to atom number {0}'.format(
                        self.conformer.number.value_si[i]))
                xyz_string += '\n{0} {1} {2} {3}'.format(element_symbol,
                                                         coorlist[0],
                                                         coorlist[1],
                                                         coorlist[2])
        else:
            xyz_string = ''
        return xyz_string

    def save_yaml(self, path):
        """
        Save the species with all statMech data to a .yml file
        """
        if not os.path.exists(os.path.join(os.path.abspath(path), 'species', '')):
            os.mkdir(os.path.join(os.path.abspath(path), 'species', ''))
        valid_chars = "-_.()<=>+ %s%s" % (string.ascii_letters, string.digits)
        filename = os.path.join('species',
                                ''.join(c for c in self.label if c in valid_chars) + '.yml')
        full_path = os.path.join(path, filename)
        with open(full_path, 'w') as f:
            yaml.dump(data=self.as_dict(), stream=f)
        logging.debug('Dumping species {0} data as {1}'.format(self.label, filename))

    def load_yaml(self, path, species, pdep=False):
        """
        Load the all statMech data from the .yml file in `path` into `species`
        `pdep` is a boolean specifying whether or not jobList includes a pressureDependentJob.
        """
        logging.info('Loading statistical mechanics parameters for {0} from .yml file...'.format(species.label))
        with open(path, 'r') as f:
            data = yaml.safe_load(stream=f)
        try:
            if species.label != data['label']:
                logging.debug('Found different labels for species: {0} in input file, and {1} in the .yml file. '
                              'Using the label "{0}" for this species.'.format(species.label, data['label']))
        except KeyError:
            # Lacking label in the YAML file is strange, but accepted
            logging.debug('Did not find label for species {0} in .yml file.'.format(species.label))
        try:
            class_name = data['class']
        except KeyError:
            raise KeyError("Can only make objects if the `class` attribute in the dictionary is known")
        if class_name != 'ArkaneSpecies':
            raise KeyError("Expected a ArkaneSpecies object, but got {0}".format(class_name))
        del data['class']
        class_dict = {'ScalarQuantity': ScalarQuantity,
                      'ArrayQuantity': ArrayQuantity,
                      'Conformer': Conformer,
                      'LinearRotor': LinearRotor,
                      'NonlinearRotor': NonlinearRotor,
                      'KRotor': KRotor,
                      'SphericalTopRotor': SphericalTopRotor,
                      'HinderedRotor': HinderedRotor,
                      'FreeRotor': FreeRotor,
                      'IdealGasTranslation': IdealGasTranslation,
                      'HarmonicOscillator': HarmonicOscillator,
                      'TransportData': TransportData,
                      'SingleExponentialDown': SingleExponentialDown,
                      'Wilhoit': Wilhoit,
                      'NASA': NASA,
                      }
        freq_data = None
        if 'imaginary_frequency' in data:
            freq_data = data['imaginary_frequency']
            del data['imaginary_frequency']
        self.make_object(data=data, class_dict=class_dict)
        if freq_data is not None:
            self.imaginary_frequency = ScalarQuantity()
            self.imaginary_frequency.make_object(data=freq_data, class_dict=dict())
        self.adjacency_list = data['adjacency_list'] if 'adjacency_list' in data else None
        self.inchi = data['inchi'] if 'inchi' in data else None
        self.smiles = data['smiles'] if 'smiles' in data else None
        self.is_ts = data['is_ts'] if 'is_ts' in data else False
        if pdep and not self.is_ts and (self.transport_data is None or self.energy_transfer_model is None):
            raise ValueError('Transport data and an energy transfer model must be given if pressure-dependent '
                             'calculations are requested. Check file {0}'.format(path))
        if pdep and not self.is_ts and self.smiles is None and self.adjacency_list is None\
                and self.inchi is None and self.molecular_weight is None:
            raise ValueError('The molecular weight was not specified, and a structure was not given so it could '
                             'not be calculated. Specify either the molecular weight or structure if '
                             'pressure-dependent calculations are requested. Check file {0}'.format(path))
        logging.debug("Parsed all YAML objects")
Exemple #2
0
class ArkaneSpecies(RMGObject):
    """
    A class for archiving an Arkane species including its statmech data into .yml files
    """
    def __init__(self,
                 species=None,
                 conformer=None,
                 author='',
                 level_of_theory='',
                 model_chemistry='',
                 frequency_scale_factor=None,
                 use_hindered_rotors=None,
                 use_bond_corrections=None,
                 atom_energies='',
                 chemkin_thermo_string='',
                 smiles=None,
                 adjacency_list=None,
                 inchi=None,
                 inchi_key=None,
                 xyz=None,
                 molecular_weight=None,
                 symmetry_number=None,
                 transport_data=None,
                 energy_transfer_model=None,
                 thermo=None,
                 thermo_data=None,
                 label=None,
                 datetime=None,
                 RMG_version=None,
                 reactants=None,
                 products=None,
                 reaction_label=None,
                 is_ts=None,
                 charge=None,
                 formula=None,
                 multiplicity=None):
        # reactants/products/reaction_label need to be in the init() to avoid error when loading a TS YAML file,
        # but we don't use them
        super(ArkaneSpecies, self).__init__()
        if species is None and conformer is None:
            # Expecting to get a species or a TS when generating the object within Arkane,
            # or a conformer when parsing from YAML.
            raise ValueError(
                'No species (or TS) or conformer was passed to the ArkaneSpecies object'
            )
        if conformer is not None:
            self.conformer = conformer
        if label is None and species is not None:
            self.label = species.label
        else:
            self.label = label
        self.author = author
        self.level_of_theory = level_of_theory
        self.model_chemistry = model_chemistry
        self.frequency_scale_factor = frequency_scale_factor
        self.use_hindered_rotors = use_hindered_rotors
        self.use_bond_corrections = use_bond_corrections
        self.atom_energies = atom_energies
        self.xyz = xyz
        self.molecular_weight = molecular_weight
        self.symmetry_number = symmetry_number
        self.charge = charge
        self.multiplicity = multiplicity
        self.is_ts = is_ts if is_ts is not None else isinstance(
            species, TransitionState)
        if not self.is_ts:
            self.chemkin_thermo_string = chemkin_thermo_string
            self.smiles = smiles
            self.adjacency_list = adjacency_list
            self.inchi = inchi
            self.inchi_key = inchi_key
            self.transport_data = transport_data
            self.energy_transfer_model = energy_transfer_model
            self.thermo = thermo
            self.thermo_data = thermo_data
            self.formula = formula
        else:
            # initialize TS-related attributes
            self.imaginary_frequency = None
            self.reaction_label = ''
            self.reactants = list()
            self.products = list()
        if species is not None:
            self.update_species_attributes(species)
        self.RMG_version = RMG_version if RMG_version is not None else __version__
        self.datetime = datetime if datetime is not None else time.strftime(
            "%Y-%m-%d %H:%M")

    def __repr__(self):
        """
        Return a string representation that can be used to reconstruct the object
        """
        result = '{0!r}'.format(self.__class__.__name__)
        result += '{'
        for key, value in self.as_dict().items():
            if key != 'class':
                result += '{0!r}: {1!r}'.format(str(key), str(value))
        result += '}'
        return result

    def update_species_attributes(self, species=None):
        """
        Update the object with a new species/TS (while keeping non-species-dependent attributes unchanged)
        """
        if species is None:
            raise ValueError('No species was passed to ArkaneSpecies')
        # Don't overwrite the label if it already exists
        self.label = self.label or species.label
        if isinstance(species, TransitionState):
            self.imaginary_frequency = species.frequency
            if species.conformer is not None:
                self.conformer = species.conformer
                self.xyz = self.update_xyz_string()
        elif species.molecule is not None and len(species.molecule) > 0:
            self.smiles = species.molecule[0].to_smiles()
            self.adjacency_list = species.molecule[0].to_adjacency_list()
            self.charge = species.molecule[0].get_net_charge()
            self.multiplicity = species.molecule[0].multiplicity
            self.formula = species.molecule[0].get_formula()
            try:
                inchi = to_inchi(species.molecule[0],
                                 backend='try-all',
                                 aug_level=0)
            except ValueError:
                inchi = ''
            try:
                inchi_key = to_inchi_key(species.molecule[0],
                                         backend='try-all',
                                         aug_level=0)
            except ValueError:
                inchi_key = ''
            self.inchi = inchi
            self.inchi_key = inchi_key
            if species.conformer is not None:
                self.conformer = species.conformer
                self.xyz = self.update_xyz_string()
            self.molecular_weight = species.molecular_weight
            if species.symmetry_number != -1:
                self.symmetry_number = species.symmetry_number
            if species.transport_data is not None:
                self.transport_data = species.transport_data  # called `collisionModel` in Arkane
            if species.energy_transfer_model is not None:
                self.energy_transfer_model = species.energy_transfer_model
            if species.thermo is not None:
                self.thermo = species.thermo.as_dict()
                data = species.get_thermo_data()
                h298 = data.get_enthalpy(298) / 4184.
                s298 = data.get_entropy(298) / 4.184
                temperatures = np.array(
                    [300, 400, 500, 600, 800, 1000, 1500, 2000, 2400])
                cp = []
                for t in temperatures:
                    cp.append(data.get_heat_capacity(t) / 4.184)

                self.thermo_data = ThermoData(
                    H298=(h298, 'kcal/mol'),
                    S298=(s298, 'cal/(mol*K)'),
                    Tdata=(temperatures, 'K'),
                    Cpdata=(cp, 'cal/(mol*K)'),
                )

    def update_xyz_string(self):
        """
        Generate an xyz string built from self.conformer, and standardize the result

        Returns:
            str: 3D coordinates in an XYZ format.
        """
        xyz_list = list()
        if self.conformer is not None and self.conformer.number is not None:
            # generate the xyz-format string from self.conformer.coordinates and self.conformer.number
            xyz_list.append(str(len(self.conformer.number.value_si)))
            xyz_list.append(self.label)
            for number, coordinate in zip(self.conformer.number.value_si,
                                          self.conformer.coordinates.value_si):
                element_symbol = get_element(int(number)).symbol
                row = '{0:4}'.format(element_symbol)
                row += '{0:14.8f}{1:14.8f}{2:14.8f}'.format(
                    *(coordinate * 1e10).tolist())  # convert m to Angstrom
                xyz_list.append(row)
        return '\n'.join(xyz_list)

    def save_yaml(self, path):
        """
        Save the species with all statMech data to a .yml file
        """
        if not os.path.exists(
                os.path.join(os.path.abspath(path), 'species', '')):
            os.mkdir(os.path.join(os.path.abspath(path), 'species', ''))
        valid_chars = "-_.()<=>+ %s%s" % (string.ascii_letters, string.digits)
        filename = os.path.join(
            'species',
            ''.join(c for c in self.label if c in valid_chars) + '.yml')
        full_path = os.path.join(path, filename)
        with open(full_path, 'w') as f:
            yaml.dump(data=self.as_dict(), stream=f)
        logging.debug('Dumping species {0} data as {1}'.format(
            self.label, filename))

    def load_yaml(self, path, label=None, pdep=False):
        """
        Load the all statMech data from the .yml file in `path` into `species`
        `pdep` is a boolean specifying whether or not job_list includes a pressureDependentJob.
        """
        yml_file = os.path.basename(path)
        if label:
            logging.info(
                'Loading statistical mechanics parameters for {0} from {1} file...'
                .format(label, yml_file))
        else:
            logging.info(
                'Loading statistical mechanics parameters from {0} file...'.
                format(yml_file))
        with open(path, 'r') as f:
            content = f.read()
        content = replace_yaml_syntax(content, label)
        data = yaml.safe_load(stream=content)
        if label:
            # First, warn the user if the label doesn't match
            try:
                if label != data['label']:
                    logging.debug(
                        'Found different labels for species: {0} in input file, and {1} in the .yml file. '
                        'Using the label "{0}" for this species.'.format(
                            label, data['label']))
            except KeyError:
                # Lacking label in the YAML file is strange, but accepted
                logging.debug(
                    'Did not find label for species {0} in .yml file.'.format(
                        label))

            # Then, set the ArkaneSpecies label to the user supplied label
            data['label'] = label
        try:
            class_name = data['class']
        except KeyError:
            raise KeyError(
                "Can only make objects if the `class` attribute in the dictionary is known"
            )
        if class_name != 'ArkaneSpecies':
            raise KeyError(
                "Expected a ArkaneSpecies object, but got {0}".format(
                    class_name))
        del data['class']
        freq_data = None
        if 'imaginary_frequency' in data:
            freq_data = data['imaginary_frequency']
            del data['imaginary_frequency']
        if not data['is_ts']:
            if 'smiles' in data:
                data['species'] = Species(smiles=data['smiles'])
            elif 'adjacency_list' in data:
                data['species'] = Species().from_adjacency_list(
                    data['adjacency_list'])
            elif 'inchi' in data:
                data['species'] = Species(inchi=data['inchi'])
            else:
                raise ValueError(
                    'Cannot load ArkaneSpecies from YAML file {0}. Either `smiles`, `adjacency_list`, or '
                    'InChI must be specified'.format(path))
            # Finally, set the species label so that the special attributes are updated properly
            data['species'].label = data['label']

        self.make_object(data=data, class_dict=ARKANE_CLASS_DICT)
        if freq_data is not None:
            self.imaginary_frequency = ScalarQuantity()
            self.imaginary_frequency.make_object(data=freq_data,
                                                 class_dict=ARKANE_CLASS_DICT)

        if pdep and not self.is_ts and self.smiles is None and self.adjacency_list is None \
                and self.inchi is None and self.molecular_weight is None:
            raise ValueError(
                'The molecular weight was not specified, and a structure was not given so it could '
                'not be calculated. Specify either the molecular weight or structure if '
                'pressure-dependent calculations are requested. Check file {0}'
                .format(path))
        logging.debug("Parsed all YAML objects")
Exemple #3
0
class ArkaneSpecies(RMGObject):
    """
    A class for archiving an Arkane species including its statmech data into .yml files
    """
    def __init__(self,
                 species=None,
                 conformer=None,
                 author='',
                 level_of_theory='',
                 model_chemistry='',
                 frequency_scale_factor=None,
                 use_hindered_rotors=None,
                 use_bond_corrections=None,
                 atom_energies='',
                 chemkin_thermo_string='',
                 smiles=None,
                 adjacency_list=None,
                 inchi=None,
                 inchi_key=None,
                 xyz=None,
                 molecular_weight=None,
                 symmetry_number=None,
                 transport_data=None,
                 energy_transfer_model=None,
                 thermo=None,
                 thermo_data=None,
                 label=None,
                 datetime=None,
                 RMG_version=None,
                 reactants=None,
                 products=None,
                 reaction_label=None,
                 is_ts=None):
        # reactants/products/reaction_label need to be in the init() to avoid error when loading a TS YAML file,
        # but we don't use them
        if species is None and conformer is None:
            # Expecting to get a species or a TS when generating the object within Arkane,
            # or a conformer when parsing from YAML.
            raise ValueError(
                'No species (or TS) or conformer was passed to the ArkaneSpecies object'
            )
        if conformer is not None:
            self.conformer = conformer
        if label is None and species is not None:
            self.label = species.label
        else:
            self.label = label
        self.author = author
        self.level_of_theory = level_of_theory
        self.model_chemistry = model_chemistry
        self.frequency_scale_factor = frequency_scale_factor
        self.use_hindered_rotors = use_hindered_rotors
        self.use_bond_corrections = use_bond_corrections
        self.atom_energies = atom_energies
        self.xyz = xyz
        self.molecular_weight = molecular_weight
        self.symmetry_number = symmetry_number
        self.is_ts = is_ts if is_ts is not None else isinstance(
            species, TransitionState)
        if not self.is_ts:
            self.chemkin_thermo_string = chemkin_thermo_string
            self.smiles = smiles
            self.adjacency_list = adjacency_list
            self.inchi = inchi
            self.inchi_key = inchi_key
            self.transport_data = transport_data
            self.energy_transfer_model = energy_transfer_model
            self.thermo = thermo
            self.thermo_data = thermo_data
        else:
            # initialize TS-related attributes
            self.imaginary_frequency = None
            self.reaction_label = ''
            self.reactants = list()
            self.products = list()
        if species is not None:
            self.update_species_attributes(species)
        self.RMG_version = RMG_version if RMG_version is not None else __version__
        self.datetime = datetime if datetime is not None else time.strftime(
            "%Y-%m-%d %H:%M")

    def __repr__(self):
        """
        Return a string representation that can be used to reconstruct the object
        """
        result = '{0!r}'.format(self.__class__.__name__)
        result += '{'
        for key, value in self.as_dict().iteritems():
            if key != 'class':
                result += '{0!r}: {1!r}'.format(str(key), str(value))
        result += '}'
        return result

    def update_species_attributes(self, species=None):
        """
        Update the object with a new species/TS (while keeping non-species-dependent attributes unchanged)
        """
        if species is None:
            raise ValueError('No species was passed to ArkaneSpecies')
        self.label = species.label
        if isinstance(species, TransitionState):
            self.imaginary_frequency = species.frequency
            if species.conformer is not None:
                self.conformer = species.conformer
                self.xyz = self.update_xyz_string()
        elif species.molecule is not None and len(species.molecule) > 0:
            self.smiles = species.molecule[0].toSMILES()
            self.adjacency_list = species.molecule[0].toAdjacencyList()
            try:
                inchi = toInChI(species.molecule[0],
                                backend='try-all',
                                aug_level=0)
            except ValueError:
                inchi = ''
            try:
                inchi_key = toInChIKey(species.molecule[0],
                                       backend='try-all',
                                       aug_level=0)
            except ValueError:
                inchi_key = ''
            self.inchi = inchi
            self.inchi_key = inchi_key
            if species.conformer is not None:
                self.conformer = species.conformer
                self.xyz = self.update_xyz_string()
            self.molecular_weight = species.molecularWeight
            if species.symmetryNumber != -1:
                self.symmetry_number = species.symmetryNumber
            if species.transportData is not None:
                self.transport_data = species.transportData  # called `collisionModel` in Arkane
            if species.energyTransferModel is not None:
                self.energy_transfer_model = species.energyTransferModel
            if species.thermo is not None:
                self.thermo = species.thermo.as_dict()
                thermo_data = species.getThermoData()
                h298 = thermo_data.getEnthalpy(298) / 4184.
                s298 = thermo_data.getEntropy(298) / 4.184
                cp = dict()
                for t in [300, 400, 500, 600, 800, 1000, 1500, 2000, 2400]:
                    temp_str = '{0} K'.format(t)
                    cp[temp_str] = '{0:.2f}'.format(
                        thermo_data.getHeatCapacity(t) / 4.184)
                self.thermo_data = {
                    'H298': '{0:.2f} kcal/mol'.format(h298),
                    'S298': '{0:.2f} cal/mol*K'.format(s298),
                    'Cp (cal/mol*K)': cp
                }

    def update_xyz_string(self):
        if self.conformer is not None and self.conformer.number is not None:
            # generate the xyz-format string from the Conformer coordinates
            xyz_string = '{0}\n{1}'.format(len(self.conformer.number.value_si),
                                           self.label)
            for i, coorlist in enumerate(self.conformer.coordinates.value_si):
                for element in elementList:
                    if element.number == int(
                            self.conformer.number.value_si[i]):
                        element_symbol = element.symbol
                        break
                else:
                    raise ValueError(
                        'Could not find element symbol corresponding to atom number {0}'
                        .format(self.conformer.number.value_si[i]))
                xyz_string += '\n{0} {1} {2} {3}'.format(
                    element_symbol, coorlist[0], coorlist[1], coorlist[2])
        else:
            xyz_string = ''
        return xyz_string

    def save_yaml(self, path):
        """
        Save the species with all statMech data to a .yml file
        """
        if not os.path.exists(
                os.path.join(os.path.abspath(path), 'species', '')):
            os.mkdir(os.path.join(os.path.abspath(path), 'species', ''))
        valid_chars = "-_.()<=>+ %s%s" % (string.ascii_letters, string.digits)
        filename = os.path.join(
            'species',
            ''.join(c for c in self.label if c in valid_chars) + '.yml')
        full_path = os.path.join(path, filename)
        with open(full_path, 'w') as f:
            yaml.dump(data=self.as_dict(), stream=f)
        logging.debug('Dumping species {0} data as {1}'.format(
            self.label, filename))

    def load_yaml(self, path, species, pdep=False):
        """
        Load the all statMech data from the .yml file in `path` into `species`
        `pdep` is a boolean specifying whether or not jobList includes a pressureDependentJob.
        """
        logging.info(
            'Loading statistical mechanics parameters for {0} from .yml file...'
            .format(species.label))
        with open(path, 'r') as f:
            data = yaml.safe_load(stream=f)
        try:
            if species.label != data['label']:
                logging.debug(
                    'Found different labels for species: {0} in input file, and {1} in the .yml file. '
                    'Using the label "{0}" for this species.'.format(
                        species.label, data['label']))
        except KeyError:
            # Lacking label in the YAML file is strange, but accepted
            logging.debug(
                'Did not find label for species {0} in .yml file.'.format(
                    species.label))
        try:
            class_name = data['class']
        except KeyError:
            raise KeyError(
                "Can only make objects if the `class` attribute in the dictionary is known"
            )
        if class_name != 'ArkaneSpecies':
            raise KeyError(
                "Expected a ArkaneSpecies object, but got {0}".format(
                    class_name))
        del data['class']
        class_dict = {
            'ScalarQuantity': ScalarQuantity,
            'ArrayQuantity': ArrayQuantity,
            'Conformer': Conformer,
            'LinearRotor': LinearRotor,
            'NonlinearRotor': NonlinearRotor,
            'KRotor': KRotor,
            'SphericalTopRotor': SphericalTopRotor,
            'HinderedRotor': HinderedRotor,
            'FreeRotor': FreeRotor,
            'IdealGasTranslation': IdealGasTranslation,
            'HarmonicOscillator': HarmonicOscillator,
            'TransportData': TransportData,
            'SingleExponentialDown': SingleExponentialDown,
            'Wilhoit': Wilhoit,
            'NASA': NASA,
        }
        freq_data = None
        if 'imaginary_frequency' in data:
            freq_data = data['imaginary_frequency']
            del data['imaginary_frequency']
        self.make_object(data=data, class_dict=class_dict)
        if freq_data is not None:
            self.imaginary_frequency = ScalarQuantity()
            self.imaginary_frequency.make_object(data=freq_data,
                                                 class_dict=dict())
        self.adjacency_list = data[
            'adjacency_list'] if 'adjacency_list' in data else None
        self.inchi = data['inchi'] if 'inchi' in data else None
        self.smiles = data['smiles'] if 'smiles' in data else None
        self.is_ts = data['is_ts'] if 'is_ts' in data else False
        if pdep and not self.is_ts and (self.transport_data is None
                                        or self.energy_transfer_model is None):
            raise ValueError(
                'Transport data and an energy transfer model must be given if pressure-dependent '
                'calculations are requested. Check file {0}'.format(path))
        if pdep and not self.is_ts and self.smiles is None and self.adjacency_list is None\
                and self.inchi is None and self.molecular_weight is None:
            raise ValueError(
                'The molecular weight was not specified, and a structure was not given so it could '
                'not be calculated. Specify either the molecular weight or structure if '
                'pressure-dependent calculations are requested. Check file {0}'
                .format(path))
        logging.debug("Parsed all YAML objects")