class TestDiffusionLimited(unittest.TestCase):
    """
    Contains unit tests of the DiffusionLimited class.
    """

    def setUp(self):
        """
        A function run before each unit test in this class.
        """
        octyl_pri = Species(
            label="",
            thermo=NASA(
                polynomials=[
                    NASAPolynomial(
                        coeffs=[-0.772759, 0.093255, -5.84447e-05, 1.8557e-08, -2.37127e-12, -3926.9, 37.6131],
                        Tmin=(298, 'K'), Tmax=(1390, 'K')),
                    NASAPolynomial(
                        coeffs=[25.051, 0.036948, -1.25765e-05, 1.94628e-09, -1.12669e-13, -13330.1, -102.557],
                        Tmin=(1390, 'K'), Tmax=(5000, 'K'))
                ],
                Tmin=(298, 'K'), Tmax=(5000, 'K'), Cp0=(33.2579, 'J/(mol*K)'), CpInf=(577.856, 'J/(mol*K)'),
                comment="""Thermo library: JetSurF0.2"""),
            molecule=[Molecule(smiles="[CH2]CCCCCCC")]
        )
        octyl_sec = Species(
            label="",
            thermo=NASA(
                polynomials=[
                    NASAPolynomial(
                        coeffs=[-0.304233, 0.0880077, -4.90743e-05, 1.21858e-08, -8.87773e-13, -5237.93, 36.6583],
                        Tmin=(298, 'K'), Tmax=(1383, 'K')),
                    NASAPolynomial(
                        coeffs=[24.9044, 0.0366394, -1.2385e-05, 1.90835e-09, -1.10161e-13, -14713.5, -101.345],
                        Tmin=(1383, 'K'), Tmax=(5000, 'K'))
                ],
                Tmin=(298, 'K'), Tmax=(5000, 'K'), Cp0=(33.2579, 'J/(mol*K)'), CpInf=(577.856, 'J/(mol*K)'),
                comment="""Thermo library: JetSurF0.2"""),
            molecule=[Molecule(smiles="CC[CH]CCCCC")]
        )
        ethane = Species(
            label="",
            thermo=ThermoData(
                Tdata=([300, 400, 500, 600, 800, 1000, 1500], 'K'),
                Cpdata=([10.294, 12.643, 14.933, 16.932, 20.033, 22.438, 26.281], 'cal/(mol*K)'),
                H298=(12.549, 'kcal/mol'),
                S298=(52.379, 'cal/(mol*K)'),
                Cp0=(33.2579, 'J/(mol*K)'), CpInf=(133.032, 'J/(mol*K)'), comment="""Thermo library: CH"""),
            molecule=[Molecule(smiles="C=C")]
        )
        decyl = Species(
            label="",
            thermo=NASA(
                polynomials=[
                    NASAPolynomial(
                        coeffs=[-1.31358, 0.117973, -7.51843e-05, 2.43331e-08, -3.17523e-12, -9689.68, 43.501],
                        Tmin=(298, 'K'), Tmax=(1390, 'K')),
                    NASAPolynomial(
                        coeffs=[31.5697, 0.0455818, -1.54995e-05, 2.39711e-09, -1.3871e-13, -21573.8, -134.709],
                        Tmin=(1390, 'K'), Tmax=(5000, 'K'))
                ],
                Tmin=(298, 'K'), Tmax=(5000, 'K'), Cp0=(33.2579, 'J/(mol*K)'), CpInf=(719.202, 'J/(mol*K)'),
                comment="""Thermo library: JetSurF0.2"""),
            molecule=[Molecule(smiles="[CH2]CCCCCCCCC")]
        )
        acetone = Species(
            label="",
            thermo=NASA(
                polynomials=[
                    NASAPolynomial(
                        coeffs=[3.75568, 0.0264934, -6.55661e-05, 1.94971e-07, -1.82059e-10, -27905.3, 9.0162],
                        Tmin=(10, 'K'), Tmax=(422.477, 'K')),
                    NASAPolynomial(
                        coeffs=[0.701289, 0.0344988, -1.9736e-05, 5.48052e-09, -5.92612e-13, -27460.6, 23.329],
                        Tmin=(422.477, 'K'), Tmax=(3000, 'K'))
                ],
                Tmin=(10, 'K'), Tmax=(3000, 'K'), E0=(-232.025, 'kJ/mol'), Cp0=(33.2579, 'J/(mol*K)'),
                CpInf=(232.805, 'J/(mol*K)')),
            molecule=[Molecule(smiles="CC(=O)C")]
        )
        peracetic_acid = Species(
            label="",
            thermo=NASA(
                polynomials=[
                    NASAPolynomial(
                        coeffs=[3.81786, 0.016419, 3.32204e-05, -8.98403e-08, 6.63474e-11, -42057.8, 9.65245],
                        Tmin=(10, 'K'), Tmax=(354.579, 'K')),
                    NASAPolynomial(
                        coeffs=[2.75993, 0.0283534, -1.72659e-05, 5.08158e-09, -5.77773e-13, -41982.8, 13.6595],
                        Tmin=(354.579, 'K'), Tmax=(3000, 'K'))
                ],
                Tmin=(10, 'K'), Tmax=(3000, 'K'), E0=(-349.698, 'kJ/mol'), Cp0=(33.2579, 'J/(mol*K)'),
                CpInf=(199.547, 'J/(mol*K)')),
            molecule=[Molecule(smiles="CC(=O)OO")]
        )
        acetic_acid = Species(
            label="",
            thermo=NASA(
                polynomials=[
                    NASAPolynomial(
                        coeffs=[3.97665, 0.00159915, 8.5542e-05, -1.76486e-07, 1.20201e-10, -53911.5, 8.99309],
                        Tmin=(10, 'K'), Tmax=(375.616, 'K')),
                    NASAPolynomial(
                        coeffs=[1.57088, 0.0272146, -1.67357e-05, 5.01453e-09, -5.82273e-13, -53730.7, 18.2442],
                        Tmin=(375.616, 'K'), Tmax=(3000, 'K'))
                ],
                Tmin=(10, 'K'), Tmax=(3000, 'K'), E0=(-448.245, 'kJ/mol'), Cp0=(33.2579, 'J/(mol*K)'),
                CpInf=(182.918, 'J/(mol*K)')),
            molecule=[Molecule(smiles="CC(=O)O")]
        )
        criegee = Species(
            label="",
            thermo=NASA(
                polynomials=[
                    NASAPolynomial(
                        coeffs=[3.23876, 0.0679583, -3.35611e-05, 7.91519e-10, 3.13038e-12, -77986, 13.6438],
                        Tmin=(10, 'K'), Tmax=(1053.46, 'K')),
                    NASAPolynomial(
                        coeffs=[9.84525, 0.0536795, -2.86165e-05, 7.39945e-09, -7.48482e-13, -79977.6, -21.4187],
                        Tmin=(1053.46, 'K'), Tmax=(3000, 'K'))
                ],
                Tmin=(10, 'K'), Tmax=(3000, 'K'), E0=(-648.47, 'kJ/mol'), Cp0=(33.2579, 'J/(mol*K)'),
                CpInf=(457.296, 'J/(mol*K)')),
            molecule=[Molecule(smiles="CC(=O)OOC(C)(O)C")]
        )
        self.database = SolvationDatabase()
        self.database.load(os.path.join(settings['database.directory'], 'solvation'))
        self.solvent = 'octane'
        diffusion_limiter.enable(self.database.get_solvent_data(self.solvent), self.database)
        self.T = 298
        self.uni_reaction = Reaction(reactants=[octyl_pri], products=[octyl_sec])
        self.uni_reaction.kinetics = Arrhenius(A=(2.0, '1/s'), n=0, Ea=(0, 'kJ/mol'))
        self.bi_uni_reaction = Reaction(reactants=[octyl_pri, ethane], products=[decyl])
        self.bi_uni_reaction.kinetics = Arrhenius(A=(1.0E-22, 'cm^3/molecule/s'), n=0, Ea=(0, 'kJ/mol'))
        self.tri_bi_reaction = Reaction(reactants=[acetone, peracetic_acid, acetic_acid],
                                        products=[criegee, acetic_acid])
        self.tri_bi_reaction.kinetics = Arrhenius(A=(1.07543e-11, 'cm^6/(mol^2*s)'), n=5.47295, Ea=(-38.5379, 'kJ/mol'))
        self.intrinsic_rates = {
            self.uni_reaction: self.uni_reaction.kinetics.get_rate_coefficient(self.T, P=100e5),
            self.bi_uni_reaction: self.bi_uni_reaction.kinetics.get_rate_coefficient(self.T, P=100e5),
            self.tri_bi_reaction: self.tri_bi_reaction.kinetics.get_rate_coefficient(self.T, P=100e5),
        }

    def tearDown(self):
        diffusion_limiter.disable()

    def test_get_effective_rate_unimolecular(self):
        """
        Tests that the effective rate is the same as the intrinsic rate for
        unimiolecular reactions.
        """
        effective_rate = diffusion_limiter.get_effective_rate(self.uni_reaction, self.T)
        self.assertEqual(effective_rate, self.intrinsic_rates[self.uni_reaction])

    def test_get_effective_rate_2_to_1(self):
        """
        Tests that the effective rate is limited in the forward direction for
        a 2 -> 1 reaction
        """
        effective_rate = diffusion_limiter.get_effective_rate(self.bi_uni_reaction, self.T)
        self.assertTrue(effective_rate < self.intrinsic_rates[self.bi_uni_reaction])
        self.assertTrue(effective_rate >= 0.2 * self.intrinsic_rates[self.bi_uni_reaction])

    def test_get_effective_rate_3_to_2(self):
        """
        Tests that the effective rate is limited for a 3 -> 2 reaction
        """
        effective_rate = diffusion_limiter.get_effective_rate(self.tri_bi_reaction, self.T)
        self.assertTrue(effective_rate < self.intrinsic_rates[self.tri_bi_reaction])
        self.assertTrue(effective_rate >= 0.2 * self.intrinsic_rates[self.tri_bi_reaction])
