1.1.3.3. Database Cleaning

The script clean_mol_db.py reads molecules from an input file, performs (optional) preprocessing, and then writes only those molecules that fulfill particular user-defined criteria to an output file.

Synopsis

python clean_mol_db.py [-h] -i <file> -o <file> [-d <file>] [-s] [-c] [-x <element list>] [-a <element list>] [-m <element count list>] [-M <element count list>] [-v <0|1|2|3>]

Mandatory options

-i <file>

Input molecule file.

-o <file>

Output molecule file.

Other options

-h, --help

Shows help message.

-d <file>

Discarded molecule output file.

-s

Keep only the largest molecule component (default: false).

-c

Minimize the number of charged atoms (default: false) by protonation/deprotonation and charge equalization.

-x <element list>

List of excluded chem. elements (default: no elements are excluded).

-a <element list>

List of allowed chem. elements (default: all elements are allowed).

-m <element count list>

Minimum chem. element specific atom counts (default: no count limits).

-M <element count list>

Maximum chem. element specific atom counts (default: no count limits).

-v <0|1|2|3>

Verbosity level (default: 1; 0 -> no console output, 1 -> print summary, 2 -> verbose, 3 -> extra verbose).

The options -a and -x both require a list of chemical elements as argument. Chemical element lists are specified in the form <S>,…,<S> where <S> is the symbol of a chemical element or generic atom type. Supported generic types are:

Symbol

Meaning

M

any metal

MH

any metal or hydrogen

A

any element except hydrogen

AH

any element

*

any element (equivalent to AH)

X

any halogen

XH

any halogen or hydrogen

Q

any element except hydrogen and carbon

QH

any element except carbon

The options -m and -M both require a list of chemical element counts as argument. Chemical element counts are specified in the form <S>:<N>,…,<S>:<N> where <S> is the symbol of a chemical element or generic atom type (see above) and <N> is the corresponding minimum or maximum count. If the count part is omitted and only <S> gets specified then the count is assumed to be 1.

Example usage

python clean_mol_db.py -i <path/to/molecule/input/file> -o <path/to/molecule/output/file> -a C,H,N,O,S,P,F,Cl,Br,I -m C,A:3 -M F:9 -c -s

When executed as shown, the script will perform the following operations on each read input molecule (in the order listed):

  1. Reduction of the number of charged atoms (if any and if possible)

  2. Removal of all but the largest molecular graph component (only if multi-comp. molecule)

  3. Check whether the chem. element of each atom of the working molecule (= result of prev. steps) is either C, H, N, O, S, P, F, Cl, Br, or I.

  4. Check whether the atom list of the working molecule contains at least one carbon and three heavy atoms

  5. Check whether the atom list of the working molecule contains not more than 9 fluorine atoms

The first check that fails leads to a rejection of the molecule. Working molecules that pass all checks will be written to the specified output file.

