Repository 'openbabel_structure_distance_finder'
hg clone https://toolshed.g2.bx.psu.edu/repos/bgruening/openbabel_structure_distance_finder

Changeset 5:8302ab092300 (2024-08-15)
Previous changeset 4:2c5c7da26e08 (2020-11-10)
Commit message:
planemo upload for repository https://github.com/bgruening/galaxytools/tree/master/chemicaltoolbox/openbabel commit d9c51279c061a1da948a2582d5b502ca7573adbf
modified:
change_title_to_metadata_value.py
cheminfolib.py
distance_finder.py
multi_obgrep.py
ob_addh.py
ob_filter.py
ob_genProp.py
ob_remIons.py
ob_spectrophore_search.py
remove_protonation_state.py
subsearch.py
added:
test-data/2_mol.sdf
b
diff -r 2c5c7da26e08 -r 8302ab092300 change_title_to_metadata_value.py
--- a/change_title_to_metadata_value.py Tue Nov 10 20:40:06 2020 +0000
+++ b/change_title_to_metadata_value.py Thu Aug 15 11:01:11 2024 +0000
[
@@ -11,6 +11,7 @@
 import string
 
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
@@ -19,14 +20,19 @@
         description="Change the title from a molecule file to metadata \
                      value of a given-id of the same molecule file.",
     )
-    parser.add_argument('--infile', '-i', required=True,
-                        help="path to the input file")
-    parser.add_argument('--outfile', '-o', required=True,
-                        help="path to the output file")
-    parser.add_argument('--key', '-k', required=True,
-                        help="the metadata key from the sdf file which should inlcude the new title")
-    parser.add_argument('--random', '-r', action="store_true",
-                        help="Add random suffix to the title.")
+    parser.add_argument("--infile", "-i", required=True, help="path to the input file")
+    parser.add_argument(
+        "--outfile", "-o", required=True, help="path to the output file"
+    )
+    parser.add_argument(
+        "--key",
+        "-k",
+        required=True,
+        help="the metadata key from the sdf file which should inlcude the new title",
+    )
+    parser.add_argument(
+        "--random", "-r", action="store_true", help="Add random suffix to the title."
+    )
 
     args = parser.parse_args()
 
@@ -35,8 +41,11 @@
         if args.key in mol.data:
             mol.title = mol.data[args.key]
             if args.random:
-                suffix = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(13))
-                mol.title += '__%s' % suffix
+                suffix = "".join(
+                    random.choice(string.ascii_lowercase + string.digits)
+                    for _ in range(13)
+                )
+                mol.title += "__%s" % suffix
         output.write(mol)
 
     output.close()
