Skip to content

Commit bc6f151

Browse files
authored
Full Release update
With this update AutoMapper enters full release version 1.0. At this stage it is ready for general use and feature updates will be infrequent.
2 parents 4ec80a2 + ba91c02 commit bc6f151

File tree

96 files changed

+10677
-2770
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

96 files changed

+10677
-2770
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
__pycache__
22
.vscode
33
.pytest*
4-
DebugTool.py
4+
DebugTool.py
5+
*.code-workspace

AtomObjectBuilder.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
##############################################################################
2+
# Developed by: Matthew Bone
3+
# Last Updated: 30/07/2021
4+
# Updated by: Matthew Bone
5+
#
6+
# Contact Details:
7+
# Bristol Composites Institute (BCI)
8+
# Department of Aerospace Engineering - University of Bristol
9+
# Queen's Building - University Walk
10+
# Bristol, BS8 1TR
11+
# U.K.
12+
# Email - matthew.bone@bristol.ac.uk
13+
#
14+
# File Description:
15+
# Contains key atom object creation and manipulation tools. The Atom object
16+
# class and builder are the building blocks for map creation.
17+
##############################################################################
18+
19+
import logging
20+
from collections import Counter
21+
22+
from LammpsSearchFuncs import get_data, find_sections, get_neighbours, get_additional_neighbours
23+
from LammpsTreatmentFuncs import clean_data
24+
25+
def build_atom_objects(fileName, elementDict, bondingAtoms):
26+
# Load molecule file
27+
with open(fileName, 'r') as f:
28+
lines = f.readlines()
29+
30+
# Clean data and get coords and bonds
31+
data = clean_data(lines)
32+
sections = find_sections(data)
33+
types = get_data('Types', data, sections)
34+
35+
atomIDs = [row[0] for row in types]
36+
bonds = get_data('Bonds', data, sections)
37+
38+
# Build neighbours dict
39+
neighboursDict = get_neighbours(atomIDs, bonds, bondingAtoms)
40+
41+
def get_elements(neighbourIDs, elementDict):
42+
return [elementDict[atomID]for atomID in neighbourIDs]
43+
44+
atomObjectDict = {}
45+
for index, atomID in enumerate(atomIDs):
46+
atomType = types[index][1]
47+
48+
# Establish all neighbours
49+
neighbours = neighboursDict[atomID]
50+
secondNeighbours = get_additional_neighbours(neighboursDict, atomID, neighbours, bondingAtoms)
51+
thirdNeighbours = get_additional_neighbours(neighboursDict, atomID, secondNeighbours, bondingAtoms)
52+
53+
neighbourElements = get_elements(neighbours, elementDict)
54+
secondNeighbourElements = get_elements(secondNeighbours, elementDict)
55+
thirdNeighbourElements = get_elements(thirdNeighbours, elementDict)
56+
57+
# Check if atom is a bonding atom, return boolean
58+
if atomID in bondingAtoms:
59+
bondingAtom = True
60+
else:
61+
bondingAtom = False
62+
63+
atom = Atom(atomID, atomType, elementDict[atomID], bondingAtom, neighbours, secondNeighbours, thirdNeighbours, neighbourElements, secondNeighbourElements, thirdNeighbourElements)
64+
atomObjectDict[atomID] = atom
65+
66+
return atomObjectDict
67+
68+
def compare_symmetric_atoms(postNeighbourAtomObjectList, preNeighbourAtom, outputType, allowInference=True):
69+
# Neighbour comparison - no inference
70+
def compare_neighbours(neighbourLevel):
71+
neighbourComparison = [getattr(atomObject, neighbourLevel) for atomObject in postNeighbourAtomObjectList]
72+
neighbourFingerprint = [''.join(sorted(elements)) for elements in neighbourComparison] # sorted to get alphabetical fingerprints
73+
74+
# Remove duplicate fingerprints
75+
countFingerprints = Counter(neighbourFingerprint)
76+
tuppledFingerprints = [(index, fingerprint) for index, fingerprint in enumerate(neighbourFingerprint) if countFingerprints[fingerprint] == 1]
77+
78+
# If any of the fingerprints are empty (i.e. the atom has no Xneighbours) return None
79+
for _, fingerprint in tuppledFingerprints:
80+
if fingerprint == '':
81+
return None
82+
83+
# Any of the potential post neighbours matches the pre atom fingerprint, return the post neighbour
84+
for index, fingerprint in tuppledFingerprints:
85+
if ''.join(sorted(getattr(preNeighbourAtom, neighbourLevel))) == fingerprint:
86+
logging.debug(f'Pre: {preNeighbourAtom.atomID}, Post: {postNeighbourAtomObjectList[index].atomID} found with {neighbourLevel}')
87+
if outputType == 'index':
88+
return index
89+
elif outputType == 'atomID':
90+
return postNeighbourAtomObjectList[index].atomID
91+
else:
92+
print('Invalid output type specified for compare_symmetric_atoms')
93+
94+
# First neighbour comparison
95+
symmetryResult = compare_neighbours('firstNeighbourElements')
96+
97+
# Second neighbour comparison
98+
if symmetryResult is None:
99+
symmetryResult = compare_neighbours('secondNeighbourElements')
100+
101+
# Third neighbour comparison
102+
if symmetryResult is None:
103+
symmetryResult = compare_neighbours('thirdNeighbourElements')
104+
105+
# If it makes it through all these, guess assignment and warn user about this
106+
if symmetryResult is not None:
107+
return symmetryResult
108+
else:
109+
if allowInference: # Only if inference is turned on
110+
# Find all potential choices by breaking the postNeighbourAtomList down into atoms that match the preAtom element
111+
possibleChoices = []
112+
for index, postNeighbourAtom in enumerate(postNeighbourAtomObjectList):
113+
if postNeighbourAtom.element == preNeighbourAtom.element:
114+
possibleChoices.append((index, postNeighbourAtom.atomID))
115+
116+
# Let the user know that an inference has been made
117+
logging.debug(f'Pre: {preNeighbourAtom.atomID}, Post: {possibleChoices[0][1]} found with symmetry inference')
118+
print(
119+
f'Note: Pre-bond atomID {preNeighbourAtom.atomID} has been assigned by inference to post-bond atomID {possibleChoices[0][1]}. The potential choices were {[atom[1] for atom in possibleChoices]}. Please check this is correct.'
120+
)
121+
if outputType == 'index':
122+
return possibleChoices[0][0]
123+
elif outputType == 'atomID':
124+
return possibleChoices[0][1]
125+
else:
126+
print('Invalid output type specified for compare_symmetric_atoms')
127+
128+
129+
class Atom():
130+
def __init__(self, atomID, atomType, element, bondingAtom, neighbourIDs, secondNeighbourIDs, thirdNeighbourIDs, neighbourElements, secondNeighbourElements, thirdNeighbourElements):
131+
self.atomID = atomID
132+
self.atomType = atomType
133+
self.element = element
134+
self.bondingAtom = bondingAtom
135+
136+
# Neighbours
137+
self.mappedNeighbourIDs = neighbourIDs # This is changed according to mapping
138+
self.firstNeighbourIDs = neighbourIDs.copy() # This is fixed throughout mapping process
139+
self.secondNeighbourIDs = secondNeighbourIDs
140+
self.thirdNeighbourIDs = thirdNeighbourIDs
141+
142+
self.mappedNeighbourElements = neighbourElements # This is changed according to mapping
143+
self.firstNeighbourElements = neighbourElements.copy() # This is fixed throughout mapping process
144+
self.secondNeighbourElements = secondNeighbourElements
145+
self.thirdNeighbourElements = thirdNeighbourElements
146+
147+
def check_mapped(self, mappedIDs, searchIndex, elementDict):
148+
"""Update neighbourIDs.
149+
150+
Updates neighbourIDs by removing IDs that have already been mapped.
151+
This will be called before all neighbour mapping attempts to stop atoms
152+
being mapped multiple times.
153+
154+
Args:
155+
mappedIDs: The total list of mappedIDs at this point in the mapping. This
156+
will contain pre- and post-atomIDs
157+
searchIndex: Determines whether to use pre- or post-atomIDs
158+
159+
Returns:
160+
Updates existing class variable self.NeighbourIDs
161+
"""
162+
searchIndexMappedIDs = [row[searchIndex] for row in mappedIDs]
163+
164+
self.mappedNeighbourIDs = [ID for ID in self.mappedNeighbourIDs if ID not in searchIndexMappedIDs]
165+
self.mappedNeighbourElements = [elementDict[atomID]for atomID in self.mappedNeighbourIDs]
166+
167+
168+
def map_elements(self, atomObject, preAtomObjectDict, postAtomObjectDict):
169+
# Output variables
170+
mapList = []
171+
missingPreAtoms = []
172+
queueAtoms = []
173+
174+
def allowed_maps(preAtom, postAtom):
175+
# Checks if elements appear the same number of times in pre and post atoms
176+
# If they don't, mapping is not allowed to take place and atoms are moved to missing lists
177+
preElementOccurences = Counter(preAtom.mappedNeighbourElements)
178+
postElementOccurences = Counter(postAtom.mappedNeighbourElements)
179+
180+
allowedMapDict = {}
181+
for element, count in preElementOccurences.items():
182+
if count == postElementOccurences[element]:
183+
allowedMapDict[element] = True
184+
else:
185+
allowedMapDict[element] = False
186+
187+
# Force all H to be be True as hydrogen can be mapped by inference
188+
if 'H' in allowedMapDict:
189+
allowedMapDict['H'] = True
190+
191+
return allowedMapDict
192+
193+
allowedMapDict = allowed_maps(self, atomObject)
194+
195+
# Match Function
196+
def matchNeighbour(preAtom, postAtom, preAtomIndex, postAtomIndex, mapList, queueList):
197+
# Append pre and post atomIDs to map
198+
mapList.append([preAtom.mappedNeighbourIDs[preAtomIndex], postAtom.mappedNeighbourIDs[postAtomIndex]])
199+
200+
# Add all non-hydrogen atom atomIDs to queue
201+
if preAtom.mappedNeighbourElements[preAtomIndex] != 'H':
202+
queueList.append([preAtom.mappedNeighbourIDs[preAtomIndex], postAtom.mappedNeighbourIDs[postAtomIndex]])
203+
204+
# Remove post atomID from mappedID and mappedElement atom object values
205+
postAtom.mappedNeighbourIDs.pop(postAtomIndex)
206+
postAtom.mappedNeighbourElements.pop(postAtomIndex)
207+
208+
# Loop through neighbours for atom in one state and compare to neighbours of atom in other state
209+
for preIndex, neighbour in enumerate(self.mappedNeighbourElements):
210+
elementOccurence = atomObject.mappedNeighbourElements.count(neighbour)
211+
212+
# Check if maps with the neighbour element are allowed, if not add current element to missing list
213+
if allowedMapDict[neighbour] == False:
214+
missingPreAtoms.append(self.mappedNeighbourIDs[preIndex])
215+
continue
216+
217+
# If no match in post atom list it is a missingPreAtom
218+
if elementOccurence == 0:
219+
missingPreAtoms.append(self.mappedNeighbourIDs[preIndex])
220+
221+
# Assign atomIDs if there is only one matching element - could this go wrong if an element moves and an identical element takes its place?
222+
elif elementOccurence == 1:
223+
postIndex = atomObject.mappedNeighbourElements.index(neighbour)
224+
logging.debug(f'Pre: {self.mappedNeighbourIDs[preIndex]}, Post: {atomObject.mappedNeighbourIDs[postIndex]} found with single element occurence')
225+
matchNeighbour(self, atomObject, preIndex, postIndex, mapList, queueAtoms)
226+
227+
# More than one matching element requires additional considerations
228+
elif elementOccurence > 1:
229+
if neighbour == 'H': # H can be handled simply as all H are equivalent to each other in this case - ignores chirality
230+
postHydrogenIndexList = [index for index, element in enumerate(atomObject.mappedNeighbourElements) if element == 'H']
231+
postIndex = postHydrogenIndexList.pop()
232+
logging.debug(f'Pre: {self.mappedNeighbourIDs[preIndex]}, Post: {atomObject.mappedNeighbourIDs[postIndex]} found with hydrogen symmetry inference')
233+
matchNeighbour(self, atomObject, preIndex, postIndex, mapList, queueAtoms)
234+
235+
else:
236+
# Get neighbour post atoms objects
237+
postNeighbourIndices = [index for index, val in enumerate(atomObject.mappedNeighbourElements) if val == neighbour]
238+
postNeighbourAtomIDs = [atomObject.mappedNeighbourIDs[i] for i in postNeighbourIndices]
239+
postNeighbourAtomObjects = [postAtomObjectDict[atomID] for atomID in postNeighbourAtomIDs]
240+
241+
# Get possible pre atom object
242+
preNeighbourAtomObject = preAtomObjectDict[self.mappedNeighbourIDs[preIndex]]
243+
244+
# Find the post atom ID for the current pre atom
245+
postNeighbourAtomID = compare_symmetric_atoms(postNeighbourAtomObjects, preNeighbourAtomObject, 'atomID')
246+
if postNeighbourAtomID is not None:
247+
postIndex = atomObject.mappedNeighbourIDs.index(postNeighbourAtomID)
248+
matchNeighbour(self, atomObject, preIndex, postIndex, mapList, queueAtoms)
249+
else:
250+
# If no post atom found, add pre atom missing atom list
251+
print(f'Could not find the symmetric pair for preAtom {self.mappedNeighbourIDs[preIndex]}')
252+
missingPreAtoms.append(self.mappedNeighbourIDs[preIndex])
253+
254+
255+
# Search mapList for missingPostAtoms
256+
mappedPostAtomList = [row[1] for row in mapList]
257+
missingPostAtoms = [neighbour for neighbour in atomObject.mappedNeighbourIDs if neighbour not in mappedPostAtomList]
258+
259+
return mapList, missingPreAtoms, missingPostAtoms, queueAtoms

0 commit comments

Comments
 (0)