Code

  1import sys
  2import argparse
  3
  4import CDPL.Chem as Chem
  5import CDPL.MolProp as MolProp
  6
  7    
  8# performs a (optional) standardization of the argument molecule and then checks whether
  9# it fulfills certain user-defined criteria for inclusion/exclusion
 10def processMolecule(mol: Chem.Molecule, args: argparse.Namespace) -> (Chem.MolecularGraph, str):
 11    chgs_mod = False
 12
 13    if args.min_charges:
 14        # will store the 'uncharged' version of the argument mol
 15        res_mol = Chem.BasicMolecule() 
 16
 17        # minimize the number of charged atoms using the corresponding functionality implemented
 18        # in class Chem.ProtonationStateStandardizer
 19        chgs_mod = Chem.ProtonationStateStandardizer().standardize(mol, res_mol, Chem.ProtonationStateStandardizer.MIN_CHARGED_ATOM_COUNT)
 20
 21        # if changes were made then from now on use the 'uncharged' molecule for further checks
 22        if chgs_mod:  
 23            mol = res_mol
 24        
 25    comps_strpd = False
 26    
 27    if args.strip_comps:
 28        # perceive components (if not done already)
 29        comps = Chem.perceiveComponents(mol, False) 
 30
 31        # find largest component (regarding heavy atom count) but only if multi-comp. molecule
 32        if comps.size > 1:    # check if multi-comp. molecule
 33            lgst_comp = None
 34            lgst_comp_hvy_atom_count = 0
 35
 36            for comp in Chem.getComponents(mol):                 # for each component
 37                hvy_atom_count = MolProp.getHeavyAtomCount(comp) # calc. non-hydrogen atom count
 38
 39                if hvy_atom_count > lgst_comp_hvy_atom_count:    # check if the largest comp. so far has been found
 40                    lgst_comp_hvy_atom_count = hvy_atom_count    # if so, store for later use
 41                    lgst_comp = comp
 42
 43            # if the input molecule has structure data then pass them on (for SDF output)
 44            if Chem.hasStructureData(mol):                       
 45                Chem.setStructureData(lgst_comp, Chem.getStructureData(mol))
 46
 47            # for further checks use only the identified largest component 
 48            comps_strpd = True
 49            mol = lgst_comp
 50
 51    # calc. implicit hydrogen counts (if not done already)
 52    Chem.calcImplicitHydrogenCounts(mol, False)
 53    
 54    # create instance of class MolProp.ElementHistogram for storing the per-element atom counts
 55    # of the molecule (or its largest comp.)
 56    elem_histo = MolProp.ElementHistogram()              
 57
 58    # get per-element atom counts
 59    MolProp.generateElementHistogram(mol, elem_histo, False)    
 60
 61    # check if the found chem. elements are all in the set of allowed elements (if specified)
 62    if not checkAllowedElements(elem_histo, args.allowed_elements):    
 63        return (None, 'prohibited element detected')
 64
 65    # check if none of the found chem. elements is in the set of excluded elements (if specified)
 66    if not checkExcludedElements(elem_histo, args.excluded_elements):
 67        return (None, 'prohibited element detected')
 68
 69    # check if all specified minium chem. element atom counts are reached
 70    if not checkMinAtomCounts(elem_histo, args.min_atom_counts):
 71        return (None, 'minimum atom counts not reached')
 72
 73    # check if none of the specified maximum chem. element atom counts gets exceeded
 74    if not checkMaxAtomCounts(elem_histo, args.max_atom_counts):
 75        return (None, 'maximum atom count exceeded')
 76
 77    log_msg = None
 78    
 79    if chgs_mod:
 80        log_msg = 'reduced charged atom count'
 81
 82    if comps_strpd:
 83        if log_msg:
 84            log_msg += ', removed components'
 85        else:
 86            log_msg = 'removed components'
 87            
 88    return (mol, log_msg)
 89
 90# checks if all found chem. elements are in the set of allowed elements (if specified)
 91def checkAllowedElements(elem_histo: MolProp.ElementHistogram, elem_list: list) -> bool:
 92    if not elem_list:
 93        return True
 94
 95    for atom_type in elem_histo.getKeys():
 96        allowed = False
 97        
 98        for all_atom_type in elem_list:
 99            if Chem.atomTypesMatch(all_atom_type, atom_type):
