Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve multiplicity handling in Species and Reaction #443

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion arc/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,7 @@ def check_torsion_change(torsions: pd.DataFrame,
differences are equal to the scan resolution.

Returns: pd.DataFrame
a DataFrame consisting of ``True``/``False``, indicating
A DataFrame consisting of ``True``/``False``, indicating
which torsions changed significantly. ``True`` for significant change.
"""
# First iteration without 180/-180 adjustment
Expand Down
169 changes: 68 additions & 101 deletions arc/reaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ def __init__(self,
self.multiplicity = multiplicity
self.charge = charge
if self.multiplicity is not None and not isinstance(self.multiplicity, int):
raise InputError('Reaction multiplicity must be an integer, got {0} of type {1}.'.format(
self.multiplicity, type(self.multiplicity)))
raise InputError(f'Reaction multiplicity must be an integer, '
f'got {self.multiplicity} which is a {type(self.multiplicity)}.')
self.reactants = reactants
self.products = products
self.rmg_reaction = rmg_reaction
Expand Down Expand Up @@ -310,115 +310,65 @@ def arc_species_from_rmg_reaction(self):

def determine_rxn_multiplicity(self):
"""A helper function for determining the surface multiplicity"""
r_species, p_species = self.get_comprehensive_species()
if self.multiplicity is None:
ordered_r_mult_list, ordered_p_mult_list = list(), list()
if len(self.r_species):
if len(self.r_species) == 1:
self.multiplicity = self.r_species[0].multiplicity
elif len(self.r_species) == 2:
ordered_r_mult_list = sorted([self.r_species[0].multiplicity,
self.r_species[1].multiplicity])
elif len(self.r_species) == 3:
ordered_r_mult_list = sorted([self.r_species[0].multiplicity,
self.r_species[1].multiplicity,
self.r_species[2].multiplicity])
if len(self.p_species) == 1:
self.multiplicity = self.p_species[0].multiplicity
elif len(self.p_species) == 2:
ordered_p_mult_list = sorted([self.p_species[0].multiplicity,
self.p_species[1].multiplicity])
elif len(self.p_species) == 3:
ordered_p_mult_list = sorted([self.p_species[0].multiplicity,
self.p_species[1].multiplicity,
self.p_species[2].multiplicity])
if len(r_species):
if len(r_species) == 1:
self.multiplicity = r_species[0].multiplicity
else:
ordered_r_mult_list = sorted([r_spc.multiplicity for r_spc in r_species])
if len(p_species) == 1:
self.multiplicity = p_species[0].multiplicity
else:
ordered_p_mult_list = sorted([p_spc.multiplicity for p_spc in p_species])

elif self.rmg_reaction is not None:
if len(self.rmg_reaction.reactants) == 1:
self.multiplicity = self.rmg_reaction.reactants[0].molecule[0].multiplicity
elif len(self.rmg_reaction.reactants) == 2:
ordered_r_mult_list = sorted([self.rmg_reaction.reactants[0].molecule[0].multiplicity,
self.rmg_reaction.reactants[1].molecule[0].multiplicity])
elif len(self.rmg_reaction.reactants) == 3:
ordered_r_mult_list = sorted([self.rmg_reaction.reactants[0].molecule[0].multiplicity,
self.rmg_reaction.reactants[1].molecule[0].multiplicity,
self.rmg_reaction.reactants[2].molecule[0].multiplicity])
else:
ordered_r_mult_list = sorted([r_spc.molecule[0].multiplicity for r_spc in self.rmg_reaction.reactants])
if len(self.rmg_reaction.products) == 1:
self.multiplicity = self.rmg_reaction.products[0].molecule[0].multiplicity
elif len(self.rmg_reaction.products) == 2:
ordered_p_mult_list = sorted([self.rmg_reaction.products[0].molecule[0].multiplicity,
self.rmg_reaction.products[1].molecule[0].multiplicity])
elif len(self.rmg_reaction.products) == 3:
ordered_p_mult_list = sorted([self.rmg_reaction.products[0].molecule[0].multiplicity,
self.rmg_reaction.products[1].molecule[0].multiplicity,
self.rmg_reaction.products[2].molecule[0].multiplicity])
else:
ordered_p_mult_list = sorted([p_spc.molecule[0].multiplicity for p_spc in self.rmg_reaction.products])
if self.multiplicity is None:
if ordered_r_mult_list == [1, 1]:
self.multiplicity = 1 # S + S = D
elif ordered_r_mult_list == [1, 2]:
self.multiplicity = 2 # S + D = D
elif ordered_r_mult_list == [2, 2]:
# D + D = S or T
if ordered_p_mult_list in [[1, 1], [1, 1, 1]]:
self.multiplicity = 1
elif ordered_p_mult_list in [[1, 3], [1, 1, 3]]:
self.multiplicity = 3
else:
self.multiplicity = 1
logger.warning(f'ASSUMING a multiplicity of 1 (singlet) for reaction {self.label}')
elif ordered_r_mult_list == [1, 3]:
self.multiplicity = 3 # S + T = T
elif ordered_r_mult_list == [2, 3]:
# D + T = D or Q
if ordered_p_mult_list in [[1, 2], [1, 1, 2]]:
self.multiplicity = 2
elif ordered_p_mult_list in [[1, 4], [1, 1, 4]]:
self.multiplicity = 4
else:
self.multiplicity = 2
logger.warning(f'ASSUMING a multiplicity of 2 (doublet) for reaction {self.label}')
elif ordered_r_mult_list == [3, 3]:
# T + T = S or T or quintet
if ordered_p_mult_list in [[1, 1], [1, 1, 1]]:
self.multiplicity = 1
elif ordered_p_mult_list in [[1, 3], [1, 1, 3]]:
self.multiplicity = 3
elif ordered_p_mult_list in [[1, 5], [1, 1, 5]]:
self.multiplicity = 5
else:
self.multiplicity = 3
logger.warning(f'ASSUMING a multiplicity of 3 (triplet) for reaction {self.label}')
elif ordered_r_mult_list == [1, 1, 1]:
self.multiplicity = 1 # S + S + S = S
elif ordered_r_mult_list == [1, 1, 2]:
self.multiplicity = 2 # S + S + D = D
elif ordered_r_mult_list == [1, 1, 3]:
self.multiplicity = 3 # S + S + T = T
elif ordered_r_mult_list == [1, 2, 2]:
# S + D + D = S or T
if ordered_p_mult_list in [[1, 1], [1, 1, 1]]:
self.multiplicity = 1
elif ordered_p_mult_list in [[1, 3], [1, 1, 3]]:
self.multiplicity = 3
else:
self.multiplicity = 1
logger.warning(f'ASSUMING a multiplicity of 1 (singlet) for reaction {self.label}')
elif ordered_r_mult_list == [2, 2, 2]:
# D + D + D = D or Q
if ordered_p_mult_list in [[1, 2], [1, 1, 2]]:
self.multiplicity = 2
elif ordered_p_mult_list in [[1, 4], [1, 1, 4]]:
self.multiplicity = 4
else:
for list_1, list_2 in [(ordered_r_mult_list, ordered_p_mult_list),
(ordered_p_mult_list, ordered_r_mult_list)]:
if all(m == 1 for m in list_1):
self.multiplicity = 1 # S + S = S
break
if 2 in list_1 and all(m == 1 for i, m in enumerate(list_1) if i != list_1.index(2)):
self.multiplicity = 2 # S + D = D
break
if 3 in list_1 and all(m == 1 for i, m in enumerate(list_1) if i != list_1.index(3)):
self.multiplicity = 3 # S + T = T
break
if 4 in list_1 and all(m == 1 for i, m in enumerate(list_1) if i != list_1.index(4)):
self.multiplicity = 4 # S + Q = Q
break
if all(m == 2 for m in list_1):
# D + D = S or T
# D + D + D = D or Q
if len(list_1) % 2 == 0: # even number of D's in list_1, m must be an odd number
if any(m > 2 for m in list_2):
self.multiplicity = max(list_2) if max(list_2) % 2 == 1 else max(list_2) - 1
else: # odd number of D's in list_1, m must be even
self.multiplicity = max(list_2) if max(list_2) % 2 == 0 else max(list_2) - 1
if all(m == 3 for m in list_1):
# T + T = S or P
# T + T + T = T or 7
if len(list_1) % 2 == 0: # even number of T's in list_1, m must be 1 or 5
self.multiplicity = 1
logger.warning(f'ASSUMING a multiplicity of 1 (singlet) for reaction {self.label}')
else: # odd number of D's in list_1, m must be 3 or 7
self.multiplicity = 3
logger.warning(f'ASSUMING a multiplicity of 3 (triplet) for reaction {self.label}')
if list_1 == [2, 3] and 4 not in list_2:
# D + T = D or Q
self.multiplicity = 2
logger.warning(f'ASSUMING a multiplicity of 2 (doublet) for reaction {self.label}')
elif ordered_r_mult_list == [1, 2, 3]:
# S + D + T = D or Q
if ordered_p_mult_list in [[1, 2], [1, 1, 2]]:
self.multiplicity = 2
elif ordered_p_mult_list in [[1, 4], [1, 1, 4]]:
self.multiplicity = 4
self.multiplicity = 2
logger.warning(f'ASSUMING a multiplicity of 2 (doublet) for reaction {self.label}')
else:
if self.multiplicity is None:
raise ReactionError(f'Could not determine multiplicity for reaction {self.label}')
logger.info(f'Setting multiplicity of reaction {self.label} to {self.multiplicity}')

Expand Down Expand Up @@ -632,6 +582,23 @@ def get_species_count(self,
well_str.endswith(f' {species.label}')
return count

def get_comprehensive_species(self):
"""
Get a list of all reactants and a list of all products, including duplicate species.
For example, for self recombination of a radical (R1 + R1), self.r_species will be just [R1],
while this method will return [R1, R1].

Returns:
Tuple[List[ARCSpecies], List[ARCSpecies]]:
The comprehensive reactants and products species lists, respectively.
"""
r_species, p_species = list(), list()
for spc in self.r_species:
r_species.extend([spc] * self.get_species_count(species=spc, well=0))
for spc in self.p_species:
p_species.extend([spc] * self.get_species_count(species=spc, well=1))
return r_species, p_species

def get_atom_map(self, verbose: int = 0) -> Optional[List[int]]:
"""
Get the atom mapping of the reactant atoms to the product atoms.
Expand Down
21 changes: 18 additions & 3 deletions arc/reactionTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def setUpClass(cls):
cls.rxn4 = ARCReaction(reactants=['[NH2]', 'N[NH]'], products=['N', 'N[N]'])
cls.rxn4.rmg_reaction = Reaction(reactants=[Species().from_smiles('[NH2]'), Species().from_smiles('N[NH]')],
products=[Species().from_smiles('N'), Species().from_smiles('N[N]')])
cls.rxn5 = ARCReaction(reactants=['CO[O]', 'CO[O]'], products=['C=O', 'CO', '[O][O]'])
cls.rxn5.rmg_reaction = Reaction(reactants=[Species().from_smiles('CO[O]'), Species().from_smiles('CO[O]')],
products=[Species().from_smiles('C=O'), Species().from_smiles('CO'),
Species().from_smiles('[O][O]')])

def test_str(self):
"""Test the string representation of the object"""
Expand Down Expand Up @@ -125,6 +129,9 @@ def test_determine_multiplicity(self):
self.rxn4.determine_rxn_multiplicity()
self.assertEqual(self.rxn4.multiplicity, 3)

self.rxn5.determine_rxn_multiplicity()
self.assertEqual(self.rxn5.multiplicity, 3)

def test_check_atom_balance(self):
"""Test the Reaction check_atom_balance method"""

Expand Down Expand Up @@ -165,6 +172,17 @@ def test_get_species_count(self):
self.assertEqual(rxn1.get_species_count(species=spc2, well=0), 1)
self.assertEqual(rxn1.get_species_count(species=spc2, well=1), 2)

def test_get_comprehensive_species(self):
"""Test identifying duplicate species in reactants/products"""
rxn1 = ARCReaction(label='methylperoxyl + methylperoxyl <=> methanol + formaldehyde + O2')
rxn1.r_species = [ARCSpecies(label='methylperoxyl', smiles='CO[O]')]
rxn1.p_species = [ARCSpecies(label='methanol', smiles='CO'),
ARCSpecies(label='formaldehyde', smiles='C=O'),
ARCSpecies(label='O2', smiles='[O][O]')]
r_species, p_species = rxn1.get_comprehensive_species()
self.assertEqual(len(r_species), 2)
self.assertEqual(len(p_species), 3)

def test_get_atom_map(self):
"""Test getting an atom map for a reaction"""

Expand Down Expand Up @@ -988,9 +1006,6 @@ def test_get_mapped_product_xyz(self):
self.assertTrue(mapped_product.get_xyz(), h2o_xyz_1)





def check_atom_map(rxn: ARCReaction) -> bool:
"""
A helper function for testing a reaction atom map.
Expand Down
19 changes: 13 additions & 6 deletions arc/species/conformers.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,11 @@ def determine_dihedrals(conformers, torsions):
return conformers


def determine_torsion_sampling_points(label, torsion_angles, smeared_scan_res=None, symmetry=1):
def determine_torsion_sampling_points(label: str,
torsion_angles: list,
smeared_scan_res: Optional[float] = None,
symmetry: int = 1,
) -> Tuple[list. list]:
"""
Determine how many points to consider in each well of a torsion for conformer combinations.

Expand All @@ -819,10 +823,10 @@ def determine_torsion_sampling_points(label, torsion_angles, smeared_scan_res=No
symmetry (int, optional): The torsion symmetry number.

Returns:
list: Sampling points for the torsion.
Returns:
list: Each entry is a well dictionary with the keys
``start_idx``, ``end_idx``, ``start_angle``, ``end_angle``, ``angles``.
Tuple[list. list]:
- Sampling points for the torsion.
- list: Each entry is a well dictionary with the keys
``start_idx``, ``end_idx``, ``start_angle``, ``end_angle``, ``angles``.
"""
smeared_scan_res = smeared_scan_res or SMEARED_SCAN_RESOLUTIONS
sampling_points = list()
Expand Down Expand Up @@ -1391,7 +1395,10 @@ def rdkit_force_field(label, rd_mol, force_field='MMFF94s', optimize=True):
return xyzs, energies


def get_wells(label, angles, blank=20):
def get_wells(label: str,
angles: list,
blank: int = 20,
) -> list:
"""
Determine the distinct wells from a list of angles.

Expand Down
Loading