class ReactionEnsembleTest(ut.TestCase): """Test the core implementation of the reaction ensemble.""" # The reaction ensemble follows the ideal titration curve only if N>>1, # Ideal curve is derived in the grandcanonical ensemble and for low N # there are systematic deviations caused by differences between the # ensembles. This is not an error but a fundamental difference (various # ensembles are equivalent only in the thermodynamic limit N \to \infty) N0 = 40 c0 = 0.00028 type_HA = 0 type_A = 1 type_H = 2 target_alpha = 0.6 # We get best statistics at alpha=0.5 Then the test is less sensitive to # the exact sequence of random numbers and does not require hard-coded # output values temperature = 1.0 exclusion_radius = 1.0 # could be in this test for example anywhere in the range 0.000001 ... 9, reactant_types = [type_HA] reactant_coefficients = [1] product_types = [type_A, type_H] product_coefficients = [1, 1] nubar = 1 system = espressomd.System(box_l=np.ones(3) * (N0 / c0)**(1.0 / 3.0)) system.seed = system.cell_system.get_state()['n_nodes'] * [67] np.random.seed(69) # make reaction code fully deterministic system.cell_system.skin = 0.4 volume = np.prod(system.box_l) # cuboid box # Calculate gamma which should lead to target_alpha with given N0 and V # Requires N0>>1, otherwise discrete character of N changes the statistics (N>20 should suffice) # gamma = prod_i (N_i / V) = alpha^2 N0 / (1-alpha)*V**(-nubar) # degree of dissociation alpha = N_A / N_HA = N_H / N_0 gamma = target_alpha**2 / (1. - target_alpha) * N0 / (volume**nubar) RE = reaction_ensemble.ReactionEnsemble( temperature=temperature, exclusion_radius=exclusion_radius, seed=12) @classmethod def setUpClass(cls): for i in range(0, 2 * cls.N0, 2): cls.system.part.add(id=i, pos=np.random.random( 3) * cls.system.box_l, type=cls.type_A) cls.system.part.add(id=i + 1, pos=np.random.random(3) * cls.system.box_l, type=cls.type_H) cls.RE.add_reaction( gamma=cls.gamma, reactant_types=cls.reactant_types, reactant_coefficients=cls.reactant_coefficients, product_types=cls.product_types, product_coefficients=cls.product_coefficients, default_charges={cls.type_HA: 0, cls.type_A: -1, cls.type_H: +1}, check_for_electroneutrality=True) def test_ideal_titration_curve(self): N0 = ReactionEnsembleTest.N0 type_A = ReactionEnsembleTest.type_A type_H = ReactionEnsembleTest.type_H type_HA = ReactionEnsembleTest.type_HA system = ReactionEnsembleTest.system gamma = ReactionEnsembleTest.gamma RE = ReactionEnsembleTest.RE target_alpha = ReactionEnsembleTest.target_alpha # chemical warmup - get close to chemical equilibrium before we start # sampling RE.reaction(20 * N0) average_NH = 0.0 average_NHA = 0.0 average_NA = 0.0 num_samples = 1000 for _ in range(num_samples): RE.reaction(10) average_NH += system.number_of_particles(type=type_H) average_NHA += system.number_of_particles(type=type_HA) average_NA += system.number_of_particles(type=type_A) average_NH /= num_samples average_NA /= num_samples average_NHA /= num_samples average_alpha = average_NA / float(N0) print(average_alpha) # Note: with 40 particles, alpha=0.5 and 1000*10 reactions, standard # deviation of average alpha is about 0.003 (determined from 40 # repeated simulations). We set the desired accuracy to 5*std = 0.015 rel_error_alpha = abs( average_alpha - target_alpha) / target_alpha # relative error self.assertLess( rel_error_alpha, 0.015, msg="\nDeviation from ideal titration curve is too big for the given input parameters.\n" + " gamma: " + str(gamma) + " average_NH: " + str(average_NH) + " average_NA: " + str(average_NA) + " average_NHA:" + str(average_NHA) + " average alpha: " + str(average_alpha) + " target_alpha: " + str(target_alpha) ) def test_reaction_system(self): RE_status = ReactionEnsembleTest.RE.get_status() forward_reaction = RE_status["reactions"][0] for i in range(len(forward_reaction["reactant_types"])): self.assertEqual( ReactionEnsembleTest.reactant_types[i], forward_reaction["reactant_types"][i], msg="reactant type not set correctly.") for i in range(len(forward_reaction["reactant_coefficients"])): self.assertEqual( ReactionEnsembleTest.reactant_coefficients[i], forward_reaction["reactant_coefficients"][i], msg="reactant coefficients not set correctly.") for i in range(len(forward_reaction["product_types"])): self.assertEqual( ReactionEnsembleTest.product_types[i], forward_reaction["product_types"][i], msg="product type not set correctly.") for i in range(len(forward_reaction["product_coefficients"])): self.assertEqual( ReactionEnsembleTest.product_coefficients[i], forward_reaction["product_coefficients"][i], msg="product coefficients not set correctly.") self.assertAlmostEqual( ReactionEnsembleTest.temperature, RE_status["temperature"], places=9, msg="reaction ensemble temperature not set correctly.") self.assertAlmostEqual( ReactionEnsembleTest.exclusion_radius, RE_status["exclusion_radius"], places=9, msg="reaction ensemble exclusion radius not set correctly.") self.assertAlmostEqual( ReactionEnsembleTest.volume, ReactionEnsembleTest.RE.get_volume(), places=9, msg="reaction ensemble volume not set correctly.") def test_change_reaction_constant(self): RE = ReactionEnsembleTest.RE new_reaction_constant = 634.0 RE.change_reaction_constant(0, new_reaction_constant) RE_status = RE.get_status() forward_reaction = RE_status["reactions"][0] backward_reaction = RE_status["reactions"][1] print(forward_reaction) self.assertEqual( new_reaction_constant, forward_reaction["gamma"], msg="new reaction constant was not set correctly.") self.assertEqual( 1.0 / new_reaction_constant, backward_reaction["gamma"], msg="new reaction constant was not set correctly.") RE.change_reaction_constant(0, ReactionEnsembleTest.gamma) def test_delete_reaction(self): RE = ReactionEnsembleTest.RE RE.add_reaction( gamma=1, reactant_types=[5], reactant_coefficients=[1], product_types=[2, 3, 4], product_coefficients=[1, 4, 3], default_charges={5: 0, 2: 0, 3: 0, 4: 0}, check_for_electroneutrality=True) nr_reactions_after_addition = len(RE.get_status()["reactions"]) RE.delete_reaction(1) nr_reactions_after_deletion = len(RE.get_status()["reactions"]) self.assertEqual( 2, nr_reactions_after_addition - nr_reactions_after_deletion, msg="the difference in single reactions does not match,\ deleting a full reaction (back and forward direction)\ should result in deleting two single reactions.")
############################################################# # type 0 = HA # type 1 = A- # type 2 = H+ N0 = 50 # number of titratable units K_diss = 0.0088 for i in range(N0): system.part.add(id=i, pos=np.random.random(3) * system.box_l, type=1) for i in range(N0, 2 * N0): system.part.add(id=i, pos=np.random.random(3) * system.box_l, type=2) RE = None if (mode == "reaction_ensemble"): RE = reaction_ensemble.ReactionEnsemble(temperature=1, exclusion_radius=1) elif (mode == "constant_pH_ensemble"): RE = reaction_ensemble.ConstantpHEnsemble(temperature=1, exclusion_radius=1) RE.constant_pH = 2 RE.add_reaction(gamma=K_diss, reactant_types=[0], reactant_coefficients=[1], product_types=[1, 2], product_coefficients=[1, 1], default_charges={ 0: 0, 1: -1, 2: +1 }) print(RE.get_status())
# type 2 = H+ for i in range(int(cs_bulk * box_l**3)): system.part.add(pos=np.random.random(3) * system.box_l, type=1, q=-1) system.part.add(pos=np.random.random(3) * system.box_l, type=2, q=1) wca_eps = 1.0 wca_sig = 1.0 types = [0, 1, 2] for type_1 in types: for type_2 in types: system.non_bonded_inter[type_1, type_2].wca.set_params(epsilon=wca_eps, sigma=wca_sig) RE = reaction_ensemble.ReactionEnsemble(temperature=temperature, exclusion_radius=2.0, seed=3) RE.add_reaction(gamma=cs_bulk**2 * np.exp(excess_chemical_potential_pair / temperature), reactant_types=[], reactant_coefficients=[], product_types=[1, 2], product_coefficients=[1, 1], default_charges={ 1: -1, 2: +1 }) print(RE.get_status()) system.setup_type_map([0, 1, 2]) RE.reaction(10000)