100                allowed = True
101                break
102
103        if not allowed:
104            return False
105
106    return True
107
108# checks if none of the found chem. elements is in the set of excluded elements (if specified)
109def checkExcludedElements(elem_histo: MolProp.ElementHistogram, elem_list: list) -> bool:
110    if not elem_list:
111        return True
112
113    for atom_type in elem_histo.getKeys():
114        for x_atom_type in elem_list:
115            if Chem.atomTypesMatch(x_atom_type, atom_type):
116                return False
117
118    return True
119
120# return the total number of atoms matching the specified atom type (element or generic type)
121def getNumAtomsOfType(elem_histo: MolProp.ElementHistogram, qry_atom_type: int) -> int:
122    tot_count = 0
123
124    for atom_type, count in elem_histo.getEntries():
125        # check if the elem. histogram entry matches the specified atom type (element or generic type)
126        # if so, add the stored count the total count
127        if Chem.atomTypesMatch(qry_atom_type, atom_type):  
128            tot_count += count
129
130    return tot_count
131
132# checks if all the specified minium counts of atoms matching a particular element or
133# generic type are reached
134def checkMinAtomCounts(elem_histo: MolProp.ElementHistogram, atom_counts: dict) -> bool:
135    if not atom_counts:
136        return True
137    
138    for atom_type, min_count in atom_counts.items():
139        if getNumAtomsOfType(elem_histo, atom_type) < min_count:
140            return False
141
142    return True
143
144# checks if none of the specified maximum counts of atoms matching a particular element or
145# generic type gets exceeded
146def checkMaxAtomCounts(elem_histo: MolProp.ElementHistogram, atom_counts: dict) -> bool:
147    if not atom_counts:
148        return True
149    
150    for atom_type, max_count in atom_counts.items():
151        if getNumAtomsOfType(elem_histo, atom_type) > max_count:
152            return False
153
154    return True
155
156# converts a comma separated list of chem. element/generic type symbols into a list of the
157# corresponding numeric atom types defined in Chem.AtomType
158def parseElementList(elem_list: str) -> [int]:
159    if not elem_list:
160        return []
161    
162    atom_types = []
163    
164    for elem in elem_list.split(','):
165        elem = elem.strip()
166        
167        if not elem: # zero length?
168            continue
169        
170        atom_type = Chem.AtomDictionary.getType(elem)
171
172        if atom_type == Chem.AtomType.UNKNOWN:
173            sys.exit('Error: unknown chemical element/generic type \'%s\'' % elem)
174
175        atom_types.append(atom_type)
176            
177    return atom_types
178
179# converts a comma separated list of chem. element (or generic type) symbol/count pairs (sep. by colon) into a dictionary
180# mapping the corresponding numeric atom types (defined in Chem.AtomType) to an integer number
181def parseElementCountList(elem_count_list: str) -> [int]:
182    if not elem_count_list:
183        return {}
184    
185    atom_type_counts = {}
186    
187    for elem_count in elem_count_list.split(','):
188        elem_count = elem_count.strip()
189        
190        if not elem_count: # zero length?
191            continue
192        
193        elem_count = elem_count.split(':')
194        elem_count[0] = elem_count[0].strip()
195        atom_type = Chem.AtomDictionary.getType(elem_count[0])
196        
197        if atom_type == Chem.AtomType.UNKNOWN:
198            sys.exit('Error: unknown chemical element/generic type \'%s\'' % elem_count[0])
199
200        count = 1
201        
202        if len(elem_count) > 1: # has count spec.?
203            count = int(elem_count[1])
204            
205        atom_type_counts[atom_type] = count
206            
207    return atom_type_counts
208
209def main() -> None:
210    args = parseArgs() # process command line arguments
211
212    # convert specified lists of chem. element/gen. type symbols into a corresponding list of
213    # numeric atom types (defined in Chem.Atomtype)
214    args.allowed_elements  = parseElementList(args.allowed_elements)
215    args.excluded_elements = parseElementList(args.excluded_elements)
216
217    # convert specified comma separated lists of chem. element (or generic type) symbol/count pairs (sep. by colon) into
218    # corresponding dictionaries mapping numeric atom types (defined in Chem.AtomType) to integers
219    args.min_atom_counts = parseElementCountList(args.min_atom_counts)
220    args.max_atom_counts = parseElementCountList(args.max_atom_counts)
221    
222    # create reader for input molecules (format specified by file extension)
223    reader = Chem.MoleculeReader(args.in_file) 
224
225    # create writer for molecules passing the checks (format specified by file extension)
226    writer = Chem.MolecularGraphWriter(args.out_file) 
227
228    if args.disc_file:
229        # create writer for sorted out molecules (format specified by file extension)
230        disc_writer = Chem.MolecularGraphWriter(args.disc_file) 
231    else:
232        disc_writer = None
233        
234    # create instances of the default implementation of the Chem.Molecule interface for the input and output molecules
235    in_mol = Chem.BasicMolecule()
236
237    i = 0
238    num_changed = 0
239    num_disc = 0
240    
241    try:
242        # read and process molecules one after the other until the end of input has been reached (or a severe error occurs)
243        while reader.read(in_mol):
244            # compose a molecule identifier
245            mol_id = Chem.getName(in_mol).strip() 
246
247            if mol_id == '':
248                mol_id = '#' + str(i + 1)  # fallback if name is empty or not available
249            else:
250                mol_id = '\'%s\' (#%s)' % (mol_id, str(i + 1))
251            
252            try:
253                # process input molecule
254                out_mol, log_msg = processMolecule(in_mol, args) 
255                
256                if not out_mol:         # check whether the molecule has been sorted out
257                    if args.verb_level > 1 and log_msg:
258                        print('- Molecule %s: discarded, %s' % (mol_id, log_msg))
259
260                    num_disc += 1
261                        
262                    if not disc_writer: # check whether discarded molecules should be saved to a separate file
263                        i += 1
264                        continue
265
266                    # write discarded molecule to the specified target file
267                    out_mol = in_mol
268                    out_mol_writer = disc_writer
269                else:
270                    if log_msg:
271                        if args.verb_level > 1 :
272                            print('- Molecule %s: %s' % (mol_id, log_msg))
273                            
274                        num_changed += 1
275                    else:
276                        if args.verb_level > 2:
277                            print('- Molecule %s: left unchanged' % mol_id)
278
279                    # molecule passed all checks and needs to be written to the regular output file
280                    out_mol_writer = writer
281                    
282                try:
283                    # calculate (if not already present) some basic properties of the output molecule
284                    # that might be required for writing (output format dependent)
285                    Chem.calcImplicitHydrogenCounts(out_mol, False)
286                    Chem.perceiveHybridizationStates(out_mol, False)
287                    Chem.perceiveSSSR(out_mol, False)
288                    Chem.setRingFlags(out_mol, False)
289                    Chem.setAromaticityFlags(out_mol, False)
290                    Chem.perceiveComponents(out_mol, False)
291                              
292                    # output molecule
293                    if not out_mol_writer.write(out_mol):
294                        sys.exit('Error: writing molecule %s failed: %s' % (mol_id, str(e)))
295
296                except Exception as e: # handle exception raised in case of severe write errors
297                    sys.exit('Error: writing molecule %s failed: %s' % (mol_id, str(e)))
298
299                i += 1
300                
301            except Exception as e: # handle exception raised in case of severe processing errors
302                sys.exit('Error: processing of molecule %s failed: %s' % (mol_id, str(e)))
303                
304    except Exception as e: # handle exception raised in case of severe read errors
305        sys.exit('Error: reading of molecule %s failed: %s' % (str(i), str(e)))
306
307    if args.verb_level > 0:
308        print('Summary:')
309        print(' - %s molecules processed' % str(i))
310        print(' - %s modified ' % str(num_changed))
311        print(' - %s discarded' % str(num_disc))
312        
313    writer.close()
314    sys.exit(0)
315                                
316def parseArgs() -> argparse.Namespace:
317    parser = argparse.ArgumentParser(description='Strips compounds that fulfill particular user-defined criteria from a molecule database')
318
319    parser.add_argument('-i',
320                        dest='in_file',
321                        required=True,
322                        metavar='<file>',
323                        help='Input molecule file')
324    parser.add_argument('-o',
325                        dest='out_file',
326                        required=True,
327                        metavar='<file>',
328                        help='Output molecule file')
329    parser.add_argument('-d',
330                        dest='disc_file',
331                        required=False,
332                        metavar='<file>',
333                        help='Discarded molecule output file')
334    parser.add_argument('-s',
335                        dest='strip_comps',
336                        required=False,
337                        action='store_true',
338                        default=False,
339                        help='Keep only the largest molecule component (default: false)')
340    parser.add_argument('-c',
341                        dest='min_charges',
342                        required=False,
343                        action='store_true',
344                        default=False,
345                        help='Minimize number of charged atoms (default: false)')
346    parser.add_argument('-x',
347                        dest='excluded_elements',
348                        required=False,
349                        metavar="<element list>",
350                        help='List of excluded chem. elements (default: no elements are excluded)')
351    parser.add_argument('-a',
352                        dest='allowed_elements',
353                        required=False,
354                        metavar="<element list>",
355                        help='List of allowed chem. elements (default: all elements are allowed)')
356    parser.add_argument('-m',
357                        dest='min_atom_counts',
358                        required=False,
359                        metavar="<element count list>",
360                        help='Minimum chem. element specific atom counts (default: no count limits)')
361    parser.add_argument('-M',
362                        dest='max_atom_counts',
363                        required=False,
364                        metavar="<element count list>",
365                        help='Maximum chem. element specific atom counts (default: no count limits)')
366    parser.add_argument('-v',
367                        dest='verb_level',
368                        required=False,
369                        metavar='<0|1|2|3>',
370                        choices=range(0, 4),
371                        default=1,
372                        help='Verbosity level (default: 1; 0 -> no console output, 1 -> print summary, 2 -> verbose, 3 -> extra verbose)',
373                        type=int)
374
375    return parser.parse_args()
376
377if __name__ == '__main__':
378    main()

Download source file