Example #2
0
class TestSoluteDatabase(TestCase):
    def setUp(self):
        self.database = SolvationDatabase()
        self.database.load(
            os.path.join(settings['database.directory'], 'solvation'))

    def tearDown(self):
        """
        Reset the database & liquid parameters for solution
        """
        import rmgpy.data.rmg
        rmgpy.data.rmg.database = None

    def test_solute_library(self):
        """Test we can obtain solute parameters from a library"""
        species = Species(molecule=[
            Molecule(smiles='COC=O')
        ])  # methyl formate - we know this is in the solute library

        library_data = self.database.get_solute_data_from_library(
            species, self.database.libraries['solute'])
        self.assertEqual(len(library_data), 3)

        solute_data = self.database.get_solute_data(species)
        self.assertTrue(isinstance(solute_data, SoluteData))

        s = solute_data.S
        self.assertEqual(s, 0.68)
        self.assertTrue(solute_data.V is not None)

    def test_mcgowan(self):
        """Test we can calculate and set the McGowan volume for species containing H,C,O,N or S"""
        self.testCases = [
            ['CCCCCCCC', 1.2358],  # n-octane, in library
            ['C(CO)O', 0.5078],  # ethylene glycol
            ['CC#N', 0.4042],  # acetonitrile
            ['CCS', 0.5539]  # ethanethiol
        ]

        for smiles, volume in self.testCases:
            species = Species(molecule=[Molecule(smiles=smiles)])
            solute_data = self.database.get_solute_data(species)
            solute_data.set_mcgowan_volume(
                species)  # even if it was found in library, recalculate
            self.assertIsNotNone(
                solute_data.V
            )  # so if it wasn't found in library, we should have calculated it
            self.assertAlmostEqual(
                solute_data.V, volume
            )  # the volume is what we expect given the atoms and bonds

    def test_diffusivity(self):
        """Test that for a given solvent viscosity and temperature we can calculate a solute's diffusivity"""
        species = Species(molecule=[Molecule(smiles='O')])  # water
        solute_data = self.database.get_solute_data(species)
        temperature = 298.
        solvent_viscosity = 0.00089  # water is about 8.9e-4 Pa.s
        d = solute_data.get_stokes_diffusivity(temperature,
                                               solvent_viscosity)  # m2/s
        self.assertAlmostEqual((d * 1e9), 1.3, 1)
        # self-diffusivity of water is about 2e-9 m2/s

    def test_solvent_library(self):
        """Test we can obtain solvent parameters from a library"""
        solvent_data = self.database.get_solvent_data('water')
        self.assertIsNotNone(solvent_data)
        self.assertEqual(solvent_data.s_h, 2.836)
        self.assertRaises(DatabaseError, self.database.get_solvent_data,
                          'orange_juice')

    def test_viscosity(self):
        """Test we can calculate the solvent viscosity given a temperature and its A-E correlation parameters"""
        solvent_data = self.database.get_solvent_data('water')
        self.assertAlmostEqual(solvent_data.get_solvent_viscosity(298),
                               0.0009155)

    def test_solute_generation(self):
        """Test we can estimate Abraham solute parameters correctly using group contributions"""

        self.testCases = [
            [
                '1,2-ethanediol', 'C(CO)O', 0.823, 0.685, 0.327, 2.572, 0.693,
                None
            ],
        ]

        for name, smiles, S, B, E, L, A, V in self.testCases:
            species = Species(smiles=smiles)
            solute_data = self.database.get_solute_data_from_groups(species)
            self.assertAlmostEqual(solute_data.S, S, places=2)
            self.assertAlmostEqual(solute_data.B, B, places=2)
            self.assertAlmostEqual(solute_data.E, E, places=2)
            self.assertAlmostEqual(solute_data.L, L, places=2)
            self.assertAlmostEqual(solute_data.A, A, places=2)

    def test_solute_with_resonance_structures(self):
        """
        Test we can estimate Abraham solute parameters correctly using group contributions
        for the solute species with resonance structures.
        """
        smiles = "CC1=CC=CC=C1N"
        species = Species(smiles=smiles)
        species.generate_resonance_structures()
        solute_data = self.database.get_solute_data(species)
        solvent_data = self.database.get_solvent_data('water')
        solvation_correction = self.database.get_solvation_correction(
            solute_data, solvent_data)
        dGsolv_spc = solvation_correction.gibbs / 1000
        for mol in species.molecule:
            spc = Species(molecule=[mol])
            solute_data = self.database.get_solute_data_from_groups(spc)
            solvation_correction = self.database.get_solvation_correction(
                solute_data, solvent_data)
            dGsolv_mol = solvation_correction.gibbs / 1000
            if mol == species.molecule[0]:
                self.assertEqual(dGsolv_spc, dGsolv_mol)
            else:
                self.assertNotAlmostEqual(dGsolv_spc, dGsolv_mol)

    def test_lone_pair_solute_generation(self):
        """Test we can obtain solute parameters via group additivity for a molecule with lone pairs"""
        molecule = Molecule().from_adjacency_list("""
            CH2_singlet
            multiplicity 1
            1 C u0 p1 c0 {2,S} {3,S}
            2 H u0 p0 c0 {1,S}
            3 H u0 p0 c0 {1,S}
            """)
        species = Species(molecule=[molecule])
        solute_data = self.database.get_solute_data_from_groups(species)
        self.assertIsNotNone(solute_data)

    def test_solute_data_generation_ammonia(self):
        """Test we can obtain solute parameters via group additivity for ammonia"""
        molecule = Molecule().from_adjacency_list("""
            1 N u0 p1 c0 {2,S} {3,S} {4,S}
            2 H u0 p0 c0 {1,S}
            3 H u0 p0 c0 {1,S}
            4 H u0 p0 c0 {1,S}
            """)
        species = Species(molecule=[molecule])
        solute_data = self.database.get_solute_data_from_groups(species)
        self.assertIsNotNone(solute_data)

    def test_solute_data_generation_amide(self):
        """Test that we can obtain solute parameters via group additivity for an amide"""
        molecule = Molecule().from_adjacency_list("""
            1 N u0 p1 {2,S} {3,S} {4,S}
            2 H u0 {1,S}
            3 C u0 {1,S} {6,S} {7,S} {8,S}
            4 C u0 {1,S} {5,D} {9,S}
            5 O u0 p2 {4,D}
            6 H u0 {3,S}
            7 H u0 {3,S}
            8 H u0 {3,S}
            9 H u0 {4,S}
            """)
        species = Species(molecule=[molecule])
        solute_data = self.database.get_solute_data_from_groups(species)
        self.assertIsNotNone(solute_data)

    def test_solute_data_generation_co(self):
        """Test that we can obtain solute parameters via group additivity for CO."""
        molecule = Molecule().from_adjacency_list("""
            1  C u0 p1 c-1 {2,T}
            2  O u0 p1 c+1 {1,T}
            """)
        species = Species(molecule=[molecule])
        solute_data = self.database.get_solute_data_from_groups(species)
        self.assertIsNotNone(solute_data)

    def test_radical_and_lone_pair_generation(self):
        """
        Test we can obtain solute parameters via group additivity for a molecule with both lone 
        pairs and a radical
        """
        molecule = Molecule().from_adjacency_list("""
            [C]OH
            multiplicity 2
            1 C u1 p1 c0 {2,S}
            2 O u0 p2 c0 {1,S} {3,S}
            3 H u0 p0 c0 {2,S}
            """)
        species = Species(molecule=[molecule])
        solute_data = self.database.get_solute_data_from_groups(species)
        self.assertIsNotNone(solute_data)

    def test_radical_solute_group(self):
        """Test that the existing radical group is found for the radical species when using group additivity"""
        # First check whether the radical group is found for the radical species
        rad_species = Species(smiles='[OH]')
        rad_solute_data = self.database.get_solute_data_from_groups(
            rad_species)
        self.assertTrue('radical' in rad_solute_data.comment)
        # Then check that the radical and its saturated species give different solvation free energies
        saturated_struct = rad_species.molecule[0].copy(deep=True)
        saturated_struct.saturate_radicals()
        sat_species = Species(molecule=[saturated_struct])
        sat_solute_data = self.database.get_solute_data_from_groups(
            sat_species)
        solvent_data = self.database.get_solvent_data('water')
        rad_solvation_correction = self.database.get_solvation_correction(
            rad_solute_data, solvent_data)
        sat_solvation_correction = self.database.get_solvation_correction(
            sat_solute_data, solvent_data)
        self.assertNotAlmostEqual(rad_solvation_correction.gibbs / 1000,
                                  sat_solvation_correction.gibbs / 1000)

    def test_correction_generation(self):
        """Test we can estimate solvation thermochemistry."""
        self.testCases = [
            # solventName, soluteName, soluteSMILES, Hsolv, Gsolv
            ['water', 'acetic acid', 'C(C)(=O)O', -56500, -6700 * 4.184],
            [
                'water', 'naphthalene', 'C1=CC=CC2=CC=CC=C12', -42800,
                -2390 * 4.184
            ],
            ['1-octanol', 'octane', 'CCCCCCCC', -40080, -4180 * 4.184],
            ['1-octanol', 'tetrahydrofuran', 'C1CCOC1', -28320, -3930 * 4.184],
            ['benzene', 'toluene', 'C1(=CC=CC=C1)C', -37660, -5320 * 4.184],
            ['benzene', '1,4-dioxane', 'C1COCCO1', -39030, -5210 * 4.184]
        ]

        for solventName, soluteName, smiles, H, G in self.testCases:
            species = Species(molecule=[Molecule(smiles=smiles)])
            solute_data = self.database.get_solute_data(species)
            solvent_data = self.database.get_solvent_data(solventName)
            solvation_correction = self.database.get_solvation_correction(
                solute_data, solvent_data)
            self.assertAlmostEqual(
                solvation_correction.enthalpy / 10000.,
                H / 10000.,
                0,  # 0 decimal place, in 10kJ.
                msg=
                "Solvation enthalpy discrepancy ({2:.0f}!={3:.0f}) for {0} in {1}"
                "".format(soluteName, solventName,
                          solvation_correction.enthalpy, H))
            self.assertAlmostEqual(
                solvation_correction.gibbs / 10000.,
                G / 10000.,
                0,
                msg=
                "Solvation Gibbs free energy discrepancy ({2:.0f}!={3:.0f}) for {0} in {1}"
                "".format(soluteName, solventName, solvation_correction.gibbs,
                          G))

    def test_initial_species(self):
        """Test we can check whether the solvent is listed as one of the initial species in various scenarios"""

        # Case 1. when SMILES for solvent is available, the molecular structures of the initial species and the solvent
        # are compared to check whether the solvent is in the initial species list

        # Case 1-1: the solvent water is not in the initialSpecies list, so it raises Exception
        rmg = RMG()
        rmg.initial_species = []
        solute = Species(label='n-octane',
                         molecule=[Molecule().from_smiles('C(CCCCC)CC')])
        rmg.initial_species.append(solute)
        rmg.solvent = 'water'
        solvent_structure = Species().from_smiles('O')
        self.assertRaises(Exception,
                          self.database.check_solvent_in_initial_species, rmg,
                          solvent_structure)

        # Case 1-2: the solvent is now octane and it is listed as the initialSpecies. Although the string
        # names of the solute and the solvent are different, because the solvent SMILES is provided,
        # it can identify the 'n-octane' as the solvent
        rmg.solvent = 'octane'
        solvent_structure = Species().from_smiles('CCCCCCCC')
        self.database.check_solvent_in_initial_species(rmg, solvent_structure)
        self.assertTrue(rmg.initial_species[0].is_solvent)

        # Case 2: the solvent SMILES is not provided. In this case, it can identify the species as the
        # solvent by looking at the string name.

        # Case 2-1: Since 'n-octane and 'octane' are not equal, it raises Exception
        solvent_structure = None
        self.assertRaises(Exception,
                          self.database.check_solvent_in_initial_species, rmg,
                          solvent_structure)

        # Case 2-2: The label 'n-ocatne' is corrected to 'octane', so it is identified as the solvent
        rmg.initial_species[0].label = 'octane'
        self.database.check_solvent_in_initial_species(rmg, solvent_structure)
        self.assertTrue(rmg.initial_species[0].is_solvent)

    def test_solvent_molecule(self):
        """Test that we can assign a proper solvent molecular structure when different formats are given"""

        # solventlibrary.entries['solvent_label'].item should be the instance of Species with the solvent's molecular
        # structure if the solvent database contains the solvent SMILES or adjacency list. If not, then item is None

        # Case 1: When the solventDatabase does not contain the solvent SMILES, the item attribute is None
        solventlibrary = SolventLibrary()
        solventlibrary.load_entry(index=1, label='water', solvent=None)
        self.assertTrue(solventlibrary.entries['water'].item is None)

        # Case 2: When the solventDatabase contains the correct solvent SMILES, the item attribute is the instance of
        # Species with the correct solvent molecular structure
        solventlibrary.load_entry(index=2,
                                  label='octane',
                                  solvent=None,
                                  molecule='CCCCCCCC')
        solvent_species = Species().from_smiles('C(CCCCC)CC')
        self.assertTrue(
            solvent_species.is_isomorphic(
                solventlibrary.entries['octane'].item[0]))

        # Case 3: When the solventDatabase contains the correct solvent adjacency list, the item attribute
        # is the instance of the species with the correct solvent molecular structure.
        # This will display the SMILES Parse Error message from the external function, but ignore it.
        solventlibrary.load_entry(index=3,
                                  label='ethanol',
                                  solvent=None,
                                  molecule="""
        1 C u0 p0 c0 {2,S} {4,S} {5,S} {6,S}
        2 C u0 p0 c0 {1,S} {3,S} {7,S} {8,S}
        3 O u0 p2 c0 {2,S} {9,S}
        4 H u0 p0 c0 {1,S}
        5 H u0 p0 c0 {1,S}
        6 H u0 p0 c0 {1,S}
        7 H u0 p0 c0 {2,S}
        8 H u0 p0 c0 {2,S}
        9 H u0 p0 c0 {3,S}
        """)
        solvent_species = Species().from_smiles('CCO')
        self.assertTrue(
            solvent_species.is_isomorphic(
                solventlibrary.entries['ethanol'].item[0]))

        # Case 4: when the solventDatabase contains incorrect values for the molecule attribute, it raises Exception
        # This will display the SMILES Parse Error message from the external function, but ignore it.
        self.assertRaises(Exception,
                          solventlibrary.load_entry,
                          index=4,
                          label='benzene',
                          solvent=None,
                          molecule='ring')

        # Case 5: when the solventDatabase contains data for co-solvents.
        solventlibrary.load_entry(index=5,
                                  label='methanol_50_water_50',
                                  solvent=None,
                                  molecule=['CO', 'O'])
        solvent_species_list = [
            Species().from_smiles('CO'),
            Species().from_smiles('O')
        ]
        self.assertEqual(
            len(solventlibrary.entries['methanol_50_water_50'].item), 2)
        for spc1 in solventlibrary.entries['methanol_50_water_50'].item:
            self.assertTrue(
                any([
                    spc1.is_isomorphic(spc2) for spc2 in solvent_species_list
                ]))