b
diff -r 2c5c7da26e08 -r 8302ab092300 cheminfolib.py
--- a/cheminfolib.py Tue Nov 10 20:40:06 2020 +0000
+++ b/cheminfolib.py Thu Aug 15 11:01:11 2024 +0000
[
b'@@ -11,28 +11,32 @@\n import tempfile\n from multiprocessing import Pool\n \n-\n try:\n     from galaxy import eggs\n-    eggs.require(\'psycopg2\')\n+\n+    eggs.require("psycopg2")\n except ImportError:\n     psycopg2 = None\n-    print(\'psycopg2 is not available. It is currently used in the pgchem wrappers, that are not shipped with default CTB\')\n+    print(\n+        "psycopg2 is not available. It is currently used in the pgchem wrappers, that are not shipped with default CTB"\n+    )\n \n try:\n     from openbabel import openbabel, pybel\n+\n     openbabel.obErrorLog.StopLogging()\n except ImportError:\n     openbabel, pybel = None, None\n-    print(\'OpenBabel could not be found. A few functions are not available without OpenBabel.\')\n+    print(\n+        "OpenBabel could not be found. A few functions are not available without OpenBabel."\n+    )\n \n \n def CountLines(path):\n-    out = subprocess.Popen([\'wc\', \'-l\', path],\n-                           stdout=subprocess.PIPE,\n-                           stderr=subprocess.STDOUT\n-                           ).communicate()[0]\n-    return int(out.partition(b\' \')[0])\n+    out = subprocess.Popen(\n+        ["wc", "-l", path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT\n+    ).communicate()[0]\n+    return int(out.partition(b" ")[0])\n \n \n def grep(pattern, file_obj):\n@@ -49,15 +53,15 @@\n     for line_counter, line in enumerate(open(filepath)):\n         if line_counter > 10000:\n             break\n-        if line.find(\'$$$$\') != -1:\n-            return \'sdf\'\n-        elif line.find(\'@<TRIPOS>MOLECULE\') != -1:\n-            return \'mol2\'\n-        elif line.find(\'ligand id\') != -1:\n-            return \'drf\'\n-        elif possible_inchi and re.findall(\'^InChI=\', line):\n-            return \'inchi\'\n-        elif re.findall(r\'^M\\s+END\', line):\n+        if line.find("$$$$") != -1:\n+            return "sdf"\n+        elif line.find("@<TRIPOS>MOLECULE") != -1:\n+            return "mol2"\n+        elif line.find("ligand id") != -1:\n+            return "drf"\n+        elif possible_inchi and re.findall("^InChI=", line):\n+            return "inchi"\n+        elif re.findall(r"^M\\s+END", line):\n             mol = True\n         # first line is not an InChI, so it can\'t be an InChI file\n         possible_inchi = False\n@@ -65,99 +69,128 @@\n     if mol:\n         # END can occures before $$$$, so and SDF file will\n         # be recognised as mol, if you not using this hack\'\n-        return \'mol\'\n-    return \'smi\'\n+        return "mol"\n+    return "smi"\n \n \n def db_connect(args):\n     try:\n-        db_conn = psycopg2.connect("dbname=%s user=%s host=%s password=%s" % (args.dbname, args.dbuser, args.dbhost, args.dbpasswd))\n+        db_conn = psycopg2.connect(\n+            "dbname=%s user=%s host=%s password=%s"\n+            % (args.dbname, args.dbuser, args.dbhost, args.dbpasswd)\n+        )\n         return db_conn\n     except psycopg2.Error:\n-        sys.exit(\'Unable to connect to the db\')\n+        sys.exit("Unable to connect to the db")\n \n \n ColumnNames = {\n-    \'can_smiles\': \'Canonical SMILES\',\n-    \'can\': \'Canonical SMILES\',\n-    \'inchi\': \'InChI\',\n-    \'inchi_key\': \'InChI key\',\n-    \'inchi_key_first\': \'InChI key first\',\n-    \'inchi_key_last\': \'InChI key last\',\n-    \'molwt\': \'Molecular weight\',\n-    \'hbd\': \'Hydrogen-bond donors\',\n-    \'donors\': \'Hydrogen-bond donors\',\n-    \'hba\': \'Hydrogen-bond acceptors\',\n-    \'acceptors\': \'Hydrogen-bond acceptors\',\n-    \'rotbonds\': \'Rotatable bonds\',\n-    \'logp\': \'logP\',\n-    \'psa\': \'Polar surface area\',\n-    \'mr\': \'Molecular refractivity\',\n-    \'atoms\': \'Number of heavy atoms\',\n-    \'rings\': \'Number of rings\',\n-    \'set_bits\': \'FP2 bits\',\n-    \'id\': \'Internal identifier\',\n-    \'tani\': \'Tanimoto coefficient\',\n-    \'spectrophore\': \'Spectrophores(TM)\',\n-    \'dist_spectrophore\': \'Spectrophores(TM) distance to target\',\n-    \'synonym\': \'Entry id\',\n+    "can_smiles": "Canonical SMILES",\n+    "can": "Canonical SMILES",\n+    "inchi": "InChI",\n+    "inchi_key": "InChI key",\n+    "inchi_key_first": "InC'..b'ol)),\n+        "acceptors": len(HBA.findall(mol)),\n+        "psa": calc_desc_dict["TPSA"],\n+        "mr": calc_desc_dict["MR"],\n+        "rotbonds": mol.OBMol.NumRotors(),\n+        "can": mol.write("can")\n+        .split()[0]\n+        .strip(),  # tthis one works fine for both zinc and chembl (no ZINC code added after can descriptor string)\n+        "inchi": mol.write("inchi").strip(),\n+        "inchi_key": get_inchikey(mol).strip(),\n+        "rings": len(mol.sssr),\n+        "atoms": mol.OBMol.NumHvyAtoms(),\n+        "spectrophore": OBspectrophore(mol),\n+    }\n \n \n def get_inchikey(mol):\n@@ -206,10 +245,12 @@\n     spectrophore = pybel.ob.OBSpectrophore()\n     # Parameters: rotation angle = 20, normalization for mean and sd, accuracy = 3.0 A and non-stereospecific cages.\n     spectrophore.SetNormalization(spectrophore.NormalizationTowardsZeroMeanAndUnitStd)\n-    return \', \'.join(["%.3f" % value for value in spectrophore.GetSpectrophore(mol.OBMol)])\n+    return ", ".join(\n+        ["%.3f" % value for value in spectrophore.GetSpectrophore(mol.OBMol)]\n+    )\n \n \n-def split_library(lib_path, lib_format=\'sdf\', package_size=None):\n+def split_library(lib_path, lib_format="sdf", package_size=None):\n     """\n     Split a library of compounds. Usage: split_library(lib_path, lib_format, package_size)\n     IT currently ONLY WORKS FOR SD-Files\n@@ -217,18 +258,39 @@\n     pack = 1\n     mol_counter = 0\n \n-    outfile = open(\'/%s/%s_pack_%i.%s\' % (\'/\'.join(lib_path.split(\'/\')[:-1]), lib_path.split(\'/\')[-1].split(\'.\')[0], pack, \'sdf\'), \'w\')\n+    outfile = open(\n+        "/%s/%s_pack_%i.%s"\n+        % (\n+            "/".join(lib_path.split("/")[:-1]),\n+            lib_path.split("/")[-1].split(".")[0],\n+            pack,\n+            "sdf",\n+        ),\n+        "w",\n+    )\n \n-    for line in open(lib_path, \'r\'):\n+    for line in open(lib_path, "r"):\n         outfile.write(line)\n-        if line.strip() == \'$$$$\':\n+        if line.strip() == "$$$$":\n             mol_counter += 1\n             if mol_counter % package_size == 0:\n                 outfile.close()\n                 pack += 1\n-                outfile = open(\'/%s/%s_pack_%i.%s\' % (\'/\'.join(lib_path.split(\'/\')[:-1]), lib_path.split(\'/\')[-1].split(\'.\')[0], pack, \'sdf\'), \'w\')\n+                outfile = open(\n+                    "/%s/%s_pack_%i.%s"\n+                    % (\n+                        "/".join(lib_path.split("/")[:-1]),\n+                        lib_path.split("/")[-1].split(".")[0],\n+                        pack,\n+                        "sdf",\n+                    ),\n+                    "w",\n+                )\n                 if mol_counter * 10 % package_size == 0:\n-                    print(\'%i molecules parsed, starting pack nr. %i\' % (mol_counter, pack - 1))\n+                    print(\n+                        "%i molecules parsed, starting pack nr. %i"\n+                        % (mol_counter, pack - 1)\n+                    )\n     outfile.close()\n \n     return True\n@@ -242,7 +304,7 @@\n     output_files = []\n     tfile = tempfile.NamedTemporaryFile(delete=False)\n \n-    smiles_handle = open(smiles_file, \'r\')\n+    smiles_handle = open(smiles_file, "r")\n     for count, line in enumerate(smiles_handle):\n         if count % structures_in_one_file == 0 and count != 0:\n             tfile.close()\n@@ -257,16 +319,19 @@\n \n def mp_run(input_path, regex, PROCESSES, function_to_call):\n     paths = []\n-    [paths.append(compound_file) for compound_file in glob.glob(str(input_path) + str(regex))]\n+    [\n+        paths.append(compound_file)\n+        for compound_file in glob.glob(str(input_path) + str(regex))\n+    ]\n     paths.sort()\n \n     pool = Pool(processes=PROCESSES)\n-    print(\'Process initialized with\', PROCESSES, \'processors\')\n+    print("Process initialized with", PROCESSES, "processors")\n     result = pool.map_async(function_to_call, paths)\n     result.get()\n \n     return paths\n \n \n-if __name__ == \'__main__\':\n+if __name__ == "__main__":\n     print(check_filetype(sys.argv[1]))\n'
b
diff -r 2c5c7da26e08 -r 8302ab092300 distance_finder.py
--- a/distance_finder.py Tue Nov 10 20:40:06 2020 +0000
+++ b/distance_finder.py Thu Aug 15 11:01:11 2024 +0000
[
@@ -19,9 +19,8 @@
 
 
 def log(*args, **kwargs):
-    """Log output to STDERR
-    """
-    print(*args, file=sys.stderr, ** kwargs)
+    """Log output to STDERR"""
+    print(*args, file=sys.stderr, **kwargs)
 
 
 def execute(ligands_sdf, points_file, outfile):
@@ -35,7 +34,7 @@
     points = []
 
     # read the points
-    with open(points_file, 'r') as f:
+    with open(points_file, "r") as f:
         for line in f.readlines():
             line.strip()
             if line:
@@ -45,7 +44,7 @@
                     log("Read points", p)
                     continue
             log("Failed to read line:", line)
-    log('Found', len(points), 'atom points')
+    log("Found", len(points), "atom points")
 
     sdf_writer = pybel.Outputfile("sdf", outfile, overwrite=True)
 
@@ -53,7 +52,7 @@
     for mol in pybel.readfile("sdf", ligands_sdf):
         count += 1
         if count % 50000 == 0:
-            log('Processed', count)
+            log("Processed", count)
 
         try:
             # print("Processing mol", mol.title)
@@ -70,32 +69,42 @@
                 distances = []
                 for i in coords:
                     # calculates distance based on cartesian coordinates
-                    distance = math.sqrt((point[0] - i[0])**2 + (point[1] - i[1])**2 + (point[2] - i[2])**2)
+                    distance = math.sqrt(
+                        (point[0] - i[0]) ** 2
+                        + (point[1] - i[1]) ** 2
+                        + (point[2] - i[2]) ** 2
+                    )
                     distances.append(distance)
                     # log("distance:", distance)
                 min_distance = min(distances)
                 # log('Min:', min_distance)
                 # log(count, p, min_distance)
 
-                mol.data['distance' + str(p)] = min_distance
+                mol.data["distance" + str(p)] = min_distance
 
             sdf_writer.write(mol)
 
         except Exception as e:
-            log('Failed to handle molecule: ' + str(e))
+            log("Failed to handle molecule: " + str(e))
             continue
 
     sdf_writer.close()
-    log('Wrote', count, 'molecules')
+    log("Wrote", count, "molecules")
 
 
 def main():
     global work_dir
 
-    parser = argparse.ArgumentParser(description='XChem distances - measure distances to particular points')
-    parser.add_argument('-i', '--input', help="SDF containing the 3D molecules to score)")
-    parser.add_argument('-p', '--points', help="PDB format file with atoms")
-    parser.add_argument('-o', '--outfile', default='output.sdf', help="File name for results")
+    parser = argparse.ArgumentParser(
+        description="XChem distances - measure distances to particular points"
+    )
+    parser.add_argument(
+        "-i", "--input", help="SDF containing the 3D molecules to score)"
+    )
+    parser.add_argument("-p", "--points", help="PDB format file with atoms")
+    parser.add_argument(
+        "-o", "--outfile", default="output.sdf", help="File name for results"
+    )
 
     args = parser.parse_args()
     log("XChem distances args: ", args)
b
diff -r 2c5c7da26e08 -r 8302ab092300 multi_obgrep.py
--- a/multi_obgrep.py Tue Nov 10 20:40:06 2020 +0000
+++ b/multi_obgrep.py Thu Aug 15 11:01:11 2024 +0000
[
@@ -15,21 +15,55 @@
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('-i', '--infile', required=True, help='Molecule file.')
-    parser.add_argument('-q', '--query', required=True, help='Query file, containing different SMARTS in each line.')
-    parser.add_argument('-o', '--outfile', required=True, help='Path to the output file.')
+    parser.add_argument("-i", "--infile", required=True, help="Molecule file.")
+    parser.add_argument(
+        "-q",
+        "--query",
+        required=True,
+        help="Query file, containing different SMARTS in each line.",
+    )
+    parser.add_argument(
+        "-o", "--outfile", required=True, help="Path to the output file."
+    )
     parser.add_argument("--iformat", help="Input format, like smi, sdf, inchi")
-    parser.add_argument("--n-times", dest="n_times", type=int,
-                        default=0, help="Print a molecule only if the pattern occurs # times inside the molecule.")
-    parser.add_argument('-p', '--processors', type=int, default=multiprocessing.cpu_count())
-    parser.add_argument("--invert-matches", dest="invert_matches", action="store_true",
-                        default=False, help="Invert the matching, print non-matching molecules.")
-    parser.add_argument("--only-name", dest="only_name", action="store_true",
-                        default=False, help="Only print the name of the molecules.")
-    parser.add_argument("--full-match", dest="full_match", action="store_true",
-                        default=False, help="Full match, print matching-molecules only when the number of heavy atoms is also equal to the number of atoms in the SMARTS pattern.")
-    parser.add_argument("--number-of-matches", dest="number_of_matches", action="store_true",
-                        default=False, help="Print the number of matches.")
+    parser.add_argument(
+        "--n-times",
+        dest="n_times",
+        type=int,
+        default=0,
+        help="Print a molecule only if the pattern occurs # times inside the molecule.",
+    )
+    parser.add_argument(
+        "-p", "--processors", type=int, default=multiprocessing.cpu_count()
+    )
+    parser.add_argument(
+        "--invert-matches",
+        dest="invert_matches",
+        action="store_true",
+        default=False,
+        help="Invert the matching, print non-matching molecules.",
+    )
+    parser.add_argument(
+        "--only-name",
+        dest="only_name",
+        action="store_true",
+        default=False,
+        help="Only print the name of the molecules.",
+    )
+    parser.add_argument(
+        "--full-match",
+        dest="full_match",
+        action="store_true",
+        default=False,
+        help="Full match, print matching-molecules only when the number of heavy atoms is also equal to the number of atoms in the SMARTS pattern.",
+    )
+    parser.add_argument(
+        "--number-of-matches",
+        dest="number_of_matches",
+        action="store_true",
+        default=False,
+        help="Print the number of matches.",
+    )
     return parser.parse_args()
 
 
@@ -42,25 +76,27 @@
 
 def mp_helper(query, args):
     """
-        Helper function for multiprocessing.
-        That function is a wrapper around obgrep.
+    Helper function for multiprocessing.
+    That function is a wrapper around obgrep.
     """
 
     cmd_list = []
     if args.invert_matches:
-        cmd_list.append('-v')
+        cmd_list.append("-v")
     if args.only_name:
-        cmd_list.append('-n')
+        cmd_list.append("-n")
     if args.full_match:
-        cmd_list.append('-f')
+        cmd_list.append("-f")
     if args.number_of_matches:
-        cmd_list.append('-c')
+        cmd_list.append("-c")
     if args.n_times:
-        cmd_list.append('-t %s' % str(args.n_times))
+        cmd_list.append("-t %s" % str(args.n_times))
 
     tmp = tempfile.NamedTemporaryFile(delete=False)
-    cmd = 'obgrep %s "%s" %s' % (' '.join(cmd_list), query, args.infile)
-    child = subprocess.Popen(shlex.split(cmd), stdout=open(tmp.name, 'w+'), stderr=subprocess.PIPE)
+    cmd = 'obgrep %s "%s" %s' % (" ".join(cmd_list), query, args.infile)
+    child = subprocess.Popen(
+        shlex.split(cmd), stdout=open(tmp.name, "w+"), stderr=subprocess.PIPE
+    )
 
     stdout, stderr = child.communicate()
     return (tmp.name, query)
@@ -80,9 +116,9 @@
     pool.close()
     pool.join()
 
-    out_handle = open(args.outfile, 'wb')
+    out_handle = open(args.outfile, "wb")
     for result_file, query in results:
-        res_handle = open(result_file, 'rb')
+        res_handle = open(result_file, "rb")
         shutil.copyfileobj(res_handle, out_handle)
         res_handle.close()
         os.remove(result_file)
@@ -93,7 +129,7 @@
 
 def __main__():
     """
-        Multiprocessing obgrep search.
+    Multiprocessing obgrep search.
     """
     args = parse_command_line()
     obgrep(args)
b
diff -r 2c5c7da26e08 -r 8302ab092300 ob_addh.py
--- a/ob_addh.py Tue Nov 10 20:40:06 2020 +0000
+++ b/ob_addh.py Thu Aug 15 11:01:11 2024 +0000
b
@@ -7,16 +7,28 @@
 import sys
 
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
 def parse_command_line(argv):
     parser = argparse.ArgumentParser()
-    parser.add_argument('--iformat', type=str, default='sdf', help='input file format')
-    parser.add_argument('-i', '--input', type=str, required=True, help='input file name')
-    parser.add_argument('-o', '--output', type=str, required=True, help='output file name')
-    parser.add_argument('--polar', action="store_true", default=False, help='Add hydrogen atoms only to polar atoms')
-    parser.add_argument('--pH', type=float, default="7.4", help='Specify target pH value')
+    parser.add_argument("--iformat", type=str, default="sdf", help="input file format")
+    parser.add_argument(
+        "-i", "--input", type=str, required=True, help="input file name"
+    )
+    parser.add_argument(
+        "-o", "--output", type=str, required=True, help="output file name"
+    )
+    parser.add_argument(
+        "--polar",
+        action="store_true",
+        default=False,
+        help="Add hydrogen atoms only to polar atoms",
+    )
+    parser.add_argument(
+        "--pH", type=float, default="7.4", help="Specify target pH value"
+    )
     return parser.parse_args()
 
 
@@ -32,7 +44,7 @@
 
 def __main__():
     """
-        Add hydrogen atoms at a certain pH value
+    Add hydrogen atoms at a certain pH value
     """
     args = parse_command_line(sys.argv)
     addh(args)
b
diff -r 2c5c7da26e08 -r 8302ab092300 ob_filter.py
--- a/ob_filter.py Tue Nov 10 20:40:06 2020 +0000
+++ b/ob_filter.py Thu Aug 15 11:01:11 2024 +0000
[
@@ -14,33 +14,36 @@
 
 import cheminfolib
 from openbabel import pybel
+
 cheminfolib.pybel_stop_logging()
 
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('-i', '--input', help='Input file name')
-    parser.add_argument('-iformat', help='Input file format')
-    parser.add_argument('-oformat', default='smi',
-                        help='Output file format')
-    parser.add_argument('-o', '--output', help='Output file name',
-                        required=True)
-    parser.add_argument('--filters', help="Specify the filters to apply",
-                        required=True)
-    parser.add_argument('--list_of_names', required=False,
-                        help="A file with list of molecule names to extract. Every name is in one line.")
+    parser.add_argument("-i", "--input", help="Input file name")
+    parser.add_argument("-iformat", help="Input file format")
+    parser.add_argument("-oformat", default="smi", help="Output file format")
+    parser.add_argument("-o", "--output", help="Output file name", required=True)
+    parser.add_argument("--filters", help="Specify the filters to apply", required=True)
+    parser.add_argument(
+        "--list_of_names",
+        required=False,
+        help="A file with list of molecule names to extract. Every name is in one line.",
+    )
     return parser.parse_args()
 
 
 def filter_precalculated_compounds(args, filters):
     outfile = pybel.Outputfile(args.oformat, args.output, overwrite=True)
-    for mol in pybel.readfile('sdf', args.input):
+    for mol in pybel.readfile("sdf", args.input):
         for key, elem in filters.items():
             # map the short description to the larger metadata names stored in the sdf file
             property = cheminfolib.ColumnNames.get(key, key)
             min = elem[0]
             max = elem[1]
-            if float(mol.data[property]) >= float(min) and float(mol.data[property]) <= float(max):
+            if float(mol.data[property]) >= float(min) and float(
+                mol.data[property]
+            ) <= float(max):
                 pass
             else:
                 # leave the filter loop, because one filter constrained are not satisfied
@@ -56,16 +59,30 @@
     if args.iformat == args.oformat:
         # use the -ocopy option from openbabel to speed up the filtering, additionally no conversion is carried out
         # http://openbabel.org/docs/dev/FileFormats/Copy_raw_text.html#copy-raw-text
-        cmd = 'obabel -i%s %s -ocopy -O %s --filter' % (args.iformat, args.input, args.output)
+        cmd = "obabel -i%s %s -ocopy -O %s --filter" % (
+            args.iformat,
+            args.input,
+            args.output,
+        )
     else:
-        cmd = 'obabel -i%s %s -o%s -O %s --filter' % (args.iformat, args.input, args.oformat, args.output)
-    filter_cmd = ''
+        cmd = "obabel -i%s %s -o%s -O %s --filter" % (
+            args.iformat,
+            args.input,
+            args.oformat,
+            args.output,
+        )
+    filter_cmd = ""
     # OBDescriptor stores a mapping from our desc shortcut to the OB name [0] and a long description [1]
     for key, elem in filters.items():
         ob_descriptor_name = cheminfolib.OBDescriptor[key][0]
         min = elem[0]
         max = elem[1]
-        filter_cmd += ' %s>=%s %s<=%s ' % (ob_descriptor_name, min, ob_descriptor_name, max)
+        filter_cmd += " %s>=%s %s<=%s " % (
+            ob_descriptor_name,
+            min,
+            ob_descriptor_name,
+            max,
+        )
 
     args = shlex.split('%s "%s"' % (cmd, filter_cmd))
     # print '%s "%s"' % (cmd, filter_cmd)
@@ -76,18 +93,18 @@
     return_code = child.returncode
 
     if return_code:
-        sys.stdout.write(stdout.decode('utf-8'))
-        sys.stderr.write(stderr.decode('utf-8'))
+        sys.stdout.write(stdout.decode("utf-8"))
+        sys.stderr.write(stderr.decode("utf-8"))
         sys.stderr.write("Return error code %i from command:\n" % return_code)
         sys.stderr.write("%s\n" % cmd)
     else:
-        sys.stdout.write(stdout.decode('utf-8'))
-        sys.stdout.write(stderr.decode('utf-8'))
+        sys.stdout.write(stdout.decode("utf-8"))
+        sys.stdout.write(stderr.decode("utf-8"))
 
 
 def filter_by_name(args):
     outfile = pybel.Outputfile(args.oformat, args.output, overwrite=True)
-    for mol in pybel.readfile('sdf', args.input):
+    for mol in pybel.readfile("sdf", args.input):
         for name in open(args.list_of_names):
             if mol.title.strip() == name.strip():
                 outfile.write(mol)
@@ -96,21 +113,21 @@
 
 def __main__():
     """
-        Select compounds with certain properties from a small library
+    Select compounds with certain properties from a small library
     """
     args = parse_command_line()
 
-    if args.filters == '__filter_by_name__':
+    if args.filters == "__filter_by_name__":
         filter_by_name(args)
         return
 
     # Its a small trick to get the parameters in an easy way from the xml file.
     # To keep it readable in the xml file, many white-spaces are included in that string it needs to be removed.
     # Also the last loop creates a ',{' that is not an valid jason expression.
-    filters = json.loads((args.filters).replace(' ', '').replace(',}', '}'))
-    if args.iformat == 'sdf':
+    filters = json.loads((args.filters).replace(" ", "").replace(",}", "}"))
+    if args.iformat == "sdf":
         # Check if the sdf file contains all of the required metadata to invoke the precalculation filtering
-        mol = next(pybel.readfile('sdf', args.input))
+        mol = next(pybel.readfile("sdf", args.input))
         for key, elem in filters.items():
             property = cheminfolib.ColumnNames.get(key, key)
             if property not in mol.data:
b
diff -r 2c5c7da26e08 -r 8302ab092300 ob_genProp.py
--- a/ob_genProp.py Tue Nov 10 20:40:06 2020 +0000
+++ b/ob_genProp.py Thu Aug 15 11:01:11 2024 +0000
[
@@ -10,43 +10,57 @@
 import cheminfolib
 import openbabel
 from openbabel import pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
 def parse_command_line(argv):
     parser = argparse.ArgumentParser()
-    parser.add_argument('--iformat', default='sdf', help='input file format')
-    parser.add_argument('-i', '--input', required=True, help='input file name')
-    parser.add_argument('--oformat', default='sdf', choices=['sdf', 'table'], help='output file format')
-    parser.add_argument('--header', type=bool, help='Include the header as the first line of the output table')
-    parser.add_argument('-o', '--output', required=True, help='output file name')
+    parser.add_argument("--iformat", default="sdf", help="input file format")
+    parser.add_argument("-i", "--input", required=True, help="input file name")
+    parser.add_argument(
+        "--oformat", default="sdf", choices=["sdf", "table"], help="output file format"
+    )
+    parser.add_argument(
+        "--header",
+        type=bool,
+        help="Include the header as the first line of the output table",
+    )
+    parser.add_argument("-o", "--output", required=True, help="output file name")
     return parser.parse_args()
 
 
 def compute_properties(args):
-    if args.oformat == 'sdf':
+    if args.oformat == "sdf":
         outfile = pybel.Outputfile(args.oformat, args.output, overwrite=True)
     else:
-        outfile = open(args.output, 'w')
+        outfile = open(args.output, "w")
         if args.header:
             mol = next(pybel.readfile(args.iformat, args.input))
             metadata = cheminfolib.get_properties_ext(mol)
-            outfile.write('%s\n' % '\t'.join([cheminfolib.ColumnNames[key] for key in metadata]))
+            outfile.write(
+                "%s\n" % "\t".join([cheminfolib.ColumnNames[key] for key in metadata])
+            )
 
     for mol in pybel.readfile(args.iformat, args.input):
         if mol.OBMol.NumHvyAtoms() > 5:
             metadata = cheminfolib.get_properties_ext(mol)
-            if args.oformat == 'sdf':
-                [mol.data.update({cheminfolib.ColumnNames[key]: metadata[key]}) for key in metadata]
+            if args.oformat == "sdf":
+                [
+                    mol.data.update({cheminfolib.ColumnNames[key]: metadata[key]})
+                    for key in metadata
+                ]
                 outfile.write(mol)
             else:
-                outfile.write('%s\n' % ('\t'.join([str(metadata[key]) for key in metadata])))
+                outfile.write(
+                    "%s\n" % ("\t".join([str(metadata[key]) for key in metadata]))
+                )
     outfile.close()
 
 
 def __main__():
     """
-        Physico-chemical properties are computed and stored as metadata in the sdf output file
+    Physico-chemical properties are computed and stored as metadata in the sdf output file
     """
     args = parse_command_line(sys.argv)
     compute_properties(args)
b
diff -r 2c5c7da26e08 -r 8302ab092300 ob_remIons.py
--- a/ob_remIons.py Tue Nov 10 20:40:06 2020 +0000
+++ b/ob_remIons.py Thu Aug 15 11:01:11 2024 +0000
[
@@ -8,37 +8,43 @@
 import argparse
 
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('-iformat', default='sdf', help='input file format')
-    parser.add_argument('-i', '--input', required=True, help='input file name')
-    parser.add_argument('-o', '--output', required=True, help='output file name')
-    parser.add_argument('-idx', default=False, action='store_true', help='should output be an indexed text table? works only for inchi/smiles, otherwise is ignored')
+    parser.add_argument("-iformat", default="sdf", help="input file format")
+    parser.add_argument("-i", "--input", required=True, help="input file name")
+    parser.add_argument("-o", "--output", required=True, help="output file name")
+    parser.add_argument(
+        "-idx",
+        default=False,
+        action="store_true",
+        help="should output be an indexed text table? works only for inchi/smiles, otherwise is ignored",
+    )
     return parser.parse_args()
 
 
 def remove_ions(args):
-    with open(args.output, 'w') as outfile:
+    with open(args.output, "w") as outfile:
         for index, mol in enumerate(pybel.readfile(args.iformat, args.input)):
             if mol.OBMol.NumHvyAtoms() > 5:
                 mol.OBMol.StripSalts(0)
-                if 'inchi' in mol.data:
-                    del mol.data['inchi']  # remove inchi cache so modified mol is saved
+                if "inchi" in mol.data:
+                    del mol.data["inchi"]  # remove inchi cache so modified mol is saved
 
-            mol = mol.write(args.iformat) if mol.OBMol.NumHvyAtoms() > 5 else '\n'
+            mol = mol.write(args.iformat) if mol.OBMol.NumHvyAtoms() > 5 else "\n"
 
-            if args.idx and args.iformat in ['inchi', 'smi']:
-                outfile.write(f'{index}\t{mol}')
-            elif mol != '\n':
-                outfile.write(f'{mol}')
+            if args.idx and args.iformat in ["inchi", "smi"]:
+                outfile.write(f"{index}\t{mol}")
+            elif mol != "\n":
+                outfile.write(f"{mol}")
 
 
 def __main__():
     """
-        Remove any counterion and delete any fragment but the largest one for each molecule.
+    Remove any counterion and delete any fragment but the largest one for each molecule.
     """
     args = parse_command_line()
     remove_ions(args)
b
diff -r 2c5c7da26e08 -r 8302ab092300 ob_spectrophore_search.py
--- a/ob_spectrophore_search.py Tue Nov 10 20:40:06 2020 +0000
+++ b/ob_spectrophore_search.py Thu Aug 15 11:01:11 2024 +0000
[
@@ -8,6 +8,7 @@
 
 import numpy as np
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 # TODO get rid of eval()
 
@@ -17,49 +18,94 @@
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('--target', required=True, help='target file name in sdf format with Spectrophores(TM) descriptors stored as meta-data')
-    parser.add_argument('--library', required=True, help='library of compounds with pre-computed physico-chemical properties, including Spectrophores(TM) in tabular format')
-    parser.add_argument('-c', '--column', required=True, type=int, help='#column containing the Spectrophores(TM) descriptors in the library file')
-    parser.add_argument('-o', '--output', required=True, help='output file name')
-    parser.add_argument('-n', '--normalization', default="ZeroMeanAndUnitStd", choices=['No', 'ZeroMean', 'UnitStd', 'ZeroMeanAndUnitStd'], help='Normalization method')
-    parser.add_argument('-a', '--accuracy', default="20", choices=['1', '2', '5', '10', '15', '20', '30', '36', '45', '60'], help='Accuracy expressed as angular stepsize')
-    parser.add_argument('-s', '--stereo', default="No", choices=['No', 'Unique', 'Mirror', 'All'], help='Stereospecificity of the cage')
-    parser.add_argument('-r', '--resolution', type=float, default="3.0", help='Resolution')
+    parser.add_argument(
+        "--target",
+        required=True,
+        help="target file name in sdf format with Spectrophores(TM) descriptors stored as meta-data",
+    )
+    parser.add_argument(
+        "--library",
+        required=True,
+        help="library of compounds with pre-computed physico-chemical properties, including Spectrophores(TM) in tabular format",
+    )
+    parser.add_argument(
+        "-c",
+        "--column",
+        required=True,
+        type=int,
+        help="#column containing the Spectrophores(TM) descriptors in the library file",
+    )
+    parser.add_argument("-o", "--output", required=True, help="output file name")
+    parser.add_argument(
+        "-n",
+        "--normalization",
+        default="ZeroMeanAndUnitStd",
+        choices=["No", "ZeroMean", "UnitStd", "ZeroMeanAndUnitStd"],
+        help="Normalization method",
+    )
+    parser.add_argument(
+        "-a",
+        "--accuracy",
+        default="20",
+        choices=["1", "2", "5", "10", "15", "20", "30", "36", "45", "60"],
+        help="Accuracy expressed as angular stepsize",
+    )
+    parser.add_argument(
+        "-s",
+        "--stereo",
+        default="No",
+        choices=["No", "Unique", "Mirror", "All"],
+        help="Stereospecificity of the cage",
+    )
+    parser.add_argument(
+        "-r", "--resolution", type=float, default="3.0", help="Resolution"
+    )
     return parser.parse_args()
 
 
 def set_parameters(args):
-    if args.normalization == 'No':
+    if args.normalization == "No":
         spectrophore.SetNormalization(spectrophore.NoNormalization)
     else:
-        spectrophore.SetNormalization(eval('spectrophore.NormalizationTowards' + args.normalization))
-    spectrophore.SetAccuracy(eval('spectrophore.AngStepSize' + args.accuracy))
-    spectrophore.SetStereo(eval('spectrophore.' + args.stereo + 'StereoSpecificProbes'))
+        spectrophore.SetNormalization(
+            eval("spectrophore.NormalizationTowards" + args.normalization)
+        )
+    spectrophore.SetAccuracy(eval("spectrophore.AngStepSize" + args.accuracy))
+    spectrophore.SetStereo(eval("spectrophore." + args.stereo + "StereoSpecificProbes"))
     spectrophore.SetResolution(args.resolution)
     return True
 
 
 def Compute_Spectrophores_distance(target_spectrophore, args):
-    outfile = open(args.output, 'w')
-    for mol in open(args.library, 'r'):
+    outfile = open(args.output, "w")
+    for mol in open(args.library, "r"):
         try:
-            distance = ((np.asarray(target_spectrophore, dtype=float) - np.asarray(mol.split('\t')[args.column - 1].strip().split(', '), dtype=float))**2).sum()
+            distance = (
+                (
+                    np.asarray(target_spectrophore, dtype=float)
+                    - np.asarray(
+                        mol.split("\t")[args.column - 1].strip().split(", "),
+                        dtype=float,
+                    )
+                )
+                ** 2
+            ).sum()
         except ValueError:
             distance = 0
-        outfile.write('%s\t%f\n' % (mol.strip(), distance))
+        outfile.write("%s\t%f\n" % (mol.strip(), distance))
     outfile.close()
 
 
 def __main__():
     """
-        Computation of Spectrophores(TM) distances to a target molecule.
+    Computation of Spectrophores(TM) distances to a target molecule.
     """
     args = parse_command_line()
     # This sets up the parameters for the Spectrophore generation. Parameters are set to fit those of our standard parsing tool
     set_parameters(args)
 
-    mol = next(pybel.readfile('sdf', args.target))
-    target_spectrophore = mol.data["Spectrophores(TM)"].strip().split(', ')
+    mol = next(pybel.readfile("sdf", args.target))
+    target_spectrophore = mol.data["Spectrophores(TM)"].strip().split(", ")
     # Compute the paired-distance between every molecule in the library and the target
     Compute_Spectrophores_distance(target_spectrophore, args)
 
b
diff -r 2c5c7da26e08 -r 8302ab092300 remove_protonation_state.py
--- a/remove_protonation_state.py Tue Nov 10 20:40:06 2020 +0000
+++ b/remove_protonation_state.py Thu Aug 15 11:01:11 2024 +0000
[
@@ -7,14 +7,15 @@
 import argparse
 
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('--iformat', default='sdf', help='input file format')
-    parser.add_argument('-i', '--input', required=True, help='input file name')
-    parser.add_argument('-o', '--output', required=True, help='output file name')
+    parser.add_argument("--iformat", default="sdf", help="input file format")
+    parser.add_argument("-i", "--input", required=True, help="input file name")
+    parser.add_argument("-o", "--output", required=True, help="output file name")
     return parser.parse_args()
 
 
@@ -22,15 +23,15 @@
     outfile = pybel.Outputfile(args.iformat, args.output, overwrite=True)
     for mol in pybel.readfile(args.iformat, args.input):
         [atom.OBAtom.SetFormalCharge(0) for atom in mol.atoms]
-        if 'inchi' in mol.data:
-            del mol.data['inchi']  # remove inchi cache so modified mol is saved
+        if "inchi" in mol.data:
+            del mol.data["inchi"]  # remove inchi cache so modified mol is saved
         outfile.write(mol)
     outfile.close()
 
 
 def __main__():
     """
-        Remove any protonation state from each atom in each molecule.
+    Remove any protonation state from each atom in each molecule.
     """
     args = parse_command_line()
     remove_protonation(args)
b
diff -r 2c5c7da26e08 -r 8302ab092300 subsearch.py
--- a/subsearch.py Tue Nov 10 20:40:06 2020 +0000
+++ b/subsearch.py Thu Aug 15 11:01:11 2024 +0000
[
@@ -13,21 +13,34 @@
 import tempfile
 
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('-i', '--infile', required=True, help='Molecule file.')
-    parser.add_argument('--iformat', help='Input format.')
-    parser.add_argument('--fastsearch-index', dest="fastsearch_index", required=True,
-                        help='Path to the openbabel fastsearch index.')
-    parser.add_argument('-o', '--outfile', required=True, help='Path to the output file.')
-    parser.add_argument('--oformat', default='smi', help='Output file format')
-    parser.add_argument("--max-candidates", dest="max_candidates", type=int, default=4000,
-                        help="The maximum number of candidates.")
-    parser.add_argument('-p', '--processors', type=int,
-                        default=multiprocessing.cpu_count())
+    parser.add_argument("-i", "--infile", required=True, help="Molecule file.")
+    parser.add_argument("--iformat", help="Input format.")
+    parser.add_argument(
+        "--fastsearch-index",
+        dest="fastsearch_index",
+        required=True,
+        help="Path to the openbabel fastsearch index.",
+    )
+    parser.add_argument(
+        "-o", "--outfile", required=True, help="Path to the output file."
+    )
+    parser.add_argument("--oformat", default="smi", help="Output file format")
+    parser.add_argument(
+        "--max-candidates",
+        dest="max_candidates",
+        type=int,
+        default=4000,
+        help="The maximum number of candidates.",
+    )
+    parser.add_argument(
+        "-p", "--processors", type=int, default=multiprocessing.cpu_count()
+    )
     return parser.parse_args()
 
 
@@ -40,20 +53,28 @@
 
 def mp_helper(query, args):
     """
-        Helper function for multiprocessing.
-        That function is a wrapper around the following command:
-        obabel file.fs -s"smarts" -Ooutfile.smi -al 999999999
+    Helper function for multiprocessing.
+    That function is a wrapper around the following command:
+    obabel file.fs -s"smarts" -Ooutfile.smi -al 999999999
     """
 
-    if args.oformat == 'names':
-        opts = '-osmi -xt'
+    if args.oformat == "names":
+        opts = "-osmi -xt"
     else:
-        opts = '-o%s' % args.oformat
+        opts = "-o%s" % args.oformat
 
     tmp = tempfile.NamedTemporaryFile(delete=False)
-    cmd = 'obabel -ifs %s -O %s %s -s%s -al %s' % (args.fastsearch_index, tmp.name, opts, query, args.max_candidates)
+    cmd = "obabel -ifs %s -O %s %s -s%s -al %s" % (
+        args.fastsearch_index,
+        tmp.name,
+        opts,
+        query,
+        args.max_candidates,
+    )
 
-    child = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    child = subprocess.Popen(
+        cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE
+    )
 
     stdout, stderr = child.communicate()
     return_code = child.returncode
@@ -73,14 +94,14 @@
     """
     Wrapper to retrieve a striped SMILES or SMARTS string from different input formats.
     """
-    if args.iformat in ['smi', 'text', 'tabular']:
+    if args.iformat in ["smi", "text", "tabular"]:
         with open(args.infile) as text_file:
             for line in text_file:
-                yield line.split('\t')[0].strip()
+                yield line.split("\t")[0].strip()
     else:
         # inchi or sdf files
         for mol in pybel.readfile(args.iformat, args.infile):
-            yield mol.write('smiles').split('\t')[0]
+            yield mol.write("smiles").split("\t")[0]
 
 
 def substructure_search(args):
@@ -91,18 +112,18 @@
     pool.close()
     pool.join()
 
-    if args.oformat == 'names':
-        out_handle = open(args.outfile, 'w')
+    if args.oformat == "names":
+        out_handle = open(args.outfile, "w")
         for result_file, query in results:
             with open(result_file) as res_handle:
                 for line in res_handle:
-                    out_handle.write('%s\t%s\n' % (line.strip(), query))
+                    out_handle.write("%s\t%s\n" % (line.strip(), query))
             os.remove(result_file)
         out_handle.close()
     else:
-        out_handle = open(args.outfile, 'wb')
+        out_handle = open(args.outfile, "wb")
         for result_file, query in results:
-            res_handle = open(result_file, 'rb')
+            res_handle = open(result_file, "rb")
             shutil.copyfileobj(res_handle, out_handle)
             res_handle.close()
             os.remove(result_file)
@@ -111,7 +132,7 @@
 
 def __main__():
     """
-        Multiprocessing Open Babel Substructure Search.
+    Multiprocessing Open Babel Substructure Search.
     """
     args = parse_command_line()
     substructure_search(args)
b
diff -r 2c5c7da26e08 -r 8302ab092300 test-data/2_mol.sdf
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/2_mol.sdf Thu Aug 15 11:01:11 2024 +0000
[
@@ -0,0 +1,66 @@
+CC(=O)Oc1ccccc1C(=O)[O-] InChI=1S/C9H8O4/c1-6(10)13-8-5-3-2-4-7(8)9(11)12/h2-5H,1H3,(H,11,12)/p-1
+ OpenBabel08132415422D
+
+ 13 13  0  0  0  0  0  0  0  0999 V2000
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  5  0  0  0  0  0  0  0  0  0  0
+  1  2  1  0  0  0  0
+  2  3  2  0  0  0  0
+  2  4  1  0  0  0  0
+  4  5  1  0  0  0  0
+  5 10  1  0  0  0  0
+  5  6  2  0  0  0  0
+  6  7  1  0  0  0  0
+  7  8  2  0  0  0  0
+  8  9  1  0  0  0  0
+  9 10  2  0  0  0  0
+ 10 11  1  0  0  0  0
+ 11 12  2  0  0  0  0
+ 11 13  1  0  0  0  0
+M  CHG  1  13  -1
+M  END
+$$$$
+CC(=O)Oc1ccccc1C(=O)[O-] InChI=1S/C9H8O4/c1-6(10)13-8-5-3-2-4-7(8)9(11)12/h2-5H,1H3,(H,11,12)/p-1
+ OpenBabel08132415422D
+
+ 13 13  0  0  0  0  0  0  0  0999 V2000
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  5  0  0  0  0  0  0  0  0  0  0
+  1  2  1  0  0  0  0
+  2  3  2  0  0  0  0
+  2  4  1  0  0  0  0
+  4  5  1  0  0  0  0
+  5 10  1  0  0  0  0
+  5  6  2  0  0  0  0
+  6  7  1  0  0  0  0
+  7  8  2  0  0  0  0
+  8  9  1  0  0  0  0
+  9 10  2  0  0  0  0
+ 10 11  1  0  0  0  0
+ 11 12  2  0  0  0  0
+ 11 13  1  0  0  0  0
+M  CHG  1  13  -1
+M  END
+$$$$