Example #3
0
class TestSoluteDatabase(TestCase):
    def setUp(self):
        self.database = SolvationDatabase()
        self.database.load(
            os.path.join(settings['database.directory'], 'solvation'))

    def tearDown(self):
        """
        Reset the database & liquid parameters for solution
        """
        import rmgpy.data.rmg
        rmgpy.data.rmg.database = None

    def test_solute_library(self):
        """Test we can obtain solute parameters from a library"""
        species = Species(molecule=[
            Molecule(smiles='COC=O')
        ])  # methyl formate - we know this is in the solute library

        library_data = self.database.get_solute_data_from_library(
            species, self.database.libraries['solute'])
        self.assertEqual(len(library_data), 3)

        solute_data = self.database.get_solute_data(species)
        self.assertTrue(isinstance(solute_data, SoluteData))

        s = solute_data.S
        self.assertEqual(s, 0.68)
        self.assertTrue(solute_data.V is not None)

    def test_mcgowan(self):
        """Test we can calculate and set the McGowan volume for species containing H,C,O,N or S"""
        self.testCases = [
            ['CCCCCCCC', 1.2358],  # n-octane, in library
            ['C(CO)O', 0.5078],  # ethylene glycol
            ['CC#N', 0.4042],  # acetonitrile
            ['CCS', 0.5539]  # ethanethiol
        ]

        for smiles, volume in self.testCases:
            species = Species(molecule=[Molecule(smiles=smiles)])
            solute_data = self.database.get_solute_data(species)
            solute_data.set_mcgowan_volume(
                species)  # even if it was found in library, recalculate
            self.assertIsNotNone(
                solute_data.V
            )  # so if it wasn't found in library, we should have calculated it
            self.assertAlmostEqual(
                solute_data.V, volume
            )  # the volume is what we expect given the atoms and bonds

    def test_diffusivity(self):
        """Test that for a given solvent viscosity and temperature we can calculate a solute's diffusivity"""
        species = Species(molecule=[Molecule(smiles='O')])  # water
        solute_data = self.database.get_solute_data(species)
        temperature = 298.
        solvent_viscosity = 0.00089  # water is about 8.9e-4 Pa.s
        d = solute_data.get_stokes_diffusivity(temperature,
                                               solvent_viscosity)  # m2/s
        self.assertAlmostEqual((d * 1e9), 1.3, 1)
        # self-diffusivity of water is about 2e-9 m2/s

    def test_solvent_library(self):
        """Test we can obtain solvent parameters and data count from a library"""
        solvent_data = self.database.get_solvent_data('water')
        self.assertIsNotNone(solvent_data)
        self.assertEqual(solvent_data.s_h, -0.75922)
        self.assertRaises(DatabaseError, self.database.get_solvent_data,
                          'orange_juice')
        solvent_data = self.database.get_solvent_data('cyclohexane')
        self.assertEqual(solvent_data.name_in_coolprop, 'CycloHexane')
        solvent_data_count = self.database.get_solvent_data_count(
            'dodecan-1-ol')
        self.assertEqual(solvent_data_count.dGsolvCount, 11)
        dHsolvMAE = (0.05, 'kcal/mol')
        self.assertTrue(solvent_data_count.dHsolvMAE == dHsolvMAE)

    def test_viscosity(self):
        """Test we can calculate the solvent viscosity given a temperature and its A-E correlation parameters"""
        solvent_data = self.database.get_solvent_data('water')
        self.assertAlmostEqual(solvent_data.get_solvent_viscosity(298),
                               0.0009155)

    def test_critical_temperature(self):
        """
        Test we can calculate the solvent critical temperature given the solvent's name_in_coolprop
        and we can raise DatabaseError when the solvent's name_in_coolprop is None.
        """
        solvent_data = self.database.get_solvent_data('water')
        self.assertAlmostEqual(solvent_data.get_solvent_critical_temperature(),
                               647.096)
        solvent_data = self.database.get_solvent_data('dibutylether')
        self.assertRaises(DatabaseError,
                          solvent_data.get_solvent_critical_temperature)

    def test_find_solvent(self):
        """ Test we can find solvents from the solvent library using SMILES"""
        # Case 1: one solvent is matched
        solvent_smiles = "NC=O"
        match_list = self.database.find_solvent_from_smiles(solvent_smiles)
        self.assertEqual(len(match_list), 1)
        self.assertTrue(match_list[0][0] == 'formamide')
        # Case 2: two solvents are matched
        solvent_smiles = "ClC=CCl"
        match_list = self.database.find_solvent_from_smiles(solvent_smiles)
        self.assertEqual(len(match_list), 2)
        self.assertTrue(match_list[0][0] == 'cis-1,2-dichloroethene')
        self.assertTrue(match_list[1][0] == 'trans-1,2-dichloroethene')
        # Case 3: no solvent is matched
        solvent_smiles = "C(CCl)O"
        match_list = self.database.find_solvent_from_smiles(solvent_smiles)
        self.assertEqual(len(match_list), 0)

    def test_solute_groups(self):
        """Test we can correctly load the solute groups from the solvation group database"""
        solute_group = self.database.groups['group'].entries['Cds-N3dCbCb']
        self.assertEqual(solute_group.data_count.S, 28)
        self.assertEqual(solute_group.data.B, 0.06652)
        solute_group = self.database.groups['ring'].entries['FourMember']
        self.assertIsNone(solute_group.data_count)
        self.assertEqual(solute_group.data, 'Cyclobutane')

    def test_solute_generation(self):
        """Test we can estimate Abraham solute parameters correctly using group contributions"""

        self.testCases = [[
            '1,2-ethanediol', 'C(CO)O', 0.809, 0.740, 0.393, 2.482, 0.584,
            0.508
        ]]

        for name, smiles, S, B, E, L, A, V in self.testCases:
            species = Species(smiles=smiles)
            solute_data = self.database.get_solute_data_from_groups(species)
            self.assertAlmostEqual(solute_data.S, S, places=2)
            self.assertAlmostEqual(solute_data.B, B, places=2)
            self.assertAlmostEqual(solute_data.E, E, places=2)
            self.assertAlmostEqual(solute_data.L, L, places=2)
            self.assertAlmostEqual(solute_data.A, A, places=2)

    def test_solute_with_resonance_structures(self):
        """
        Test we can estimate Abraham solute parameters correctly using group contributions
        for the solute species with resonance structures.
        """
        smiles = "CC1=CC=CC=C1N"
        species = Species(smiles=smiles)
        species.generate_resonance_structures()
        solute_data = self.database.get_solute_data(species)
        solvent_data = self.database.get_solvent_data('water')
        solvation_correction = self.database.get_solvation_correction(
            solute_data, solvent_data)
        dGsolv_spc = solvation_correction.gibbs / 1000
        for mol in species.molecule:
            spc = Species(molecule=[mol])
            solute_data = self.database.get_solute_data_from_groups(spc)
            solvation_correction = self.database.get_solvation_correction(
                solute_data, solvent_data)
            dGsolv_mol = solvation_correction.gibbs / 1000
            if mol == species.molecule[0]:
                self.assertEqual(dGsolv_spc, dGsolv_mol)
            else:
                self.assertNotAlmostEqual(dGsolv_spc, dGsolv_mol)

    def test_lone_pair_solute_generation(self):
        """Test we can obtain solute parameters via group additivity for a molecule with lone pairs"""
        molecule = Molecule().from_adjacency_list("""
            CH2_singlet
            multiplicity 1
            1 C u0 p1 c0 {2,S} {3,S}
            2 H u0 p0 c0 {1,S}
            3 H u0 p0 c0 {1,S}
            """)
        species = Species(molecule=[molecule])
        solute_data = self.database.get_solute_data_from_groups(species)
        self.assertIsNotNone(solute_data)

    def test_solute_data_generation_ammonia(self):
        """Test we can obtain solute parameters via group additivity for ammonia"""
        molecule = Molecule().from_adjacency_list("""
            1 N u0 p1 c0 {2,S} {3,S} {4,S}
            2 H u0 p0 c0 {1,S}
            3 H u0 p0 c0 {1,S}
            4 H u0 p0 c0 {1,S}
            """)
        species = Species(molecule=[molecule])
        solute_data = self.database.get_solute_data_from_groups(species)
        self.assertIsNotNone(solute_data)

    def test_solute_data_generation_amide(self):
        """Test that we can obtain solute parameters via group additivity for an amide"""
        molecule = Molecule().from_adjacency_list("""
            1 N u0 p1 {2,S} {3,S} {4,S}
            2 H u0 {1,S}
            3 C u0 {1,S} {6,S} {7,S} {8,S}
            4 C u0 {1,S} {5,D} {9,S}
            5 O u0 p2 {4,D}
            6 H u0 {3,S}
            7 H u0 {3,S}
            8 H u0 {3,S}
            9 H u0 {4,S}
            """)
        species = Species(molecule=[molecule])
        solute_data = self.database.get_solute_data_from_groups(species)
        self.assertIsNotNone(solute_data)

    def test_solute_data_generation_co(self):
        """Test that we can obtain solute parameters via group additivity for CO."""
        molecule = Molecule().from_adjacency_list("""
            1  C u0 p1 c-1 {2,T}
            2  O u0 p1 c+1 {1,T}
            """)
        species = Species(molecule=[molecule])
        solute_data = self.database.get_solute_data_from_groups(species)
        self.assertIsNotNone(solute_data)

    def test_radical_and_lone_pair_generation(self):
        """
        Test we can obtain solute parameters via group additivity for a molecule with both lone 
        pairs and a radical
        """
        molecule = Molecule().from_adjacency_list("""
            [C]OH
            multiplicity 2
            1 C u1 p1 c0 {2,S}
            2 O u0 p2 c0 {1,S} {3,S}
            3 H u0 p0 c0 {2,S}
            """)
        species = Species(molecule=[molecule])
        solute_data = self.database.get_solute_data_from_groups(species)
        self.assertIsNotNone(solute_data)

    def test_radical_solute_group(self):
        """Test that the existing radical group is found for the radical species when using group additivity"""
        # First check whether the radical group is found for the radical species
        rad_species = Species(smiles='[OH]')
        rad_solute_data = self.database.get_solute_data_from_groups(
            rad_species)
        self.assertTrue('radical' in rad_solute_data.comment)
        # Then check that the radical and its saturated species give different solvation free energies
        saturated_struct = rad_species.molecule[0].copy(deep=True)
        saturated_struct.saturate_radicals()
        sat_species = Species(molecule=[saturated_struct])
        sat_solute_data = self.database.get_solute_data_from_groups(
            sat_species)
        solvent_data = self.database.get_solvent_data('water')
        rad_solvation_correction = self.database.get_solvation_correction(
            rad_solute_data, solvent_data)
        sat_solvation_correction = self.database.get_solvation_correction(
            sat_solute_data, solvent_data)
        self.assertNotAlmostEqual(rad_solvation_correction.gibbs / 1000,
                                  sat_solvation_correction.gibbs / 1000)

    def test_halogen_solute_group(self):
        """Test that the correct halogen groups can be found for the halogenated species using get_solute_data method"""
        # Check the species whose halogen-replaced form can be found from solute library
        species = Species().from_smiles('CCCCCCl')
        solute_data = self.database.get_solute_data(species)
        self.assertTrue("Solute library: n-pentane + halogen(Cl-(Cs-CsHH))" in
                        solute_data.comment)
        # Check the species whose halogen-replaced form cannot be found from solute library
        species = Species().from_smiles('OCCCCCCC(Br)CCCCCO')
        solute_data = self.database.get_solute_data(species)
        self.assertTrue("+ group(Cs-Cs(Os-H)HH) + halogen(Br-(Cs-CsCsH))" in
                        solute_data.comment)

    def test_radical_halogen_solute_group(self):
        """Test that the correct halogen and radical groups can be found for the halogenated radical species
         using get_solute_data method"""
        # Check the species whose saturated and halogenated form can be found from solute library
        species = Species().from_smiles('[O]CCCCl')
        solute_data = self.database.get_solute_data(species)
        self.assertTrue("Solute library: 3-Chloropropan-1-ol + radical(ROJ)" ==
                        solute_data.comment)
        # Check the species whose saturated and halogen-replaced form can be found from solute library
        species = Species().from_smiles('[O]CCCC(Br)(I)Cl')
        solute_data = self.database.get_solute_data(species)
        self.assertTrue("Solute library: butan-1-ol + halogen(I-(Cs-CsHH)) + halogen(Br-(Cs-CsFCl)) + halogen(Cl-(Cs-CsFBr)) + radical(ROJ)" \
                        == solute_data.comment)
        # Check the species whose saturated and halogen-replaced form cannot be found from solute library
        species = Species().from_smiles('[NH]C(=O)CCCl')
        solute_data = self.database.get_solute_data(species)
        self.assertTrue(
            "group(Cds-Od(N3s-HH)Cs) + halogen(Cl-(Cs-CsHH)) + radical(N3_amide_pri)"
            in solute_data.comment)
        # Check the species whose radical site is bonded to halogen
        species = Species().from_smiles('F[N]C(=O)CCCl')
        solute_data = self.database.get_solute_data(species)
        self.assertTrue(
            "group(Cds-Od(N3s-HH)Cs) + halogen(Cl-(Cs-CsHH)) + halogen(F-N3s) + radical(N3_amide_sec)"
            in solute_data.comment)

    def test_correction_generation(self):
        """Test we can estimate solvation thermochemistry."""
        self.testCases = [
            # solventName, soluteName, soluteSMILES, Hsolv, Gsolv in kJ/mol
            ['water', 'acetic acid', 'C(C)(=O)O', -48.48, -28.12],
            ['water', 'naphthalene', 'C1=CC=CC2=CC=CC=C12', -37.15, -11.21],
            ['1-octanol', 'octane', 'CCCCCCCC', -39.44, -16.83],
            ['1-octanol', 'tetrahydrofuran', 'C1CCOC1', -32.27, -17.81],
            ['benzene', 'toluene', 'C1(=CC=CC=C1)C', -39.33, -23.81],
            ['benzene', '1,4-dioxane', 'C1COCCO1', -39.15, -22.01]
        ]

        for solventName, soluteName, smiles, H, G in self.testCases:
            species = Species().from_smiles(smiles)
            species.generate_resonance_structures()
            solute_data = self.database.get_solute_data(species)
            solvent_data = self.database.get_solvent_data(solventName)
            solvation_correction = self.database.get_solvation_correction(
                solute_data, solvent_data)
            self.assertAlmostEqual(
                solvation_correction.enthalpy / 1000,
                H,
                2,  # 2 decimal places, in kJ.
                msg=
                "Solvation enthalpy discrepancy ({2:.2f}!={3:.2f}) for {0} in {1}"
                "".format(soluteName, solventName,
                          solvation_correction.enthalpy / 1000, H))
            self.assertAlmostEqual(
                solvation_correction.gibbs / 1000,
                G,
                2,  # 2 decimal places, in kJ.
                msg=
                "Solvation Gibbs free energy discrepancy ({2:.2f}!={3:.2f}) for {0} in {1}"
                "".format(soluteName, solventName,
                          solvation_correction.gibbs / 1000, G))

    def test_Kfactor_parameters(self):
        """Test we can calculate the parameters for K-factor relationships"""
        species = Species().from_smiles('CCC(C)=O')  # 2-Butanone for a solute
        solute_data = self.database.get_solute_data(species)
        solvent_data = self.database.get_solvent_data('water')
        kfactor_parameters = self.database.get_Kfactor_parameters(
            solute_data, solvent_data)
        self.assertAlmostEqual(kfactor_parameters.lower_T[0], -9.780,
                               3)  # check up to 3 decimal places
        self.assertAlmostEqual(kfactor_parameters.lower_T[1], 0.492, 3)
        self.assertAlmostEqual(kfactor_parameters.lower_T[2], 10.485, 3)
        self.assertAlmostEqual(kfactor_parameters.higher_T, 1.147, 3)
        self.assertAlmostEqual(kfactor_parameters.T_transition, 485.3, 1)
        # check that DatabaseError is raised when the solvent's name_in_coolprop is None
        solvent_data = self.database.get_solvent_data('chloroform')
        self.assertRaises(DatabaseError, self.database.get_Kfactor_parameters,
                          solute_data, solvent_data)

    def test_Tdep_solvation_calculation(self):
        '''Test we can calculate the temperature dependent K-factor and solvation free energy'''
        species = Species().from_smiles('CCC1=CC=CC=C1')  # ethylbenzene
        species.generate_resonance_structures()
        solute_data = self.database.get_solute_data(species)
        solvent_data = self.database.get_solvent_data('benzene')
        T = 500  # in K
        Kfactor = self.database.get_Kfactor(solute_data, solvent_data, T)
        delG = self.database.get_T_dep_solvation_energy(
            solute_data, solvent_data, T) / 1000  # in kJ/mol
        self.assertAlmostEqual(Kfactor, 0.403, 3)
        self.assertAlmostEqual(delG, -13.59, 2)
        # For temperature greater than or equal to the critical temperature of the solvent,
        # it should raise InputError
        T = 1000
        self.assertRaises(InputError, self.database.get_T_dep_solvation_energy,
                          solute_data, solvent_data, T)

    def test_initial_species(self):
        """Test we can check whether the solvent is listed as one of the initial species in various scenarios"""

        # Case 1. when SMILES for solvent is available, the molecular structures of the initial species and the solvent
        # are compared to check whether the solvent is in the initial species list

        # Case 1-1: the solvent water is not in the initialSpecies list, so it raises Exception
        rmg = RMG()
        rmg.initial_species = []
        solute = Species(label='n-octane',
                         molecule=[Molecule().from_smiles('C(CCCCC)CC')])
        rmg.initial_species.append(solute)
        rmg.solvent = 'water'
        solvent_structure = Species().from_smiles('O')
        self.assertRaises(Exception,
                          self.database.check_solvent_in_initial_species, rmg,
                          solvent_structure)

        # Case 1-2: the solvent is now octane and it is listed as the initialSpecies. Although the string
        # names of the solute and the solvent are different, because the solvent SMILES is provided,
        # it can identify the 'n-octane' as the solvent
        rmg.solvent = 'octane'
        solvent_structure = Species().from_smiles('CCCCCCCC')
        self.database.check_solvent_in_initial_species(rmg, solvent_structure)
        self.assertTrue(rmg.initial_species[0].is_solvent)

        # Case 2: the solvent SMILES is not provided. In this case, it can identify the species as the
        # solvent by looking at the string name.

        # Case 2-1: Since 'n-octane and 'octane' are not equal, it raises Exception
        solvent_structure = None
        self.assertRaises(Exception,
                          self.database.check_solvent_in_initial_species, rmg,
                          solvent_structure)

        # Case 2-2: The label 'n-ocatne' is corrected to 'octane', so it is identified as the solvent
        rmg.initial_species[0].label = 'octane'
        self.database.check_solvent_in_initial_species(rmg, solvent_structure)
        self.assertTrue(rmg.initial_species[0].is_solvent)

    def test_solvent_molecule(self):
        """Test that we can assign a proper solvent molecular structure when different formats are given"""

        # solventlibrary.entries['solvent_label'].item should be the instance of Species with the solvent's molecular
        # structure if the solvent database contains the solvent SMILES or adjacency list. If not, then item is None

        # Case 1: When the solventDatabase does not contain the solvent SMILES, the item attribute is None
        solventlibrary = SolventLibrary()
        solventlibrary.load_entry(index=1, label='water', solvent=None)
        self.assertTrue(solventlibrary.entries['water'].item is None)

        # Case 2: When the solventDatabase contains the correct solvent SMILES, the item attribute is the instance of
        # Species with the correct solvent molecular structure
        solventlibrary.load_entry(index=2,
                                  label='octane',
                                  solvent=None,
                                  molecule='CCCCCCCC')
        solvent_species = Species().from_smiles('C(CCCCC)CC')
        self.assertTrue(
            solvent_species.is_isomorphic(
                solventlibrary.entries['octane'].item[0]))

        # Case 3: When the solventDatabase contains the correct solvent adjacency list, the item attribute
        # is the instance of the species with the correct solvent molecular structure.
        # This will display the SMILES Parse Error message from the external function, but ignore it.
        solventlibrary.load_entry(index=3,
                                  label='ethanol',
                                  solvent=None,
                                  molecule="""
        1 C u0 p0 c0 {2,S} {4,S} {5,S} {6,S}
        2 C u0 p0 c0 {1,S} {3,S} {7,S} {8,S}
        3 O u0 p2 c0 {2,S} {9,S}
        4 H u0 p0 c0 {1,S}
        5 H u0 p0 c0 {1,S}
        6 H u0 p0 c0 {1,S}
        7 H u0 p0 c0 {2,S}
        8 H u0 p0 c0 {2,S}
        9 H u0 p0 c0 {3,S}
        """)
        solvent_species = Species().from_smiles('CCO')
        self.assertTrue(
            solvent_species.is_isomorphic(
                solventlibrary.entries['ethanol'].item[0]))

        # Case 4: when the solventDatabase contains incorrect values for the molecule attribute, it raises Exception
        # This will display the SMILES Parse Error message from the external function, but ignore it.
        self.assertRaises(Exception,
                          solventlibrary.load_entry,
                          index=4,
                          label='benzene',
                          solvent=None,
                          molecule='ring')

        # Case 5: when the solventDatabase contains data for co-solvents.
        solventlibrary.load_entry(index=5,
                                  label='methanol_50_water_50',
                                  solvent=None,
                                  molecule=['CO', 'O'])
        solvent_species_list = [
            Species().from_smiles('CO'),
            Species().from_smiles('O')
        ]
        self.assertEqual(
            len(solventlibrary.entries['methanol_50_water_50'].item), 2)
        for spc1 in solventlibrary.entries['methanol_50_water_50'].item:
            self.assertTrue(
                any([
                    spc1.is_isomorphic(spc2) for spc2 in solvent_species_list
                ]))