From 8713b7329dde38c70bf647f53f16d34c74dfc746 Mon Sep 17 00:00:00 2001 From: Kohulan Date: Thu, 24 Nov 2022 15:15:49 +0100 Subject: [PATCH 1/3] fix: Clean up irrelevant files in repository, modify .gitignore and reformat the files --- .DS_Store | Bin 10244 -> 10244 bytes .gitignore | 164 +- example_scripts/amino_acid_composition.py | 107 +- example_scripts/correctness_analysis.py | 4 +- example_scripts/draw_molecule_rdkit.py | 6 +- example_scripts/draw_npatlas_structures.py | 19 +- example_scripts/ketoreductase.py | 67 +- example_scripts/read_valency_5.py | 2 +- .../ring_detection_visualisation.py | 33 +- example_scripts/substructure_test.py | 26 +- example_scripts/tanimoto_distance.py | 107 +- example_scripts/test_svg_drawer.py | 5 +- pikachu/chem/aromatic_system.py | 4 +- pikachu/chem/atom.py | 176 +- pikachu/chem/atom_properties.py | 1577 +++++++------- pikachu/chem/bond.py | 102 +- pikachu/chem/bond_properties.py | 60 +- pikachu/chem/chirality.py | 40 +- pikachu/chem/electron.py | 12 +- pikachu/chem/kekulisation.py | 23 +- pikachu/chem/lone_pair.py | 4 +- pikachu/chem/molfile/read_molfile.py | 25 +- pikachu/chem/molfile/write_molfile.py | 125 +- pikachu/chem/orbital.py | 99 +- pikachu/chem/rings/find_cycles.py | 40 +- pikachu/chem/rings/ring_identification.py | 62 +- pikachu/chem/shell.py | 70 +- pikachu/chem/structure.py | 525 +++-- pikachu/chem/substructure_matching.py | 116 +- pikachu/drawing/colours.py | 285 +-- pikachu/drawing/drawing.py | 1869 ++++++++++++----- pikachu/drawing/rings.py | 37 +- pikachu/drawing/sssr.py | 53 +- pikachu/errors.py | 29 +- pikachu/fingerprinting/daylight.py | 10 +- pikachu/fingerprinting/ecfp_4.py | 40 +- pikachu/fingerprinting/hashing.py | 2 +- pikachu/fingerprinting/similarity.py | 11 +- pikachu/general.py | 193 +- pikachu/inchi/inchi.py | 155 +- pikachu/math_functions.py | 375 ++-- pikachu/parsers/coconut.py | 4 +- pikachu/parsers/np_atlas.py | 6 +- pikachu/reactions/basic_reactions.py | 41 +- pikachu/reactions/functional_groups.py | 30 +- pikachu/smiles/graph_to_smiles.py | 488 +++-- pikachu/smiles/smiles.py | 292 +-- setup.py | 8 +- validation/drawing_validation.py | 28 +- validation/speed_assessment.py | 38 +- validation/steric_clashes.py | 27 +- validation/test_drawing.py | 18 +- validation/test_read_molfile.py | 4 +- validation/writing_validation.py | 3 - 54 files changed, 4842 insertions(+), 2804 deletions(-) diff --git a/.DS_Store b/.DS_Store index ef173957df4513d3eb54eb6f36e8c74b3a9be745..b3513c7517452afb612dca53002799354b7b51cd 100644 GIT binary patch delta 41 xcmZn(XbG6$&uFkQU^hRb!DJqR-p}yTUJ)&4!}P%m6aa4etN| delta 195 zcmZn(XbG6$&uF|cU^hRb@njx>-#Q!&h75@exeTQYdJM@7nG9*3Ir+&+Ir&Kp3=9H5 zydH?>{Rabv&CG&U9NFAJ)%grL3~4~sISi>lePBMSKBjaeeLyDAEL2_b3@HrRKod%^ f>t}FeU|{5%oFFN+*+){8Z8N*VFP6<0MVOfZ$&EA2 diff --git a/.gitignore b/.gitignore index 46f6ec5..39a4c37 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,163 @@ -*.smi +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# MacOS +.DS_Store \ No newline at end of file diff --git a/example_scripts/amino_acid_composition.py b/example_scripts/amino_acid_composition.py index acd8857..d12610f 100644 --- a/example_scripts/amino_acid_composition.py +++ b/example_scripts/amino_acid_composition.py @@ -1,4 +1,10 @@ -from pikachu.general import smiles_from_file, highlight_subsmiles_single, highlight_subsmiles_all, highlight_subsmiles_multiple, highlight_substructure +from pikachu.general import ( + smiles_from_file, + highlight_subsmiles_single, + highlight_subsmiles_all, + highlight_subsmiles_multiple, + highlight_substructure, +) tryptophan = r"N[C@H](C=O)CC1=CNC2=CC=CC=C21" d_asparagine = r"N[C@@H](C=O)CC(N)=O" @@ -22,21 +28,88 @@ vancomycin = r"C[C@H]1[C@H]([C@@](C[C@@H](O1)O[C@@H]2[C@H]([C@@H]([C@H](O[C@H]2OC3=C4C=C5C=C3OC6=C(C=C(C=C6)[C@H]([C@H](C(=O)N[C@H](C(=O)N[C@H]5C(=O)N[C@@H]7C8=CC(=C(C=C8)O)C9=C(C=C(C=C9[C@H](NC(=O)[C@H]([C@@H](C1=CC(=C(O4)C=C1)Cl)O)NC7=O)C(=O)O)O)O)CC(=O)N)NC(=O)[C@@H](CC(C)C)NC)O)Cl)CO)O)O)(C)N)O" # daptomycin = smiles_from_file('daptomycin.smi') -highlight_subsmiles_single(aspartate, daptomycin, colour='light blue', visualisation='svg', out_file='daptomycin_aspartate_single.svg') -highlight_subsmiles_all(aspartate, daptomycin, colour='light blue', visualisation='svg', out_file='daptomycin_aspartate_all.svg') -highlight_subsmiles_multiple([aspartate, tryptophan], daptomycin, colours=['red', 'blue'], visualisation='svg', out_file='daptomycin_multiple.svg') +highlight_subsmiles_single( + aspartate, + daptomycin, + colour="light blue", + visualisation="svg", + out_file="daptomycin_aspartate_single.svg", +) +highlight_subsmiles_all( + aspartate, + daptomycin, + colour="light blue", + visualisation="svg", + out_file="daptomycin_aspartate_all.svg", +) +highlight_subsmiles_multiple( + [aspartate, tryptophan], + daptomycin, + colours=["red", "blue"], + visualisation="svg", + out_file="daptomycin_multiple.svg", +) -highlight_subsmiles_multiple([glycine, d_alanine, d_serine, threonine, d_asparagine, aspartate, - glutamate, kynurenine, ornithine, tryptophan], daptomycin, - colours=['red', 'blue', 'orange', 'hot pink', 'light blue', - 'dark blue', 'dark red', 'purple', 'lime', 'yellow'], - visualisation='svg', out_file='daptomycin_substructures.svg') -highlight_subsmiles_multiple([asparagine, d_leucine, d_hydroxyphenylglycine, dihydroxyphenylglycine, - d_tyrosine, tyrosine], vancomycin, - colours=['red', 'blue', 'hot pink', 'lime', 'light blue', - 'yellow'], - visualisation='svg', out_file='vancomycin_substructures.svg') +highlight_subsmiles_multiple( + [ + glycine, + d_alanine, + d_serine, + threonine, + d_asparagine, + aspartate, + glutamate, + kynurenine, + ornithine, + tryptophan, + ], + daptomycin, + colours=[ + "red", + "blue", + "orange", + "hot pink", + "light blue", + "dark blue", + "dark red", + "purple", + "lime", + "yellow", + ], + visualisation="svg", + out_file="daptomycin_substructures.svg", +) +highlight_subsmiles_multiple( + [ + asparagine, + d_leucine, + d_hydroxyphenylglycine, + dihydroxyphenylglycine, + d_tyrosine, + tyrosine, + ], + vancomycin, + colours=["red", "blue", "hot pink", "lime", "light blue", "yellow"], + visualisation="svg", + out_file="vancomycin_substructures.svg", +) -highlight_substructure(aspartate, daptomycin, search_mode='single', colour='light blue', visualisation='svg', out_file='daptomycin_aspartate_single.svg') -highlight_substructure(aspartate, daptomycin, search_mode='all', colour='light blue', visualisation='svg', out_file='daptomycin_aspartate_all.svg') -highlight_substructure([aspartate, tryptophan], daptomycin, search_mode='multiple', colour=['red', 'blue']) +highlight_substructure( + aspartate, + daptomycin, + search_mode="single", + colour="light blue", + visualisation="svg", + out_file="daptomycin_aspartate_single.svg", +) +highlight_substructure( + aspartate, + daptomycin, + search_mode="all", + colour="light blue", + visualisation="svg", + out_file="daptomycin_aspartate_all.svg", +) +highlight_substructure( + [aspartate, tryptophan], daptomycin, search_mode="multiple", colour=["red", "blue"] +) diff --git a/example_scripts/correctness_analysis.py b/example_scripts/correctness_analysis.py index 9d41c54..84d3430 100644 --- a/example_scripts/correctness_analysis.py +++ b/example_scripts/correctness_analysis.py @@ -4,11 +4,11 @@ def parse_smiles(tbd_file): name_to_compound = {} - with open(tbd_file, 'r') as tbd: + with open(tbd_file, "r") as tbd: for line in tbd: line = line.strip() if line: - compound_name, smiles = line.split('\t') + compound_name, smiles = line.split("\t") if compound_name not in name_to_compound: name_to_compound[compound_name] = [] name_to_compound[compound_name].append(smiles) diff --git a/example_scripts/draw_molecule_rdkit.py b/example_scripts/draw_molecule_rdkit.py index 475f8aa..f148b46 100644 --- a/example_scripts/draw_molecule_rdkit.py +++ b/example_scripts/draw_molecule_rdkit.py @@ -5,6 +5,7 @@ from rdkit.Chem import Draw from rdkit.Chem.Draw import rdMolDraw2D from rdkit.Chem import rdDepictor + rdDepictor.SetPreferCoordGen(True) @@ -15,12 +16,11 @@ def draw_molecule(smiles, out_file): drawer.DrawMolecule(mol) drawer.FinishDrawing() svg = drawer.GetDrawingText() - with open(out_file, 'w') as out: + with open(out_file, "w") as out: out.write(svg) + if __name__ == "__main__": smiles = argv[1] out_file = argv[2] draw_molecule(smiles, out_file) - - diff --git a/example_scripts/draw_npatlas_structures.py b/example_scripts/draw_npatlas_structures.py index 0b9d6c5..4411462 100644 --- a/example_scripts/draw_npatlas_structures.py +++ b/example_scripts/draw_npatlas_structures.py @@ -8,7 +8,7 @@ def parse_npatlas_smiles(npatlas_file): smiles = [] - with open(npatlas_file, 'r') as npatlas: + with open(npatlas_file, "r") as npatlas: for line in npatlas: line = line.strip() if line: @@ -16,6 +16,7 @@ def parse_npatlas_smiles(npatlas_file): return smiles + @timeout_decorator.timeout(20) def draw(smiles, drawing_file): svg_from_smiles(smiles, drawing_file) @@ -27,27 +28,27 @@ def draw_npatlas(npatlas_file, drawing_dir, failed_smiles_dir): os.mkdir(drawing_dir) if not os.path.exists(failed_smiles_dir): os.mkdir(failed_smiles_dir) - failed_smiles_file = os.path.join(failed_smiles_dir, 'failed_smiles.txt') - failed_drawing_file = os.path.join(failed_smiles_dir, 'failed_drawings.txt') - failed_drawings = open(failed_drawing_file, 'w') - with open(failed_smiles_file, 'w') as failed_smiles: + failed_smiles_file = os.path.join(failed_smiles_dir, "failed_smiles.txt") + failed_drawing_file = os.path.join(failed_smiles_dir, "failed_drawings.txt") + failed_drawings = open(failed_drawing_file, "w") + with open(failed_smiles_file, "w") as failed_smiles: for i, smiles in enumerate(smiles_strings): try: structure = read_smiles(smiles) if not structure: print(smiles) - failed_smiles.write(f'{smiles}\tSmilesError\n') + failed_smiles.write(f"{smiles}\tSmilesError\n") else: - drawing_file = os.path.join(drawing_dir, f'{i}.svg') + drawing_file = os.path.join(drawing_dir, f"{i}.svg") try: draw(smiles, drawing_file) except Exception as e: print("Drawing failure:", smiles) - failed_drawings.write(f'{smiles}\t{e}\n') + failed_drawings.write(f"{smiles}\t{e}\n") except Exception as e: print(smiles) - failed_smiles.write(f'{smiles}\t{e}\n') + failed_smiles.write(f"{smiles}\t{e}\n") print(f"Handled smiles number {i}: {smiles}.") diff --git a/example_scripts/ketoreductase.py b/example_scripts/ketoreductase.py index 720c2a8..e021869 100644 --- a/example_scripts/ketoreductase.py +++ b/example_scripts/ketoreductase.py @@ -3,7 +3,8 @@ from pikachu.reactions.functional_groups import BondDefiner from pikachu.general import read_smiles, draw_structure -RECENT_ELONGATION = BondDefiner('recent_elongation', 'O=CCC(=O)S', 0, 1) +RECENT_ELONGATION = BondDefiner("recent_elongation", "O=CCC(=O)S", 0, 1) + def carbonyl_to_hydroxyl(double_bond): """Alters the double bond in a carbonyl group to a single bond @@ -11,50 +12,50 @@ def carbonyl_to_hydroxyl(double_bond): double_bond: Bond object of the bond between the C and O atom of a carbonyl group """ - #Assert the correct bond is parsed - assert double_bond.type == 'double' + # Assert the correct bond is parsed + assert double_bond.type == "double" atom_types_in_bond = [] for atom in double_bond.neighbours: atom_types_in_bond.append(atom.type) - assert 'O' in atom_types_in_bond and 'C' in atom_types_in_bond + assert "O" in atom_types_in_bond and "C" in atom_types_in_bond # Define the two electrons in the pi bond (inside the db) that needs # to be broken electrons_in_db = double_bond.electrons for electron in electrons_in_db: - if electron.atom.type == 'O' and electron.orbital_type == 'p': + if electron.atom.type == "O" and electron.orbital_type == "p": electron_1 = electron - elif electron.atom.type == 'C' and electron.orbital_type == 'p': + elif electron.atom.type == "C" and electron.orbital_type == "p": electron_2 = electron - #Remove the pi electrons from their respective orbital + # Remove the pi electrons from their respective orbital orbital_1 = electron_1.orbital orbital_2 = electron_2.orbital orbital_1.remove_electron(electron_2) orbital_2.remove_electron(electron_1) - #Set bond type to single + # Set bond type to single for bond in double_bond.atom_1.bonds: if bond == double_bond: - bond.type = 'single' + bond.type = "single" for bond in double_bond.atom_2.bonds: if bond == double_bond: - bond.type = 'single' + bond.type = "single" - #Remove pi electrons from the Bond between the C and O atom + # Remove pi electrons from the Bond between the C and O atom for electron in double_bond.electrons[:]: if electron == electron_1 or electron == electron_2: double_bond.electrons.remove(electron) - #Change hybridisation of both C and O atoms to sp3 + # Change hybridisation of both C and O atoms to sp3 atom_1, atom_2 = double_bond.neighbours atom_1.valence_shell.dehybridise() - atom_1.valence_shell.hybridise('sp3') + atom_1.valence_shell.hybridise("sp3") atom_2.valence_shell.dehybridise() - atom_2.valence_shell.hybridise('sp3') + atom_2.valence_shell.hybridise("sp3") - #Change bond_type of Bond instance to single - double_bond.type = 'single' + # Change bond_type of Bond instance to single + double_bond.type = "single" new_single_bond = double_bond new_single_bond.set_bond_summary() @@ -86,39 +87,38 @@ def ketoreductase(chain_intermediate): chain_intermediate: Structure object of a PKS chain intermediate just after an elongation step using (methyl)malonyl-CoA """ - #Reset all colours to black: + # Reset all colours to black: for atom in chain_intermediate.graph: - atom.draw.colour = 'black' + atom.draw.colour = "black" for bond_nr, bond in chain_intermediate.bonds.items(): bond.set_bond_summary() - #Identify beta-ketone bond, identify O- and C-atom participating in bond + # Identify beta-ketone bond, identify O- and C-atom participating in bond beta_ketone_bond = find_betaketon(chain_intermediate) for bond in beta_ketone_bond: for atom in bond.neighbours: - if atom.type == 'O': + if atom.type == "O": carbonyl_oxygen = atom - elif atom.type == 'C': + elif atom.type == "C": carbonyl_carbon = atom else: - raise Exception('Cannot find atoms in beta ketone bond') + raise Exception("Cannot find atoms in beta ketone bond") - #Change carbonyl bond to single bond + # Change carbonyl bond to single bond for bond in beta_ketone_bond: new_single_bond = carbonyl_to_hydroxyl(bond) + # Add H atom to form hydroxyl group and another H to the C + chain_intermediate.add_atom("H", [carbonyl_oxygen]) + chain_intermediate.add_atom("H", [carbonyl_carbon]) + carbonyl_carbon.chiral = "counterclockwise" - #Add H atom to form hydroxyl group and another H to the C - chain_intermediate.add_atom('H', [carbonyl_oxygen]) - chain_intermediate.add_atom('H', [carbonyl_carbon]) - carbonyl_carbon.chiral = 'counterclockwise' - - #Set bond summary for newly formed bond (cannot do from struct.bonds?) + # Set bond summary for newly formed bond (cannot do from struct.bonds?) for atom in chain_intermediate.graph: if atom == carbonyl_carbon: for bond in atom.bonds: for neighbour in bond.neighbours: - if neighbour.type == 'O': + if neighbour.type == "O": the_bond = bond the_bond.set_bond_summary() @@ -131,14 +131,15 @@ def ketoreductase(chain_intermediate): for bond_nr, bond in chain_intermediate.bonds.items(): bond.set_bond_summary() - #Add colouring to the tailored group + # Add colouring to the tailored group for atom in new_single_bond.neighbours: - atom.draw.colour = 'red' + atom.draw.colour = "red" return chain_intermediate + if __name__ == "__main__": - structure = read_smiles('SC(=O)CC(=O)CCCCC') + structure = read_smiles("SC(=O)CC(=O)CCCCC") draw_structure(structure) reduced_structure = ketoreductase(structure) draw_structure(reduced_structure) diff --git a/example_scripts/read_valency_5.py b/example_scripts/read_valency_5.py index df32ed2..4b2f174 100644 --- a/example_scripts/read_valency_5.py +++ b/example_scripts/read_valency_5.py @@ -5,7 +5,7 @@ def draw_nitrogen_smiles(smiles_file): - with open(smiles_file, 'r') as smi: + with open(smiles_file, "r") as smi: for line in smi: smiles = line.strip() draw_smiles(smiles) diff --git a/example_scripts/ring_detection_visualisation.py b/example_scripts/ring_detection_visualisation.py index 6db497a..f8299f9 100644 --- a/example_scripts/ring_detection_visualisation.py +++ b/example_scripts/ring_detection_visualisation.py @@ -5,9 +5,32 @@ s = read_smiles(vancomycin) print(s.aromatic_cycles) -daptomycin_substructures = ["c1ccccc1", "c1c[nH]c2c1cccc2", "C1CNCCNCCNCCNCCNCCNCCNCCNCCNCCOC1"] -vancomycin_substructures = ["C1CCCCO1", "C(Cc1ccc(O)cc1)CNC", "CNC(c1ccccc1)", "CNCCNC(c1ccccc1)", "NC(c1ccccc1)"] #"C(Cc1ccc2cc1)CNCCNC(c3cc(O2)cc4c3)CNC(c5cccc6c5)CNC(Cc7ccc(O4)cc7)CNCc8c6cccc8"] - -highlight_subsmiles_multiple(daptomycin_substructures, daptomycin, colours=['blue', 'hot pink', 'blue'], visualisation='svg', out_file='daptomycin_rings.svg', check_chiral_centres=False) -highlight_subsmiles_multiple(vancomycin_substructures, vancomycin, colours=['blue', 'pink', 'pink', 'pink', 'pink'], visualisation='svg', out_file='vancomycin_rings.svg', check_chiral_centres=False) +daptomycin_substructures = [ + "c1ccccc1", + "c1c[nH]c2c1cccc2", + "C1CNCCNCCNCCNCCNCCNCCNCCNCCNCCOC1", +] +vancomycin_substructures = [ + "C1CCCCO1", + "C(Cc1ccc(O)cc1)CNC", + "CNC(c1ccccc1)", + "CNCCNC(c1ccccc1)", + "NC(c1ccccc1)", +] # "C(Cc1ccc2cc1)CNCCNC(c3cc(O2)cc4c3)CNC(c5cccc6c5)CNC(Cc7ccc(O4)cc7)CNCc8c6cccc8"] +highlight_subsmiles_multiple( + daptomycin_substructures, + daptomycin, + colours=["blue", "hot pink", "blue"], + visualisation="svg", + out_file="daptomycin_rings.svg", + check_chiral_centres=False, +) +highlight_subsmiles_multiple( + vancomycin_substructures, + vancomycin, + colours=["blue", "pink", "pink", "pink", "pink"], + visualisation="svg", + out_file="vancomycin_rings.svg", + check_chiral_centres=False, +) diff --git a/example_scripts/substructure_test.py b/example_scripts/substructure_test.py index a6acd3a..cc1dfd8 100644 --- a/example_scripts/substructure_test.py +++ b/example_scripts/substructure_test.py @@ -1,17 +1,29 @@ #!/usr/bin/env python -from pikachu.general import read_smiles, draw_smiles, highlight_subsmiles_single, highlight_subsmiles_all, highlight_subsmiles_multiple +from pikachu.general import ( + read_smiles, + draw_smiles, + highlight_subsmiles_single, + highlight_subsmiles_all, + highlight_subsmiles_multiple, +) -substructure = 'OC1=CC=C(C[C@@H](N)C=O)C=C1' +substructure = "OC1=CC=C(C[C@@H](N)C=O)C=C1" substructure = "C1=CC(=CC=C1C[C@H](C(=O))N)O" daptomycin = r"CCCCCCCCCC(=O)N[C@@H](CC1=CNC2=CC=CC=C21)C(=O)N[C@H](CC(=O)N)C(=O)N[C@@H](CC(=O)O)C(=O)N[C@H]3[C@H](OC(=O)[C@@H](NC(=O)[C@@H](NC(=O)[C@H](NC(=O)CNC(=O)[C@@H](NC(=O)[C@H](NC(=O)[C@@H](NC(=O)[C@@H](NC(=O)CNC3=O)CCCN)CC(=O)O)C)CC(=O)O)CO)[C@H](C)CC(=O)O)CC(=O)C4=CC=CC=C4N)C" vancomycin = r"C[C@H]1[C@H]([C@@](C[C@@H](O1)O[C@@H]2[C@H]([C@@H]([C@H](O[C@H]2OC3=C4C=C5C=C3OC6=C(C=C(C=C6)[C@H]([C@H](C(=O)N[C@H](C(=O)N[C@H]5C(=O)N[C@@H]7C8=CC(=C(C=C8)O)C9=C(C=C(C=C9[C@H](NC(=O)[C@H]([C@@H](C1=CC(=C(O4)C=C1)Cl)O)NC7=O)C(=O)O)O)O)CC(=O)N)NC(=O)[C@@H](CC(C)C)NC)O)Cl)CO)O)O)(C)N)O" -#read_smiles(vancomycin).print_graph() -#draw_smiles(vancomycin) +# read_smiles(vancomycin).print_graph() +# draw_smiles(vancomycin) -#draw_smiles(substructure) -#read_smiles(substructure).print_graph() +# draw_smiles(substructure) +# read_smiles(substructure).print_graph() # highlight_subsmiles_single(substructure, daptomycin, colour='light blue', visualisation='svg', out_file='substructure_daptomycin.svg') -highlight_subsmiles_single(substructure, vancomycin, colour='light blue', visualisation='svg', out_file='substructure_vancomycin.svg') \ No newline at end of file +highlight_subsmiles_single( + substructure, + vancomycin, + colour="light blue", + visualisation="svg", + out_file="substructure_vancomycin.svg", +) diff --git a/example_scripts/tanimoto_distance.py b/example_scripts/tanimoto_distance.py index 633dbad..3b014ff 100644 --- a/example_scripts/tanimoto_distance.py +++ b/example_scripts/tanimoto_distance.py @@ -8,28 +8,29 @@ from matplotlib.pyplot import figure from sklearn.manifold import TSNE -PROTEINOGENIC = {"alanine", - "cysteine", - "aspartate", - "glutamate" - "aspartic acid", - "glutamic acid", - "phenylalanine", - "glycine", - "histidine", - "isoleucine", - "lysine", - "leucine", - "methionine", - "asparagine", - "proline", - "glutamine", - "arginine", - "serine", - "threonine", - "valine", - "tryptophan", - "tyrosine"} +PROTEINOGENIC = { + "alanine", + "cysteine", + "aspartate", + "glutamate" "aspartic acid", + "glutamic acid", + "phenylalanine", + "glycine", + "histidine", + "isoleucine", + "lysine", + "leucine", + "methionine", + "asparagine", + "proline", + "glutamine", + "arginine", + "serine", + "threonine", + "valine", + "tryptophan", + "tyrosine", +} def plot_tanimoto_distances(matrix): @@ -50,16 +51,18 @@ def plot_tanimoto_distances(matrix): coords = results.embedding_ plt.subplots_adjust(bottom=0.1) - plt.scatter( - coords[:, 0], coords[:, 1], marker='o' - ) + plt.scatter(coords[:, 0], coords[:, 1], marker="o") for label, x, y in zip(sorted_compounds, coords[:, 0], coords[:, 1]): plt.annotate( label, - xy=(x, y), xytext=(0, 20), - textcoords='offset points', ha='right', va='bottom', - bbox=dict(boxstyle='round,pad=0.5', fc='grey', alpha=0.2), - arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0')) + xy=(x, y), + xytext=(0, 20), + textcoords="offset points", + ha="right", + va="bottom", + bbox=dict(boxstyle="round,pad=0.5", fc="grey", alpha=0.2), + arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=0"), + ) plt.savefig("tanimoto.svg") @@ -109,8 +112,9 @@ def make_tsne_matrix(matrix): tsne_dict = {} - res_tsne = TSNE(n_components=2, metric='precomputed', - random_state=0).fit_transform(distances) + res_tsne = TSNE(n_components=2, metric="precomputed", random_state=0).fit_transform( + distances + ) for i in range(len(sorted_compounds)): tsne_dict[sorted_compounds[i]] = list(res_tsne[i, :]) @@ -140,23 +144,29 @@ def plot_tsne(tsne_vals): for label, x, y in zip(sorted_compounds, x_coors, y_coors): plt.annotate( label, - xy=(x, y), xytext=(0, 20), - textcoords='offset points', ha='right', va='bottom', - bbox=dict(boxstyle='round,pad=0.5', fc='grey', alpha=0.2), - arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0')) + xy=(x, y), + xytext=(0, 20), + textcoords="offset points", + ha="right", + va="bottom", + bbox=dict(boxstyle="round,pad=0.5", fc="grey", alpha=0.2), + arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=0"), + ) plt.savefig("tanimoto.svg") - # plt.show() + + +# plt.show() def parse_smiles(tbd_file): name_to_compound = {} - with open(tbd_file, 'r') as tbd: + with open(tbd_file, "r") as tbd: tbd.readline() for line in tbd: line = line.strip() if line: - compound_name, smiles = line.split('\t') + compound_name, smiles = line.split("\t") try: structure = read_smiles(smiles) except Exception: @@ -167,20 +177,24 @@ def parse_smiles(tbd_file): print(f"Couldn't convert {compound_name}.") return name_to_compound + def write_network(matrix, out_file): - with open(out_file, 'w') as out: + with open(out_file, "w") as out: out.write("Compound 1\tDistance\tCompound 2\n") for compound_1 in matrix: for compound_2 in matrix[compound_1]: if compound_1 != compound_2: - out.write(f"{compound_1.title()}\t{1 - matrix[compound_1][compound_2]}\t{compound_2.title()}\n") + out.write( + f"{compound_1.title()}\t{1 - matrix[compound_1][compound_2]}\t{compound_2.title()}\n" + ) + def is_amino_acid(name_to_compound, out_dir): if not os.path.exists(out_dir): os.mkdir(out_dir) out_file = os.path.join(out_dir, "substrate_identities.txt") - with open(out_file, 'w') as out: + with open(out_file, "w") as out: out.write("Substrate\tamino acid\n") for name, structure in name_to_compound.items(): is_amino_acid = False @@ -205,11 +219,6 @@ def is_amino_acid(name_to_compound, out_dir): svg_from_structure(structure, os.path.join(out_dir, f"{name}.svg")) - - - - - if __name__ == "__main__": tbd_file = argv[1] out_file = argv[2] @@ -219,6 +228,6 @@ def is_amino_acid(name_to_compound, out_dir): write_network(matrix, out_file) is_amino_acid(name_to_compound, out_2) - # plot_tanimoto_distances(matrix) - # tsne_dict = make_tsne_matrix(matrix) - # plot_tsne(tsne_dict) +# plot_tanimoto_distances(matrix) +# tsne_dict = make_tsne_matrix(matrix) +# plot_tsne(tsne_dict) diff --git a/example_scripts/test_svg_drawer.py b/example_scripts/test_svg_drawer.py index 9cc68ab..1e76fd3 100644 --- a/example_scripts/test_svg_drawer.py +++ b/example_scripts/test_svg_drawer.py @@ -1,7 +1,10 @@ from pikachu.general import svg_from_smiles from sys import argv + + def test_drawer(smiles_string): svg_from_smiles(smiles_string, "test.svg") + if __name__ == "__main__": - test_drawer(argv[1]) \ No newline at end of file + test_drawer(argv[1]) diff --git a/pikachu/chem/aromatic_system.py b/pikachu/chem/aromatic_system.py index c974be3..7882ed8 100644 --- a/pikachu/chem/aromatic_system.py +++ b/pikachu/chem/aromatic_system.py @@ -41,7 +41,7 @@ def get_contributed_electrons(self, atom): def set_electrons(self): for atom in self.atoms: - p_orbital = atom.get_orbitals('p')[0] + p_orbital = atom.get_orbitals("p")[0] electrons_participate_in_system = True for electron in p_orbital.electrons: if electron.atom not in self.atoms: @@ -59,7 +59,7 @@ def add_atom(self, atom): def relocalise_electrons(self): for atom in self.atoms: - p_orbital = atom.get_orbitals('p')[0] + p_orbital = atom.get_orbitals("p")[0] electrons = self.get_contributed_electrons(atom) for electron in electrons: p_orbital.add_electron(electron) diff --git a/pikachu/chem/atom.py b/pikachu/chem/atom.py index 37bf762..1d099be 100644 --- a/pikachu/chem/atom.py +++ b/pikachu/chem/atom.py @@ -9,7 +9,6 @@ class Atom: - def __new__(cls, atom_type, atom_nr, chiral, charge, aromatic): self = super().__new__(cls) # Must explicitly create the new object # Aside from explicit construction and return, rest of __new__ @@ -45,7 +44,7 @@ def __init__(self, atom_type, atom_nr, chiral, charge, aromatic): self.lone_pairs = [] self.draw = AtomDrawProperties() self.annotations = AtomAnnotations() - self.hybridisation = '' + self.hybridisation = "" self.connectivity = () self.neighbours = [] self.drawn_neighbours = [] @@ -63,19 +62,19 @@ def __hash__(self): def __repr__(self): if self.charge == 0: - charge_string = '' + charge_string = "" elif self.charge > 0: if self.charge == 1: - charge_string = '+' + charge_string = "+" else: - charge_string = str(self.charge) + '+' + charge_string = str(self.charge) + "+" else: if self.charge == -1: - charge_string = '-' + charge_string = "-" else: - charge_string = str(abs(self.charge)) + '-' + charge_string = str(abs(self.charge)) + "-" - return f'{self.type}{charge_string}_{self.nr}' + return f"{self.type}{charge_string}_{self.nr}" def copy(self): atom_copy = Atom(self.type, self.nr, self.chiral, self.charge, self.aromatic) @@ -157,9 +156,9 @@ def get_connectivity(self): for bond in self.bonds: for atom in bond.neighbours: - if atom.type != 'H' and atom != self: + if atom.type != "H" and atom != self: bond_type = bond.type - connectivity.append(f'{atom.type}_{bond_type}') + connectivity.append(f"{atom.type}_{bond_type}") connectivity = tuple(sorted(connectivity)) return connectivity @@ -195,7 +194,7 @@ def potential_same_connectivity(self, substructure_connectivity): def set_order(self): self.order = 0 for neighbour in self.neighbours: - if neighbour.type != 'H': + if neighbour.type != "H": self.order += 1 def add_electron_shells(self): @@ -207,25 +206,30 @@ def add_electron_shells(self): single_bonds = 0 for bond in self.bonds: - if bond.type == 'double': + if bond.type == "double": double_bonds += 1 - elif bond.type == 'single': + elif bond.type == "single": single_bonds += 1 - if self.type == 'N' and self.charge == 0 and double_bonds == 2 and single_bonds == 1: + if ( + self.type == "N" + and self.charge == 0 + and double_bonds == 2 + and single_bonds == 1 + ): oxygen_bonds = [] oxygens = [] for bond in self.bonds: neighbour = bond.get_connected_atom(self) - if bond.type == 'double' and neighbour.type == 'O': + if bond.type == "double" and neighbour.type == "O": oxygens.append(neighbour) oxygen_bonds.append(bond) if len(oxygens) >= 1: oxygen = oxygens[0] bond = oxygen_bonds[0] - bond.type = 'single' + bond.type = "single" bond.set_bond_summary() oxygen.charge = -1 self.charge = 1 @@ -240,7 +244,7 @@ def add_electron_shells(self): # Can't excite carbon if it has more than 4 electrons - if (self.type == 'C' or self.type == 'B') and self.excitable: + if (self.type == "C" or self.type == "B") and self.excitable: self.excite() else: @@ -249,7 +253,7 @@ def add_electron_shells(self): aromatic_count = 0 for bond in self.bonds: - if bond.type == 'aromatic': + if bond.type == "aromatic": aromatic_count += 1 # if not bond.has_neighbour('H'): bond_weights.append(BOND_PROPERTIES.bond_type_to_weight[bond.type]) @@ -259,12 +263,17 @@ def add_electron_shells(self): h_bonds = 0 for bond in self.bonds: - if bond.get_connected_atom(self).type == 'H': + if bond.get_connected_atom(self).type == "H": h_bonds += 1 nr_of_nonH_bonds = sum(bond_weights) + int(aromatic_count / 2) - if self.pyrrole or self.furan or self.thiophene or self.is_aromatic_nitrogen(): + if ( + self.pyrrole + or self.furan + or self.thiophene + or self.is_aromatic_nitrogen() + ): nr_of_nonH_bonds -= 1 # Does this work for all atoms? Doesn't for carbon. Should this be made general? @@ -282,10 +291,10 @@ def add_electron_shells(self): self.excite() else: - raise StructureError('violated_bonding_laws') + raise StructureError("violated_bonding_laws") else: - raise StructureError('violated_bonding_laws') + raise StructureError("violated_bonding_laws") def get_bonding_electrons(self): counter = 0 @@ -319,7 +328,9 @@ def adjacent_to_stereobond(self): def fill_shells(self): - electrons_remaining = ATOM_PROPERTIES.element_to_atomic_nr[self.type] - self.charge + electrons_remaining = ( + ATOM_PROPERTIES.element_to_atomic_nr[self.type] - self.charge + ) electron_nr = 1 # Iterate over the orbitals in order of them being filled @@ -340,7 +351,7 @@ def fill_shells(self): else: # All electrons have been placed and we can break out of the loop break - + def get_neighbour(self, atom_type): for neighbour in self.neighbours: if neighbour.type == atom_type: @@ -362,14 +373,14 @@ def excite(self): def get_non_hydrogen_neighbours(self): neighbours = [] for atom in self.neighbours: - if atom.type != 'H' and atom.type != '*': + if atom.type != "H" and atom.type != "*": neighbours.append(atom) return neighbours def get_non_hydrogen_bonds(self): bonds = [] for bond in self.bonds: - if bond.atom_1.type != 'H' and bond.atom_2.type != 'H': + if bond.atom_1.type != "H" and bond.atom_2.type != "H": bonds.append(bond) return bonds @@ -422,19 +433,24 @@ def calc_bond_nr(self): aromatic_bond_nr = 0 for bond in self.bonds: - if bond.type == 'single': + if bond.type == "single": bond_nr += 1 - elif bond.type == 'double': + elif bond.type == "double": bond_nr += 2 - elif bond.type == 'triple': + elif bond.type == "triple": bond_nr += 3 - elif bond.type == 'quadruple': + elif bond.type == "quadruple": bond_nr += 4 - elif bond.type == 'aromatic': + elif bond.type == "aromatic": aromatic_bond_nr += 1 if aromatic_bond_nr == 2: - if self.pyrrole or self.furan or self.thiophene or self.is_aromatic_nitrogen(): + if ( + self.pyrrole + or self.furan + or self.thiophene + or self.is_aromatic_nitrogen() + ): bond_nr += 2 elif self.aromatic: @@ -442,7 +458,7 @@ def calc_bond_nr(self): for bond in self.bonds: connected_atom = bond.get_connected_atom(self) - if bond.type == 'double' and connected_atom.type == 'O': + if bond.type == "double" and connected_atom.type == "O": oxygen = connected_atom if oxygen and oxygen.resonance_possible(self): @@ -452,9 +468,9 @@ def calc_bond_nr(self): else: bond_nr += 3 - elif aromatic_bond_nr == 3 and self.type == 'C': + elif aromatic_bond_nr == 3 and self.type == "C": bond_nr += 4 - elif aromatic_bond_nr == 3 and self.type == 'N': + elif aromatic_bond_nr == 3 and self.type == "N": if self.charge == 1: bond_nr += 4 else: @@ -465,24 +481,34 @@ def calc_bond_nr(self): def is_promotable(self): promotable = False for orbital_set in self.valence_shell.orbital_sets: - if 'd' in orbital_set: + if "d" in orbital_set: promotable = True return promotable def is_aromatic_nitrogen(self): - if self.type == 'N' and len(self.bonds) == 3 and self.aromatic and self.charge == 0: + if ( + self.type == "N" + and len(self.bonds) == 3 + and self.aromatic + and self.charge == 0 + ): return True return False def resonance_possible(self, neighbour): - if self.type == 'O' and len(self.bonds) == 1 and self.bonds[0].type == 'double' and neighbour.aromatic: + if ( + self.type == "O" + and len(self.bonds) == 1 + and self.bonds[0].type == "double" + and neighbour.aromatic + ): return True return False def promote_lone_pair_to_p_orbital(self): - assert self.hybridisation == 'sp3' + assert self.hybridisation == "sp3" self.valence_shell.dehybridise() @@ -492,15 +518,19 @@ def promote_lone_pair_to_p_orbital(self): for orbital in self.valence_shell.orbitals: if orbital.electron_nr == 2: # Any orbitals that are already bonded will become sp2 orbitals - if orbital.electrons[0].atom != orbital.electrons[1].atom and \ - (orbital.orbital_type == 's' or orbital.orbital_type == 'p'): + if orbital.electrons[0].atom != orbital.electrons[1].atom and ( + orbital.orbital_type == "s" or orbital.orbital_type == "p" + ): sp2_orbitals.append(orbital) # Any orbitals that are not bonded yet will become p orbitals - elif orbital.electrons[0].atom == orbital.electrons[1].atom == self and \ - (orbital.orbital_type == 's' or orbital.orbital_type == 'p'): + elif orbital.electrons[0].atom == orbital.electrons[ + 1 + ].atom == self and ( + orbital.orbital_type == "s" or orbital.orbital_type == "p" + ): p_orbitals.append(orbital) else: - if orbital.orbital_type == 's' or orbital.orbital_type == 'p': + if orbital.orbital_type == "s" or orbital.orbital_type == "p": sp2_orbitals.append(orbital) # Should more than one p-orbital be found, make sure we only use 1. @@ -511,20 +541,20 @@ def promote_lone_pair_to_p_orbital(self): p_orbital = p_orbitals[-1] - p_orbital.orbital_type = 'p' + p_orbital.orbital_type = "p" p_orbital.orbital_nr = 1 for i, orbital in enumerate(sp2_orbitals): - orbital.orbital_type = 'sp2' + orbital.orbital_type = "sp2" orbital.orbital_nr = i + 1 - self.hybridisation = 'sp2' + self.hybridisation = "sp2" for orbital in self.valence_shell.orbitals: for electron in orbital.electrons: if electron.atom == self: electron.set_orbital(orbital) - + def get_orbitals(self, orbital_type): orbitals = [] for orbital in self.valence_shell.orbitals: @@ -540,19 +570,23 @@ def get_hybrid_orbitals(self, orbital_type): orbitals.append(orbital) return orbitals - + def promote_pi_bonds_to_d_orbitals(self): - if self.is_promotable() and 'd' in self.hybridisation: - + if self.is_promotable() and "d" in self.hybridisation: + donor_orbitals = [] receiver_orbitals = [] for orbital in self.valence_shell.orbitals: - if 'p' in orbital.orbital_type and orbital.orbital_type != 'p' and orbital.electron_nr == 2: + if ( + "p" in orbital.orbital_type + and orbital.orbital_type != "p" + and orbital.electron_nr == 2 + ): if orbital.electrons[0].atom != orbital.electrons[1].atom: donor_orbitals.append(orbital) - - elif orbital.orbital_type == 'd' and orbital.electron_nr == 1: + + elif orbital.orbital_type == "d" and orbital.electron_nr == 1: receiver_orbitals.append(orbital) if donor_orbitals and receiver_orbitals: @@ -568,7 +602,7 @@ def promote_pi_bonds_to_d_orbitals(self): donor_orbital.remove_electron(moved_electron) receiver_orbital.add_electron(moved_electron) - receiver_orbital.set_bond(donor_orbital.bond, 'pi') + receiver_orbital.set_bond(donor_orbital.bond, "pi") donor_orbital.remove_bond() def promote_pi_bond_to_d_orbital(self): @@ -577,11 +611,11 @@ def promote_pi_bond_to_d_orbital(self): donor_orbitals = [] receiver_orbitals = [] for orbital in self.valence_shell.orbitals: - if orbital.orbital_type == 'p' and orbital.electron_nr == 2: + if orbital.orbital_type == "p" and orbital.electron_nr == 2: if orbital.electrons[0].atom != orbital.electrons[1].atom: donor_orbitals.append(orbital) - elif orbital.orbital_type == 'd' and orbital.electron_nr == 1: + elif orbital.orbital_type == "d" and orbital.electron_nr == 1: receiver_orbitals.append(orbital) donor_orbital = donor_orbitals[0] @@ -596,7 +630,7 @@ def promote_pi_bond_to_d_orbital(self): donor_orbital.remove_electron(moved_electron) receiver_orbital.add_electron(moved_electron) - receiver_orbital.set_bond(donor_orbital.bond, 'pi') + receiver_orbital.set_bond(donor_orbital.bond, "pi") donor_orbital.remove_bond() self.valence_shell.dehybridise() @@ -609,7 +643,7 @@ def reset_hybridisation(self): def calc_hydrogens(self): hydrogens = 0 - if self.type in ['B', 'C', 'N', 'O', 'P', 'S', 'F', 'Cl', 'Br', 'I']: + if self.type in ["B", "C", "N", "O", "P", "S", "F", "Cl", "Br", "I"]: bond_nr = self.calc_bond_nr() if bond_nr in ATOM_PROPERTIES.element_to_valences[self.type]: @@ -630,9 +664,9 @@ def hybridise(self): self.set_hybridisation() def set_hybridisation(self): - self.hybridisation = 's' + self.hybridisation = "s" for orbital in self.valence_shell.orbitals: - if orbital.orbital_type in {'sp', 'sp2', 'sp3', 'sp3d', 'sp3d2'}: + if orbital.orbital_type in {"sp", "sp2", "sp3", "sp3d", "sp3d2"}: self.hybridisation = orbital.orbital_type break @@ -641,17 +675,17 @@ def get_hybridisation(self): # Make dict if steric_number == 1: - hybridisation = 's' + hybridisation = "s" elif steric_number == 2: - hybridisation = 'sp' + hybridisation = "sp" elif steric_number == 3: - hybridisation = 'sp2' + hybridisation = "sp2" elif steric_number == 4: - hybridisation = 'sp3' + hybridisation = "sp3" elif steric_number == 5: - hybridisation = 'sp3d' + hybridisation = "sp3d" elif steric_number == 6: - hybridisation = 'sp3d2' + hybridisation = "sp3d2" elif steric_number == 0: hybridisation = None else: @@ -663,7 +697,7 @@ def get_steric_number(self): return self.calc_electron_pair_nr() + len(self.bonds) def get_valence(self): - + if self.type in ATOM_PROPERTIES.element_to_valences[self.type]: return ATOM_PROPERTIES.element_to_valences[self.type][0] else: @@ -680,7 +714,7 @@ def get_coords(self): def get_hydrogen_nr(self, structure): hydrogen_count = 0 for atom in structure.graph[self]: - if atom.type == 'H': + if atom.type == "H": hydrogen_count += 1 return hydrogen_count @@ -704,7 +738,7 @@ def __init__(self, x=0, y=0): self.connected_to_ring = False self.draw_explicit = False self.previous_atom = None - self.colour = 'black' + self.colour = "black" def set_position(self, vector): self.position = vector @@ -741,7 +775,7 @@ def copy(self): def add_annotation(self, name, default): - assert getattr(self, name, 'zahar') == 'zahar' + assert getattr(self, name, "zahar") == "zahar" setattr(self, name, default) self.annotations.add(name) diff --git a/pikachu/chem/atom_properties.py b/pikachu/chem/atom_properties.py index 98ca340..e797bd6 100644 --- a/pikachu/chem/atom_properties.py +++ b/pikachu/chem/atom_properties.py @@ -39,742 +39,899 @@ class AtomProperties: def __init__(self): # Define R group variables as '[RXZ][0-99]?' indices = [str(index) for index in range(100)] - r_group_symbols = ['R', 'X', 'Z'] - r_group_symbols_with_indices = ['R', 'X', 'Z'] + r_group_symbols = ["R", "X", "Z"] + r_group_symbols_with_indices = ["R", "X", "Z"] for symbol in r_group_symbols: for index in indices: r_group_symbols_with_indices.append(symbol + index) # Add R group variables and treat them like "C" for info_dict_name in dir(self): - if info_dict_name[0] != '_': + if info_dict_name[0] != "_": info_dict = getattr(self, info_dict_name) if type(info_dict) == dict: - if 'C' in info_dict.keys(): - r_group_dict = {symbol: info_dict['C'] - for symbol - in r_group_symbols_with_indices} + if "C" in info_dict.keys(): + r_group_dict = { + symbol: info_dict["C"] + for symbol in r_group_symbols_with_indices + } info_dict.update(r_group_dict) setattr(self, info_dict_name, info_dict) - element_to_valences = {'C': [4], - 'O': [2], - 'N': [3], - 'P': [3, 5], - 'S': [2, 4, 6], - 'Cl': [1, 7], - 'Br': [1, 7], - 'Se': [2], - 'Si': [4], - 'B': [3], - 'As': [3, 5], - 'F': [1], - 'H': [1], - 'I': [1, 7], - 'Na': [1], - 'Fe': [2, 3], - '*': [1]} + element_to_valences = { + "C": [4], + "O": [2], + "N": [3], + "P": [3, 5], + "S": [2, 4, 6], + "Cl": [1, 7], + "Br": [1, 7], + "Se": [2], + "Si": [4], + "B": [3], + "As": [3, 5], + "F": [1], + "H": [1], + "I": [1, 7], + "Na": [1], + "Fe": [2, 3], + "*": [1], + } - group_to_valence = {1: 1, - 2: 2, - 3: None, - 4: None, - 5: None, - 6: None, - 7: None, - 8: None, - 9: None, - 10: None, - 11: None, - 12: None, - 13: 3, - 14: 4, - 15: 3, - 16: 2, - 17: 1, - 18: 0, - None: None} + group_to_valence = { + 1: 1, + 2: 2, + 3: None, + 4: None, + 5: None, + 6: None, + 7: None, + 8: None, + 9: None, + 10: None, + 11: None, + 12: None, + 13: 3, + 14: 4, + 15: 3, + 16: 2, + 17: 1, + 18: 0, + None: None, + } - element_to_amu = {'H': 1.00797, - '*': -1.0, - 'He': 4.00260, - 'Li': 6.941, - 'Be': 9.01218, - 'B': 10.81, - 'C': 12.011, - 'N': 14.0067, - 'O': 15.9994, - 'F': 18.998403, - 'Ne': 20.179, - 'Na': 22.98977, - 'Mg': 24.305, - 'Al': 26.98154, - 'Si': 28.0855, - 'P': 30.97376, - 'S': 32.06, - 'Cl': 35.453, - 'Ar': 39.948, - 'K': 39.0983, - 'Ca': 40.08, - 'Sc': 44.9559, - 'Ti': 47.90, - 'V': 50.9415, - 'Cr': 51.996, - 'Mn': 54.9380, - 'Fe': 55.847, - 'Co': 58.9332, - 'Ni': 58.70, - 'Cu': 63.546, - 'Zn': 65.38, - 'Ga': 69.72, - 'Ge': 72.59, - 'As': 74.9216, - 'Se': 78.96, - 'Br': 79.904, - 'Kr': 83.80, - 'Rb': 85.4678, - 'Sr': 87.62, - 'Y': 88.9059, - 'Zr': 91.22, - 'Nb': 92.9064, - 'Mo': 95.94, - 'Tc': 98, - 'Ru': 101.07, - 'Rh': 102.9055, - 'Pd': 106.4, - 'Ag': 107.868, - 'Cd': 112.41, - 'In': 114.82, - 'Sn': 118.69, - 'Sb': 121.75, - 'I': 126.9045, - 'Te': 127.60, - 'Xe': 131.30, - 'Cs': 132.9054, - 'Ba': 137.33, - 'La': 138.9055, - 'Ce': 140.12, - 'Pr': 140.9077, - 'Nd': 144.24, - 'Pm': 145, - 'Sm': 150.4, - 'Eu': 151.96, - 'Gd': 157.25, - 'Tb': 158.9254, - 'Dy': 162.50, - 'Ho': 164.9304, - 'Er': 167.26, - 'Tm': 168.9342, - 'Yb': 173.04, - 'Lu': 174.967, - 'Hf': 178.49, - 'Ta': 180.9479, - 'W': 183.85, - 'Re': 186.207, - 'Os': 190.2, - 'Ir': 192.22, - 'Pt': 195.09, - 'Au': 196.9665, - 'Hg': 200.59, - 'Tl': 204.37, - 'Pb': 207.2, - 'Bi': 208.9804, - 'Po': 209, - 'At': 210, - 'Rn': 222, - 'Fr': 223, - 'Ra': 226.0254, - 'Ac': 227.0278, - 'Pa': 231.0359, - 'Th': 232.0381, - 'Np': 237.0482, - 'U': 238.029, - 'Pu': 242, - 'Am': 243, - 'Bk': 247, - 'Cm': 247, - 'No': 250, - 'Cf': 251, - 'Es': 252, - 'Hs': 255, - 'Mt': 256, - 'Fm': 257, - 'Md': 258, - 'Lr': 260, - 'Rf': 261, - 'Bh': 262, - 'Db': 262, - 'Sg': 263, - 'Uun': 269, - 'Uuu': 272, - 'Uub': 277, - 'Uuq': None} + element_to_amu = { + "H": 1.00797, + "*": -1.0, + "He": 4.00260, + "Li": 6.941, + "Be": 9.01218, + "B": 10.81, + "C": 12.011, + "N": 14.0067, + "O": 15.9994, + "F": 18.998403, + "Ne": 20.179, + "Na": 22.98977, + "Mg": 24.305, + "Al": 26.98154, + "Si": 28.0855, + "P": 30.97376, + "S": 32.06, + "Cl": 35.453, + "Ar": 39.948, + "K": 39.0983, + "Ca": 40.08, + "Sc": 44.9559, + "Ti": 47.90, + "V": 50.9415, + "Cr": 51.996, + "Mn": 54.9380, + "Fe": 55.847, + "Co": 58.9332, + "Ni": 58.70, + "Cu": 63.546, + "Zn": 65.38, + "Ga": 69.72, + "Ge": 72.59, + "As": 74.9216, + "Se": 78.96, + "Br": 79.904, + "Kr": 83.80, + "Rb": 85.4678, + "Sr": 87.62, + "Y": 88.9059, + "Zr": 91.22, + "Nb": 92.9064, + "Mo": 95.94, + "Tc": 98, + "Ru": 101.07, + "Rh": 102.9055, + "Pd": 106.4, + "Ag": 107.868, + "Cd": 112.41, + "In": 114.82, + "Sn": 118.69, + "Sb": 121.75, + "I": 126.9045, + "Te": 127.60, + "Xe": 131.30, + "Cs": 132.9054, + "Ba": 137.33, + "La": 138.9055, + "Ce": 140.12, + "Pr": 140.9077, + "Nd": 144.24, + "Pm": 145, + "Sm": 150.4, + "Eu": 151.96, + "Gd": 157.25, + "Tb": 158.9254, + "Dy": 162.50, + "Ho": 164.9304, + "Er": 167.26, + "Tm": 168.9342, + "Yb": 173.04, + "Lu": 174.967, + "Hf": 178.49, + "Ta": 180.9479, + "W": 183.85, + "Re": 186.207, + "Os": 190.2, + "Ir": 192.22, + "Pt": 195.09, + "Au": 196.9665, + "Hg": 200.59, + "Tl": 204.37, + "Pb": 207.2, + "Bi": 208.9804, + "Po": 209, + "At": 210, + "Rn": 222, + "Fr": 223, + "Ra": 226.0254, + "Ac": 227.0278, + "Pa": 231.0359, + "Th": 232.0381, + "Np": 237.0482, + "U": 238.029, + "Pu": 242, + "Am": 243, + "Bk": 247, + "Cm": 247, + "No": 250, + "Cf": 251, + "Es": 252, + "Hs": 255, + "Mt": 256, + "Fm": 257, + "Md": 258, + "Lr": 260, + "Rf": 261, + "Bh": 262, + "Db": 262, + "Sg": 263, + "Uun": 269, + "Uuu": 272, + "Uub": 277, + "Uuq": None, + } - element_to_atomic_nr = {'H': 1, - '*': 1, - 'He': 2, - 'Li': 3, - 'Be': 4, - 'B': 5, - 'C': 6, - 'N': 7, - 'O': 8, - 'F': 9, - 'Ne': 10, - 'Na': 11, - 'Mg': 12, - 'Al': 13, - 'Si': 14, - 'P': 15, - 'S': 16, - 'Cl': 17, - 'Ar': 18, - 'K': 19, - 'Ca': 20, - 'Sc': 21, - 'Ti': 22, - 'V': 23, - 'Cr': 24, - 'Mn': 25, - 'Fe': 26, - 'Co': 27, - 'Ni': 28, - 'Cu': 29, - 'Zn': 30, - 'Ga': 31, - 'Ge': 32, - 'As': 33, - 'Se': 34, - 'Br': 35, - 'Kr': 36, - 'Rb': 37, - 'Sr': 38, - 'Y': 39, - 'Zr': 40, - 'Nb': 41, - 'Mo': 42, - 'Tc': 43, - 'Ru': 44, - 'Rh': 45, - 'Pd': 46, - 'Ag': 47, - 'Cd': 48, - 'In': 49, - 'Sn': 50, - 'Sb': 51, - 'Te': 52, - 'I': 53, - 'Xe': 54, - 'Cs': 55, - 'Ba': 56, - 'La': 57, - 'Ce': 58, - 'Pr': 59, - 'Nd': 60, - 'Pm': 61, - 'Sm': 62, - 'Eu': 63, - 'Gd': 64, - 'Tb': 65, - 'Dy': 66, - 'Ho': 67, - 'Er': 68, - 'Tm': 69, - 'Yb': 70, - 'Lu': 71, - 'Hf': 72, - 'Ta': 73, - 'W': 74, - 'Re': 75, - 'Os': 76, - 'Ir': 77, - 'Pt': 78, - 'Au': 79, - 'Hg': 80, - 'Tl': 81, - 'Pb': 82, - 'Bi': 83, - 'Po': 84, - 'At': 85, - 'Rn': 86, - 'Fr': 87, - 'Ra': 88, - 'Ac': 89, - 'Th': 90, - 'Pa': 91, - 'U': 92, - 'Np': 93, - 'Pu': 94, - 'Am': 95, - 'Cm': 96, - 'Bk': 97, - 'Cf': 98, - 'Es': 99, - 'Fm': 100, - 'Md': 101, - 'No': 102, - 'Lr': 103, - 'Rf': 104, - 'Db': 105, - 'Sg': 106, - 'Bh': 107, - 'Hs': 108, - 'Mt': 109, - 'Uun': 110, - 'Uuu': 111, - 'Uub': 112, - 'Uuq': 114} + element_to_atomic_nr = { + "H": 1, + "*": 1, + "He": 2, + "Li": 3, + "Be": 4, + "B": 5, + "C": 6, + "N": 7, + "O": 8, + "F": 9, + "Ne": 10, + "Na": 11, + "Mg": 12, + "Al": 13, + "Si": 14, + "P": 15, + "S": 16, + "Cl": 17, + "Ar": 18, + "K": 19, + "Ca": 20, + "Sc": 21, + "Ti": 22, + "V": 23, + "Cr": 24, + "Mn": 25, + "Fe": 26, + "Co": 27, + "Ni": 28, + "Cu": 29, + "Zn": 30, + "Ga": 31, + "Ge": 32, + "As": 33, + "Se": 34, + "Br": 35, + "Kr": 36, + "Rb": 37, + "Sr": 38, + "Y": 39, + "Zr": 40, + "Nb": 41, + "Mo": 42, + "Tc": 43, + "Ru": 44, + "Rh": 45, + "Pd": 46, + "Ag": 47, + "Cd": 48, + "In": 49, + "Sn": 50, + "Sb": 51, + "Te": 52, + "I": 53, + "Xe": 54, + "Cs": 55, + "Ba": 56, + "La": 57, + "Ce": 58, + "Pr": 59, + "Nd": 60, + "Pm": 61, + "Sm": 62, + "Eu": 63, + "Gd": 64, + "Tb": 65, + "Dy": 66, + "Ho": 67, + "Er": 68, + "Tm": 69, + "Yb": 70, + "Lu": 71, + "Hf": 72, + "Ta": 73, + "W": 74, + "Re": 75, + "Os": 76, + "Ir": 77, + "Pt": 78, + "Au": 79, + "Hg": 80, + "Tl": 81, + "Pb": 82, + "Bi": 83, + "Po": 84, + "At": 85, + "Rn": 86, + "Fr": 87, + "Ra": 88, + "Ac": 89, + "Th": 90, + "Pa": 91, + "U": 92, + "Np": 93, + "Pu": 94, + "Am": 95, + "Cm": 96, + "Bk": 97, + "Cf": 98, + "Es": 99, + "Fm": 100, + "Md": 101, + "No": 102, + "Lr": 103, + "Rf": 104, + "Db": 105, + "Sg": 106, + "Bh": 107, + "Hs": 108, + "Mt": 109, + "Uun": 110, + "Uuu": 111, + "Uub": 112, + "Uuq": 114, + } - element_to_radius = {'H': 0.37, - '*': 0.37, - 'He': 0.32, - 'Li': 1.34, - 'Be': 0.90, - 'B': 0.82, - 'C': 0.77, - 'N': 0.75, - 'O': 0.73, - 'F': 0.71, - 'Ne': 0.69, - 'Na': 1.54, - 'Mg': 1.30, - 'Al': 1.18, - 'Si': 1.11, - 'P': 1.06, - 'S': 1.02, - 'Cl': 0.99, - 'Ar': 0.97, - 'K': 1.96, - 'Ca': 1.74, - 'Sc': 1.44, - 'Ti': 1.36, - 'V': 1.25, - 'Cr': 1.27, - 'Mn': 1.39, - 'Fe': 1.25, - 'Co': 1.26, - 'Ni': 1.21, - 'Cu': 1.38, - 'Zn': 1.31, - 'Ga': 1.26, - 'Ge': 1.22, - 'As': 1.19, - 'Se': 1.16, - 'Br': 1.14, - 'Kr': 1.10, - 'I': 1.33} + element_to_radius = { + "H": 0.37, + "*": 0.37, + "He": 0.32, + "Li": 1.34, + "Be": 0.90, + "B": 0.82, + "C": 0.77, + "N": 0.75, + "O": 0.73, + "F": 0.71, + "Ne": 0.69, + "Na": 1.54, + "Mg": 1.30, + "Al": 1.18, + "Si": 1.11, + "P": 1.06, + "S": 1.02, + "Cl": 0.99, + "Ar": 0.97, + "K": 1.96, + "Ca": 1.74, + "Sc": 1.44, + "Ti": 1.36, + "V": 1.25, + "Cr": 1.27, + "Mn": 1.39, + "Fe": 1.25, + "Co": 1.26, + "Ni": 1.21, + "Cu": 1.38, + "Zn": 1.31, + "Ga": 1.26, + "Ge": 1.22, + "As": 1.19, + "Se": 1.16, + "Br": 1.14, + "Kr": 1.10, + "I": 1.33, + } - element_to_valence_electrons = {'H': 1, - '*': 1, - 'B': 3, - 'C': 4, - 'Si': 4, - 'As': 5, - 'Te': 5, - 'O': 6, - 'N': 5, - 'P': 5, - 'S': 6, - 'Se': 6, - 'Cl': 7, - 'Br': 7, - 'F': 7, - 'I': 7, - 'Na': 1} + element_to_valence_electrons = { + "H": 1, + "*": 1, + "B": 3, + "C": 4, + "Si": 4, + "As": 5, + "Te": 5, + "O": 6, + "N": 5, + "P": 5, + "S": 6, + "Se": 6, + "Cl": 7, + "Br": 7, + "F": 7, + "I": 7, + "Na": 1, + } - element_to_electronegativity = {'Xe': 0.0, - 'Fr': 0.7, - 'Cs': 0.79, - 'Rb': 0.82, - 'K': 0.82, - 'Ba': 0.89, - 'Ra': 0.9, - 'Na': 0.93, - 'Sr': 0.95, - 'Li': 0.98, - 'Ca': 1.0, - 'Yb': 1.1, - 'La': 1.1, - 'Ac': 1.1, - 'Ce': 1.12, - 'Pr': 1.13, - 'Pm': 1.13, - 'Nd': 1.14, - 'Sm': 1.17, - 'Tb': 1.2, - 'Gd': 1.2, - 'Eu': 1.2, - 'Dy': 1.22, - 'Y': 1.22, - 'Ho': 1.23, - 'Er': 1.24, - 'Tm': 1.25, - 'Lu': 1.27, - 'Pu': 1.28, - 'No': 1.3, - 'Es': 1.3, - 'Th': 1.3, - 'Hf': 1.3, - 'Md': 1.3, - 'Bk': 1.3, - 'Am': 1.3, - 'Lr': 1.3, - 'Cf': 1.3, - 'Cm': 1.3, - 'Fm': 1.3, - 'Mg': 1.31, - 'Zr': 1.33, - 'Np': 1.36, - 'Sc': 1.36, - 'U': 1.38, - 'Ta': 1.5, - 'Pa': 1.5, - 'Ti': 1.54, - 'Mn': 1.55, - 'Be': 1.57, - 'Nb': 1.6, - 'Al': 1.61, - 'V': 1.63, - 'Zn': 1.65, - 'Cr': 1.66, - 'Cd': 1.69, - 'In': 1.78, - 'Ga': 1.81, - 'Fe': 1.83, - 'Co': 1.88, - 'Si': 1.9, - 'Re': 1.9, - 'Tc': 1.9, - 'Cu': 1.9, - 'Ni': 1.91, - 'Ag': 1.93, - 'Sn': 1.96, - 'Po': 2.0, - 'Hg': 2.0, - 'Ge': 2.01, - 'Bi': 2.02, - 'Tl': 2.04, - 'B': 2.04, - 'Sb': 2.05, - 'Te': 2.1, - 'Mo': 2.16, - 'As': 2.18, - 'P': 2.19, - 'H': 2.2, - 'Ir': 2.2, - 'Ru': 2.2, - 'Os': 2.2, - 'At': 2.2, - 'Pd': 2.2, - 'Pt': 2.28, - 'Rh': 2.28, - 'Pb': 2.33, - 'W': 2.36, - 'Au': 2.54, - 'C': 2.55, - 'Se': 2.55, - 'S': 2.58, - 'I': 2.66, - 'Br': 2.96, - 'N': 3.04, - 'Cl': 3.16, - 'O': 3.44, - 'F': 3.98} + element_to_electronegativity = { + "Xe": 0.0, + "Fr": 0.7, + "Cs": 0.79, + "Rb": 0.82, + "K": 0.82, + "Ba": 0.89, + "Ra": 0.9, + "Na": 0.93, + "Sr": 0.95, + "Li": 0.98, + "Ca": 1.0, + "Yb": 1.1, + "La": 1.1, + "Ac": 1.1, + "Ce": 1.12, + "Pr": 1.13, + "Pm": 1.13, + "Nd": 1.14, + "Sm": 1.17, + "Tb": 1.2, + "Gd": 1.2, + "Eu": 1.2, + "Dy": 1.22, + "Y": 1.22, + "Ho": 1.23, + "Er": 1.24, + "Tm": 1.25, + "Lu": 1.27, + "Pu": 1.28, + "No": 1.3, + "Es": 1.3, + "Th": 1.3, + "Hf": 1.3, + "Md": 1.3, + "Bk": 1.3, + "Am": 1.3, + "Lr": 1.3, + "Cf": 1.3, + "Cm": 1.3, + "Fm": 1.3, + "Mg": 1.31, + "Zr": 1.33, + "Np": 1.36, + "Sc": 1.36, + "U": 1.38, + "Ta": 1.5, + "Pa": 1.5, + "Ti": 1.54, + "Mn": 1.55, + "Be": 1.57, + "Nb": 1.6, + "Al": 1.61, + "V": 1.63, + "Zn": 1.65, + "Cr": 1.66, + "Cd": 1.69, + "In": 1.78, + "Ga": 1.81, + "Fe": 1.83, + "Co": 1.88, + "Si": 1.9, + "Re": 1.9, + "Tc": 1.9, + "Cu": 1.9, + "Ni": 1.91, + "Ag": 1.93, + "Sn": 1.96, + "Po": 2.0, + "Hg": 2.0, + "Ge": 2.01, + "Bi": 2.02, + "Tl": 2.04, + "B": 2.04, + "Sb": 2.05, + "Te": 2.1, + "Mo": 2.16, + "As": 2.18, + "P": 2.19, + "H": 2.2, + "Ir": 2.2, + "Ru": 2.2, + "Os": 2.2, + "At": 2.2, + "Pd": 2.2, + "Pt": 2.28, + "Rh": 2.28, + "Pb": 2.33, + "W": 2.36, + "Au": 2.54, + "C": 2.55, + "Se": 2.55, + "S": 2.58, + "I": 2.66, + "Br": 2.96, + "N": 3.04, + "Cl": 3.16, + "O": 3.44, + "F": 3.98, + } - element_to_shell_nr = {'H': 1, - 'He': 1, - 'Li': 2, - 'Be': 2, - 'B': 2, - 'C': 2, - 'N': 2, - 'O': 2, - 'F': 2, - 'Ne': 2, - 'Na': 3, - 'Mg': 3, - 'Al': 3, - 'Si': 3, - 'P': 3, - 'S': 3, - 'Cl': 3, - 'Ar': 3, - 'K': 4, - 'Ca': 4, - 'Sc': 4, - 'Ti': 4, - 'V': 4, - 'Cr': 4, - 'Mn': 4, - 'Fe': 4, - 'Co': 4, - 'Ni': 4, - 'Cu': 4, - 'Zn': 4, - 'Ga': 4, - 'Ge': 4, - 'As': 4, - 'Se': 4, - 'Br': 4, - 'Kr': 4, - 'Rb': 5, - 'Sr': 5, - 'Y': 5, - 'Zr': 5, - 'Nb': 5, - 'Mo': 5, - 'Tc': 5, - 'Ru': 5, - 'Rh': 5, - 'Pd': 5, - 'Ag': 5, - 'Cd': 5, - 'In': 5, - 'Sn': 5, - 'Sb': 5, - 'Te': 5, - 'I': 5, - 'Xe': 5, - 'Cs': 6, - 'Ba': 6, - 'La': 6, - 'Ce': 6, - 'Pr': 6, - 'Nd': 6, - 'Pm': 6, - 'Sm': 6, - 'Eu': 6, - 'Gd': 6, - 'Tb': 6, - 'Dy': 6, - 'Ho': 6, - 'Er': 6, - 'Tm': 6, - 'Yb': 6, - 'Lu': 6, - 'Hf': 6, - 'Ta': 6, - 'W': 6, - 'Re': 6, - 'Os': 6, - 'Ir': 6, - 'Pt': 6, - 'Au': 6, - 'Hg': 6, - 'Tl': 6, - 'Pb': 6, - 'Bi': 6, - 'Po': 6, - 'At': 6, - 'Rn': 6, - 'Fr': 7, - 'Ra': 7, - 'Ac': 7, - 'Th': 7, - 'Pa': 7, - 'U': 7, - 'Np': 7, - 'Pu': 7, - 'Am': 7, - 'Cm': 7, - 'Bk': 7, - 'Cf': 7, - 'Es': 7, - 'Fm': 7, - 'Md': 7, - 'No': 7, - 'Lr': 7, - 'Rf': 7, - 'Db': 7, - 'Sg': 7, - 'Bh': 7, - 'Hs': 7, - 'Mt': 7, - 'Ds': 7, - 'Rg': 7, - 'Cn': 7, - 'Nh': 7, - 'Fl': 7, - 'Mc': 7, - 'Lv': 7, - 'Ts': 7, - 'Og': 7, + element_to_shell_nr = { + "H": 1, + "He": 1, + "Li": 2, + "Be": 2, + "B": 2, + "C": 2, + "N": 2, + "O": 2, + "F": 2, + "Ne": 2, + "Na": 3, + "Mg": 3, + "Al": 3, + "Si": 3, + "P": 3, + "S": 3, + "Cl": 3, + "Ar": 3, + "K": 4, + "Ca": 4, + "Sc": 4, + "Ti": 4, + "V": 4, + "Cr": 4, + "Mn": 4, + "Fe": 4, + "Co": 4, + "Ni": 4, + "Cu": 4, + "Zn": 4, + "Ga": 4, + "Ge": 4, + "As": 4, + "Se": 4, + "Br": 4, + "Kr": 4, + "Rb": 5, + "Sr": 5, + "Y": 5, + "Zr": 5, + "Nb": 5, + "Mo": 5, + "Tc": 5, + "Ru": 5, + "Rh": 5, + "Pd": 5, + "Ag": 5, + "Cd": 5, + "In": 5, + "Sn": 5, + "Sb": 5, + "Te": 5, + "I": 5, + "Xe": 5, + "Cs": 6, + "Ba": 6, + "La": 6, + "Ce": 6, + "Pr": 6, + "Nd": 6, + "Pm": 6, + "Sm": 6, + "Eu": 6, + "Gd": 6, + "Tb": 6, + "Dy": 6, + "Ho": 6, + "Er": 6, + "Tm": 6, + "Yb": 6, + "Lu": 6, + "Hf": 6, + "Ta": 6, + "W": 6, + "Re": 6, + "Os": 6, + "Ir": 6, + "Pt": 6, + "Au": 6, + "Hg": 6, + "Tl": 6, + "Pb": 6, + "Bi": 6, + "Po": 6, + "At": 6, + "Rn": 6, + "Fr": 7, + "Ra": 7, + "Ac": 7, + "Th": 7, + "Pa": 7, + "U": 7, + "Np": 7, + "Pu": 7, + "Am": 7, + "Cm": 7, + "Bk": 7, + "Cf": 7, + "Es": 7, + "Fm": 7, + "Md": 7, + "No": 7, + "Lr": 7, + "Rf": 7, + "Db": 7, + "Sg": 7, + "Bh": 7, + "Hs": 7, + "Mt": 7, + "Ds": 7, + "Rg": 7, + "Cn": 7, + "Nh": 7, + "Fl": 7, + "Mc": 7, + "Lv": 7, + "Ts": 7, + "Og": 7, + "*": 1, + } - '*': 1} + shell_to_electron_nr = {1: 2, 2: 8, 3: 18, 4: 32, 5: 32} - shell_to_electron_nr = {1: 2, - 2: 8, - 3: 18, - 4: 32, - 5: 32} - - shell_to_orbitals = {1: ['1s'], - 2: ['2s', '2p1', '2p2', '2p3'], - 3: ['3s', '3p1', '3p2', '3p3', '3d1', '3d2', '3d3', '3d4', '3d5'], - 4: ['4s', '4p1', '4p2', '4p3', '4d1', '4d2', '4d3', '4d4', '4d5', '4f1', - '4f2', '4f3', '4f4', '4f5', '4f6', '4f7'], - 5: ['5s', '5p1', '5p2', '5p3', '5d1', '5d2', '5d3', '5d4', '5d5', '5f1', - '5f2', '5f3', '5f4', '5f5', '5f6', '5f7']} + shell_to_orbitals = { + 1: ["1s"], + 2: ["2s", "2p1", "2p2", "2p3"], + 3: ["3s", "3p1", "3p2", "3p3", "3d1", "3d2", "3d3", "3d4", "3d5"], + 4: [ + "4s", + "4p1", + "4p2", + "4p3", + "4d1", + "4d2", + "4d3", + "4d4", + "4d5", + "4f1", + "4f2", + "4f3", + "4f4", + "4f5", + "4f6", + "4f7", + ], + 5: [ + "5s", + "5p1", + "5p2", + "5p3", + "5d1", + "5d2", + "5d3", + "5d4", + "5d5", + "5f1", + "5f2", + "5f3", + "5f4", + "5f5", + "5f6", + "5f7", + ], + } orbital_order = ( - '1s', '2s', '2p', '3s', '3p', '4s', '3d', '4p', '5s', '4d', '5p', '6s', '4f', '5d', '6p', '7s', '5f', '6d', - '7p', '8s', '5g', '6f', '7d', '8p', '9s') + "1s", + "2s", + "2p", + "3s", + "3p", + "4s", + "3d", + "4p", + "5s", + "4d", + "5p", + "6s", + "4f", + "5d", + "6p", + "7s", + "5f", + "6d", + "7p", + "8s", + "5g", + "6f", + "7d", + "8p", + "9s", + ) - orbital_type_to_orbital_number = {'s': 1, - 'p': 3, - 'd': 5, - 'f': 7, - 'g': 9} + orbital_type_to_orbital_number = {"s": 1, "p": 3, "d": 5, "f": 7, "g": 9} - metals = {'Li', 'Be', 'Na', 'Mg', 'Al', 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', - 'Ga', 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Cs', 'Ba', - 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu', 'Hf', 'Ta', - 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi', 'Po', 'Fr', 'Ra', 'Ac', 'Th', 'Pa', 'U', 'Np', - 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr', 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds', - 'Rg', 'Cn', 'Nh', 'Fl', 'Mc', 'Lv'} + metals = { + "Li", + "Be", + "Na", + "Mg", + "Al", + "K", + "Ca", + "Sc", + "Ti", + "V", + "Cr", + "Mn", + "Fe", + "Co", + "Ni", + "Cu", + "Zn", + "Ga", + "Rb", + "Sr", + "Y", + "Zr", + "Nb", + "Mo", + "Tc", + "Ru", + "Rh", + "Pd", + "Ag", + "Cd", + "In", + "Sn", + "Cs", + "Ba", + "La", + "Ce", + "Pr", + "Nd", + "Pm", + "Sm", + "Eu", + "Gd", + "Tb", + "Dy", + "Ho", + "Er", + "Tm", + "Yb", + "Lu", + "Hf", + "Ta", + "W", + "Re", + "Os", + "Ir", + "Pt", + "Au", + "Hg", + "Tl", + "Pb", + "Bi", + "Po", + "Fr", + "Ra", + "Ac", + "Th", + "Pa", + "U", + "Np", + "Pu", + "Am", + "Cm", + "Bk", + "Cf", + "Es", + "Fm", + "Md", + "No", + "Lr", + "Rf", + "Db", + "Sg", + "Bh", + "Hs", + "Mt", + "Ds", + "Rg", + "Cn", + "Nh", + "Fl", + "Mc", + "Lv", + } - organic = {'C', 'O', 'N', 'P', 'H', 'S', - 'Cl', 'Br', 'I', 'F', '*'} + organic = {"C", "O", "N", "P", "H", "S", "Cl", "Br", "I", "F", "*"} - element_to_atomic_group = {'H': 1, - '*': 1, - 'He': 8, - 'Li': 1, - 'Be': 2, - 'B': 13, - 'C': 14, - 'N': 15, - 'O': 16, - 'F': 17, - 'Ne': 18, - 'Na': 1, - 'Mg': 2, - 'Al': 13, - 'Si': 14, - 'P': 15, - 'S': 16, - 'Cl': 17, - 'Ar': 18, - 'K': 1, - 'Ca': 2, - 'Sc': 3, - 'Ti': 4, - 'V': 5, - 'Cr': 6, - 'Mn': 7, - 'Fe': 8, - 'Co': 9, - 'Ni': 10, - 'Cu': 11, - 'Zn': 12, - 'Ga': 13, - 'Ge': 14, - 'As': 15, - 'Se': 16, - 'Br': 17, - 'Kr': 18, - 'Rb': 1, - 'Sr': 2, - 'Y': 3, - 'Zr': 4, - 'Nb': 5, - 'Mo': 6, - 'Tc': 7, - 'Ru': 8, - 'Rh': 9, - 'Pd': 10, - 'Ag': 11, - 'Cd': 12, - 'In': 13, - 'Sn': 14, - 'Sb': 15, - 'Te': 16, - 'I': 17, - 'Xe': 18, - 'Cs': 1, - 'Ba': 2, - 'La': None, - 'Ce': None, - 'Pr': None, - 'Nd': None, - 'Pm': None, - 'Sm': None, - 'Eu': None, - 'Gd': None, - 'Tb': None, - 'Dy': None, - 'Ho': None, - 'Er': None, - 'Tm': None, - 'Yb': None, - 'Lu': None, - 'Hf': 4, - 'Ta': 5, - 'W': 6, - 'Re': 7, - 'Os': 8, - 'Ir': 9, - 'Pt': 10, - 'Au': 11, - 'Hg': 12, - 'Tl': 13, - 'Pb': 14, - 'Bi': 15, - 'Po': 16, - 'At': 17, - 'Rn': 18, - 'Fr': 1, - 'Ra': 2, - 'Ac': None, - 'Th': None, - 'Pa': None, - 'U': None, - 'Np': None, - 'Pu': None, - 'Am': None, - 'Cm': None, - 'Bk': None, - 'Cf': None, - 'Es': None, - 'Fm': None, - 'Md': None, - 'No': None, - 'Lr': None, - 'Rf': 4, - 'Db': 5, - 'Sg': 6, - 'Bh': 7, - 'Hs': 8, - 'Mt': 9, - 'Uun': 10, - 'Uuu': 11, - 'Uub': 12, - 'Uuq': 14} + element_to_atomic_group = { + "H": 1, + "*": 1, + "He": 8, + "Li": 1, + "Be": 2, + "B": 13, + "C": 14, + "N": 15, + "O": 16, + "F": 17, + "Ne": 18, + "Na": 1, + "Mg": 2, + "Al": 13, + "Si": 14, + "P": 15, + "S": 16, + "Cl": 17, + "Ar": 18, + "K": 1, + "Ca": 2, + "Sc": 3, + "Ti": 4, + "V": 5, + "Cr": 6, + "Mn": 7, + "Fe": 8, + "Co": 9, + "Ni": 10, + "Cu": 11, + "Zn": 12, + "Ga": 13, + "Ge": 14, + "As": 15, + "Se": 16, + "Br": 17, + "Kr": 18, + "Rb": 1, + "Sr": 2, + "Y": 3, + "Zr": 4, + "Nb": 5, + "Mo": 6, + "Tc": 7, + "Ru": 8, + "Rh": 9, + "Pd": 10, + "Ag": 11, + "Cd": 12, + "In": 13, + "Sn": 14, + "Sb": 15, + "Te": 16, + "I": 17, + "Xe": 18, + "Cs": 1, + "Ba": 2, + "La": None, + "Ce": None, + "Pr": None, + "Nd": None, + "Pm": None, + "Sm": None, + "Eu": None, + "Gd": None, + "Tb": None, + "Dy": None, + "Ho": None, + "Er": None, + "Tm": None, + "Yb": None, + "Lu": None, + "Hf": 4, + "Ta": 5, + "W": 6, + "Re": 7, + "Os": 8, + "Ir": 9, + "Pt": 10, + "Au": 11, + "Hg": 12, + "Tl": 13, + "Pb": 14, + "Bi": 15, + "Po": 16, + "At": 17, + "Rn": 18, + "Fr": 1, + "Ra": 2, + "Ac": None, + "Th": None, + "Pa": None, + "U": None, + "Np": None, + "Pu": None, + "Am": None, + "Cm": None, + "Bk": None, + "Cf": None, + "Es": None, + "Fm": None, + "Md": None, + "No": None, + "Lr": None, + "Rf": 4, + "Db": 5, + "Sg": 6, + "Bh": 7, + "Hs": 8, + "Mt": 9, + "Uun": 10, + "Uuu": 11, + "Uub": 12, + "Uuq": 14, + } - metal_to_charge = {'Li': {1}, - 'Na': {1}, - 'K': {1}, - 'Rb': {1}, - 'Cs': {1}, - 'Fr': {1}, - 'Be': {2}, - 'Mg': {2}, - 'Ca': {2}, - 'Sr': {2}, - 'Ba': {2}, - 'Ra': {2}, - 'Cr': {3, 6}, - 'Mn': {2}, - 'Fe': {2, 3}, - 'Co': {2}, - 'Ni': {2}, - 'Pt': {2}, - 'Cu': {1, 2}, - 'Ag': {1}, - 'Au': {1, 3}, - 'Zn': {2}, - 'Cd': {2}, - 'Hg': {2}, - 'Al': {3}} + metal_to_charge = { + "Li": {1}, + "Na": {1}, + "K": {1}, + "Rb": {1}, + "Cs": {1}, + "Fr": {1}, + "Be": {2}, + "Mg": {2}, + "Ca": {2}, + "Sr": {2}, + "Ba": {2}, + "Ra": {2}, + "Cr": {3, 6}, + "Mn": {2}, + "Fe": {2, 3}, + "Co": {2}, + "Ni": {2}, + "Pt": {2}, + "Cu": {1, 2}, + "Ag": {1}, + "Au": {1, 3}, + "Zn": {2}, + "Cd": {2}, + "Hg": {2}, + "Al": {3}, + } ATOM_PROPERTIES = AtomProperties() diff --git a/pikachu/chem/bond.py b/pikachu/chem/bond.py index 0cf2a88..f2c0c39 100644 --- a/pikachu/chem/bond.py +++ b/pikachu/chem/bond.py @@ -3,7 +3,15 @@ class Bond: - bond_types = {'single', 'double', 'triple', 'quadruple', 'aromatic', 'ionic', 'dummy'} + bond_types = { + "single", + "double", + "triple", + "quadruple", + "aromatic", + "ionic", + "dummy", + } def __init__(self, atom_1, atom_2, bond_type, bond_nr): atoms = [atom_1, atom_2] @@ -24,16 +32,16 @@ def __init__(self, atom_1, atom_2, bond_type, bond_nr): self.type = bond_type self.nr = bond_nr self.aromatic = False - if bond_type == 'aromatic': + if bond_type == "aromatic": self.aromatic = True self.electrons = [] - self.bond_summary = '' + self.bond_summary = "" self.set_bond_summary() self.chiral = False self.chiral_dict = {} - if self.type == 'dummy': + if self.type == "dummy": self.cbond = 0.3 else: @@ -52,7 +60,7 @@ def __hash__(self): return self.nr def __repr__(self): - return f'{self.type}_{self.nr}:{self.atom_1}_{self.atom_2}' + return f"{self.type}_{self.nr}:{self.atom_1}_{self.atom_2}" def get_connected_atom(self, atom): assert atom in self.neighbours @@ -99,12 +107,12 @@ def get_neighbouring_bonds(self): def set_bond_summary(self): atom_types = sorted([atom.type for atom in self.neighbours]) - self.bond_summary = '_'.join([atom_types[0], self.type, atom_types[1]]) + self.bond_summary = "_".join([atom_types[0], self.type, atom_types[1]]) def remove_pi_bond_electrons(self): electrons_to_remove = [] for electron in self.electrons: - if electron.orbital.bonding_orbital == 'pi': + if electron.orbital.bonding_orbital == "pi": electrons_to_remove.append(electron) for electron in electrons_to_remove: @@ -117,10 +125,10 @@ def make_aromatic(self): TODO: Keep track of the ids of aromatic systems """ - if self.type == 'double': + if self.type == "double": self.remove_pi_bond_electrons() - self.type = 'aromatic' + self.type = "aromatic" self.atom_1.aromatic = True self.atom_2.aromatic = True @@ -146,13 +154,15 @@ def check_same_chirality(self, parent_bond, match): """ same_chirality = True for atom in self.chiral_dict: - if atom.type != 'H': + if atom.type != "H": parent_atom = match[atom] for atom_2 in self.chiral_dict[atom]: - if atom_2.type != 'H': + if atom_2.type != "H": parent_atom_2 = match[atom_2] orientation = self.chiral_dict[atom][atom_2] - parent_orientation = parent_bond.chiral_dict[parent_atom][parent_atom_2] + parent_orientation = parent_bond.chiral_dict[parent_atom][ + parent_atom_2 + ] if orientation != parent_orientation: same_chirality = False break @@ -167,7 +177,7 @@ def break_bond(self): Note: the products left behind will be radicals! """ - assert self.type == 'single' + assert self.type == "single" electron_1, electron_2 = self.electrons orbital_1 = electron_1.orbital @@ -193,7 +203,7 @@ def combine_hybrid_orbitals(self): s_bonding_orbital_1 = None s_bonding_orbital_2 = None - s_bonding_orbitals_1 = self.atom_1.get_hybrid_orbitals('s') + s_bonding_orbitals_1 = self.atom_1.get_hybrid_orbitals("s") for orbital in s_bonding_orbitals_1: if orbital.electron_nr == 1: s_bonding_orbital_1 = orbital @@ -201,7 +211,7 @@ def combine_hybrid_orbitals(self): if not s_bonding_orbital_1: if self.atom_1.is_promotable(): self.atom_1.promote_pi_bond_to_d_orbital() - s_bonding_orbitals_1 = self.atom_1.get_hybrid_orbitals('s') + s_bonding_orbitals_1 = self.atom_1.get_hybrid_orbitals("s") for orbital in s_bonding_orbitals_1: if orbital.electron_nr == 1: @@ -212,7 +222,7 @@ def combine_hybrid_orbitals(self): self.atom_1.valence_shell.print_shell() raise StructureError("sigma bond") - s_bonding_orbitals_2 = self.atom_2.get_hybrid_orbitals('s') + s_bonding_orbitals_2 = self.atom_2.get_hybrid_orbitals("s") for orbital in s_bonding_orbitals_2: if orbital.electron_nr == 1: s_bonding_orbital_2 = orbital @@ -220,7 +230,7 @@ def combine_hybrid_orbitals(self): if not s_bonding_orbital_2: if self.atom_2.is_promotable(): self.atom_2.promote_pi_bond_to_d_orbital() - s_bonding_orbitals_2 = self.atom_2.get_hybrid_orbitals('s') + s_bonding_orbitals_2 = self.atom_2.get_hybrid_orbitals("s") for orbital in s_bonding_orbitals_2: if orbital.electron_nr == 1: @@ -240,17 +250,17 @@ def combine_hybrid_orbitals(self): s_bonding_orbital_1.add_electron(electron_2) s_bonding_orbital_2.add_electron(electron_1) - s_bonding_orbital_1.set_bond(self, 'sigma') - s_bonding_orbital_2.set_bond(self, 'sigma') + s_bonding_orbital_1.set_bond(self, "sigma") + s_bonding_orbital_2.set_bond(self, "sigma") def make_single(self): - assert self.type == 'double' + assert self.type == "double" double_bond_electrons = [] for electron in self.electrons: - if electron.orbital.bonding_orbital == 'pi': + if electron.orbital.bonding_orbital == "pi": double_bond_electrons.append(electron) assert len(double_bond_electrons) == 2 @@ -269,11 +279,11 @@ def make_single(self): self.electrons.remove(electron_1) self.electrons.remove(electron_2) - self.type = 'single' + self.type = "single" self.set_bond_summary() def make_double(self): - assert self.type == 'single' + assert self.type == "single" electron_1 = None electron_2 = None @@ -296,19 +306,19 @@ def make_double(self): orbital_1.add_electron(electron_2) orbital_2.add_electron(electron_1) - orbital_1.set_bond(self, 'pi') - orbital_2.set_bond(self, 'pi') + orbital_1.set_bond(self, "pi") + orbital_2.set_bond(self, "pi") self.electrons.append(electron_1) self.electrons.append(electron_2) - self.type = 'double' + self.type = "double" self.atom_1.reset_hybridisation() self.atom_2.reset_hybridisation() self.atom_1.chiral = None self.atom_2.chiral = None - + self.set_bond_summary() def combine_p_orbitals(self): @@ -316,15 +326,21 @@ def combine_p_orbitals(self): Combine the electrons of two p-orbitals to form a pi-bond """ - assert self.type != 'single' + assert self.type != "single" - if self.atom_1.pyrrole or self.atom_2.pyrrole or self.atom_1.thiophene or self.atom_2.thiophene or \ - self.atom_1.furan or self.atom_2.furan: + if ( + self.atom_1.pyrrole + or self.atom_2.pyrrole + or self.atom_1.thiophene + or self.atom_2.thiophene + or self.atom_1.furan + or self.atom_2.furan + ): pass else: p_bonding_orbitals_1 = [] electrons_found = 0 - p_orbitals_1 = self.atom_1.get_orbitals('p') + p_orbitals_1 = self.atom_1.get_orbitals("p") for p_orbital in p_orbitals_1: if p_orbital.electron_nr == 1: @@ -333,12 +349,15 @@ def combine_p_orbitals(self): # Look up how many p orbitals are required for the formation of a certain type of bond - if electrons_found == BOND_PROPERTIES.bond_type_to_p_orbitals[self.type]: + if ( + electrons_found + == BOND_PROPERTIES.bond_type_to_p_orbitals[self.type] + ): break p_bonding_orbitals_2 = [] electrons_found = 0 - p_orbitals_2 = self.atom_2.get_orbitals('p') + p_orbitals_2 = self.atom_2.get_orbitals("p") for p_orbital in p_orbitals_2: if p_orbital.electron_nr == 1: @@ -347,15 +366,18 @@ def combine_p_orbitals(self): # Look up how many p orbitals are required for the formation of a certain type of bond - if electrons_found == BOND_PROPERTIES.bond_type_to_p_orbitals[self.type]: + if ( + electrons_found + == BOND_PROPERTIES.bond_type_to_p_orbitals[self.type] + ): break - + if not len(p_bonding_orbitals_1) == len(p_bonding_orbitals_2): - raise StructureError('pi bond') + raise StructureError("pi bond") - if self.type == 'aromatic': + if self.type == "aromatic": if not len(p_bonding_orbitals_1) == len(p_bonding_orbitals_2) == 1: - raise StructureError('pi bond') + raise StructureError("pi bond") else: @@ -369,8 +391,8 @@ def combine_p_orbitals(self): self.electrons.append(electron_1) self.electrons.append(electron_2) - p_bonding_orbitals_1[i].set_bond(self, 'pi') - p_bonding_orbitals_2[i].set_bond(self, 'pi') + p_bonding_orbitals_1[i].set_bond(self, "pi") + p_bonding_orbitals_2[i].set_bond(self, "pi") class BondDrawProperties: diff --git a/pikachu/chem/bond_properties.py b/pikachu/chem/bond_properties.py index 1c5e681..9755ba9 100644 --- a/pikachu/chem/bond_properties.py +++ b/pikachu/chem/bond_properties.py @@ -16,33 +16,33 @@ class BondProperties: the number of p orbitals involved in the formation of that bond """ - type_to_dash2d_input = {'single': 1, - 'double': 2, - 'triple': 3, - 'quadruple': 4} - - bond_type_to_weight = {'single': 1, - 'double': 2, - 'triple': 3, - 'quadruple': 4, - 'aromatic': 1} - - bond_type_to_symbol = {'single': '', - 'double': '=', - 'triple': '#', - 'aromatic': ''} - - bond_type_to_p_orbitals = {'single': 0, - 'double': 1, - 'triple': 2, - 'quadruple': 3, - 'aromatic': 1} - - bond_type_to_order = {'single': 1, - 'double': 2, - 'triple': 3, - 'aromatic': 4, - 'quadruple': 5} - - -BOND_PROPERTIES = BondProperties() \ No newline at end of file + type_to_dash2d_input = {"single": 1, "double": 2, "triple": 3, "quadruple": 4} + + bond_type_to_weight = { + "single": 1, + "double": 2, + "triple": 3, + "quadruple": 4, + "aromatic": 1, + } + + bond_type_to_symbol = {"single": "", "double": "=", "triple": "#", "aromatic": ""} + + bond_type_to_p_orbitals = { + "single": 0, + "double": 1, + "triple": 2, + "quadruple": 3, + "aromatic": 1, + } + + bond_type_to_order = { + "single": 1, + "double": 2, + "triple": 3, + "aromatic": 4, + "quadruple": 5, + } + + +BOND_PROPERTIES = BondProperties() diff --git a/pikachu/chem/chirality.py b/pikachu/chem/chirality.py index 2dee669..25c4f36 100644 --- a/pikachu/chem/chirality.py +++ b/pikachu/chem/chirality.py @@ -1,16 +1,18 @@ def get_chiral_permutations(order): - permutations = [tuple(order), - (order[0], order[3], order[1], order[2]), - (order[0], order[2], order[3], order[1]), - (order[1], order[0], order[3], order[2]), - (order[1], order[2], order[0], order[3]), - (order[1], order[3], order[2], order[0]), - (order[2], order[0], order[1], order[3]), - (order[2], order[3], order[0], order[1]), - (order[2], order[1], order[3], order[0]), - (order[3], order[0], order[2], order[1]), - (order[3], order[1], order[0], order[2]), - (order[3], order[2], order[1], order[0])] + permutations = [ + tuple(order), + (order[0], order[3], order[1], order[2]), + (order[0], order[2], order[3], order[1]), + (order[1], order[0], order[3], order[2]), + (order[1], order[2], order[0], order[3]), + (order[1], order[3], order[2], order[0]), + (order[2], order[0], order[1], order[3]), + (order[2], order[3], order[0], order[1]), + (order[2], order[1], order[3], order[0]), + (order[3], order[0], order[2], order[1]), + (order[3], order[1], order[0], order[2]), + (order[3], order[2], order[1], order[0]), + ] return permutations @@ -24,9 +26,11 @@ def same_chirality(order_1, order_2): def get_chiral_permutations_lonepair(order): - permutations = [tuple(order), - (order[1], order[2], order[0]), - (order[2], order[0], order[1])] + permutations = [ + tuple(order), + (order[1], order[2], order[0]), + (order[2], order[0], order[1]), + ] return permutations @@ -38,7 +42,7 @@ def find_chirality_from_nonh(neighbours, order, chirality): if tuple(permutation[:3]) == tuple(order): return chirality - if chirality == 'counterclockwise': - return 'clockwise' + if chirality == "counterclockwise": + return "clockwise" else: - return 'counterclockwise' + return "counterclockwise" diff --git a/pikachu/chem/electron.py b/pikachu/chem/electron.py index 78d2bbf..409bca0 100644 --- a/pikachu/chem/electron.py +++ b/pikachu/chem/electron.py @@ -13,16 +13,18 @@ def __init__(self, electron_id, shell_nr, orbital_type, orbital_nr, spin, atom): def __repr__(self): if self.aromatic: - aromatic_string = '*' + aromatic_string = "*" else: - aromatic_string = '' + aromatic_string = "" if self.orbital_nr: - return f'{self.atom}_{self.id}_{self.shell_nr}{self.orbital_type}{self.orbital_nr}_{self.spin}{aromatic_string}' + return f"{self.atom}_{self.id}_{self.shell_nr}{self.orbital_type}{self.orbital_nr}_{self.spin}{aromatic_string}" else: - return f'{self.atom}_{self.id}_{self.shell_nr}{self.orbital_type}_{self.spin}{aromatic_string}' + return f"{self.atom}_{self.id}_{self.shell_nr}{self.orbital_type}_{self.spin}{aromatic_string}" def __eq__(self, other): - if type(self) == type(other) and hash((self.id, self.atom.nr)) == hash((other.id, other.atom.nr)): + if type(self) == type(other) and hash((self.id, self.atom.nr)) == hash( + (other.id, other.atom.nr) + ): return True return False diff --git a/pikachu/chem/kekulisation.py b/pikachu/chem/kekulisation.py index dfdf00a..490c909 100644 --- a/pikachu/chem/kekulisation.py +++ b/pikachu/chem/kekulisation.py @@ -22,7 +22,6 @@ def set_atom(self, atom): class SuperNode(Node): - def __init__(self): Node.__init__(self) self.subnodes = [] @@ -34,15 +33,18 @@ def circle(self, node): break assert i < len(self.subnodes) - if i > 0 and self.subnodes[i].mate == self.subnodes[i - 1] or i == 0 and self.subnodes[i].mate == self.subnodes[ - -1]: + if ( + i > 0 + and self.subnodes[i].mate == self.subnodes[i - 1] + or i == 0 + and self.subnodes[i].mate == self.subnodes[-1] + ): return self.subnodes[i::-1] + self.subnodes[:i:-1] else: return self.subnodes[i::] + self.subnodes[:i] class Path: - def __init__(self): self.nodes = [] @@ -83,7 +85,6 @@ def __repr__(self): class Match: - def __init__(self, nodes): self.nodes = nodes self.freenodes = [] @@ -137,7 +138,7 @@ def find_augmenting_path(self, root): elif node.is_visited: cycle = self.find_cycles(node, cur_node) if len(cycle) % 2 == 1: - logging.debug('blossom: {}'.format(cycle)) + logging.debug("blossom: {}".format(cycle)) snode = self.shrink_blossom(cycle) self.supernodes.append(snode) for v in cycle: @@ -159,7 +160,7 @@ def find_augmenting_path(self, root): node.parent = cur_node node.mate.parent = node queue.append(node.mate) - raise Exception('cannot find an augmenting path') + raise Exception("cannot find an augmenting path") def unmatched_nodes(self): self.maximum_matching() @@ -173,12 +174,12 @@ def unmatched_nodes(self): def maximum_matching(self): while len(self.freenodes) > 0: - logging.debug('freenodes: {}'.format(self.freenodes)) + logging.debug("freenodes: {}".format(self.freenodes)) for node in self.freenodes: try: path = self.find_augmenting_path(node) - logging.debug('augmenting path: {}'.format(path.nodes)) + logging.debug("augmenting path: {}".format(path.nodes)) self.invert_path(path) self.freenodes.remove(path.nodes[0]) self.freenodes.remove(path.nodes[-1]) @@ -186,7 +187,7 @@ def maximum_matching(self): except Exception as e: logging.info(e) else: - logging.info('Tried all free nodes, no more augmenting path.') + logging.info("Tried all free nodes, no more augmenting path.") break @@ -237,7 +238,7 @@ def find_ancestors(node): i -= 1 j -= 1 - cycle = ancestors1[:i + 1] + ancestors2[j + 1::-1] + cycle = ancestors1[: i + 1] + ancestors2[j + 1 :: -1] return cycle def shrink_blossom(self, blossom): diff --git a/pikachu/chem/lone_pair.py b/pikachu/chem/lone_pair.py index ea85e07..e0d13e9 100644 --- a/pikachu/chem/lone_pair.py +++ b/pikachu/chem/lone_pair.py @@ -6,9 +6,9 @@ def __init__(self, atom, nr): def __hash__(self): return self.nr - + def __repr__(self): return f"LonePair_{self.parent.nr}_{self.nr - 10000}" - + def add_electron(self, electron): self.electrons.append(electron) diff --git a/pikachu/chem/molfile/read_molfile.py b/pikachu/chem/molfile/read_molfile.py index 9b528a4..1f327e4 100644 --- a/pikachu/chem/molfile/read_molfile.py +++ b/pikachu/chem/molfile/read_molfile.py @@ -4,22 +4,11 @@ class MolFileReader: - value_to_charge = {7: -3, - 6: -2, - 5: -1, - 0: 0, - 3: 1, - 2: 2, - 1: 3} + value_to_charge = {7: -3, 6: -2, 5: -1, 0: 0, 3: 1, 2: 2, 1: 3} - value_to_bond = {1: 'single', - 2: 'double', - 3: 'triple', - 4: 'aromatic'} + value_to_bond = {1: "single", 2: "double", 3: "triple", 4: "aromatic"} - value_to_chiral_symbol = {0: None, - 1: '/', - 6: '\\'} + value_to_chiral_symbol = {0: None, 1: "/", 6: "\\"} def __init__(self, molfile=None, molfile_str=None): # Instantiate MolFileReader with mol_file_str or mol_file_path @@ -31,7 +20,7 @@ def __init__(self, molfile=None, molfile_str=None): raise ValueError(err) # Raise error when too many arguments were given if not molfile and not molfile_str: - raise ValueError(f'{err} (both were given)') + raise ValueError(f"{err} (both were given)") self.molfile_lines = self.get_molfile_lines() self.structure = Structure() @@ -72,11 +61,11 @@ def parse_bond_info(self, bonds): def get_molfile_lines(self): # Access lines of molfile if self.molfile_str: - molfile_lines = self.molfile_str.split('\n') + molfile_lines = self.molfile_str.split("\n") else: - with open(self.molfile_path, 'r') as mol_file: + with open(self.molfile_path, "r") as mol_file: molfile_lines = mol_file.read() - molfile_lines = molfile_lines.split('\n') + molfile_lines = molfile_lines.split("\n") return molfile_lines def get_molfile_components(self): diff --git a/pikachu/chem/molfile/write_molfile.py b/pikachu/chem/molfile/write_molfile.py index cd68faf..85f22ff 100644 --- a/pikachu/chem/molfile/write_molfile.py +++ b/pikachu/chem/molfile/write_molfile.py @@ -10,22 +10,11 @@ class MolFileWriter: not be interpreted as angstrom. """ - charge_to_value = {-3: 7, - -2: 6, - -1: 5, - 0: 0, - 1: 3, - 2: 2, - 3: 1} - - bond_to_value = {'single': 1, - 'double': 2, - 'triple': 3, - 'aromatic': 4} - - chiral_symbol_to_value = {None: 0, - '/': 1, - '\\': 6} + charge_to_value = {-3: 7, -2: 6, -1: 5, 0: 0, 1: 3, 2: 2, 3: 1} + + bond_to_value = {"single": 1, "double": 2, "triple": 3, "aromatic": 4} + + chiral_symbol_to_value = {None: 0, "/": 1, "\\": 6} def __init__(self, structure, filename, drawing_options=None, multiple=False): self.original_structure = structure @@ -36,15 +25,19 @@ def __init__(self, structure, filename, drawing_options=None, multiple=False): self.drawing = Drawer(structure, coords_only=True) else: if multiple: - self.drawing = draw_multiple(structure, coords_only=True, options=drawing_options) + self.drawing = draw_multiple( + structure, coords_only=True, options=drawing_options + ) else: - self.drawing = Drawer(structure, coords_only=True, options=drawing_options) + self.drawing = Drawer( + structure, coords_only=True, options=drawing_options + ) self.drawn_structure = self.drawing.structure self.filename = filename - self.title = filename.split('.')[0] + self.title = filename.split(".")[0] self.atom_to_coords = self.get_atom_coords() self.datetime = datetime.datetime.now() - self.software_version = pkg_resources.get_distribution('pikachu-chem').version + self.software_version = pkg_resources.get_distribution("pikachu-chem").version self.atom_count = self.get_atom_count() self.bond_count, self.drawn_bonds = self.get_bond_count() @@ -63,7 +56,11 @@ def get_atom_count(self): original_atom = self.original_structure.atoms[atom.nr] if atom.draw.is_drawn: count += 1 - elif original_atom.type == 'H' and original_atom.has_neighbour('N') and original_atom.get_neighbour('N').pyrrole: + elif ( + original_atom.type == "H" + and original_atom.has_neighbour("N") + and original_atom.get_neighbour("N").pyrrole + ): count += 1 return count @@ -73,9 +70,11 @@ def get_bond_count(self): bonds = [] for bond_nr, bond in self.drawn_structure.bonds.items(): original_bond = self.original_structure.bonds[bond_nr] - if (bond.atom_1.draw.is_drawn and bond.atom_2.draw.is_drawn)\ - or (original_bond.atom_1.pyrrole and original_bond.atom_2.type == 'H')\ - or (original_bond.atom_2.pyrrole and original_bond.atom_1.type == 'H'): + if ( + (bond.atom_1.draw.is_drawn and bond.atom_2.draw.is_drawn) + or (original_bond.atom_1.pyrrole and original_bond.atom_2.type == "H") + or (original_bond.atom_2.pyrrole and original_bond.atom_1.type == "H") + ): count += 1 bonds.append(bond) @@ -83,43 +82,59 @@ def get_bond_count(self): def write_mol_file(self): atom_to_line_nr = {} - with open(self.filename, 'w') as molfile: - molfile.write(f'{self.title}\n') - molfile.write(f' PIKAChU {self.software_version} {self.datetime}\n') - molfile.write('\n') - molfile.write(f'{str(self.atom_count).rjust(3)}{str(self.bond_count).rjust(3)} 0 0 1 0 0 0 0 0999 V2000\n') + with open(self.filename, "w") as molfile: + molfile.write(f"{self.title}\n") + molfile.write(f" PIKAChU {self.software_version} {self.datetime}\n") + molfile.write("\n") + molfile.write( + f"{str(self.atom_count).rjust(3)}{str(self.bond_count).rjust(3)} 0 0 1 0 0 0 0 0999 V2000\n" + ) line_nr = 0 for atom in self.drawn_structure.graph: original_atom = self.original_structure.atoms[atom.nr] if atom.draw.is_drawn: line_nr += 1 atom_to_line_nr[atom] = line_nr - x_string = f'{atom.draw.position.x:.4f}'.rjust(10) - y_string = f'{atom.draw.position.y:.4f}'.rjust(10) - z_string = f' 0.0000' - charge_string = f'{str(self.charge_to_value[atom.charge]).rjust(3)}' + x_string = f"{atom.draw.position.x:.4f}".rjust(10) + y_string = f"{atom.draw.position.y:.4f}".rjust(10) + z_string = f" 0.0000" + charge_string = f"{str(self.charge_to_value[atom.charge]).rjust(3)}" - molfile.write(f'{x_string}{y_string}{z_string} {atom.type.ljust(3)} 0{charge_string} 0 0 0 0 0 0 0 0 0 0\n') - elif original_atom.type == 'H' and original_atom.has_neighbour('N') and original_atom.get_neighbour('N').pyrrole: + molfile.write( + f"{x_string}{y_string}{z_string} {atom.type.ljust(3)} 0{charge_string} 0 0 0 0 0 0 0 0 0 0\n" + ) + elif ( + original_atom.type == "H" + and original_atom.has_neighbour("N") + and original_atom.get_neighbour("N").pyrrole + ): line_nr += 1 atom_to_line_nr[atom] = line_nr - position = Vector.add_vectors(atom.get_neighbour('N').draw.position, Vector(0, -15)) - x_string = f'{position.x:.4f}'.rjust(10) - y_string = f'{position.y:.4f}'.rjust(10) - z_string = f' 0.0000' + position = Vector.add_vectors( + atom.get_neighbour("N").draw.position, Vector(0, -15) + ) + x_string = f"{position.x:.4f}".rjust(10) + y_string = f"{position.y:.4f}".rjust(10) + z_string = f" 0.0000" molfile.write( - f'{x_string}{y_string}{z_string} {atom.type.ljust(3)} 0{charge_string} 0 0 0 0 0 0 0 0 0 0\n') + f"{x_string}{y_string}{z_string} {atom.type.ljust(3)} 0{charge_string} 0 0 0 0 0 0 0 0 0 0\n" + ) for bond_nr, bond in self.original_structure.bonds.items(): drawn_bond = self.drawn_structure.bonds[bond_nr] chiral_val = None - if (drawn_bond.atom_1.draw.is_drawn and drawn_bond.atom_2.draw.is_drawn)\ - or (bond.atom_1.pyrrole and bond.atom_2.type == 'H')\ - or (bond.atom_2.pyrrole and bond.atom_1.type == 'H'): + if ( + ( + drawn_bond.atom_1.draw.is_drawn + and drawn_bond.atom_2.draw.is_drawn + ) + or (bond.atom_1.pyrrole and bond.atom_2.type == "H") + or (bond.atom_2.pyrrole and bond.atom_1.type == "H") + ): if drawn_bond in self.drawing.chiral_bonds: wedge, atom = self.drawing.chiral_bond_to_orientation[bond] - if wedge == 'front': + if wedge == "front": chiral_val = 1 else: chiral_val = 6 @@ -130,18 +145,26 @@ def write_mol_file(self): if chiral_val: if reverse: molfile.write( - f'{str(atom_to_line_nr[bond.atom_2]).rjust(3)}{str(atom_to_line_nr[bond.atom_1]).rjust(3)}{str(self.bond_to_value[bond.type]).rjust(3)}{str(chiral_val).rjust(3)} 0 0 0\n') + f"{str(atom_to_line_nr[bond.atom_2]).rjust(3)}{str(atom_to_line_nr[bond.atom_1]).rjust(3)}{str(self.bond_to_value[bond.type]).rjust(3)}{str(chiral_val).rjust(3)} 0 0 0\n" + ) else: molfile.write( - f'{str(atom_to_line_nr[bond.atom_1]).rjust(3)}{str(atom_to_line_nr[bond.atom_2]).rjust(3)}{str(self.bond_to_value[bond.type]).rjust(3)}{str(chiral_val).rjust(3)} 0 0 0\n') - elif bond.type == 'double' and not bond.chiral and not bond.atom_1.chiral and not bond.atom_2.chiral: + f"{str(atom_to_line_nr[bond.atom_1]).rjust(3)}{str(atom_to_line_nr[bond.atom_2]).rjust(3)}{str(self.bond_to_value[bond.type]).rjust(3)}{str(chiral_val).rjust(3)} 0 0 0\n" + ) + elif ( + bond.type == "double" + and not bond.chiral + and not bond.atom_1.chiral + and not bond.atom_2.chiral + ): molfile.write( - f'{str(atom_to_line_nr[bond.atom_1]).rjust(3)}{str(atom_to_line_nr[bond.atom_2]).rjust(3)}{str(self.bond_to_value[bond.type]).rjust(3)} 3 0 0 0\n') + f"{str(atom_to_line_nr[bond.atom_1]).rjust(3)}{str(atom_to_line_nr[bond.atom_2]).rjust(3)}{str(self.bond_to_value[bond.type]).rjust(3)} 3 0 0 0\n" + ) else: molfile.write( - f'{str(atom_to_line_nr[bond.atom_1]).rjust(3)}{str(atom_to_line_nr[bond.atom_2]).rjust(3)}{str(self.bond_to_value[bond.type]).rjust(3)} 0 0 0 0\n') - - molfile.write('M END\n') + f"{str(atom_to_line_nr[bond.atom_1]).rjust(3)}{str(atom_to_line_nr[bond.atom_2]).rjust(3)}{str(self.bond_to_value[bond.type]).rjust(3)} 0 0 0 0\n" + ) + molfile.write("M END\n") diff --git a/pikachu/chem/orbital.py b/pikachu/chem/orbital.py index 5217d1b..c2641d3 100644 --- a/pikachu/chem/orbital.py +++ b/pikachu/chem/orbital.py @@ -12,41 +12,41 @@ def __init__(self, atom, shell_nr, orbital_type): self.capacity = len(self.orbitals) * 2 def __repr__(self): - return f'{self.shell_nr}{self.orbital_type}' + return f"{self.shell_nr}{self.orbital_type}" def define_orbitals(self): - if self.orbital_type == 's': + if self.orbital_type == "s": self.append_s_orbital() - if self.orbital_type == 'p': + if self.orbital_type == "p": self.append_p_orbitals() - if self.orbital_type == 'd': + if self.orbital_type == "d": self.append_d_orbitals() - if self.orbital_type == 'f': + if self.orbital_type == "f": self.append_f_orbitals() def append_s_orbital(self): - self.orbitals.append(Orbital(self.atom, self.shell_nr, 's')) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "s")) def append_p_orbitals(self): - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'p', 1)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'p', 2)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'p', 3)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "p", 1)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "p", 2)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "p", 3)) def append_d_orbitals(self): - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'd', 1)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'd', 2)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'd', 3)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'd', 4)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'd', 5)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "d", 1)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "d", 2)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "d", 3)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "d", 4)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "d", 5)) def append_f_orbitals(self): - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'f', 1)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'f', 2)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'f', 3)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'f', 4)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'f', 5)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'f', 6)) - self.orbitals.append(Orbital(self.atom, self.shell_nr, 'f', 7)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "f", 1)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "f", 2)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "f", 3)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "f", 4)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "f", 5)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "f", 6)) + self.orbitals.append(Orbital(self.atom, self.shell_nr, "f", 7)) def fill_orbitals(self, electrons, electron_nr): while electrons > 0: @@ -60,22 +60,19 @@ def fill_orbitals(self, electrons, electron_nr): class Orbital: - subtype_dict = {'p': {1: 'x', - 2: 'y', - 3: 'z'}, - 'd': {1: 'z^2', - 2: 'zx', - 3: 'yz', - 4: 'xy', - 5: 'x^2-y^2'}, - 'f': {1: 'z^3-3/5zr^2', - 2: 'x^3-3/5xr^2', - 3: 'y^3-3/5yr^2', - 4: 'xyz', - 5: 'y(x^2-z^2)', - 6: 'x(z^2-y^2)', - 7: 'z(x^2-y^2)'} - } + subtype_dict = { + "p": {1: "x", 2: "y", 3: "z"}, + "d": {1: "z^2", 2: "zx", 3: "yz", 4: "xy", 5: "x^2-y^2"}, + "f": { + 1: "z^3-3/5zr^2", + 2: "x^3-3/5xr^2", + 3: "y^3-3/5yr^2", + 4: "xyz", + 5: "y(x^2-z^2)", + 6: "x(z^2-y^2)", + 7: "z(x^2-y^2)", + }, + } def __init__(self, atom, shell_nr, orbital_type, orbital_nr=None): self.shell_nr = shell_nr @@ -90,18 +87,18 @@ def __init__(self, atom, shell_nr, orbital_type, orbital_nr=None): def __hash__(self): if self.orbital_nr: - return f'{self.shell_nr}{self.orbital_type}{self.orbital_nr}' + return f"{self.shell_nr}{self.orbital_type}{self.orbital_nr}" else: - return f'{self.shell_nr}{self.orbital_type}' + return f"{self.shell_nr}{self.orbital_type}" def __repr__(self): if self.orbital_nr: - return f'{self.shell_nr}{self.orbital_type}{self.orbital_nr}' + return f"{self.shell_nr}{self.orbital_type}{self.orbital_nr}" else: - return f'{self.shell_nr}{self.orbital_type}' + return f"{self.shell_nr}{self.orbital_type}" def set_electron_nr(self): self.electron_nr = len(self.electrons) @@ -115,20 +112,26 @@ def remove_bond(self): self.bonding_orbital = None def fill_orbital(self, electron_id): - """ - """ + """ """ assert self.electron_nr < 2 - self.electrons.append(Electron(electron_id, self.shell_nr, self.orbital_type, - self.orbital_nr, 0.5, self.atom)) + self.electrons.append( + Electron( + electron_id, + self.shell_nr, + self.orbital_type, + self.orbital_nr, + 0.5, + self.atom, + ) + ) self.set_electron_nr() if self.electron_nr == 2: self.electrons[0].pair(self.electrons[1]) def empty_orbital(self): - """ - """ + """ """ assert self.electron_nr > 0 electron_id = self.electrons[-1].id @@ -138,7 +141,7 @@ def empty_orbital(self): if self.electron_nr == 1: self.electrons[0].unpair() - + return electron_id def add_electron(self, electron): diff --git a/pikachu/chem/rings/find_cycles.py b/pikachu/chem/rings/find_cycles.py index c7a15e3..80e11b7 100755 --- a/pikachu/chem/rings/find_cycles.py +++ b/pikachu/chem/rings/find_cycles.py @@ -58,7 +58,8 @@ def _unblock(thisnode, blocked, B): blocked.remove(node) stack.update(B[node]) B[node].clear() - G = {v: set(nbrs) for (v, nbrs) in G.items()} # make a copy of the graph + + G = {v: set(nbrs) for (v, nbrs) in G.items()} # make a copy of the graph sccs = strongly_connected_components(G) while sccs: scc = sccs.pop() @@ -68,7 +69,7 @@ def _unblock(thisnode, blocked, B): closed = set() blocked.add(startnode) B = defaultdict(set) - stack = [ (startnode, list(G[startnode])) ] + stack = [(startnode, list(G[startnode]))] while stack: thisnode, nbrs = stack[-1] if nbrs: @@ -78,13 +79,13 @@ def _unblock(thisnode, blocked, B): closed.update(path) elif nextnode not in blocked: path.append(nextnode) - stack.append( (nextnode,list(G[nextnode])) ) + stack.append((nextnode, list(G[nextnode]))) closed.discard(nextnode) blocked.add(nextnode) continue if not nbrs: if thisnode in closed: - _unblock(thisnode,blocked,B) + _unblock(thisnode, blocked, B) else: for nbr in G[thisnode]: if thisnode not in B[nbr]: @@ -107,20 +108,20 @@ def strongly_connected_components(graph): lowlink = {} index = {} result = [] - + def _strong_connect(node): index[node] = index_counter[0] lowlink[node] = index_counter[0] index_counter[0] += 1 stack.append(node) - + successors = graph[node] for successor in successors: if successor not in index: _strong_connect(successor) - lowlink[node] = min(lowlink[node],lowlink[successor]) + lowlink[node] = min(lowlink[node], lowlink[successor]) elif successor in stack: - lowlink[node] = min(lowlink[node],index[successor]) + lowlink[node] = min(lowlink[node], index[successor]) if lowlink[node] == index[node]: connected_component = [] @@ -128,13 +129,14 @@ def _strong_connect(node): while True: successor = stack.pop() connected_component.append(successor) - if successor == node: break + if successor == node: + break result.append(connected_component[:]) - + for node in graph: if node not in index: _strong_connect(node) - + return result @@ -196,7 +198,7 @@ def find_sssr(self): break if add_cycle: sssr.append(cycle) - + for atom in cycle: atoms.add(atom) @@ -215,7 +217,7 @@ def find_unique_cycles(self, structure): cycle_components = tuple(cycle_components) if len(cycle_components) < 10: unique_cycles.add(cycle_components) - + self.unique_cycles = unique_cycles def make_microcycle_graph(self): @@ -311,14 +313,14 @@ def find_start_nodes(self, paths): bond_dict: dict of {atom: remaining_bonds, ->}, with atom tuple of (str, int), with str atom type and int atom number, and remaining bonds int - + Output: start_atoms: list of [atom, ->], with each atom a tuple of (str, int), with str atom type and int atom number """ - + start_atoms = [] for path in paths: for atom in path: @@ -345,7 +347,7 @@ def find_a_path(self, start_atom): """ nodes = list(self.graph.keys()) - + current_atom = start_atom path = [current_atom] @@ -353,7 +355,6 @@ def find_a_path(self, start_atom): path = [current_atom] return path - # keep trying to extend the path until there are no bonds to traverse while True: try: @@ -379,10 +380,10 @@ def find_a_path(self, start_atom): if not self.graph[current_atom]: del self.graph[current_atom] - + except KeyError: break - + return path def remove_connectors(self): @@ -450,4 +451,3 @@ def find_new_start_node(self): start_nodes.append(atom) return start_nodes - diff --git a/pikachu/chem/rings/ring_identification.py b/pikachu/chem/rings/ring_identification.py index 1ed2d4d..ed9f220 100644 --- a/pikachu/chem/rings/ring_identification.py +++ b/pikachu/chem/rings/ring_identification.py @@ -6,22 +6,27 @@ def get_permissible_double_bond_number(aromatic_bonds): non_doubleable = set() for aromatic_bond in aromatic_bonds: for bond_1 in aromatic_bond.atom_1.bonds: - if bond_1 not in aromatic_bonds and bond_1.type == 'double': + if bond_1 not in aromatic_bonds and bond_1.type == "double": non_doubleable.add(aromatic_bond) for bond_2 in aromatic_bond.atom_2.bonds: - if bond_2 not in aromatic_bonds and bond_2.type == 'double': + if bond_2 not in aromatic_bonds and bond_2.type == "double": non_doubleable.add(aromatic_bond) - if aromatic_bond.atom_1.hybridisation == 'sp3': + if aromatic_bond.atom_1.hybridisation == "sp3": non_doubleable.add(aromatic_bond) - if aromatic_bond.atom_2.hybridisation == 'sp3': + if aromatic_bond.atom_2.hybridisation == "sp3": non_doubleable.add(aromatic_bond) permissible = list(set(aromatic_bonds) - non_doubleable) aromatic_stretches = get_neighbouring_bonds(permissible) - nr_permissible = sum([int(math.ceil(len(aromatic_stretch) / 2)) for aromatic_stretch in aromatic_stretches]) + nr_permissible = sum( + [ + int(math.ceil(len(aromatic_stretch) / 2)) + for aromatic_stretch in aromatic_stretches + ] + ) return nr_permissible @@ -32,7 +37,7 @@ def is_aromatic(atom_set): sp3 = [] for atom_1 in atom_set: - if atom_1.hybridisation == 'sp3': + if atom_1.hybridisation == "sp3": has_lone_pair = False for orbital in atom_1.valence_shell.orbitals: @@ -47,16 +52,21 @@ def is_aromatic(atom_set): for bond in atom_1.bonds: connected_atom = bond.get_connected_atom(atom_1) - if bond.type == 'double' and connected_atom not in atom_set and connected_atom.type not in {'O', 'S'} and connected_atom.charge <= 0: + if ( + bond.type == "double" + and connected_atom not in atom_set + and connected_atom.type not in {"O", "S"} + and connected_atom.charge <= 0 + ): return False for atom_2 in atom_set: if atom_1 != atom_2: bond = atom_1.get_bond(atom_2) if bond: - if bond.type == 'double': + if bond.type == "double": double_bonds.add(bond) - elif bond.type == 'aromatic': + elif bond.type == "aromatic": aromatic_bonds.add(bond) if len(aromatic_bonds) == len(atom_set): @@ -71,13 +81,13 @@ def is_aromatic(atom_set): for atom in atom_set: if atom.pyrrole: pyrroles.append(atom) - elif atom.type == 'N': + elif atom.type == "N": nitrogens.append(atom) if len(pyrroles) == 2 and len(nitrogens) == 2: pi_electrons = 18 - + if pi_electrons % 4 != 2: - raise StructureError('aromaticity') + raise StructureError("aromaticity") elif not aromatic_bonds: pi_electrons = (len(sp3) + len(double_bonds)) * 2 else: @@ -86,11 +96,20 @@ def is_aromatic(atom_set): neighbouring_aromatic_bonds = get_neighbouring_bonds(aromatic_bonds) if len(neighbouring_aromatic_bonds) == 2: - if len(neighbouring_aromatic_bonds[0]) == 1 and len(neighbouring_aromatic_bonds[1]) == 1: + if ( + len(neighbouring_aromatic_bonds[0]) == 1 + and len(neighbouring_aromatic_bonds[1]) == 1 + ): aromatic_bond_1 = neighbouring_aromatic_bonds[0][0] aromatic_bond_2 = neighbouring_aromatic_bonds[1][0] - if aromatic_bond_1.atom_1.hybridisation == aromatic_bond_1.atom_2.hybridisation == aromatic_bond_2.atom_1.hybridisation == aromatic_bond_2.atom_2.hybridisation == 'sp2': + if ( + aromatic_bond_1.atom_1.hybridisation + == aromatic_bond_1.atom_2.hybridisation + == aromatic_bond_2.atom_1.hybridisation + == aromatic_bond_2.atom_2.hybridisation + == "sp2" + ): pi_electrons = (2 + len(sp3) + len(double_bonds)) * 2 else: @@ -101,12 +120,19 @@ def is_aromatic(atom_set): elif len(neighbouring_aromatic_bonds) == 1: inaccessible_aromatic_bonds = [] for aromatic_bond in neighbouring_aromatic_bonds[0]: - if aromatic_bond.atom_1.hybridisation != 'sp2' or aromatic_bond.atom_2.hybridisation != 'sp2': + if ( + aromatic_bond.atom_1.hybridisation != "sp2" + or aromatic_bond.atom_2.hybridisation != "sp2" + ): inaccessible_aromatic_bonds.append(aromatic_bond) - bond_nr = len(neighbouring_aromatic_bonds[0]) - len(inaccessible_aromatic_bonds) + bond_nr = len(neighbouring_aromatic_bonds[0]) - len( + inaccessible_aromatic_bonds + ) - pi_electrons = (int(math.ceil(bond_nr / 2)) + len(double_bonds) + len(sp3)) * 2 + pi_electrons = ( + int(math.ceil(bond_nr / 2)) + len(double_bonds) + len(sp3) + ) * 2 else: return False @@ -165,4 +191,4 @@ def get_neighbouring_bonds(bonds): bond_group_nr = len(bond_groups) - return bond_groups \ No newline at end of file + return bond_groups diff --git a/pikachu/chem/shell.py b/pikachu/chem/shell.py index 980f1d8..880709d 100644 --- a/pikachu/chem/shell.py +++ b/pikachu/chem/shell.py @@ -4,7 +4,6 @@ class Shell: - def __init__(self, atom, shell_nr): self.shell_nr = shell_nr self.orbital_sets = {} @@ -18,34 +17,42 @@ def __init__(self, atom, shell_nr): def define_orbitals(self): self.orbitals = [] - self.orbital_sets[f'{self.shell_nr}s'] = OrbitalSet(self.atom, self.shell_nr, 's') + self.orbital_sets[f"{self.shell_nr}s"] = OrbitalSet( + self.atom, self.shell_nr, "s" + ) if self.shell_nr >= 2: - self.orbital_sets[f'{self.shell_nr}p'] = OrbitalSet(self.atom, self.shell_nr, 'p') + self.orbital_sets[f"{self.shell_nr}p"] = OrbitalSet( + self.atom, self.shell_nr, "p" + ) if self.shell_nr >= 3: - self.orbital_sets[f'{self.shell_nr}d'] = OrbitalSet(self.atom, self.shell_nr, 'd') + self.orbital_sets[f"{self.shell_nr}d"] = OrbitalSet( + self.atom, self.shell_nr, "d" + ) if self.shell_nr >= 4: - self.orbital_sets[f'{self.shell_nr}f'] = OrbitalSet(self.atom, self.shell_nr, 'f') + self.orbital_sets[f"{self.shell_nr}f"] = OrbitalSet( + self.atom, self.shell_nr, "f" + ) for orbital_set in self.orbital_sets: for orbital in self.orbital_sets[orbital_set].orbitals: self.orbitals.append(orbital) def __hash__(self): - return f'{self.atom.nr}_{self.shell_nr}' + return f"{self.atom.nr}_{self.shell_nr}" def __repr__(self): - return f'{self.atom.nr}_{self.shell_nr}' + return f"{self.atom.nr}_{self.shell_nr}" def hybridise(self, hybridisation): - if hybridisation == 'sp3': + if hybridisation == "sp3": self.sp_hybridise(3) - elif hybridisation == 'sp2': + elif hybridisation == "sp2": self.sp_hybridise(2) - elif hybridisation == 'sp': + elif hybridisation == "sp": self.sp_hybridise(1) - elif hybridisation == 'sp3d': + elif hybridisation == "sp3d": self.spd_hybridise(1) - elif hybridisation == 'sp3d2': + elif hybridisation == "sp3d2": self.spd_hybridise(2) elif hybridisation is None: pass @@ -58,7 +65,7 @@ def hybridise(self, hybridisation): def count_p_orbitals(self): count = 0 for orbital in self.orbitals: - if orbital.orbital_type == 'p': + if orbital.orbital_type == "p": count += 1 return count @@ -66,7 +73,7 @@ def count_p_orbitals(self): def count_d_orbitals(self): count = 0 for orbital in self.orbitals: - if orbital.orbital_type == 'd': + if orbital.orbital_type == "d": count += 1 return count @@ -74,9 +81,9 @@ def count_d_orbitals(self): def dehybridise(self): for orbital_set in self.orbital_sets: for i, orbital in enumerate(self.orbital_sets[orbital_set].orbitals): - if orbital.orbital_type not in {'s', 'p', 'd', 'f'}: + if orbital.orbital_type not in {"s", "p", "d", "f"}: new_orbital_type = self.orbital_sets[orbital_set].orbital_type - if new_orbital_type != 's': + if new_orbital_type != "s": new_orbital_nr = i + 1 else: new_orbital_nr = None @@ -91,20 +98,20 @@ def dehybridise(self): def sp_hybridise(self, p_nr): if p_nr == 1: - orbital_type = 'sp' + orbital_type = "sp" else: - orbital_type = f'sp{p_nr}' + orbital_type = f"sp{p_nr}" hybridised_p = 0 orbital_nr = 1 for orbital in self.orbitals: - if orbital.orbital_type == 's': + if orbital.orbital_type == "s": orbital.orbital_nr = orbital_nr orbital.orbital_type = orbital_type orbital_nr += 1 - elif orbital.orbital_type == 'p': - if not orbital.bond or orbital.bonding_orbital == 'sigma': + elif orbital.orbital_type == "p": + if not orbital.bond or orbital.bonding_orbital == "sigma": if hybridised_p < p_nr: orbital.orbital_type = orbital_type orbital.orbital_nr = orbital_nr @@ -117,21 +124,21 @@ def spd_hybridise(self, d_nr): orbital_nr = 1 if d_nr == 1: - orbital_type = 'sp3d' + orbital_type = "sp3d" else: - orbital_type = f'sp3d{d_nr}' + orbital_type = f"sp3d{d_nr}" for orbital in self.orbitals: - if orbital.orbital_type == 's': + if orbital.orbital_type == "s": orbital.orbital_type = orbital_type orbital.orbital_nr = orbital_nr orbital_nr += 1 - if orbital.orbital_type == 'p': + if orbital.orbital_type == "p": orbital.orbital_type = orbital_type orbital.orbital_nr = orbital_nr orbital_nr += 1 - elif orbital.orbital_type == 'd': - if not orbital.bond or orbital.bonding_orbital == 'sigma': + elif orbital.orbital_type == "d": + if not orbital.bond or orbital.bonding_orbital == "sigma": if hybridised_d < d_nr: orbital.orbital_type = orbital_type hybridised_d += 1 @@ -142,7 +149,7 @@ def excite(self): try: assert self.is_excitable() except AssertionError: - raise StructureError('violated_bonding_laws') + raise StructureError("violated_bonding_laws") electron_nr = self.count_electrons() electron_ids = [] @@ -221,8 +228,10 @@ def drop_electrons(self): if not orbital.electrons[0].aromatic: lone_orbitals.append(orbital) - while len(lone_orbitals) > 1 and (lone_orbitals[0].orbital_type != lone_orbitals[-1].orbital_type or - lone_orbitals[0].orbital_nr != lone_orbitals[-1].orbital_nr): + while len(lone_orbitals) > 1 and ( + lone_orbitals[0].orbital_type != lone_orbitals[-1].orbital_type + or lone_orbitals[0].orbital_nr != lone_orbitals[-1].orbital_nr + ): receiver_orbital = lone_orbitals[0] donor_orbital = lone_orbitals[-1] @@ -239,4 +248,3 @@ def print_shell(self): for orbital in self.orbitals: print(orbital) print(orbital.electrons) - \ No newline at end of file diff --git a/pikachu/chem/structure.py b/pikachu/chem/structure.py index 1bd38e6..d384ce5 100644 --- a/pikachu/chem/structure.py +++ b/pikachu/chem/structure.py @@ -8,8 +8,12 @@ from pikachu.chem.atom import Atom from pikachu.chem.bond import Bond from pikachu.chem.kekulisation import Match -from pikachu.chem.substructure_matching import check_same_chirality, compare_all_matches, SubstructureMatch, \ - find_substructures +from pikachu.chem.substructure_matching import ( + check_same_chirality, + compare_all_matches, + SubstructureMatch, + find_substructures, +) from pikachu.chem.rings.ring_identification import is_aromatic import pikachu.chem.rings.find_cycles as find_cycles from pikachu.chem.aromatic_system import AromaticSystem @@ -80,7 +84,7 @@ def set_atoms(self): self.atoms = {} for atom in self.graph: self.atoms[atom.nr] = atom - + def deepcopy(self): new_graph = {} @@ -89,14 +93,14 @@ def deepcopy(self): for atom_nr, atom in self.atoms.items(): new_atoms[atom_nr] = atom.copy() - + for atom_1, atoms in self.graph.items(): new_atom_1 = new_atoms[atom_1.nr] new_graph[new_atom_1] = [] for atom_2 in atoms: new_atom_2 = new_atoms[atom_2.nr] new_graph[new_atom_1].append(new_atom_2) - + for bond_nr, bond in self.bonds.items(): new_atom_1 = new_atoms[bond.atom_1.nr] new_atom_2 = new_atoms[bond.atom_2.nr] @@ -127,7 +131,7 @@ def deepcopy(self): new_atom_1.bonds.append(new_bond) if new_bond not in new_atom_2.bonds: new_atom_2.bonds.append(new_bond) - + new_structure = Structure(new_graph, new_bonds) new_structure.add_shells_non_hydrogens() @@ -156,10 +160,7 @@ def deepcopy(self): def get_priority_groups(self, priority_groups): ordered_priorities = sorted(priorities, reverse=True) - priority_groups = {0: [], - 1: [], - 2: [], - 3: []} + priority_groups = {0: [], 1: [], 2: [], 3: []} previous_priority = None previous_priority_idx = 0 @@ -172,7 +173,6 @@ def get_priority_groups(self, priority_groups): else: priority_groups[previous_priority_idx].append(priority) - def get_absolute_chirality(self, chiral_center): assert chiral_center.chiral @@ -184,19 +184,13 @@ def get_absolute_chirality(self, chiral_center): priorities.append((ATOM_PROPERTIES.element_to_atomic_nr[atom.type], 1)) next_atoms.append(atom) - - unresolved_priority_groups = [] - - while len(set(priorities)) != len(priorities): - for priority in priorities: pass - def copy(self): new_graph = {} new_bonds = {} @@ -253,11 +247,18 @@ def get_next_in_ring(self, ring, current_atom, previous_atom): return None - def colour_substructure_single(self, substructure, colour="hot pink", check_chiral_centres=True, - check_bond_chirality=True): - matches = self.find_substructures(substructure, - check_chiral_centres=check_chiral_centres, - check_chiral_double_bonds=check_bond_chirality) + def colour_substructure_single( + self, + substructure, + colour="hot pink", + check_chiral_centres=True, + check_bond_chirality=True, + ): + matches = self.find_substructures( + substructure, + check_chiral_centres=check_chiral_centres, + check_chiral_double_bonds=check_bond_chirality, + ) if matches: match = matches[0] @@ -266,11 +267,18 @@ def colour_substructure_single(self, substructure, colour="hot pink", check_chir if atom == parent_atom: atom.draw.colour = colour - def colour_substructure_all(self, substructure, colour="hot pink", check_chiral_centres=True, - check_bond_chirality=True): - matches = self.find_substructures(substructure, - check_chiral_centres=check_chiral_centres, - check_chiral_double_bonds=check_bond_chirality) + def colour_substructure_all( + self, + substructure, + colour="hot pink", + check_chiral_centres=True, + check_bond_chirality=True, + ): + matches = self.find_substructures( + substructure, + check_chiral_centres=check_chiral_centres, + check_chiral_double_bonds=check_bond_chirality, + ) for match in matches: for parent_atom in match.atoms.values(): @@ -285,19 +293,20 @@ def to_dash_molecule2d_input(self): kekulised_structure = self.kekulise() for atom in kekulised_structure.graph: - atom_dict = {'id': atom.nr, - 'atom': atom.type} + atom_dict = {"id": atom.nr, "atom": atom.type} nodes.append(atom_dict) for bond_nr, bond in kekulised_structure.bonds.items(): - assert bond.type != 'aromatic' - bond_dict = {'id': bond_nr, - 'source': bond.atom_1.nr, - 'target': bond.atom_2.nr, - 'bond': BOND_PROPERTIES.bond_type_to_weight[bond.type]} + assert bond.type != "aromatic" + bond_dict = { + "id": bond_nr, + "source": bond.atom_1.nr, + "target": bond.atom_2.nr, + "bond": BOND_PROPERTIES.bond_type_to_weight[bond.type], + } links.append(bond_dict) - dash_molecule2d_input = {'nodes': nodes, 'links': links} + dash_molecule2d_input = {"nodes": nodes, "links": links} return dash_molecule2d_input def get_drawn_atoms(self): @@ -322,13 +331,16 @@ def set_double_bond_chirality(self): # iterate over all double bonds - if bond.type == 'double' or bond.type == 'triple': + if bond.type == "double" or bond.type == "triple": # double bonds neighboured by three bonds on each atom, e.g. a C=C bond - if len(bond.atom_1.bonds) + len(bond.atom_1.lone_pairs) == 3 and \ - len(bond.atom_2.bonds) + len(bond.atom_2.lone_pairs) == 3 and \ - len(bond.atom_1.lone_pairs) < 2 and len(bond.atom_2.lone_pairs) < 2: + if ( + len(bond.atom_1.bonds) + len(bond.atom_1.lone_pairs) == 3 + and len(bond.atom_2.bonds) + len(bond.atom_2.lone_pairs) == 3 + and len(bond.atom_1.lone_pairs) < 2 + and len(bond.atom_2.lone_pairs) < 2 + ): # define atoms adjacent to the atoms involved in the double bond # also keep track of the chiral symbol that defines these bonds @@ -352,14 +364,14 @@ def set_double_bond_chirality(self): for bond_1 in bond.atom_1.bonds: - if bond_1.type == 'single': + if bond_1.type == "single": # Looks at the bonds between the atom adjacent to the stereobond and its neighbours if bond.atom_1 == bond_1.atom_1: - if bond_1.chiral_symbol == '/': - direction = 'up' - elif bond_1.chiral_symbol == '\\': - direction = 'down' + if bond_1.chiral_symbol == "/": + direction = "up" + elif bond_1.chiral_symbol == "\\": + direction = "down" else: direction = None @@ -376,10 +388,10 @@ def set_double_bond_chirality(self): elif bond.atom_1 == bond_1.atom_2: - if bond_1.chiral_symbol == '/': - direction = 'down' - elif bond_1.chiral_symbol == '\\': - direction = 'up' + if bond_1.chiral_symbol == "/": + direction = "down" + elif bond_1.chiral_symbol == "\\": + direction = "up" else: direction = None @@ -392,12 +404,12 @@ def set_double_bond_chirality(self): for bond_2 in bond.atom_2.bonds: - if bond_2.type == 'single': + if bond_2.type == "single": if bond.atom_2 == bond_2.atom_1: - if bond_2.chiral_symbol == '/': - direction = 'up' - elif bond_2.chiral_symbol == '\\': - direction = 'down' + if bond_2.chiral_symbol == "/": + direction = "up" + elif bond_2.chiral_symbol == "\\": + direction = "down" else: direction = None @@ -409,10 +421,10 @@ def set_double_bond_chirality(self): chiral_2_2 = direction elif bond.atom_2 == bond_2.atom_2: - if bond_2.chiral_symbol == '/': - direction = 'down' - elif bond_2.chiral_symbol == '\\': - direction = 'up' + if bond_2.chiral_symbol == "/": + direction = "down" + elif bond_2.chiral_symbol == "\\": + direction = "up" else: direction = None @@ -434,9 +446,9 @@ def set_double_bond_chirality(self): if chiral_1 and chiral_2: if chiral_1_1 == chiral_1_2: - raise StructureError('chiral double bond') + raise StructureError("chiral double bond") if chiral_2_2 == chiral_2_1: - raise StructureError('chiral double bond') + raise StructureError("chiral double bond") if chiral_1_1: first_atom = atom_1_1 @@ -446,18 +458,41 @@ def set_double_bond_chirality(self): if not chiral_1_2 and type(atom_1_2) == Atom: # Make sure where chiral symbols are not defined, they are added - if (atom_1_1.nr > bond.atom_1.nr and atom_1_2.nr > bond.atom_1.nr) or \ - (atom_1_1.nr < bond.atom_1.nr and atom_1_2.nr < bond.atom_1.nr): - if self.bond_lookup[bond.atom_1][atom_1_1].chiral_symbol == '/': - self.bond_lookup[bond.atom_1][atom_1_2].chiral_symbol = '\\' + if ( + atom_1_1.nr > bond.atom_1.nr + and atom_1_2.nr > bond.atom_1.nr + ) or ( + atom_1_1.nr < bond.atom_1.nr + and atom_1_2.nr < bond.atom_1.nr + ): + if ( + self.bond_lookup[bond.atom_1][ + atom_1_1 + ].chiral_symbol + == "/" + ): + self.bond_lookup[bond.atom_1][ + atom_1_2 + ].chiral_symbol = "\\" else: - self.bond_lookup[bond.atom_1][atom_1_2].chiral_symbol = '/' + self.bond_lookup[bond.atom_1][ + atom_1_2 + ].chiral_symbol = "/" else: - if self.bond_lookup[bond.atom_1][atom_1_1].chiral_symbol == '/': - self.bond_lookup[bond.atom_1][atom_1_2].chiral_symbol = '/' + if ( + self.bond_lookup[bond.atom_1][ + atom_1_1 + ].chiral_symbol + == "/" + ): + self.bond_lookup[bond.atom_1][ + atom_1_2 + ].chiral_symbol = "/" else: - self.bond_lookup[bond.atom_1][atom_1_2].chiral_symbol = '\\' + self.bond_lookup[bond.atom_1][ + atom_1_2 + ].chiral_symbol = "\\" else: first_atom = atom_1_2 @@ -468,18 +503,41 @@ def set_double_bond_chirality(self): if type(atom_1_1) == Atom: - if (atom_1_1.nr > bond.atom_1.nr and atom_1_2.nr > bond.atom_1.nr) or \ - (atom_1_1.nr < bond.atom_1.nr and atom_1_2.nr < bond.atom_1.nr): - if self.bond_lookup[bond.atom_1][atom_1_2].chiral_symbol == '/': - self.bond_lookup[bond.atom_1][atom_1_1].chiral_symbol = '\\' + if ( + atom_1_1.nr > bond.atom_1.nr + and atom_1_2.nr > bond.atom_1.nr + ) or ( + atom_1_1.nr < bond.atom_1.nr + and atom_1_2.nr < bond.atom_1.nr + ): + if ( + self.bond_lookup[bond.atom_1][ + atom_1_2 + ].chiral_symbol + == "/" + ): + self.bond_lookup[bond.atom_1][ + atom_1_1 + ].chiral_symbol = "\\" else: - self.bond_lookup[bond.atom_1][atom_1_1].chiral_symbol = '/' + self.bond_lookup[bond.atom_1][ + atom_1_1 + ].chiral_symbol = "/" else: - if self.bond_lookup[bond.atom_1][atom_1_2].chiral_symbol == '/': - self.bond_lookup[bond.atom_1][atom_1_1].chiral_symbol = '/' + if ( + self.bond_lookup[bond.atom_1][ + atom_1_2 + ].chiral_symbol + == "/" + ): + self.bond_lookup[bond.atom_1][ + atom_1_1 + ].chiral_symbol = "/" else: - self.bond_lookup[bond.atom_1][atom_1_1].chiral_symbol = '\\' + self.bond_lookup[bond.atom_1][ + atom_1_1 + ].chiral_symbol = "\\" if chiral_2_1: second_atom = atom_2_1 @@ -490,18 +548,41 @@ def set_double_bond_chirality(self): # Make sure where chiral symbols are not defined, they are added - if (atom_2_1.nr > bond.atom_2.nr and atom_2_2.nr > bond.atom_2.nr) or \ - (atom_2_1.nr < bond.atom_2.nr and atom_2_2.nr < bond.atom_2.nr): - if self.bond_lookup[bond.atom_2][atom_2_1].chiral_symbol == '/': - self.bond_lookup[bond.atom_2][atom_2_2].chiral_symbol = '\\' + if ( + atom_2_1.nr > bond.atom_2.nr + and atom_2_2.nr > bond.atom_2.nr + ) or ( + atom_2_1.nr < bond.atom_2.nr + and atom_2_2.nr < bond.atom_2.nr + ): + if ( + self.bond_lookup[bond.atom_2][ + atom_2_1 + ].chiral_symbol + == "/" + ): + self.bond_lookup[bond.atom_2][ + atom_2_2 + ].chiral_symbol = "\\" else: - self.bond_lookup[bond.atom_2][atom_2_2].chiral_symbol = '/' + self.bond_lookup[bond.atom_2][ + atom_2_2 + ].chiral_symbol = "/" else: - if self.bond_lookup[bond.atom_2][atom_2_1].chiral_symbol == '/': - self.bond_lookup[bond.atom_2][atom_2_2].chiral_symbol = '/' + if ( + self.bond_lookup[bond.atom_2][ + atom_2_1 + ].chiral_symbol + == "/" + ): + self.bond_lookup[bond.atom_2][ + atom_2_2 + ].chiral_symbol = "/" else: - self.bond_lookup[bond.atom_2][atom_2_2].chiral_symbol = '\\' + self.bond_lookup[bond.atom_2][ + atom_2_2 + ].chiral_symbol = "\\" else: second_atom = atom_2_2 @@ -511,18 +592,41 @@ def set_double_bond_chirality(self): # Make sure where chiral symbols are not defined, they are added if type(atom_2_1) == Atom: - if (atom_2_1.nr > bond.atom_2.nr and atom_2_2.nr > bond.atom_2.nr) or \ - (atom_2_1.nr < bond.atom_2.nr and atom_2_2.nr < bond.atom_2.nr): - if self.bond_lookup[bond.atom_2][atom_2_2].chiral_symbol == '/': - self.bond_lookup[bond.atom_2][atom_2_1].chiral_symbol = '\\' + if ( + atom_2_1.nr > bond.atom_2.nr + and atom_2_2.nr > bond.atom_2.nr + ) or ( + atom_2_1.nr < bond.atom_2.nr + and atom_2_2.nr < bond.atom_2.nr + ): + if ( + self.bond_lookup[bond.atom_2][ + atom_2_2 + ].chiral_symbol + == "/" + ): + self.bond_lookup[bond.atom_2][ + atom_2_1 + ].chiral_symbol = "\\" else: - self.bond_lookup[bond.atom_2][atom_2_1].chiral_symbol = '/' + self.bond_lookup[bond.atom_2][ + atom_2_1 + ].chiral_symbol = "/" else: - if self.bond_lookup[bond.atom_2][atom_2_2].chiral_symbol == '/': - self.bond_lookup[bond.atom_2][atom_2_1].chiral_symbol = '/' + if ( + self.bond_lookup[bond.atom_2][ + atom_2_2 + ].chiral_symbol + == "/" + ): + self.bond_lookup[bond.atom_2][ + atom_2_1 + ].chiral_symbol = "/" else: - self.bond_lookup[bond.atom_2][atom_2_1].chiral_symbol = '\\' + self.bond_lookup[bond.atom_2][ + atom_2_1 + ].chiral_symbol = "\\" if type(first_atom) == Atom: bond.chiral_dict[first_atom] = {} @@ -535,37 +639,71 @@ def set_double_bond_chirality(self): if first_chiral_symbol == second_chiral_symbol: if type(first_atom) == Atom and type(second_atom) == Atom: - bond.chiral_dict[first_atom][second_atom] = 'cis' - bond.chiral_dict[second_atom][first_atom] = 'cis' - - if type(first_other_atom) == Atom and type(second_other_atom) == Atom: - bond.chiral_dict[first_other_atom][second_other_atom] = 'cis' - bond.chiral_dict[second_other_atom][first_other_atom] = 'cis' - - if type(first_atom) == Atom and type(second_other_atom) == Atom: - bond.chiral_dict[first_atom][second_other_atom] = 'trans' - bond.chiral_dict[second_other_atom][first_atom] = 'trans' - - if type(first_other_atom) == Atom and type(second_atom) == Atom: - bond.chiral_dict[first_other_atom][second_atom] = 'trans' - bond.chiral_dict[second_atom][first_other_atom] = 'trans' + bond.chiral_dict[first_atom][second_atom] = "cis" + bond.chiral_dict[second_atom][first_atom] = "cis" + + if ( + type(first_other_atom) == Atom + and type(second_other_atom) == Atom + ): + bond.chiral_dict[first_other_atom][ + second_other_atom + ] = "cis" + bond.chiral_dict[second_other_atom][ + first_other_atom + ] = "cis" + + if ( + type(first_atom) == Atom + and type(second_other_atom) == Atom + ): + bond.chiral_dict[first_atom][ + second_other_atom + ] = "trans" + bond.chiral_dict[second_other_atom][ + first_atom + ] = "trans" + + if ( + type(first_other_atom) == Atom + and type(second_atom) == Atom + ): + bond.chiral_dict[first_other_atom][ + second_atom + ] = "trans" + bond.chiral_dict[second_atom][ + first_other_atom + ] = "trans" else: if type(first_atom) == Atom and type(second_atom) == Atom: - bond.chiral_dict[first_atom][second_atom] = 'trans' - bond.chiral_dict[second_atom][first_atom] = 'trans' - - if type(first_other_atom) == Atom and type(second_other_atom) == Atom: - bond.chiral_dict[first_other_atom][second_other_atom] = 'trans' - bond.chiral_dict[second_other_atom][first_other_atom] = 'trans' - - if type(first_atom) == Atom and type(second_other_atom) == Atom: - bond.chiral_dict[first_atom][second_other_atom] = 'cis' - bond.chiral_dict[second_other_atom][first_atom] = 'cis' - - if type(first_other_atom) == Atom and type(second_atom) == Atom: - bond.chiral_dict[first_other_atom][second_atom] = 'cis' - bond.chiral_dict[second_atom][first_other_atom] = 'cis' + bond.chiral_dict[first_atom][second_atom] = "trans" + bond.chiral_dict[second_atom][first_atom] = "trans" + + if ( + type(first_other_atom) == Atom + and type(second_other_atom) == Atom + ): + bond.chiral_dict[first_other_atom][ + second_other_atom + ] = "trans" + bond.chiral_dict[second_other_atom][ + first_other_atom + ] = "trans" + + if ( + type(first_atom) == Atom + and type(second_other_atom) == Atom + ): + bond.chiral_dict[first_atom][second_other_atom] = "cis" + bond.chiral_dict[second_other_atom][first_atom] = "cis" + + if ( + type(first_other_atom) == Atom + and type(second_atom) == Atom + ): + bond.chiral_dict[first_other_atom][second_atom] = "cis" + bond.chiral_dict[second_atom][first_other_atom] = "cis" bond.chiral = True @@ -727,13 +865,13 @@ def find_cycles(self): def promote_lone_pairs_in_aromatic_cycles(cycles): for cycle in cycles: for atom in cycle: - if atom.hybridisation == 'sp3': + if atom.hybridisation == "sp3": atom.promote_lone_pair_to_p_orbital() - if atom.type == 'N': + if atom.type == "N": atom.pyrrole = True - elif atom.type == 'S': + elif atom.type == "S": atom.thiophene = True - elif atom.type == 'O': + elif atom.type == "O": atom.furan = True def get_bounding_box(self): @@ -752,7 +890,7 @@ def get_bounding_box(self): min_y = atom.draw.position.y if atom.draw.position.y > max_y: max_y = atom.draw.position.y - + return min_x, min_y, max_x, max_y def find_aromatic_cycles(self): @@ -794,7 +932,9 @@ def find_aromatic_systems(self): previous_system_nr = -1 current_system_nr = 1 - aromatic_systems = [list(aromatic_cycle) for aromatic_cycle in self.aromatic_cycles] + aromatic_systems = [ + list(aromatic_cycle) for aromatic_cycle in self.aromatic_cycles + ] while current_system_nr != previous_system_nr: previous_system_nr = current_system_nr @@ -818,7 +958,7 @@ def find_aromatic_systems(self): aromatic_systems.pop(index) aromatic_systems.append(new_system) - + current_system_nr = len(aromatic_systems) aromatic_ring_systems = [] @@ -832,7 +972,7 @@ def find_aromatic_systems(self): def find_double_bond_sequences(self): double_bond_fragments = [] for bond in self.bonds.values(): - if bond.type == 'single': + if bond.type == "single": stereobond_1 = None stereobond_2 = None for bond_1 in bond.atom_1.bonds: @@ -864,9 +1004,13 @@ def find_double_bond_sequences(self): if fragment_1[-1] == fragment_2[0]: new_fragment = fragment_1[:] + fragment_2[1:] elif fragment_1[-1] == fragment_2[-1]: - new_fragment = fragment_1[:] + list(reversed(fragment_2[:-1])) + new_fragment = fragment_1[:] + list( + reversed(fragment_2[:-1]) + ) elif fragment_1[0] == fragment_2[0]: - new_fragment = list(reversed(fragment_2[1:])) + fragment_1[:] + new_fragment = ( + list(reversed(fragment_2[1:])) + fragment_1[:] + ) elif fragment_1[0] == fragment_2[-1]: new_fragment = fragment_2[:-1] + fragment_1[:] @@ -895,12 +1039,11 @@ def make_cycle_aromatic(cycle): for atom_2 in cycle: if atom_1 != atom_2: bond = atom_1.get_bond(atom_2) - if bond and bond.type != 'aromatic': + if bond and bond.type != "aromatic": bond.make_aromatic() def refine_structure(self): - """ - """ + """ """ self.add_shells() self.add_hydrogens() @@ -1025,7 +1168,7 @@ def remove_atom(self, atom_to_remove): def set_connectivities(self): for atom in self.graph: - if atom.type != 'H': + if atom.type != "H": atom.set_connectivity() def set_atom_neighbours(self): @@ -1035,7 +1178,7 @@ def set_atom_neighbours(self): def get_connectivities(self): connectivities = {} for atom in self.graph: - if atom.type != 'H': + if atom.type != "H": connectivity = atom.connectivity if connectivity not in connectivities: connectivities[connectivity] = [] @@ -1067,7 +1210,9 @@ def check_chiral_double_bonds(self, child, match): chirality_matches = False break else: - matching_chirality = chiral_bond.check_same_chirality(parent_bond, match) + matching_chirality = chiral_bond.check_same_chirality( + parent_bond, match + ) if not matching_chirality: chirality_matches = False break @@ -1083,7 +1228,9 @@ def check_chiral_centres(child, match): parent_atom = match[chiral_centre] if parent_atom.chiral: - chirality_matches = check_same_chirality(chiral_centre, parent_atom, match) + chirality_matches = check_same_chirality( + chiral_centre, parent_atom, match + ) if not chirality_matches: break else: @@ -1098,12 +1245,12 @@ def is_substructure_bond_composition(self, substructure): for bond_nr, bond in self.bonds: bond_summary = bond.bond_summary - if 'H' not in [atom.type for atom in bond.neighbours]: + if "H" not in [atom.type for atom in bond.neighbours]: if bond_summary not in bond_summary_to_count_parent: bond_summary_to_count_parent[bond_summary] = 0 bond_summary_to_count_parent[bond_summary] += 1 for bond_nr, bond in substructure.bonds: - if 'H' not in [atom.type for atom in bond.neighbours]: + if "H" not in [atom.type for atom in bond.neighbours]: bond_summary = bond.bond_summary if bond_summary not in bond_summary_to_count_child: bond_summary_to_count_child[bond_summary] = 0 @@ -1121,7 +1268,9 @@ def is_substructure_bond_composition(self, substructure): return can_be_substructure - def find_substructures(self, substructure, check_chiral_centres=True, check_chiral_double_bonds=True): + def find_substructures( + self, substructure, check_chiral_centres=True, check_chiral_double_bonds=True + ): matches = [] if self.is_substructure_atom_composition(substructure): if self.is_substructure_atom_connectivity(substructure): @@ -1173,7 +1322,7 @@ def is_substructure_atom_composition(self, child): def get_connectivity_counts(self): connectivities = {} for atom in self.graph: - if atom.type != 'H': + if atom.type != "H": if atom.type not in connectivities: connectivities[atom.type] = {} connectivity = atom.connectivity @@ -1190,7 +1339,9 @@ def get_substructure_connectivity_counts(self, atom_connectivities_child): for connectivity in atom_connectivities_child[atom_type]: substructure_connectivity_counts[atom_type][connectivity] = 0 for atom in self.graph: - if atom.type == atom_type and atom.potential_same_connectivity(connectivity): + if atom.type == atom_type and atom.potential_same_connectivity( + connectivity + ): substructure_connectivity_counts[atom_type][connectivity] += 1 return substructure_connectivity_counts @@ -1200,7 +1351,9 @@ def get_substructure_connectivities(self, atom_connectivities_child): for substructure_connectivity in atom_connectivities_child: substructure_connectivities[substructure_connectivity] = [] for atom in self.graph: - if atom.type != 'H' and atom.potential_same_connectivity(substructure_connectivity): + if atom.type != "H" and atom.potential_same_connectivity( + substructure_connectivity + ): substructure_connectivities[substructure_connectivity].append(atom) return substructure_connectivities @@ -1208,14 +1361,18 @@ def get_substructure_connectivities(self, atom_connectivities_child): def is_substructure_atom_connectivity(self, child): atom_connectivities_child = child.get_connectivity_counts() - atom_connectivities_self = self.get_substructure_connectivity_counts(atom_connectivities_child) + atom_connectivities_self = self.get_substructure_connectivity_counts( + atom_connectivities_child + ) can_be_substructure = True for atom_type in atom_connectivities_child: for connectivity in atom_connectivities_child[atom_type]: connectivity_nr_self = atom_connectivities_self[atom_type][connectivity] - connectivity_nr_child = atom_connectivities_child[atom_type][connectivity] + connectivity_nr_child = atom_connectivities_child[atom_type][ + connectivity + ] if connectivity_nr_child > connectivity_nr_self: can_be_substructure = False break @@ -1226,10 +1383,10 @@ def make_bond_dict(self): bond_dict = {} for atom in self.graph: - if atom.type != 'H': + if atom.type != "H": bond_dict[atom] = 0 for neighbour in atom.neighbours: - if neighbour.type != 'H': + if neighbour.type != "H": bond_dict[atom] += 1 return bond_dict @@ -1240,7 +1397,7 @@ def drop_electrons(self): def add_shells_non_hydrogens(self): for atom in self.graph: - if atom.type != 'H': + if atom.type != "H": atom.add_electron_shells() def add_shells(self): @@ -1279,13 +1436,13 @@ def add_hydrogens(self): for i in range(hydrogens_to_add): max_atom_nr += 1 max_bond_nr += 1 - hydrogen = Atom('H', max_atom_nr, None, 0, False) - self.add_bond(atom, hydrogen, 'single', max_bond_nr) + hydrogen = Atom("H", max_atom_nr, None, 0, False) + self.add_bond(atom, hydrogen, "single", max_bond_nr) def form_pi_bonds(self): for bond_nr in self.bonds: bond = self.bonds[bond_nr] - if bond.type != 'single': + if bond.type != "single": bond.combine_p_orbitals() def form_sigma_bonds(self): @@ -1295,7 +1452,7 @@ def form_sigma_bonds(self): def get_atom_counts(self): atom_counts = {} for atom in self.graph: - if atom.type != 'H': + if atom.type != "H": if atom.type not in atom_counts: atom_counts[atom.type] = 0 atom_counts[atom.type] += 1 @@ -1312,9 +1469,9 @@ def sort_by_nr(self): def make_dummy_bond(self, atom_1, atom_2, bond_nr, dummy=False): if dummy: - bond_type = 'dummy' + bond_type = "dummy" else: - bond_type = 'single' + bond_type = "single" if atom_1 in self.graph: self.graph[atom_1].append(atom_2) @@ -1343,7 +1500,7 @@ def make_dummy_bond(self, atom_1, atom_2, bond_nr, dummy=False): def make_bond(self, atom_1, atom_2, bond_nr): - bond = Bond(atom_1, atom_2, 'single', bond_nr) + bond = Bond(atom_1, atom_2, "single", bond_nr) electron_1 = None electron_2 = None @@ -1366,8 +1523,8 @@ def make_bond(self, atom_1, atom_2, bond_nr): orbital_1.add_electron(electron_2) orbital_2.add_electron(electron_1) - orbital_1.set_bond(bond, 'sigma') - orbital_2.set_bond(bond, 'sigma') + orbital_1.set_bond(bond, "sigma") + orbital_2.set_bond(bond, "sigma") atom_1.add_bond(bond) atom_2.add_bond(bond) @@ -1449,7 +1606,7 @@ def reset_attributes(self, annotations, defaults=None, boolean=False): default = None atom.annotations.set_annotation(annotation, default) - + def add_attributes(self, annotations, defaults=None, boolean=False): if defaults: assert len(defaults) == len(annotations) @@ -1503,7 +1660,7 @@ def find_pi_subgraph(self, prune=True): pi_subgraph = {} for bond in self.bonds.values(): - if bond.type == 'aromatic': + if bond.type == "aromatic": # prune the subgraph as kekulisation can only occur in atoms # that have an unpaired electron @@ -1511,10 +1668,16 @@ def find_pi_subgraph(self, prune=True): unpaired_electrons_1 = 0 unpaired_electrons_2 = 0 - if len(bond.aromatic_system.get_contributed_electrons(bond.atom_1)) == 1: + if ( + len(bond.aromatic_system.get_contributed_electrons(bond.atom_1)) + == 1 + ): unpaired_electrons_1 += 1 - if len(bond.aromatic_system.get_contributed_electrons(bond.atom_2)) == 1: + if ( + len(bond.aromatic_system.get_contributed_electrons(bond.atom_2)) + == 1 + ): unpaired_electrons_2 += 1 if unpaired_electrons_1 and unpaired_electrons_2: @@ -1557,13 +1720,17 @@ def kekulise(self): single_bond_pairs = set() for node in matching.nodes: - double_bond_pair = tuple(sorted([node.atom, node.mate.atom], key=lambda x: x.nr)) + double_bond_pair = tuple( + sorted([node.atom, node.mate.atom], key=lambda x: x.nr) + ) if double_bond_pair not in double_bond_pairs: double_bond_pairs.add(double_bond_pair) for neighbour in node.neighbors: if neighbour.index != node.mate.index: - single_bond_pair = tuple(sorted([node.atom, neighbour.atom], key=lambda x: x.nr)) + single_bond_pair = tuple( + sorted([node.atom, neighbour.atom], key=lambda x: x.nr) + ) if single_bond_pair not in single_bond_pairs: single_bond_pairs.add(single_bond_pair) @@ -1572,7 +1739,9 @@ def kekulise(self): for atom in aromatic_unmatched: for neighbour in atom.neighbours: if neighbour in atom.aromatic_system.atoms: - single_bond_pair = tuple(sorted([atom, neighbour], key=lambda x: x.nr)) + single_bond_pair = tuple( + sorted([atom, neighbour], key=lambda x: x.nr) + ) if single_bond_pair not in single_bond_pairs: single_bond_pairs.add(single_bond_pair) @@ -1583,31 +1752,34 @@ def kekulise(self): new_atom_1 = kekule_structure.atoms[pair[0].nr] new_atom_2 = kekule_structure.atoms[pair[1].nr] - + bond = kekule_structure.bond_lookup[new_atom_1][new_atom_2] - bond.type = 'double' + bond.type = "double" bond.aromatic = False - + bond.atom_1.aromatic = False bond.atom_2.aromatic = False bond.set_bond_summary() - - orbitals_1 = new_atom_1.get_orbitals('p') - orbitals_2 = new_atom_2.get_orbitals('p') - + + orbitals_1 = new_atom_1.get_orbitals("p") + orbitals_2 = new_atom_2.get_orbitals("p") + if orbitals_1 and orbitals_2: orbital_1 = orbitals_1[0] orbital_2 = orbitals_2[0] - - if not len(orbital_1.electrons) == 1 or not len(orbital_2.electrons) == 1: + + if ( + not len(orbital_1.electrons) == 1 + or not len(orbital_2.electrons) == 1 + ): raise KekulisationError(bond.aromatic_system.__repr__()) orbital_1.add_electron(orbital_2.electrons[0]) orbital_2.add_electron(orbital_1.electrons[0]) - orbital_1.set_bond(bond, 'pi') - orbital_2.set_bond(bond, 'pi') + orbital_1.set_bond(bond, "pi") + orbital_2.set_bond(bond, "pi") bond.electrons.append(orbital_1.electrons[0]) bond.electrons.append(orbital_2.electrons[0]) @@ -1620,7 +1792,7 @@ def kekulise(self): new_atom_2 = kekule_structure.atoms[pair[1].nr] bond = kekule_structure.bond_lookup[new_atom_1][new_atom_2] - bond.type = 'single' + bond.type = "single" bond.aromatic = False bond.atom_1.aromatic = False @@ -1789,8 +1961,7 @@ def get_bond_nr(self, atom_1, atom_2): return bond_nr def make_bond_nr_dict(self): - """ - """ + """ """ self.bond_nr_dict = {} for atom, neighbours in self.graph.items(): self.bond_nr_dict[atom] = len(neighbours) diff --git a/pikachu/chem/substructure_matching.py b/pikachu/chem/substructure_matching.py index 3771374..5af9941 100644 --- a/pikachu/chem/substructure_matching.py +++ b/pikachu/chem/substructure_matching.py @@ -24,15 +24,15 @@ def __init__(self, child, parent): def initialise_match(self): for atom in self.child.graph: - if atom.type != 'H': + if atom.type != "H": self.atoms[atom] = None for bond in self.child.bonds.values(): - if not bond.has_neighbour('H'): + if not bond.has_neighbour("H"): self.bonds[bond] = None for atom in self.parent.graph: - if atom.type != 'H': + if atom.type != "H": self.parent_atom_to_bonds[atom] = [] def initialise_seed(self, child_seed, parent_seed): @@ -48,15 +48,39 @@ def match(self, child_seed, parent_seed): counter += 1 next_child_bond = self.get_next_child_bond() if next_child_bond: - next_child_atom, next_parent_atom, next_parent_bond = self.find_next_matching_atoms(next_child_bond) + ( + next_child_atom, + next_parent_atom, + next_parent_bond, + ) = self.find_next_matching_atoms(next_child_bond) if next_child_atom and next_parent_atom and next_parent_bond: - self.add_match(next_child_atom, next_parent_atom, next_child_bond, next_parent_bond) + self.add_match( + next_child_atom, + next_parent_atom, + next_child_bond, + next_parent_bond, + ) else: self.failed_attempted_paths.add(tuple(self.current_attempted_path)) - next_child_atom, next_parent_atom, next_child_bond, next_parent_bond = self.traceback() - if next_child_atom and next_parent_atom and next_child_bond and next_parent_bond: - self.add_match(next_child_atom, next_parent_atom, next_child_bond, next_parent_bond) + ( + next_child_atom, + next_parent_atom, + next_child_bond, + next_parent_bond, + ) = self.traceback() + if ( + next_child_atom + and next_parent_atom + and next_child_bond + and next_parent_bond + ): + self.add_match( + next_child_atom, + next_parent_atom, + next_child_bond, + next_parent_bond, + ) else: self.active = False else: @@ -70,7 +94,7 @@ def traceback(self): current_parent_atom = self.current_attempted_path[i] previous_parent_atom = self.current_attempted_path[i - 1] - if current_parent_atom == 'hop' or previous_parent_atom == 'hop': + if current_parent_atom == "hop" or previous_parent_atom == "hop": self.failed_attempted_paths.add(tuple(self.current_attempted_path[:i])) continue @@ -91,7 +115,9 @@ def traceback(self): if not self.child.bond_exists(current_child_atom, previous_child_atom): continue else: - next_child_bond = self.child.bond_lookup[current_child_atom][previous_child_atom] + next_child_bond = self.child.bond_lookup[current_child_atom][ + previous_child_atom + ] self.remove_match(current_child_atom, next_child_bond) new_path = self.current_attempted_path[:i] @@ -105,18 +131,28 @@ def traceback(self): # Make sure that we did not try this route before - if tuple(new_path + [next_parent_atom]) not in self.failed_attempted_paths: + if ( + tuple(new_path + [next_parent_atom]) + not in self.failed_attempted_paths + ): self.current_child_atom = previous_child_atom self.current_parent_atom = previous_parent_atom next_parent_bond = bond self.current_attempted_path = new_path - return next_child_atom, next_parent_atom, next_child_bond, next_parent_bond + return ( + next_child_atom, + next_parent_atom, + next_child_bond, + next_parent_bond, + ) self.failed_attempted_paths.add(tuple(new_path)) return None, None, None, None - def add_match(self, next_child_atom, next_parent_atom, next_child_bond, next_parent_bond): + def add_match( + self, next_child_atom, next_parent_atom, next_child_bond, next_parent_bond + ): self.atoms[next_child_atom] = next_parent_atom self.bonds[next_child_bond] = next_parent_bond @@ -142,16 +178,27 @@ def find_next_matching_atoms(self, child_bond): for parent_bond in candidate_parent_bonds: if parent_bond.bond_summary == child_bond.bond_summary: next_child_atom = child_bond.get_connected_atom(self.current_child_atom) - next_parent_atom = parent_bond.get_connected_atom(self.current_parent_atom) + next_parent_atom = parent_bond.get_connected_atom( + self.current_parent_atom + ) # Make sure the atom matching is possible: either the match must already exist, or if it doesn't, # both parent atom and child atom must be free to match to one another. - if self.atoms[next_child_atom] == next_parent_atom or \ - (not self.atoms[next_child_atom] and next_parent_atom not in self.atoms.values()): - - if next_parent_atom.potential_same_connectivity(next_child_atom.connectivity): - if parent_bond not in self.parent_atom_to_bonds[self.current_parent_atom]: - self.parent_atom_to_bonds[self.current_parent_atom].append(parent_bond) + if self.atoms[next_child_atom] == next_parent_atom or ( + not self.atoms[next_child_atom] + and next_parent_atom not in self.atoms.values() + ): + + if next_parent_atom.potential_same_connectivity( + next_child_atom.connectivity + ): + if ( + parent_bond + not in self.parent_atom_to_bonds[self.current_parent_atom] + ): + self.parent_atom_to_bonds[self.current_parent_atom].append( + parent_bond + ) return next_child_atom, next_parent_atom, parent_bond @@ -164,9 +211,13 @@ def get_candidate_parent_bonds(self): for parent_bond in self.current_parent_atom.bonds: # Ignore bonds that are attached to a hydrogen # Only consider bonds that haven't been matched to another bond already - if parent_bond not in self.bonds.values() and not parent_bond.has_neighbour('H'): + if parent_bond not in self.bonds.values() and not parent_bond.has_neighbour( + "H" + ): # makes sure a matching attempt wasn't made before between parent bond and child bond. - next_parent_atom = parent_bond.get_connected_atom(self.current_parent_atom) + next_parent_atom = parent_bond.get_connected_atom( + self.current_parent_atom + ) attempted_path = tuple(self.current_attempted_path + [next_parent_atom]) if attempted_path not in self.failed_attempted_paths: candidate_parent_bonds.append(parent_bond) @@ -175,7 +226,7 @@ def get_candidate_parent_bonds(self): def get_next_child_bond(self): for child_bond in self.current_child_atom.bonds: - if not child_bond.has_neighbour('H') and not self.bonds[child_bond]: + if not child_bond.has_neighbour("H") and not self.bonds[child_bond]: return child_bond # Happens if one or multiple cycles need closing somewhere. @@ -187,7 +238,7 @@ def get_next_child_bond(self): self.current_child_atom = child_bond.atom_1 self.current_parent_atom = self.atoms[self.current_child_atom] - self.current_attempted_path.append('hop') + self.current_attempted_path.append("hop") self.current_attempted_path.append(self.current_parent_atom) @@ -208,7 +259,7 @@ def get_next_child_bond(self): self.current_child_atom = child_bond.atom_2 self.current_parent_atom = self.atoms[self.current_child_atom] - self.current_attempted_path.append('hop') + self.current_attempted_path.append("hop") self.current_attempted_path.append(self.current_parent_atom) return child_bond @@ -297,9 +348,9 @@ def filter_duplicate_matches(matches): # refactor to 'filter_duplicate_matches' def check_same_chirality(atom_1, atom_2, match): equivalent_atom_list = [] for atom in atom_1.neighbours: - if atom.type == 'H': + if atom.type == "H": for atom_b in atom_2.neighbours: - if atom_b.type == 'H': + if atom_b.type == "H": equivalent_atom_list.append(atom_b) break else: @@ -313,7 +364,7 @@ def check_same_chirality(atom_1, atom_2, match): try: assert len(equivalent_atom_list) + len(lone_pairs) == 4 except AssertionError: - raise StructureError('chiral centre') + raise StructureError("chiral centre") permutation += lone_pairs @@ -333,11 +384,14 @@ def check_same_chirality(atom_1, atom_2, match): def find_substructures(structure, child): atom_connectivities_child = child.get_connectivities() - atom_connectivities_parent = structure.get_substructure_connectivities(atom_connectivities_child) + atom_connectivities_parent = structure.get_substructure_connectivities( + atom_connectivities_child + ) # Sort based on the complexity of the connectivity - connectivities = sorted(list(atom_connectivities_child.keys()), - key=lambda x: len(set(x)), reverse=True) + connectivities = sorted( + list(atom_connectivities_child.keys()), key=lambda x: len(set(x)), reverse=True + ) starting_connectivity = connectivities[0] diff --git a/pikachu/drawing/colours.py b/pikachu/drawing/colours.py index 340b3d1..cd9606a 100644 --- a/pikachu/drawing/colours.py +++ b/pikachu/drawing/colours.py @@ -1,7 +1,7 @@ from pikachu.errors import ColourError -BLACK = '#000000' -WHITE = '#ffffff' +BLACK = "#000000" +WHITE = "#ffffff" LIGHT_GREY = "#CFCFCF" GREY = "#B2B2B2" @@ -9,17 +9,17 @@ LIGHT_BLUE = "#8FC3FA" BLUE = "#3989DE" -DARK_BLUE = '#23268A' +DARK_BLUE = "#23268A" LIGHT_RED = "#FF7E7E" RED = "#DC3D3D" -DARK_RED = '#9B2727' +DARK_RED = "#9B2727" LIGHT_GREEN = "#63DD63" -GREEN = '#198719' +GREEN = "#198719" DARK_GREEN = "#125F12" -PINK = '#E766D0' +PINK = "#E766D0" HOT_PINK = "#D722A6" ORANGE = "#FF7400" YELLOW = "#FFD100" @@ -30,128 +30,165 @@ RASPBERRY = "#E1005F" BROWN = "#814724" -RANDOM_PALETTE_1 = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', - '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', - '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', - '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080'] +RANDOM_PALETTE_1 = [ + "#e6194b", + "#3cb44b", + "#ffe119", + "#4363d8", + "#f58231", + "#911eb4", + "#46f0f0", + "#f032e6", + "#bcf60c", + "#fabebe", + "#008080", + "#e6beff", + "#9a6324", + "#fffac8", + "#800000", + "#aaffc3", + "#808000", + "#ffd8b1", + "#000075", + "#808080", +] -RANDOM_PALETTE_2 = [BLUE, RED, GREEN, PINK, ORANGE, CYAN, PURPLE, LIME, RASPBERRY, - DARK_RED, LIGHT_GREEN, DARK_BLUE, YELLOW, HOT_PINK, LIGHT_BLUE, - TURQUOISE, LIGHT_RED, DARK_GREEN] +RANDOM_PALETTE_2 = [ + BLUE, + RED, + GREEN, + PINK, + ORANGE, + CYAN, + PURPLE, + LIME, + RASPBERRY, + DARK_RED, + LIGHT_GREEN, + DARK_BLUE, + YELLOW, + HOT_PINK, + LIGHT_BLUE, + TURQUOISE, + LIGHT_RED, + DARK_GREEN, +] + +string_to_colour = { + "Red": RED, + "red": RED, + "Blue": BLUE, + "blue": BLUE, + "Green": GREEN, + "green": GREEN, + "Grey": GREY, + "Gray": GREY, + "grey": GREY, + "gray": GREY, + "Black": BLACK, + "black": BLACK, + "White": WHITE, + "white": WHITE, + "Orange": ORANGE, + "orange": ORANGE, + "Brown": BROWN, + "brown": BROWN, + "Cyan": CYAN, + "cyan": CYAN, + "Raspberry": RASPBERRY, + "raspberry": RASPBERRY, + "Purple": PURPLE, + "purple": PURPLE, + "Pink": PINK, + "pink": PINK, + "Lime": LIME, + "lime": LIME, + "Turquoise": TURQUOISE, + "turquoise": TURQUOISE, + "Yellow": YELLOW, + "yellow": YELLOW, + "hotpink": HOT_PINK, + "hot-pink": HOT_PINK, + "hot pink": HOT_PINK, + "Hot Pink": HOT_PINK, + "hotPink": HOT_PINK, + "Hot pink": HOT_PINK, + "hot_pink": HOT_PINK, + "lightred": LIGHT_RED, + "light-red": LIGHT_RED, + "light red": LIGHT_RED, + "Light Red": LIGHT_RED, + "lightRed": LIGHT_RED, + "Light red": LIGHT_RED, + "light_red": LIGHT_RED, + "lightblue": LIGHT_BLUE, + "light-blue": LIGHT_BLUE, + "light blue": LIGHT_BLUE, + "Light Blue": LIGHT_BLUE, + "lightBlue": LIGHT_BLUE, + "Light blue": LIGHT_BLUE, + "light_blue": LIGHT_BLUE, + "lightgreen": LIGHT_GREEN, + "light-green": LIGHT_GREEN, + "light green": LIGHT_GREEN, + "Light Green": LIGHT_GREEN, + "lightGreen": LIGHT_GREEN, + "Light green": LIGHT_GREEN, + "light_green": LIGHT_GREEN, + "darkred": DARK_RED, + "dark-red": DARK_RED, + "dark red": DARK_RED, + "Dark Red": DARK_RED, + "darkRed": DARK_RED, + "Dark red": DARK_RED, + "dark_red": DARK_RED, + "darkblue": DARK_BLUE, + "dark-blue": DARK_BLUE, + "dark blue": DARK_BLUE, + "Dark Blue": DARK_BLUE, + "darkBlue": DARK_BLUE, + "Dark blue": DARK_BLUE, + "dark_blue": DARK_BLUE, + "darkgreen": DARK_GREEN, + "dark-green": DARK_GREEN, + "dark green": DARK_GREEN, + "Dark Green": DARK_GREEN, + "darkGreen": DARK_GREEN, + "Dark green": DARK_GREEN, + "dark_green": DARK_GREEN, + "lightgrey": LIGHT_GREY, + "light-grey": LIGHT_GREY, + "light grey": LIGHT_GREY, + "Light Grey": LIGHT_GREY, + "lightGrey": LIGHT_GREY, + "Light grey": LIGHT_GREY, + "light_grey": LIGHT_GREY, + "lightgray": LIGHT_GREY, + "light-gray": LIGHT_GREY, + "light gray": LIGHT_GREY, + "Light Gray": LIGHT_GREY, + "lightGray": LIGHT_GREY, + "Light gray": LIGHT_GREY, + "light_gray": LIGHT_GREY, + "darkgrey": DARK_GREY, + "dark-grey": DARK_GREY, + "dark grey": DARK_GREY, + "Dark Grey": DARK_GREY, + "darkGrey": DARK_GREY, + "Dark grey": DARK_GREY, + "dark_grey": DARK_GREY, + "darkgray": DARK_GREY, + "dark-gray": DARK_GREY, + "dark gray": DARK_GREY, + "Dark Gray": DARK_GREY, + "darkGray": DARK_GREY, + "Dark gray": DARK_GREY, + "dark_gray": DARK_GREY, +} -string_to_colour = {"Red": RED, - "red": RED, - "Blue": BLUE, - "blue": BLUE, - "Green": GREEN, - "green": GREEN, - "Grey": GREY, - "Gray": GREY, - "grey": GREY, - "gray": GREY, - "Black": BLACK, - "black": BLACK, - "White": WHITE, - "white": WHITE, - "Orange": ORANGE, - "orange": ORANGE, - "Brown": BROWN, - "brown": BROWN, - "Cyan": CYAN, - "cyan": CYAN, - "Raspberry": RASPBERRY, - "raspberry": RASPBERRY, - "Purple": PURPLE, - "purple": PURPLE, - "Pink": PINK, - "pink": PINK, - "Lime": LIME, - "lime": LIME, - "Turquoise": TURQUOISE, - "turquoise": TURQUOISE, - "Yellow": YELLOW, - "yellow": YELLOW, - "hotpink": HOT_PINK, - "hot-pink": HOT_PINK, - "hot pink": HOT_PINK, - "Hot Pink": HOT_PINK, - "hotPink": HOT_PINK, - "Hot pink": HOT_PINK, - "hot_pink": HOT_PINK, - "lightred": LIGHT_RED, - "light-red": LIGHT_RED, - "light red": LIGHT_RED, - "Light Red": LIGHT_RED, - "lightRed": LIGHT_RED, - "Light red": LIGHT_RED, - "light_red": LIGHT_RED, - "lightblue": LIGHT_BLUE, - "light-blue": LIGHT_BLUE, - "light blue": LIGHT_BLUE, - "Light Blue": LIGHT_BLUE, - "lightBlue": LIGHT_BLUE, - "Light blue": LIGHT_BLUE, - "light_blue": LIGHT_BLUE, - "lightgreen": LIGHT_GREEN, - "light-green": LIGHT_GREEN, - "light green": LIGHT_GREEN, - "Light Green": LIGHT_GREEN, - "lightGreen": LIGHT_GREEN, - "Light green": LIGHT_GREEN, - "light_green": LIGHT_GREEN, - "darkred": DARK_RED, - "dark-red": DARK_RED, - "dark red": DARK_RED, - "Dark Red": DARK_RED, - "darkRed": DARK_RED, - "Dark red": DARK_RED, - "dark_red": DARK_RED, - "darkblue": DARK_BLUE, - "dark-blue": DARK_BLUE, - "dark blue": DARK_BLUE, - "Dark Blue": DARK_BLUE, - "darkBlue": DARK_BLUE, - "Dark blue": DARK_BLUE, - "dark_blue": DARK_BLUE, - "darkgreen": DARK_GREEN, - "dark-green": DARK_GREEN, - "dark green": DARK_GREEN, - "Dark Green": DARK_GREEN, - "darkGreen": DARK_GREEN, - "Dark green": DARK_GREEN, - "dark_green": DARK_GREEN, - "lightgrey": LIGHT_GREY, - "light-grey": LIGHT_GREY, - "light grey": LIGHT_GREY, - "Light Grey": LIGHT_GREY, - "lightGrey": LIGHT_GREY, - "Light grey": LIGHT_GREY, - "light_grey": LIGHT_GREY, - "lightgray": LIGHT_GREY, - "light-gray": LIGHT_GREY, - "light gray": LIGHT_GREY, - "Light Gray": LIGHT_GREY, - "lightGray": LIGHT_GREY, - "Light gray": LIGHT_GREY, - "light_gray": LIGHT_GREY, - "darkgrey": DARK_GREY, - "dark-grey": DARK_GREY, - "dark grey": DARK_GREY, - "Dark Grey": DARK_GREY, - "darkGrey": DARK_GREY, - "Dark grey": DARK_GREY, - "dark_grey": DARK_GREY, - "darkgray": DARK_GREY, - "dark-gray": DARK_GREY, - "dark gray": DARK_GREY, - "Dark Gray": DARK_GREY, - "darkGray": DARK_GREY, - "Dark gray": DARK_GREY, - "dark_gray": DARK_GREY, - } def get_hex(colour): - if colour.startswith('#') and len(colour) == 7: + if colour.startswith("#") and len(colour) == 7: return colour elif colour in string_to_colour: return string_to_colour[colour] diff --git a/pikachu/drawing/drawing.py b/pikachu/drawing/drawing.py index 645a826..a25cf74 100644 --- a/pikachu/drawing/drawing.py +++ b/pikachu/drawing/drawing.py @@ -8,8 +8,13 @@ from io import StringIO import re -from pikachu.drawing.rings import Ring, RingOverlap, find_neighbouring_rings, rings_connected_by_bridge, \ - find_bridged_systems +from pikachu.drawing.rings import ( + Ring, + RingOverlap, + find_neighbouring_rings, + rings_connected_by_bridge, + find_bridged_systems, +) from pikachu.math_functions import Vector, Polygon, Line from pikachu.chem.chirality import get_chiral_permutations from pikachu.chem.atom_properties import ATOM_PROPERTIES @@ -26,7 +31,7 @@ def __init__(self): self.bond_thickness = 2 self.bond_length = 15 self.chiral_bond_width = self.bond_length * 0.1 - self.bond_length_squared = self.bond_length ** 2 + self.bond_length_squared = self.bond_length**2 self.short_bond_length = 0.50 self.double_bond_length = 0.80 self.bond_spacing = 0.18 * self.bond_length @@ -41,7 +46,7 @@ def __init__(self): self.kk_max_energy = 1e9 self.overlap_sensitivity = 0.10 self.overlap_resolution_iterations = 5 - self.background_color = 'white' + self.background_color = "white" self.draw_hydrogens = False self.finetune = True self.strict_mode = False @@ -52,10 +57,19 @@ def __init__(self): class KKLayout: - - def __init__(self, structure, atoms, center, start_atom, bond_length, - threshold=0.1, inner_threshold=0.1, max_iteration=2000, - max_inner_iteration=50, max_energy=1e9): + def __init__( + self, + structure, + atoms, + center, + start_atom, + bond_length, + threshold=0.1, + inner_threshold=0.1, + max_iteration=2000, + max_inner_iteration=50, + max_energy=1e9, + ): self.structure = structure self.atoms = atoms self.center = center @@ -112,8 +126,12 @@ def initialise_matrices(self): self.energy_sums_y[atom_1] = None for atom_2 in self.atoms: - self.length_matrix[atom_1][atom_2] = self.edge_strength * self.distance_matrix[atom_1][atom_2] - self.spring_strengths[atom_1][atom_2] = self.edge_strength * self.distance_matrix[atom_1][atom_2] ** -2.0 + self.length_matrix[atom_1][atom_2] = ( + self.edge_strength * self.distance_matrix[atom_1][atom_2] + ) + self.spring_strengths[atom_1][atom_2] = ( + self.edge_strength * self.distance_matrix[atom_1][atom_2] ** -2.0 + ) self.energy_matrix[atom_1][atom_2] = None for atom_1 in self.atoms: @@ -132,8 +150,18 @@ def initialise_matrices(self): denom = 1.0 / math.sqrt((ux - vx) ** 2 + (uy - vy) ** 2) - self.energy_matrix[atom_1][atom_2] = (self.spring_strengths[atom_1][atom_2] * ((ux - vx) - self.length_matrix[atom_1][atom_2] * (ux - vx) * denom), - self.spring_strengths[atom_1][atom_2] * ((uy - vy) - self.length_matrix[atom_1][atom_2] * (uy - vy) * denom)) + self.energy_matrix[atom_1][atom_2] = ( + self.spring_strengths[atom_1][atom_2] + * ( + (ux - vx) + - self.length_matrix[atom_1][atom_2] * (ux - vx) * denom + ), + self.spring_strengths[atom_1][atom_2] + * ( + (uy - vy) + - self.length_matrix[atom_1][atom_2] * (uy - vy) * denom + ), + ) self.energy_matrix[atom_2][atom_1] = self.energy_matrix[atom_1][atom_2] @@ -154,7 +182,10 @@ def get_kk_layout(self): delta = max_energy inner_iteration = 0 - while delta > self.inner_threshold and self.max_inner_iteration > inner_iteration: + while ( + delta > self.inner_threshold + and self.max_inner_iteration > inner_iteration + ): inner_iteration += 1 self.update(max_energy_atom, d_ex, d_ey) delta, d_ex, d_ey = self.energy(max_energy_atom) @@ -166,7 +197,11 @@ def get_kk_layout(self): atom.draw.force_positioned = True def energy(self, atom): - energy = [self.energy_sums_x[atom]**2 + self.energy_sums_y[atom]**2, self.energy_sums_x[atom], self.energy_sums_y[atom]] + energy = [ + self.energy_sums_x[atom] ** 2 + self.energy_sums_y[atom] ** 2, + self.energy_sums_x[atom], + self.energy_sums_y[atom], + ] return energy def highest_energy(self): @@ -248,8 +283,12 @@ def update(self, atom, d_ex, d_ey): previous_ey = self.energy_matrix[atom][atom_2][1] denom = 1.0 / math.sqrt((ux - vx) ** 2 + (uy - vy) ** 2) - dx = strengths_array[atom_2] * ((ux - vx) - lengths_array[atom_2] * (ux - vx) * denom) - dy = strengths_array[atom_2] * ((uy - vy) - lengths_array[atom_2] * (uy - vy) * denom) + dx = strengths_array[atom_2] * ( + (ux - vx) - lengths_array[atom_2] * (ux - vx) * denom + ) + dy = strengths_array[atom_2] * ( + (uy - vy) - lengths_array[atom_2] * (uy - vy) * denom + ) self.energy_matrix[atom][atom_2] = [dx, dy] @@ -270,15 +309,22 @@ def get_subgraph_distance_matrix(self, atoms): if atom_1 not in distance_matrix: distance_matrix[atom_1] = {} for atom_2 in atoms: - distance_matrix[atom_1][atom_2] = float('inf') + distance_matrix[atom_1][atom_2] = float("inf") if adjacency_matrix[atom_1][atom_2] == 1: distance_matrix[atom_1][atom_2] = 1 for atom_1 in atoms: for atom_2 in atoms: for atom_3 in atoms: - if distance_matrix[atom_2][atom_3] > distance_matrix[atom_2][atom_1] + distance_matrix[atom_1][atom_3]: - distance_matrix[atom_2][atom_3] = distance_matrix[atom_2][atom_1] + distance_matrix[atom_1][atom_3] + if ( + distance_matrix[atom_2][atom_3] + > distance_matrix[atom_2][atom_1] + + distance_matrix[atom_1][atom_3] + ): + distance_matrix[atom_2][atom_3] = ( + distance_matrix[atom_2][atom_1] + + distance_matrix[atom_1][atom_3] + ) return distance_matrix @@ -300,8 +346,13 @@ def get_subgraph_adjacency_matrix(self, atoms): class Drawer: - def __init__(self, structure: Structure, options: Union[Options, None] = None, - coords_only: bool = False, multiple: bool = False) -> None: + def __init__( + self, + structure: Structure, + options: Union[Options, None] = None, + coords_only: bool = False, + multiple: bool = False, + ) -> None: if options is None: self.options = Options() @@ -345,7 +396,7 @@ def find_shortest_path(self, atom_1: Atom, atom_2: Atom) -> List[Bond]: unvisited = set() for atom in self.structure.graph: - distances[atom] = float('inf') + distances[atom] = float("inf") previous_hop[atom] = None unvisited.add(atom) @@ -354,7 +405,7 @@ def find_shortest_path(self, atom_1: Atom, atom_2: Atom) -> List[Bond]: while unvisited: current_atom = None - minimum = float('inf') + minimum = float("inf") for atom in unvisited: dist = distances[atom] if dist < minimum: @@ -405,14 +456,16 @@ def finetune_overlap_resolution(self) -> None: average_distance = len(shortest_path) / 2 - distance_metric = abs(average_distance - distance_1) + abs(average_distance - distance_2) + distance_metric = abs(average_distance - distance_1) + abs( + average_distance - distance_2 + ) if self.bond_is_rotatable(bond): rotatable_bonds.append(bond) distances.append(distance_metric) best_bond = None - optimal_distance = float('inf') + optimal_distance = float("inf") for i, distance in enumerate(distances): if distance < optimal_distance: best_bond = rotatable_bonds[i] @@ -426,8 +479,12 @@ def finetune_overlap_resolution(self) -> None: for best_bond in best_bonds: if self.total_overlap_score > self.options.overlap_sensitivity: - subtree_size_1 = self.get_subgraph_size(best_bond.atom_1, {best_bond.atom_2}) - subtree_size_2 = self.get_subgraph_size(best_bond.atom_2, {best_bond.atom_1}) + subtree_size_1 = self.get_subgraph_size( + best_bond.atom_1, {best_bond.atom_2} + ) + subtree_size_2 = self.get_subgraph_size( + best_bond.atom_2, {best_bond.atom_1} + ) if subtree_size_1 < subtree_size_2: rotating_atom = best_bond.atom_1 @@ -441,7 +498,12 @@ def finetune_overlap_resolution(self) -> None: scores = [overlap_score] for i in range(12): - self.rotate_subtree(rotating_atom, parent_atom, math.radians(30), parent_atom.draw.position) + self.rotate_subtree( + rotating_atom, + parent_atom, + math.radians(30), + parent_atom.draw.position, + ) new_overlap_score, _, _ = self.get_overlap_score() scores.append(new_overlap_score) @@ -459,8 +521,12 @@ def finetune_overlap_resolution(self) -> None: self.total_overlap_score = best_score - self.rotate_subtree(rotating_atom, parent_atom, math.radians(30 * best_i + 1), - parent_atom.draw.position) + self.rotate_subtree( + rotating_atom, + parent_atom, + math.radians(30 * best_i + 1), + parent_atom.draw.position, + ) @staticmethod def find_ring_neighbour(atom: Atom, bond: Bond) -> Atom: @@ -469,7 +535,11 @@ def find_ring_neighbour(atom: Atom, bond: Bond) -> Atom: cyclic_neighbour = None for neighbour in atom.neighbours: - if len(set(neighbour.draw.rings).intersection(rings)) > 0 and neighbour.draw.is_drawn and neighbour != bond.get_connected_atom(atom): + if ( + len(set(neighbour.draw.rings).intersection(rings)) > 0 + and neighbour.draw.is_drawn + and neighbour != bond.get_connected_atom(atom) + ): cyclic_neighbour = neighbour break @@ -477,9 +547,9 @@ def find_ring_neighbour(atom: Atom, bond: Bond) -> Atom: return cyclic_neighbour - def find_ring_branch_to_flip(self, bond: Bond, neighbours_1: List[Atom], - neighbours_2: List[Atom]) -> Tuple[Union[None, Atom], - Union[None, List[Atom]]]: + def find_ring_branch_to_flip( + self, bond: Bond, neighbours_1: List[Atom], neighbours_2: List[Atom] + ) -> Tuple[Union[None, Atom], Union[None, List[Atom]]]: rings = set(bond.atom_1.draw.rings).intersection(set(bond.atom_2.draw.rings)) @@ -623,34 +693,58 @@ def flip_stereobond_in_ring(self, bond: Bond) -> None: if bond_2.chiral and bond_2 in self.fixed_chiral_bonds: neighbours_2_adjacent_to_fixed_stereobond = True - if not neighbours_1_adjacent_to_stereobond and not neighbours_2_adjacent_to_stereobond: + if ( + not neighbours_1_adjacent_to_stereobond + and not neighbours_2_adjacent_to_stereobond + ): - central_atom, flanking_atoms = self.find_ring_branch_to_flip(bond, neighbours_1, neighbours_2) + central_atom, flanking_atoms = self.find_ring_branch_to_flip( + bond, neighbours_1, neighbours_2 + ) if not central_atom: resolvable = False - if neighbours_1_adjacent_to_stereobond and not neighbours_2_adjacent_to_stereobond: + if ( + neighbours_1_adjacent_to_stereobond + and not neighbours_2_adjacent_to_stereobond + ): central_atom = bond.atom_2 ring_neighbour = self.find_ring_neighbour(bond.atom_2, bond) flanking_atoms = (bond.atom_1, ring_neighbour) - elif neighbours_2_adjacent_to_stereobond and not neighbours_1_adjacent_to_stereobond: + elif ( + neighbours_2_adjacent_to_stereobond + and not neighbours_1_adjacent_to_stereobond + ): central_atom = bond.atom_1 ring_neighbour = self.find_ring_neighbour(bond.atom_1, bond) flanking_atoms = (bond.atom_2, ring_neighbour) - elif neighbours_1_adjacent_to_stereobond and neighbours_2_adjacent_to_stereobond: - if neighbours_1_adjacent_to_fixed_stereobond and not neighbours_2_adjacent_to_fixed_stereobond: + elif ( + neighbours_1_adjacent_to_stereobond and neighbours_2_adjacent_to_stereobond + ): + if ( + neighbours_1_adjacent_to_fixed_stereobond + and not neighbours_2_adjacent_to_fixed_stereobond + ): central_atom = bond.atom_2 ring_neighbour = self.find_ring_neighbour(bond.atom_2, bond) flanking_atoms = (bond.atom_1, ring_neighbour) - elif neighbours_2_adjacent_to_fixed_stereobond and not neighbours_1_adjacent_to_fixed_stereobond: + elif ( + neighbours_2_adjacent_to_fixed_stereobond + and not neighbours_1_adjacent_to_fixed_stereobond + ): central_atom = bond.atom_1 ring_neighbour = self.find_ring_neighbour(bond.atom_1, bond) flanking_atoms = (bond.atom_2, ring_neighbour) - elif not neighbours_1_adjacent_to_fixed_stereobond and not neighbours_2_adjacent_to_fixed_stereobond: - central_atom, flanking_atoms = self.find_ring_branch_to_flip(bond, neighbours_1, neighbours_2) + elif ( + not neighbours_1_adjacent_to_fixed_stereobond + and not neighbours_2_adjacent_to_fixed_stereobond + ): + central_atom, flanking_atoms = self.find_ring_branch_to_flip( + bond, neighbours_1, neighbours_2 + ) if not central_atom: resolvable = False else: @@ -661,17 +755,23 @@ def flip_stereobond_in_ring(self, bond: Bond) -> None: else: if self.options.strict_mode: - raise DrawingError('chiral bond ring') + raise DrawingError("chiral bond ring") else: - print("Warning! Cis/trans stereochemistry of cyclic system incorrectly drawn.") - + print( + "Warning! Cis/trans stereochemistry of cyclic system incorrectly drawn." + ) + def flip_subtree(self, root: Atom, atom_1: Atom, atom_2: Atom) -> None: for atom in self.traverse_substructure(root, {atom_1, atom_2}): - atom.draw.position.mirror_about_line(atom_1.draw.position, atom_2.draw.position) + atom.draw.position.mirror_about_line( + atom_1.draw.position, atom_2.draw.position + ) for anchored_ring in atom.draw.anchored_rings: if anchored_ring.center: - anchored_ring.center.mirror_about_line(atom_1.draw.position, atom_2.draw.position) + anchored_ring.center.mirror_about_line( + atom_1.draw.position, atom_2.draw.position + ) def find_clashing_atoms(self) -> List[Tuple[Atom, Atom]]: clashing_atoms = [] @@ -679,7 +779,9 @@ def find_clashing_atoms(self) -> List[Tuple[Atom, Atom]]: for j in range(i + 1, len(self.drawn_atoms)): atom_2 = self.drawn_atoms[j] if not self.structure.bond_exists(atom_1, atom_2): - distance = Vector.subtract_vectors(atom_1.draw.position, atom_2.draw.position).get_squared_length() + distance = Vector.subtract_vectors( + atom_1.draw.position, atom_2.draw.position + ).get_squared_length() if distance < self.options.bond_length_squared: clashing_atoms.append((atom_1, atom_2)) @@ -687,23 +789,37 @@ def find_clashing_atoms(self) -> List[Tuple[Atom, Atom]]: def prioritise_chiral_bonds(self, chiral_center: Atom) -> List[Atom]: - subtree_1_size = self.get_subgraph_size(chiral_center.neighbours[0], {chiral_center}) - subtree_2_size = self.get_subgraph_size(chiral_center.neighbours[1], {chiral_center}) - subtree_3_size = self.get_subgraph_size(chiral_center.neighbours[2], {chiral_center}) - - sizes_and_atoms = [(subtree_1_size, chiral_center.neighbours[0]), - (subtree_2_size, chiral_center.neighbours[1]), - (subtree_3_size, chiral_center.neighbours[2])] + subtree_1_size = self.get_subgraph_size( + chiral_center.neighbours[0], {chiral_center} + ) + subtree_2_size = self.get_subgraph_size( + chiral_center.neighbours[1], {chiral_center} + ) + subtree_3_size = self.get_subgraph_size( + chiral_center.neighbours[2], {chiral_center} + ) + + sizes_and_atoms = [ + (subtree_1_size, chiral_center.neighbours[0]), + (subtree_2_size, chiral_center.neighbours[1]), + (subtree_3_size, chiral_center.neighbours[2]), + ] if len(chiral_center.neighbours) == 4: - subtree_4_size = self.get_subgraph_size(chiral_center.neighbours[3], {chiral_center}) + subtree_4_size = self.get_subgraph_size( + chiral_center.neighbours[3], {chiral_center} + ) - sizes_and_atoms = [(subtree_1_size, chiral_center.neighbours[0]), - (subtree_2_size, chiral_center.neighbours[1]), - (subtree_3_size, chiral_center.neighbours[2]), - (subtree_4_size, chiral_center.neighbours[3])] + sizes_and_atoms = [ + (subtree_1_size, chiral_center.neighbours[0]), + (subtree_2_size, chiral_center.neighbours[1]), + (subtree_3_size, chiral_center.neighbours[2]), + (subtree_4_size, chiral_center.neighbours[3]), + ] - sizes_and_atoms.sort(key=lambda x: (x[0], ATOM_PROPERTIES.element_to_atomic_nr[x[1].type])) + sizes_and_atoms.sort( + key=lambda x: (x[0], ATOM_PROPERTIES.element_to_atomic_nr[x[1].type]) + ) options_h = [] options = [] @@ -715,7 +831,7 @@ def prioritise_chiral_bonds(self, chiral_center: Atom) -> List[Atom]: non_options = [] for neighbour in [size_and_atom[1] for size_and_atom in sizes_and_atoms]: - if neighbour.type == 'H': + if neighbour.type == "H": options_h.append(neighbour) else: other_chiral_centre = False @@ -727,7 +843,10 @@ def prioritise_chiral_bonds(self, chiral_center: Atom) -> List[Atom]: in_ring = neighbour.in_ring(self.structure) - if self.structure.bond_lookup[chiral_center][neighbour].type != 'single': + if ( + self.structure.bond_lookup[chiral_center][neighbour].type + != "single" + ): non_options.append(neighbour) elif not other_chiral_centre and not in_ring and not neighbour.chiral: @@ -748,18 +867,29 @@ def prioritise_chiral_bonds(self, chiral_center: Atom) -> List[Atom]: else: backup_options_chiral_neighbour_ring.append(neighbour) - priority = options_h + options + backup_options_chiral_noring + \ - backup_options_rings + backup_options_chiral_ring + backup_options_chiral_neighbour + \ - backup_options_chiral_neighbour_ring + non_options - - if chiral_center.type == 'C': + priority = ( + options_h + + options + + backup_options_chiral_noring + + backup_options_rings + + backup_options_chiral_ring + + backup_options_chiral_neighbour + + backup_options_chiral_neighbour_ring + + non_options + ) + + if chiral_center.type == "C": assert len(priority) == 4 return priority @staticmethod - def place_eclipsed_bond(hydrogen: Atom, angles_between_lines: List[float], atom_order: List[Atom], - wedge_atom: Atom) -> None: + def place_eclipsed_bond( + hydrogen: Atom, + angles_between_lines: List[float], + atom_order: List[Atom], + wedge_atom: Atom, + ) -> None: position = None for i, angle in enumerate(angles_between_lines): @@ -784,7 +914,7 @@ def move_structure(self, x: float = 0.0, y: float = 0.0) -> None: if atom.draw.is_drawn: atom.draw.position.x += x atom.draw.position.y += y - + def determine_chirality(self, chiral_center: Atom) -> Tuple[Bond, str]: # Get all angles of all drawn atoms to the chiral center @@ -793,7 +923,9 @@ def determine_chirality(self, chiral_center: Atom) -> Tuple[Bond, str]: for neighbour in chiral_center.neighbours: if neighbour.draw.is_drawn: - angle = Vector.get_line_angle(chiral_center.draw.position, neighbour.draw.position) + angle = Vector.get_line_angle( + chiral_center.draw.position, neighbour.draw.position + ) if angle < 0: angle += 2 * math.pi angles_and_atoms.append((angle, neighbour)) @@ -820,11 +952,11 @@ def determine_chirality(self, chiral_center: Atom) -> Tuple[Bond, str]: angles_between_lines.append(angle_between_lines) priority = self.prioritise_chiral_bonds(chiral_center) - + if len(atom_order) == 3: - if chiral_center.has_neighbour('H'): - hydrogen = chiral_center.get_neighbour('H') + if chiral_center.has_neighbour("H"): + hydrogen = chiral_center.get_neighbour("H") assert not hydrogen.draw.is_drawn eclipsed_element = hydrogen wedge_atom = priority[1] @@ -836,7 +968,9 @@ def determine_chirality(self, chiral_center: Atom) -> Tuple[Bond, str]: else: raise DrawingError("chiral center") - self.place_eclipsed_bond(eclipsed_element, angles_between_lines, atom_order, wedge_atom) + self.place_eclipsed_bond( + eclipsed_element, angles_between_lines, atom_order, wedge_atom + ) else: wedge_atom = priority[0] @@ -859,14 +993,14 @@ def determine_chirality(self, chiral_center: Atom) -> Tuple[Bond, str]: if tuple(atom_order) in chiral_permutations: order_matches_chirality = True - if order_matches_chirality and chiral_center.chiral == 'counterclockwise': - wedge = 'front' - elif order_matches_chirality and chiral_center.chiral == 'clockwise': - wedge = 'back' - elif not order_matches_chirality and chiral_center.chiral == 'counterclockwise': - wedge = 'back' + if order_matches_chirality and chiral_center.chiral == "counterclockwise": + wedge = "front" + elif order_matches_chirality and chiral_center.chiral == "clockwise": + wedge = "back" + elif not order_matches_chirality and chiral_center.chiral == "counterclockwise": + wedge = "back" else: - wedge = 'front' + wedge = "front" wedge_bond = self.structure.bond_lookup[wedge_atom][chiral_center] @@ -904,9 +1038,18 @@ def move_to_positive_coords(self) -> None: x_translation = abs(min(0, min_x)) y_translation = abs(min(0, min_y)) - self.move_structure(x_translation + self.options.padding + 1, y_translation + self.options.padding + 1) - - def draw_text(self, text: Union[str, int], x: float, y: float, font_size: Union[int, None] = None) -> str: + self.move_structure( + x_translation + self.options.padding + 1, + y_translation + self.options.padding + 1, + ) + + def draw_text( + self, + text: Union[str, int], + x: float, + y: float, + font_size: Union[int, None] = None, + ) -> str: if font_size is None: font_size = self.options.svg_font_size svg_text = f"""{text}""" @@ -928,21 +1071,34 @@ def draw_svg(self, annotation: Union[None, str] = None) -> str: for bond_nr, bond in self.structure.bonds.items(): if bond.atom_1.draw.positioned and bond.atom_2.draw.positioned: - line = Line(bond.atom_1.draw.position, bond.atom_2.draw.position, bond.atom_1, bond.atom_2) + line = Line( + bond.atom_1.draw.position, + bond.atom_2.draw.position, + bond.atom_1, + bond.atom_2, + ) midpoint = line.get_midpoint() - if bond.type == 'single': + if bond.type == "single": if bond in self.chiral_bonds: - orientation, chiral_center = self.chiral_bond_to_orientation[bond] - self.draw_chiral_bond(orientation, chiral_center, line, midpoint) + orientation, chiral_center = self.chiral_bond_to_orientation[ + bond + ] + self.draw_chiral_bond( + orientation, chiral_center, line, midpoint + ) else: self.draw_halflines(line, midpoint) - elif bond.type == 'double': - if not self.is_terminal(bond.atom_1) and not self.is_terminal(bond.atom_2): + elif bond.type == "double": + if not self.is_terminal(bond.atom_1) and not self.is_terminal( + bond.atom_2 + ): self.draw_halflines(line, midpoint) - common_ring_numbers = self.get_common_rings(bond.atom_1, bond.atom_2) + common_ring_numbers = self.get_common_rings( + bond.atom_1, bond.atom_2 + ) if common_ring_numbers: common_rings = [] @@ -952,38 +1108,72 @@ def draw_svg(self, annotation: Union[None, str] = None) -> str: common_rings.sort(key=lambda x: len(x.members)) common_ring = common_rings[0] ring_centre = common_ring.center - second_line = line.double_line_towards_center(ring_centre, self.options.bond_spacing, - self.options.double_bond_length) + second_line = line.double_line_towards_center( + ring_centre, + self.options.bond_spacing, + self.options.double_bond_length, + ) second_line_midpoint = second_line.get_midpoint() - self.draw_halflines_double(second_line, second_line_midpoint) + self.draw_halflines_double( + second_line, second_line_midpoint + ) else: - bond_neighbours = bond.atom_1.drawn_neighbours + bond.atom_2.drawn_neighbours + bond_neighbours = ( + bond.atom_1.drawn_neighbours + + bond.atom_2.drawn_neighbours + ) if bond_neighbours: - vectors = [atom.draw.position for atom in bond_neighbours] + vectors = [ + atom.draw.position for atom in bond_neighbours + ] gravitational_point = Vector.get_average(vectors) - second_line = line.double_line_towards_center(gravitational_point, - self.options.bond_spacing, - self.options.double_bond_length) + second_line = line.double_line_towards_center( + gravitational_point, + self.options.bond_spacing, + self.options.double_bond_length, + ) second_line_midpoint = second_line.get_midpoint() - self.draw_halflines_double(second_line, second_line_midpoint) + self.draw_halflines_double( + second_line, second_line_midpoint + ) else: print("Shouldn't happen!") else: - if self.is_terminal(bond.atom_1) and self.is_terminal(bond.atom_2): - dummy_1 = Vector(bond.atom_1.draw.position.x + 1, bond.atom_1.draw.position.y + 1) - dummy_2 = Vector(bond.atom_1.draw.position.x - 1, bond.atom_1.draw.position.y - 1) - double_bond_line_1 = line.double_line_towards_center(dummy_1, - self.options.bond_spacing / 2.0, - self.options.double_bond_length) - double_bond_line_1_midpoint = double_bond_line_1.get_midpoint() - double_bond_line_2 = line.double_line_towards_center(dummy_2, - self.options.bond_spacing / 2.0, - self.options.double_bond_length) - double_bond_line_2_midpoint = double_bond_line_2.get_midpoint() - - self.draw_halflines_double(double_bond_line_1, double_bond_line_1_midpoint) - self.draw_halflines_double(double_bond_line_2, double_bond_line_2_midpoint) + if self.is_terminal(bond.atom_1) and self.is_terminal( + bond.atom_2 + ): + dummy_1 = Vector( + bond.atom_1.draw.position.x + 1, + bond.atom_1.draw.position.y + 1, + ) + dummy_2 = Vector( + bond.atom_1.draw.position.x - 1, + bond.atom_1.draw.position.y - 1, + ) + double_bond_line_1 = line.double_line_towards_center( + dummy_1, + self.options.bond_spacing / 2.0, + self.options.double_bond_length, + ) + double_bond_line_1_midpoint = ( + double_bond_line_1.get_midpoint() + ) + double_bond_line_2 = line.double_line_towards_center( + dummy_2, + self.options.bond_spacing / 2.0, + self.options.double_bond_length, + ) + double_bond_line_2_midpoint = ( + double_bond_line_2.get_midpoint() + ) + + self.draw_halflines_double( + double_bond_line_1, double_bond_line_1_midpoint + ) + self.draw_halflines_double( + double_bond_line_2, double_bond_line_2_midpoint + ) else: @@ -995,67 +1185,133 @@ def draw_svg(self, annotation: Union[None, str] = None) -> str: branched_atom = bond.atom_1 if len(branched_atom.drawn_neighbours) >= 3: - closest_two = self.get_sorted_distances_from_list(terminal_atom, - branched_atom.drawn_neighbours) + closest_two = self.get_sorted_distances_from_list( + terminal_atom, branched_atom.drawn_neighbours + ) closest_atom_1 = closest_two[0][1] closest_atom_2 = closest_two[1][1] - line = Line(terminal_atom.draw.position, branched_atom.draw.position, terminal_atom, - branched_atom) - - double_bond_line_1, double_bond_line_2 = line.get_perpendicular_lines( - self.options.bond_spacing / 2.0) - terminal_atom_pos_1 = double_bond_line_1.get_atom_coords(terminal_atom) - terminal_atom_pos_2 = double_bond_line_2.get_atom_coords(terminal_atom) - - closest_atom_to_pos_1 = terminal_atom_pos_1.get_closest_atom(closest_atom_1, - closest_atom_2) - closest_atom_to_pos_2 = terminal_atom_pos_2.get_closest_atom(closest_atom_1, - closest_atom_2) - - bond_1_line = Line(branched_atom.draw.position, closest_atom_to_pos_1.draw.position, - branched_atom, closest_atom_to_pos_1) - bond_2_line = Line(branched_atom.draw.position, closest_atom_to_pos_2.draw.position, - branched_atom, closest_atom_to_pos_2) - - double_bond_line_1_midpoint = double_bond_line_1.get_midpoint() - double_bond_line_2_midpoint = double_bond_line_2.get_midpoint() - - intersection_1 = double_bond_line_1.find_intersection(bond_1_line) - intersection_2 = double_bond_line_2.find_intersection(bond_2_line) - - if terminal_atom.draw.position.x > branched_atom.draw.position.x: + line = Line( + terminal_atom.draw.position, + branched_atom.draw.position, + terminal_atom, + branched_atom, + ) + + ( + double_bond_line_1, + double_bond_line_2, + ) = line.get_perpendicular_lines( + self.options.bond_spacing / 2.0 + ) + terminal_atom_pos_1 = ( + double_bond_line_1.get_atom_coords(terminal_atom) + ) + terminal_atom_pos_2 = ( + double_bond_line_2.get_atom_coords(terminal_atom) + ) + + closest_atom_to_pos_1 = ( + terminal_atom_pos_1.get_closest_atom( + closest_atom_1, closest_atom_2 + ) + ) + closest_atom_to_pos_2 = ( + terminal_atom_pos_2.get_closest_atom( + closest_atom_1, closest_atom_2 + ) + ) + + bond_1_line = Line( + branched_atom.draw.position, + closest_atom_to_pos_1.draw.position, + branched_atom, + closest_atom_to_pos_1, + ) + bond_2_line = Line( + branched_atom.draw.position, + closest_atom_to_pos_2.draw.position, + branched_atom, + closest_atom_to_pos_2, + ) + + double_bond_line_1_midpoint = ( + double_bond_line_1.get_midpoint() + ) + double_bond_line_2_midpoint = ( + double_bond_line_2.get_midpoint() + ) + + intersection_1 = double_bond_line_1.find_intersection( + bond_1_line + ) + intersection_2 = double_bond_line_2.find_intersection( + bond_2_line + ) + + if ( + terminal_atom.draw.position.x + > branched_atom.draw.position.x + ): # check for parallel lines - if intersection_1 and intersection_1.x < 100000 and intersection_1.y < 100000: + if ( + intersection_1 + and intersection_1.x < 100000 + and intersection_1.y < 100000 + ): double_bond_line_1.point_1 = intersection_1 - if intersection_2 and intersection_2.x < 100000 and intersection_2.y < 100000: + if ( + intersection_2 + and intersection_2.x < 100000 + and intersection_2.y < 100000 + ): double_bond_line_2.point_1 = intersection_2 else: # check for parallel lines - if intersection_1 and intersection_1.x < 100000 and intersection_1.y < 100000: + if ( + intersection_1 + and intersection_1.x < 100000 + and intersection_1.y < 100000 + ): double_bond_line_1.point_2 = intersection_1 - if intersection_2 and intersection_2.x < 100000 and intersection_2.y < 100000: + if ( + intersection_2 + and intersection_2.x < 100000 + and intersection_2.y < 100000 + ): double_bond_line_2.point_2 = intersection_2 - self.draw_halflines(double_bond_line_1, double_bond_line_1_midpoint) - self.draw_halflines(double_bond_line_2, double_bond_line_2_midpoint) + self.draw_halflines( + double_bond_line_1, double_bond_line_1_midpoint + ) + self.draw_halflines( + double_bond_line_2, double_bond_line_2_midpoint + ) else: self.draw_halflines(line, midpoint) - bond_neighbours = bond.atom_1.drawn_neighbours + bond.atom_2.drawn_neighbours + bond_neighbours = ( + bond.atom_1.drawn_neighbours + + bond.atom_2.drawn_neighbours + ) if bond_neighbours: - vectors = [atom.draw.position for atom in bond_neighbours] + vectors = [ + atom.draw.position for atom in bond_neighbours + ] gravitational_point = Vector.get_average(vectors) - second_line = line.get_parallel_line(gravitational_point, - self.options.bond_spacing) + second_line = line.get_parallel_line( + gravitational_point, self.options.bond_spacing + ) second_line_midpoint = second_line.get_midpoint() - self.draw_halflines(second_line, second_line_midpoint) + self.draw_halflines( + second_line, second_line_midpoint + ) else: print("Shouldn't happen!") - elif bond.type == 'triple': + elif bond.type == "triple": self.draw_halflines(line, midpoint) line_1, line_2 = line.get_parallel_lines(self.options.bond_spacing) line_1_midpoint = line_1.get_midpoint() @@ -1065,16 +1321,20 @@ def draw_svg(self, annotation: Union[None, str] = None) -> str: for atom in self.structure.graph: if atom.draw.positioned: - svg_text = '' - svg_h_text = '' - svg_charge_text = '' - svg_h_count_text = '' - - if atom.type != 'C' or atom.draw.draw_explicit or atom.charge: - if atom.type == 'C' and not atom.charge: - svg_text = self.draw_text('.', atom.draw.position.x, atom.draw.position.y - 2) + svg_text = "" + svg_h_text = "" + svg_charge_text = "" + svg_h_count_text = "" + + if atom.type != "C" or atom.draw.draw_explicit or atom.charge: + if atom.type == "C" and not atom.charge: + svg_text = self.draw_text( + ".", atom.draw.position.x, atom.draw.position.y - 2 + ) else: - svg_text = self.draw_text(atom.type, atom.draw.position.x, atom.draw.position.y) + svg_text = self.draw_text( + atom.type, atom.draw.position.x, atom.draw.position.y + ) # TODO: Make this possible in svg writing # text = self.set_r_group_indices_subscript(atom.type) @@ -1082,17 +1342,17 @@ def draw_svg(self, annotation: Union[None, str] = None) -> str: orientation = self.get_hydrogen_text_orientation(atom) # Swap up-down orientation due to swapped svg coordinate system - if orientation == 'H_below_atom': - orientation = 'H_above_atom' - elif orientation == 'H_above_atom': - orientation = 'H_below_atom' + if orientation == "H_below_atom": + orientation = "H_above_atom" + elif orientation == "H_above_atom": + orientation = "H_below_atom" - if atom.type != 'C' or atom.draw.draw_explicit or atom.charge: + if atom.type != "C" or atom.draw.draw_explicit or atom.charge: hydrogen_count = 0 for neighbour in atom.neighbours: - if neighbour.type == 'H' and not neighbour.draw.is_drawn: + if neighbour.type == "H" and not neighbour.draw.is_drawn: hydrogen_count += 1 h_x = atom.draw.position.x @@ -1102,15 +1362,15 @@ def draw_svg(self, annotation: Union[None, str] = None) -> str: charge_x = atom.draw.position.x + 6 charge_y = atom.draw.position.y - 3 - if orientation == 'H_below_atom': + if orientation == "H_below_atom": h_y = atom.draw.position.y + 7 h_subscript_x += 2 h_subscript_y += 10 - elif orientation == 'H_above_atom': + elif orientation == "H_above_atom": h_y = atom.draw.position.y - 7 h_subscript_x += 2 h_subscript_y -= 4 - elif orientation == 'H_before_atom': + elif orientation == "H_before_atom": if hydrogen_count > 1: h_x -= 10 h_subscript_x -= 5 @@ -1121,7 +1381,7 @@ def draw_svg(self, annotation: Union[None, str] = None) -> str: h_subscript_x += len(atom.type) * 6 + 5 if abs(atom.charge): - if hydrogen_count and orientation == 'H_after_atom': + if hydrogen_count and orientation == "H_after_atom": if hydrogen_count > 1: charge_x += len(atom.type) * 6 + 7 else: @@ -1132,23 +1392,31 @@ def draw_svg(self, annotation: Union[None, str] = None) -> str: charge_pos = Vector(charge_x, charge_y) if hydrogen_count: - svg_h_text = self.draw_text('H', h_pos.x, h_pos.y) + svg_h_text = self.draw_text("H", h_pos.x, h_pos.y) if hydrogen_count > 1: - svg_h_count_text = self.draw_text(hydrogen_count, h_subscript_pos.x, h_subscript_pos.y, - font_size=self.options.svg_font_size_small) + svg_h_count_text = self.draw_text( + hydrogen_count, + h_subscript_pos.x, + h_subscript_pos.y, + font_size=self.options.svg_font_size_small, + ) if atom.charge: if atom.charge > 0: - charge_symbol = '+' + charge_symbol = "+" else: - charge_symbol = '-' + charge_symbol = "-" if abs(atom.charge) > 1: charge_text = f"{abs(atom.charge)}{charge_symbol}" else: charge_text = charge_symbol - svg_charge_text = self.draw_text(charge_text, charge_pos.x, charge_pos.y, - font_size=self.options.svg_font_size_small) + svg_charge_text = self.draw_text( + charge_text, + charge_pos.x, + charge_pos.y, + font_size=self.options.svg_font_size_small, + ) if svg_text or svg_h_text or svg_charge_text or svg_h_count_text: if self.structure_id: @@ -1157,17 +1425,17 @@ def draw_svg(self, annotation: Union[None, str] = None) -> str: text_group = f'\n' if svg_text: text_group += svg_text - text_group += '\n' + text_group += "\n" if svg_h_text: text_group += svg_h_text - text_group += '\n' + text_group += "\n" if svg_charge_text: text_group += svg_charge_text - text_group += '\n' + text_group += "\n" if svg_h_count_text: text_group += svg_h_count_text - text_group += '\n' - text_group += '' + text_group += "\n" + text_group += "" self.add_svg_element(text_group, atom) svg = self.assemble_svg() @@ -1208,12 +1476,12 @@ def write_svg(self, out_file: str, annotation: Union[str, None] = None) -> None: svg_string += self.draw_svg(annotation=annotation) svg_string += "" - with open(out_file, 'w') as out: + with open(out_file, "w") as out: out.write(svg_string) def assemble_svg(self) -> str: - svg_string = '' + svg_string = "" for svg_group, atom_to_elements in self.svg_groups.items(): svg_string += f"""\n""" @@ -1222,7 +1490,7 @@ def assemble_svg(self) -> str: for element in elements: svg_string += element - svg_string += '\n' + svg_string += "\n" svg_string += "\n" svg_string += "\n" @@ -1246,28 +1514,35 @@ def draw(self, coords_only: bool = False) -> None: @staticmethod def get_hydrogen_text_orientation(atom: Atom) -> str: - four_positions = [Vector(atom.draw.position.x, atom.draw.position.y + 3), - Vector(atom.draw.position.x, atom.draw.position.y - 3), - Vector(atom.draw.position.x + 3, atom.draw.position.y), - Vector(atom.draw.position.x - 3, atom.draw.position.y)] + four_positions = [ + Vector(atom.draw.position.x, atom.draw.position.y + 3), + Vector(atom.draw.position.x, atom.draw.position.y - 3), + Vector(atom.draw.position.x + 3, atom.draw.position.y), + Vector(atom.draw.position.x - 3, atom.draw.position.y), + ] positions_to_angles = [[], [], [], []] for neighbour in atom.drawn_neighbours: for i, position in enumerate(four_positions): - angle = Vector.get_angle_between_vectors(position, neighbour.draw.position, atom.draw.position) + angle = Vector.get_angle_between_vectors( + position, neighbour.draw.position, atom.draw.position + ) positions_to_angles[i].append(angle) orientation = None if not positions_to_angles[0]: - orientation = 'H_after_atom' - elif min(positions_to_angles[2]) > 1.57078 or min(positions_to_angles[3]) > 1.57078: + orientation = "H_after_atom" + elif ( + min(positions_to_angles[2]) > 1.57078 + or min(positions_to_angles[3]) > 1.57078 + ): if min(positions_to_angles[2]) >= min(positions_to_angles[3]): - orientation = 'H_after_atom' + orientation = "H_after_atom" else: - orientation = 'H_before_atom' + orientation = "H_before_atom" else: smallest_angles = [min(angles) for angles in positions_to_angles] @@ -1281,20 +1556,22 @@ def get_hydrogen_text_orientation(atom: Atom) -> str: position = j if position == 0: - orientation = 'H_above_atom' + orientation = "H_above_atom" elif position == 1: - orientation = 'H_below_atom' + orientation = "H_below_atom" elif position == 2: - orientation = 'H_after_atom' + orientation = "H_after_atom" elif position == 3: - orientation = 'H_before_atom' + orientation = "H_before_atom" return orientation @staticmethod def in_same_ring(atom_1: Atom, atom_2: Atom) -> bool: if atom_1.draw.rings and atom_2.draw.rings: - joined_rings = list(set(atom_1.draw.rings).intersection(set(atom_2.draw.rings))) + joined_rings = list( + set(atom_1.draw.rings).intersection(set(atom_2.draw.rings)) + ) if joined_rings: return True @@ -1303,13 +1580,17 @@ def in_same_ring(atom_1: Atom, atom_2: Atom) -> bool: @staticmethod def get_common_rings(atom_1: Atom, atom_2: Atom) -> Union[List[int], None]: if atom_1.draw.rings and atom_2.draw.rings: - joined_rings = list(set(atom_1.draw.rings).intersection(set(atom_2.draw.rings))) + joined_rings = list( + set(atom_1.draw.rings).intersection(set(atom_2.draw.rings)) + ) return joined_rings return None @staticmethod - def plot_chiral_bond_front(polygon: List[Vector], ax: Axes, color: str = 'black') -> None: + def plot_chiral_bond_front( + polygon: List[Vector], ax: Axes, color: str = "black" + ) -> None: x = [] y = [] for point in polygon: @@ -1318,44 +1599,65 @@ def plot_chiral_bond_front(polygon: List[Vector], ax: Axes, color: str = 'black' ax.fill(x, y, color=color) - def plot_chiral_bond_back(self, lines: List[Line], ax: Axes, color: str = 'black') -> None: + def plot_chiral_bond_back( + self, lines: List[Line], ax: Axes, color: str = "black" + ) -> None: for line in lines: self.plot_line(line, ax, color=color) - def plot_chiral_bond(self, orientation: str, chiral_center: Atom, line: Line, ax: Axes, midpoint: Vector) -> None: + def plot_chiral_bond( + self, + orientation: str, + chiral_center: Atom, + line: Line, + ax: Axes, + midpoint: Vector, + ) -> None: halflines = line.divide_in_two(midpoint) for halfline in halflines: truncated_line = halfline.get_truncated_line(self.options.short_bond_length) - if orientation == 'front': - bond_wedge = truncated_line.get_bond_wedge_front(self.options.chiral_bond_width, chiral_center) - self.plot_chiral_bond_front(bond_wedge, ax, color=halfline.atom.draw.colour) + if orientation == "front": + bond_wedge = truncated_line.get_bond_wedge_front( + self.options.chiral_bond_width, chiral_center + ) + self.plot_chiral_bond_front( + bond_wedge, ax, color=halfline.atom.draw.colour + ) else: - bond_lines = halfline.get_bond_wedge_back(self.options.chiral_bond_width, chiral_center) - self.plot_chiral_bond_back(bond_lines, ax, color=halfline.atom.draw.colour) - - def draw_chiral_bond_front(self, polygon: List[Vector], color: str = 'black') -> str: + bond_lines = halfline.get_bond_wedge_back( + self.options.chiral_bond_width, chiral_center + ) + self.plot_chiral_bond_back( + bond_lines, ax, color=halfline.atom.draw.colour + ) + + def draw_chiral_bond_front( + self, polygon: List[Vector], color: str = "black" + ) -> str: svg_string = ' str: - svg_string = f'\n ' + def draw_chiral_bond_back(self, lines: List[Line], color: str = "black") -> str: + svg_string = ( + f'\n ' + ) line_strings = [] for line in lines: line_string = f'' line_strings.append(line_string) - line_string = '\n '.join(line_strings) + line_string = "\n ".join(line_strings) svg_string += line_string - svg_string += '\n' + svg_string += "\n" return svg_string @@ -1364,10 +1666,13 @@ def set_structure_id(self, structure_id: str) -> None: def add_svg_element(self, svg_element: str, atom: Atom) -> None: - if self.annotation is not None and self.annotation in atom.annotations.annotations: + if ( + self.annotation is not None + and self.annotation in atom.annotations.annotations + ): annotation_value = atom.annotations.get_attribute(self.annotation) else: - annotation_value = 'unlabeled' + annotation_value = "unlabeled" if self.structure_id: annotation_value = f"{annotation_value}_{self.structure_id}" @@ -1375,31 +1680,43 @@ def add_svg_element(self, svg_element: str, atom: Atom) -> None: if annotation_value not in self.svg_groups: self.svg_groups[annotation_value] = {} - atom_label = f'atom_{atom}' + atom_label = f"atom_{atom}" if self.structure_id: - atom_label = f'atom_{atom}_{self.structure_id}' + atom_label = f"atom_{atom}_{self.structure_id}" if atom_label not in self.svg_groups[annotation_value]: self.svg_groups[annotation_value][atom_label] = [] self.svg_groups[annotation_value][atom_label].append(svg_element) - def draw_chiral_bond(self, orientation: str, chiral_center: Atom, line: Line, midpoint: Vector) -> None: + def draw_chiral_bond( + self, orientation: str, chiral_center: Atom, line: Line, midpoint: Vector + ) -> None: halflines = line.divide_in_two(midpoint) for halfline in halflines: truncated_line = halfline.get_truncated_line(self.options.short_bond_length) - if orientation == 'front': - bond_wedge = truncated_line.get_bond_wedge_front(self.options.chiral_bond_width, chiral_center) - svg_polygon = self.draw_chiral_bond_front(bond_wedge, color=halfline.atom.draw.colour) + if orientation == "front": + bond_wedge = truncated_line.get_bond_wedge_front( + self.options.chiral_bond_width, chiral_center + ) + svg_polygon = self.draw_chiral_bond_front( + bond_wedge, color=halfline.atom.draw.colour + ) self.add_svg_element(svg_polygon, halfline.atom) - elif orientation == 'back': - bond_lines = halfline.get_bond_wedge_back(self.options.chiral_bond_width, chiral_center) - svg_lines = self.draw_chiral_bond_back(bond_lines, color=halfline.atom.draw.colour) + elif orientation == "back": + bond_lines = halfline.get_bond_wedge_back( + self.options.chiral_bond_width, chiral_center + ) + svg_lines = self.draw_chiral_bond_back( + bond_lines, color=halfline.atom.draw.colour + ) self.add_svg_element(svg_lines, halfline.atom) else: - raise ValueError(f"Unrecognised chiral bond orientation: {orientation}.") + raise ValueError( + f"Unrecognised chiral bond orientation: {orientation}." + ) def draw_halflines(self, line: Line, midpoint: Vector) -> None: @@ -1416,8 +1733,8 @@ def draw_halflines_double(self, line: Line, midpoint: Vector) -> None: svg_line = self.draw_line(halfline, color=halfline.atom.draw.colour) self.add_svg_element(svg_line, halfline.atom) - def draw_line(self, line: Line, color: str = 'black') -> str: - if color != 'black': + def draw_line(self, line: Line, color: str = "black") -> str: + if color != "black": svg_line = f'' else: svg_line = f'' @@ -1434,48 +1751,52 @@ def plot_halflines_double(self, line: Line, ax: Axes, midpoint: Vector) -> None: for halfline in halflines: self.plot_line(halfline, ax, color=halfline.atom.draw.colour) - def plot_line(self, line: Line, ax: Axes, color: str = 'black') -> None: - ax.plot([line.point_1.x, line.point_2.x], - [line.point_1.y, line.point_2.y], color=color, linewidth=self.options.bond_thickness) + def plot_line(self, line: Line, ax: Axes, color: str = "black") -> None: + ax.plot( + [line.point_1.x, line.point_2.x], + [line.point_1.y, line.point_2.y], + color=color, + linewidth=self.options.bond_thickness, + ) @staticmethod def get_image_as_array() -> np.ndarray: # Return image as np.ndarray that represents RGB image canvas = plt.gca().figure.canvas canvas.draw() - image = np.frombuffer(canvas.tostring_rgb(), dtype='uint8') + image = np.frombuffer(canvas.tostring_rgb(), dtype="uint8") image = image.reshape(canvas.get_width_height()[::-1] + (3,)) - plt.close('all') + plt.close("all") return image @staticmethod def save_svg(out_file: str) -> None: - if out_file.endswith('.svg'): + if out_file.endswith(".svg"): pass else: - out_file += '.svg' + out_file += ".svg" plt.savefig(out_file) plt.clf() plt.close(plt.gcf()) - plt.close('all') + plt.close("all") @staticmethod def save_svg_string() -> str: svg_string = StringIO() - plt.savefig(svg_string, format='svg') + plt.savefig(svg_string, format="svg") svg = svg_string.getvalue() plt.clf() plt.close(plt.gcf()) - plt.close('all') + plt.close("all") return svg @staticmethod def save_png(out_file: str) -> None: - if out_file.endswith('.png'): + if out_file.endswith(".png"): pass else: - out_file += '.png' + out_file += ".png" plt.savefig(out_file) plt.clf() plt.close() @@ -1498,16 +1819,20 @@ def chirality_correct(bond: Bond) -> bool: if neighbour_2 != bond.atom_1: if neighbour_1.draw.is_drawn and neighbour_2.draw.is_drawn: - placement_1 = Vector.get_position_relative_to_line(bond.atom_1.draw.position, - bond.atom_2.draw.position, - neighbour_1.draw.position) - placement_2 = Vector.get_position_relative_to_line(bond.atom_1.draw.position, - bond.atom_2.draw.position, - neighbour_2.draw.position) + placement_1 = Vector.get_position_relative_to_line( + bond.atom_1.draw.position, + bond.atom_2.draw.position, + neighbour_1.draw.position, + ) + placement_2 = Vector.get_position_relative_to_line( + bond.atom_1.draw.position, + bond.atom_2.draw.position, + neighbour_2.draw.position, + ) orientation = bond.chiral_dict[neighbour_1][neighbour_2] - if orientation == 'cis': + if orientation == "cis": if placement_1 != placement_2: must_be_fixed = True else: @@ -1521,8 +1846,16 @@ def chirality_correct(bond: Bond) -> bool: return True def fix_chiral_bond(self, double_bond: Bond) -> None: - if len(double_bond.atom_1.draw.rings) and len(double_bond.atom_2.draw.rings) and \ - len(set(double_bond.atom_1.draw.rings).intersection(set(double_bond.atom_2.draw.rings))) >= 1: + if ( + len(double_bond.atom_1.draw.rings) + and len(double_bond.atom_2.draw.rings) + and len( + set(double_bond.atom_1.draw.rings).intersection( + set(double_bond.atom_2.draw.rings) + ) + ) + >= 1 + ): self.flip_stereobond_in_ring(double_bond) else: @@ -1543,8 +1876,15 @@ def fix_chiral_bond(self, double_bond: Bond) -> None: # Only need to flip once if both neighbours are in the same ring - elif len(neighbours) == 2 and len( - set(neighbours[0].draw.rings).intersection(set(neighbours[1].draw.rings))) >= 1: + elif ( + len(neighbours) == 2 + and len( + set(neighbours[0].draw.rings).intersection( + set(neighbours[1].draw.rings) + ) + ) + >= 1 + ): self.flip_subtree(neighbours[0], root_atom, parent_atom) @@ -1569,7 +1909,11 @@ def fix_chiral_bonds_in_rings(self) -> None: self.fix_chiral_bond(double_bond) for bond in self.structure.bonds.values(): - if bond.type == 'double' and bond.chiral and bond not in self.fixed_chiral_bonds: + if ( + bond.type == "double" + and bond.chiral + and bond not in self.fixed_chiral_bonds + ): chirality_correct = self.chirality_correct(bond) if chirality_correct: self.fixed_chiral_bonds.add(bond) @@ -1599,17 +1943,22 @@ def draw_structure(self) -> None: height = max_y - min_y width = max_x - min_x - fig, ax = plt.subplots(figsize=((width + 2 * self.options.padding) / 50.0, - (height + 2 * self.options.padding) / 50.0), dpi=100) + fig, ax = plt.subplots( + figsize=( + (width + 2 * self.options.padding) / 50.0, + (height + 2 * self.options.padding) / 50.0, + ), + dpi=100, + ) - ax.set_aspect('equal', adjustable='box') - ax.axis('off') + ax.set_aspect("equal", adjustable="box") + ax.axis("off") ax.set_xlim([min_x - self.options.padding, max_x + self.options.padding]) ax.set_ylim([min_y - self.options.padding, max_y + self.options.padding]) plt.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} plt.rcParams.update(params) ring_centers_x = [] @@ -1623,20 +1972,33 @@ def draw_structure(self) -> None: for bond_nr, bond in self.structure.bonds.items(): if bond.atom_1.draw.positioned and bond.atom_2.draw.positioned: - line = Line(bond.atom_1.draw.position, bond.atom_2.draw.position, bond.atom_1, bond.atom_2) + line = Line( + bond.atom_1.draw.position, + bond.atom_2.draw.position, + bond.atom_1, + bond.atom_2, + ) midpoint = line.get_midpoint() - if bond.type == 'single': + if bond.type == "single": if bond in self.chiral_bonds: - orientation, chiral_center = self.chiral_bond_to_orientation[bond] - self.plot_chiral_bond(orientation, chiral_center, line, ax, midpoint) + orientation, chiral_center = self.chiral_bond_to_orientation[ + bond + ] + self.plot_chiral_bond( + orientation, chiral_center, line, ax, midpoint + ) else: self.plot_halflines(line, ax, midpoint) - elif bond.type == 'double': - if not self.is_terminal(bond.atom_1) and not self.is_terminal(bond.atom_2): + elif bond.type == "double": + if not self.is_terminal(bond.atom_1) and not self.is_terminal( + bond.atom_2 + ): self.plot_halflines(line, ax, midpoint) - common_ring_numbers = self.get_common_rings(bond.atom_1, bond.atom_2) + common_ring_numbers = self.get_common_rings( + bond.atom_1, bond.atom_2 + ) if common_ring_numbers: common_rings = [] @@ -1646,35 +2008,72 @@ def draw_structure(self) -> None: common_rings.sort(key=lambda x: len(x.members)) common_ring = common_rings[0] ring_centre = common_ring.center - second_line = line.double_line_towards_center(ring_centre, self.options.bond_spacing, self.options.double_bond_length) + second_line = line.double_line_towards_center( + ring_centre, + self.options.bond_spacing, + self.options.double_bond_length, + ) second_line_midpoint = second_line.get_midpoint() - self.plot_halflines_double(second_line, ax, second_line_midpoint) + self.plot_halflines_double( + second_line, ax, second_line_midpoint + ) else: - bond_neighbours = bond.atom_1.drawn_neighbours + bond.atom_2.drawn_neighbours + bond_neighbours = ( + bond.atom_1.drawn_neighbours + + bond.atom_2.drawn_neighbours + ) if bond_neighbours: - vectors = [atom.draw.position for atom in bond_neighbours] + vectors = [ + atom.draw.position for atom in bond_neighbours + ] gravitational_point = Vector.get_average(vectors) - second_line = line.double_line_towards_center(gravitational_point, self.options.bond_spacing, self.options.double_bond_length) + second_line = line.double_line_towards_center( + gravitational_point, + self.options.bond_spacing, + self.options.double_bond_length, + ) second_line_midpoint = second_line.get_midpoint() - self.plot_halflines_double(second_line, ax, second_line_midpoint) + self.plot_halflines_double( + second_line, ax, second_line_midpoint + ) else: print("Shouldn't happen!") else: - if self.is_terminal(bond.atom_1) and self.is_terminal(bond.atom_2): - dummy_1 = Vector(bond.atom_1.draw.position.x + 1, bond.atom_1.draw.position.y + 1) - dummy_2 = Vector(bond.atom_1.draw.position.x - 1, bond.atom_1.draw.position.y - 1) - double_bond_line_1 = line.double_line_towards_center(dummy_1, - self.options.bond_spacing / 2.0, - self.options.double_bond_length) - double_bond_line_1_midpoint = double_bond_line_1.get_midpoint() - double_bond_line_2 = line.double_line_towards_center(dummy_2, - self.options.bond_spacing / 2.0, - self.options.double_bond_length) - double_bond_line_2_midpoint = double_bond_line_2.get_midpoint() - - self.plot_halflines_double(double_bond_line_1, ax, double_bond_line_1_midpoint) - self.plot_halflines_double(double_bond_line_2, ax, double_bond_line_2_midpoint) + if self.is_terminal(bond.atom_1) and self.is_terminal( + bond.atom_2 + ): + dummy_1 = Vector( + bond.atom_1.draw.position.x + 1, + bond.atom_1.draw.position.y + 1, + ) + dummy_2 = Vector( + bond.atom_1.draw.position.x - 1, + bond.atom_1.draw.position.y - 1, + ) + double_bond_line_1 = line.double_line_towards_center( + dummy_1, + self.options.bond_spacing / 2.0, + self.options.double_bond_length, + ) + double_bond_line_1_midpoint = ( + double_bond_line_1.get_midpoint() + ) + double_bond_line_2 = line.double_line_towards_center( + dummy_2, + self.options.bond_spacing / 2.0, + self.options.double_bond_length, + ) + double_bond_line_2_midpoint = ( + double_bond_line_2.get_midpoint() + ) + + self.plot_halflines_double( + double_bond_line_1, ax, double_bond_line_1_midpoint + ) + self.plot_halflines_double( + double_bond_line_2, ax, double_bond_line_2_midpoint + ) else: @@ -1686,60 +2085,133 @@ def draw_structure(self) -> None: branched_atom = bond.atom_1 if len(branched_atom.drawn_neighbours) >= 3: - closest_two = self.get_sorted_distances_from_list(terminal_atom, branched_atom.drawn_neighbours) + closest_two = self.get_sorted_distances_from_list( + terminal_atom, branched_atom.drawn_neighbours + ) closest_atom_1 = closest_two[0][1] closest_atom_2 = closest_two[1][1] - line = Line(terminal_atom.draw.position, branched_atom.draw.position, terminal_atom, branched_atom) - - double_bond_line_1, double_bond_line_2 = line.get_perpendicular_lines(self.options.bond_spacing / 2.0) - terminal_atom_pos_1 = double_bond_line_1.get_atom_coords(terminal_atom) - terminal_atom_pos_2 = double_bond_line_2.get_atom_coords(terminal_atom) - - closest_atom_to_pos_1 = terminal_atom_pos_1.get_closest_atom(closest_atom_1, closest_atom_2) - closest_atom_to_pos_2 = terminal_atom_pos_2.get_closest_atom(closest_atom_1, closest_atom_2) - - bond_1_line = Line(branched_atom.draw.position, closest_atom_to_pos_1.draw.position, branched_atom, closest_atom_to_pos_1) - bond_2_line = Line(branched_atom.draw.position, closest_atom_to_pos_2.draw.position, branched_atom, closest_atom_to_pos_2) - - double_bond_line_1_midpoint = double_bond_line_1.get_midpoint() - double_bond_line_2_midpoint = double_bond_line_2.get_midpoint() - - intersection_1 = double_bond_line_1.find_intersection(bond_1_line) - intersection_2 = double_bond_line_2.find_intersection(bond_2_line) - - if terminal_atom.draw.position.x > branched_atom.draw.position.x: + line = Line( + terminal_atom.draw.position, + branched_atom.draw.position, + terminal_atom, + branched_atom, + ) + + ( + double_bond_line_1, + double_bond_line_2, + ) = line.get_perpendicular_lines( + self.options.bond_spacing / 2.0 + ) + terminal_atom_pos_1 = ( + double_bond_line_1.get_atom_coords(terminal_atom) + ) + terminal_atom_pos_2 = ( + double_bond_line_2.get_atom_coords(terminal_atom) + ) + + closest_atom_to_pos_1 = ( + terminal_atom_pos_1.get_closest_atom( + closest_atom_1, closest_atom_2 + ) + ) + closest_atom_to_pos_2 = ( + terminal_atom_pos_2.get_closest_atom( + closest_atom_1, closest_atom_2 + ) + ) + + bond_1_line = Line( + branched_atom.draw.position, + closest_atom_to_pos_1.draw.position, + branched_atom, + closest_atom_to_pos_1, + ) + bond_2_line = Line( + branched_atom.draw.position, + closest_atom_to_pos_2.draw.position, + branched_atom, + closest_atom_to_pos_2, + ) + + double_bond_line_1_midpoint = ( + double_bond_line_1.get_midpoint() + ) + double_bond_line_2_midpoint = ( + double_bond_line_2.get_midpoint() + ) + + intersection_1 = double_bond_line_1.find_intersection( + bond_1_line + ) + intersection_2 = double_bond_line_2.find_intersection( + bond_2_line + ) + + if ( + terminal_atom.draw.position.x + > branched_atom.draw.position.x + ): # check for parallel lines - if intersection_1 and intersection_1.x < 100000 and intersection_1.y < 100000: + if ( + intersection_1 + and intersection_1.x < 100000 + and intersection_1.y < 100000 + ): double_bond_line_1.point_1 = intersection_1 - if intersection_2 and intersection_2.x < 100000 and intersection_2.y < 100000: + if ( + intersection_2 + and intersection_2.x < 100000 + and intersection_2.y < 100000 + ): double_bond_line_2.point_1 = intersection_2 else: # check for parallel lines - if intersection_1 and intersection_1.x < 100000 and intersection_1.y < 100000: + if ( + intersection_1 + and intersection_1.x < 100000 + and intersection_1.y < 100000 + ): double_bond_line_1.point_2 = intersection_1 - if intersection_2 and intersection_2.x < 100000 and intersection_2.y < 100000: + if ( + intersection_2 + and intersection_2.x < 100000 + and intersection_2.y < 100000 + ): double_bond_line_2.point_2 = intersection_2 - self.plot_halflines(double_bond_line_1, ax, double_bond_line_1_midpoint) - self.plot_halflines(double_bond_line_2, ax, double_bond_line_2_midpoint) + self.plot_halflines( + double_bond_line_1, ax, double_bond_line_1_midpoint + ) + self.plot_halflines( + double_bond_line_2, ax, double_bond_line_2_midpoint + ) else: self.plot_halflines(line, ax, midpoint) - bond_neighbours = bond.atom_1.drawn_neighbours + bond.atom_2.drawn_neighbours + bond_neighbours = ( + bond.atom_1.drawn_neighbours + + bond.atom_2.drawn_neighbours + ) if bond_neighbours: - vectors = [atom.draw.position for atom in bond_neighbours] + vectors = [ + atom.draw.position for atom in bond_neighbours + ] gravitational_point = Vector.get_average(vectors) - second_line = line.get_parallel_line(gravitational_point, - self.options.bond_spacing) + second_line = line.get_parallel_line( + gravitational_point, self.options.bond_spacing + ) second_line_midpoint = second_line.get_midpoint() - self.plot_halflines(second_line, ax, second_line_midpoint) + self.plot_halflines( + second_line, ax, second_line_midpoint + ) else: print("Shouldn't happen!") - elif bond.type == 'triple': + elif bond.type == "triple": self.plot_halflines(line, ax, midpoint) line_1, line_2 = line.get_parallel_lines(self.options.bond_spacing) line_1_midpoint = line_1.get_midpoint() @@ -1749,195 +2221,243 @@ def draw_structure(self) -> None: for atom in self.structure.graph: if atom.draw.positioned: - text_h = '' + text_h = "" text_h_pos = None - if atom.type != 'C' or atom.draw.draw_explicit: - if atom.type == 'C': - text = '.' + if atom.type != "C" or atom.draw.draw_explicit: + if atom.type == "C": + text = "." else: text = self.set_r_group_indices_subscript(atom.type) else: - text = '' + text = "" - horizontal_alignment = 'center' + horizontal_alignment = "center" orientation = self.get_hydrogen_text_orientation(atom) - if orientation == 'H_above_atom': + if orientation == "H_above_atom": text_h_pos = Vector(atom.draw.position.x, atom.draw.position.y + 6) - if orientation == 'H_below_atom': + if orientation == "H_below_atom": text_h_pos = Vector(atom.draw.position.x, atom.draw.position.y - 6) atom_draw_position = Vector(atom.draw.position.x, atom.draw.position.y) - if text == '.': + if text == ".": atom_draw_position.y += 2 - if not atom.charge and (atom.type != 'C' or atom.draw.draw_explicit): + if not atom.charge and (atom.type != "C" or atom.draw.draw_explicit): if atom.draw.has_hydrogen: hydrogen_count = 0 for neighbour in atom.neighbours: - if neighbour.type == 'H' and not neighbour.draw.is_drawn: + if neighbour.type == "H" and not neighbour.draw.is_drawn: hydrogen_count += 1 - if hydrogen_count and atom.type != 'C': + if hydrogen_count and atom.type != "C": if hydrogen_count > 1: - if orientation == 'H_before_atom': - text = r'$H_{hydrogens}{atom_type}$'.format(hydrogens=hydrogen_count, - atom_type=atom.type) - horizontal_alignment = 'right' + if orientation == "H_before_atom": + text = r"$H_{hydrogens}{atom_type}$".format( + hydrogens=hydrogen_count, atom_type=atom.type + ) + horizontal_alignment = "right" atom_draw_position.x += 3 - elif orientation == 'H_below_atom' or orientation == 'H_above_atom': + elif ( + orientation == "H_below_atom" + or orientation == "H_above_atom" + ): text = atom.type - text_h = r'$H_{hydrogens}$'.format(hydrogens=hydrogen_count) + text_h = r"$H_{hydrogens}$".format( + hydrogens=hydrogen_count + ) else: - text = r'${atom_type}H_{hydrogens}$'.format(hydrogens=hydrogen_count, - atom_type=atom.type) - horizontal_alignment = 'left' + text = r"${atom_type}H_{hydrogens}$".format( + hydrogens=hydrogen_count, atom_type=atom.type + ) + horizontal_alignment = "left" atom_draw_position.x -= 3 elif hydrogen_count == 1: - if orientation == 'H_before_atom': - text = f'H{atom.type}' - horizontal_alignment = 'right' + if orientation == "H_before_atom": + text = f"H{atom.type}" + horizontal_alignment = "right" atom_draw_position.x += 3 - elif orientation == 'H_below_atom' or orientation == 'H_above_atom': + elif ( + orientation == "H_below_atom" + or orientation == "H_above_atom" + ): text = atom.type - text_h = 'H' + text_h = "H" else: - text = f'{atom.type}H' - horizontal_alignment = 'left' + text = f"{atom.type}H" + horizontal_alignment = "left" atom_draw_position.x -= 3 elif atom.charge: if atom.charge > 0: - charge_symbol = '+' + charge_symbol = "+" else: - charge_symbol = '-' + charge_symbol = "-" hydrogen_count = 0 for neighbour in atom.neighbours: - if neighbour.type == 'H' and not neighbour.draw.is_drawn: + if neighbour.type == "H" and not neighbour.draw.is_drawn: hydrogen_count += 1 if not hydrogen_count: if abs(atom.charge) > 1: charge_repr = f"{abs(atom.charge)}{charge_symbol}" - text = '$' + atom.type + '^{' + charge_repr + '}$' + text = "$" + atom.type + "^{" + charge_repr + "}$" # text = r'${atom_type}^{charge}{charge_symbol}$'.format(charge=atom.charge, # atom_type=atom.type, # charge_symbol=charge_symbol) elif abs(atom.charge) == 1: - text = r'${atom_type}^{charge_symbol}$'.format(atom_type=atom.type, - charge_symbol=charge_symbol) + text = r"${atom_type}^{charge_symbol}$".format( + atom_type=atom.type, charge_symbol=charge_symbol + ) - horizontal_alignment = 'left' + horizontal_alignment = "left" atom_draw_position.x -= 3 else: - # elif atom.type != 'C' or atom.draw.draw_explicit: + # elif atom.type != 'C' or atom.draw.draw_explicit: if hydrogen_count > 1: - if orientation == 'H_before_atom': + if orientation == "H_before_atom": if abs(atom.charge) > 1: charge_repr = f"{abs(atom.charge)}{charge_symbol}" - text = '$H_' + str(hydrogen_count) + atom.type + '^{' + charge_repr + '}$' + text = ( + "$H_" + + str(hydrogen_count) + + atom.type + + "^{" + + charge_repr + + "}$" + ) # text = r'$H_{hydrogens}{atom_type}^{charge}{charge_symbol}$'.format(hydrogens=hydrogen_count, # atom_type=atom.type, # charge=abs(atom.charge), # charge_symbol=charge_symbol) elif abs(atom.charge) == 1: - text = r'$H_{hydrogens}{atom_type}^{charge_symbol}$'.format(hydrogens=hydrogen_count, - atom_type=atom.type, - charge_symbol=charge_symbol) + text = r"$H_{hydrogens}{atom_type}^{charge_symbol}$".format( + hydrogens=hydrogen_count, + atom_type=atom.type, + charge_symbol=charge_symbol, + ) - horizontal_alignment = 'right' + horizontal_alignment = "right" atom_draw_position.x += 3 - elif orientation == 'H_above_atom' or orientation == 'H_below_atom': - text_h = r'$H_{hydrogens}$'.format(hydrogens=hydrogen_count) + elif ( + orientation == "H_above_atom" + or orientation == "H_below_atom" + ): + text_h = r"$H_{hydrogens}$".format( + hydrogens=hydrogen_count + ) if abs(atom.charge) > 1: charge_repr = f"{abs(atom.charge)}{charge_symbol}" - text = '$' + atom.type + '^{' + charge_repr + '}$' + text = "$" + atom.type + "^{" + charge_repr + "}$" # text = r'${atom_type}^{charge}{charge_symbol}$'.format(atom_type=atom.type, # charge=abs(atom.charge), # charge_symbol=charge_symbol) elif abs(atom.charge) == 1: - text = r'${atom_type}^{charge_symbol}$'.format(atom_type=atom.type, - charge_symbol=charge_symbol) + text = r"${atom_type}^{charge_symbol}$".format( + atom_type=atom.type, charge_symbol=charge_symbol + ) else: if abs(atom.charge) > 1: charge_repr = f"{abs(atom.charge)}{charge_symbol}" - text = '$' + atom.type + 'H_' + str(hydrogen_count) + '^{' + charge_repr + '}$' + text = ( + "$" + + atom.type + + "H_" + + str(hydrogen_count) + + "^{" + + charge_repr + + "}$" + ) # text = r'${atom_type}H_{hydrogens}^{charge}{charge_symbol}$'.format(hydrogens=hydrogen_count, # atom_type=atom.type, # charge=abs(atom.charge), # charge_symbol=charge_symbol) elif abs(atom.charge) == 1: - text = r'${atom_type}H_{hydrogens}^{charge_symbol}$'.format(hydrogens=hydrogen_count, - atom_type=atom.type, - charge_symbol=charge_symbol) + text = r"${atom_type}H_{hydrogens}^{charge_symbol}$".format( + hydrogens=hydrogen_count, + atom_type=atom.type, + charge_symbol=charge_symbol, + ) - horizontal_alignment = 'left' + horizontal_alignment = "left" atom_draw_position.x -= 3 elif hydrogen_count == 1: - if orientation == 'H_before_atom': + if orientation == "H_before_atom": if abs(atom.charge) > 1: charge_repr = f"{abs(atom.charge)}{charge_symbol}" - text = '$H' + atom.type + '^{' + charge_repr + '}$' + text = "$H" + atom.type + "^{" + charge_repr + "}$" elif abs(atom.charge) == 1: - text = r'$H{atom_type}^{charge_symbol}$'.format(atom_type=atom.type, - charge_symbol=charge_symbol) - horizontal_alignment = 'right' + text = r"$H{atom_type}^{charge_symbol}$".format( + atom_type=atom.type, charge_symbol=charge_symbol + ) + horizontal_alignment = "right" atom_draw_position.x += 3 - elif orientation == 'H_above_atom' or orientation == 'H_below_atom': - text_h = 'H' + elif ( + orientation == "H_above_atom" + or orientation == "H_below_atom" + ): + text_h = "H" if abs(atom.charge) > 1: charge_repr = f"{abs(atom.charge)}{charge_symbol}" - text = '$' + atom.type + '^{' + charge_repr + '}$' + text = "$" + atom.type + "^{" + charge_repr + "}$" # text = r'${atom_type}^{charge}{charge_symbol}$'.format(atom_type=atom.type, # charge=abs(atom.charge), # charge_symbol=charge_symbol) elif abs(atom.charge) == 1: - text = r'${atom_type}^{charge_symbol}$'.format(atom_type=atom.type, - charge_symbol=charge_symbol) + text = r"${atom_type}^{charge_symbol}$".format( + atom_type=atom.type, charge_symbol=charge_symbol + ) else: if abs(atom.charge) > 1: charge_repr = f"{abs(atom.charge)}{charge_symbol}" - text = '$' + atom.type + 'H^{' + charge_repr + '}$' + text = "$" + atom.type + "H^{" + charge_repr + "}$" # text = r'${atom_type}H^{charge}{charge_symbol}$'.format(atom_type=atom.type, # charge=abs(atom.charge), # charge_symbol=charge_symbol) elif abs(atom.charge) == 1: - text = r'${atom_type}H^{charge_symbol}$'.format(atom_type=atom.type, - charge_symbol=charge_symbol) - horizontal_alignment = 'left' + text = r"${atom_type}H^{charge_symbol}$".format( + atom_type=atom.type, charge_symbol=charge_symbol + ) + horizontal_alignment = "left" atom_draw_position.x -= 3 if text: - plt.text(atom_draw_position.x, atom_draw_position.y, - text, - horizontalalignment=horizontal_alignment, - verticalalignment='center', - color=atom.draw.colour) + plt.text( + atom_draw_position.x, + atom_draw_position.y, + text, + horizontalalignment=horizontal_alignment, + verticalalignment="center", + color=atom.draw.colour, + ) if text_h: - plt.text(text_h_pos.x, text_h_pos.y, - text_h, - horizontalalignment='center', - verticalalignment='center', - color=atom.draw.colour) + plt.text( + text_h_pos.x, + text_h_pos.y, + text_h, + horizontalalignment="center", + verticalalignment="center", + color=atom.draw.colour, + ) @staticmethod def set_r_group_indices_subscript(atom_text: str) -> str: # Take str and return the same str with subscript digits # (pattern is necessary to not to get confused with isotopes) sub_translation = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉") - match = re.search('[RXZ]\d+', atom_text) + match = re.search("[RXZ]\d+", atom_text) if match: matched_pattern = match.group() adapted_pattern = matched_pattern.translate(sub_translation) @@ -1960,7 +2480,11 @@ def process_structure(self) -> None: self.resolve_primary_overlaps() - self.total_overlap_score, sorted_overlap_scores, atom_to_scores = self.get_overlap_score() + ( + self.total_overlap_score, + sorted_overlap_scores, + atom_to_scores, + ) = self.get_overlap_score() for i in range(self.options.overlap_resolution_iterations): for bond in self.drawn_bonds: @@ -1975,11 +2499,17 @@ def process_structure(self) -> None: atom_2_rotatable = True for neighbouring_bond in bond.atom_1.bonds: - if neighbouring_bond.type == 'double' and neighbouring_bond.chiral: + if ( + neighbouring_bond.type == "double" + and neighbouring_bond.chiral + ): atom_1_rotatable = False for neighbouring_bond in bond.atom_2.bonds: - if neighbouring_bond.type == 'double' and neighbouring_bond.chiral: + if ( + neighbouring_bond.type == "double" + and neighbouring_bond.chiral + ): atom_2_rotatable = False # If neither are rotatable, continue @@ -2000,20 +2530,32 @@ def process_structure(self) -> None: atom_1 = bond.atom_1 atom_2 = bond.atom_2 - subtree_overlap_score, _ = self.get_subtree_overlap_score(atom_2, atom_1, atom_to_scores) + subtree_overlap_score, _ = self.get_subtree_overlap_score( + atom_2, atom_1, atom_to_scores + ) if subtree_overlap_score > self.options.overlap_sensitivity: neighbours_2 = atom_2.drawn_neighbours[:] neighbours_2.remove(atom_1) if len(neighbours_2) == 1: neighbour = neighbours_2[0] - angle = neighbour.draw.position.get_rotation_away_from_vector(atom_1.draw.position, atom_2.draw.position, math.radians(120)) - - self.rotate_subtree(neighbour, atom_2, angle, atom_2.draw.position) + angle = ( + neighbour.draw.position.get_rotation_away_from_vector( + atom_1.draw.position, + atom_2.draw.position, + math.radians(120), + ) + ) + + self.rotate_subtree( + neighbour, atom_2, angle, atom_2.draw.position + ) new_overlap_score, _, _ = self.get_overlap_score() if new_overlap_score > self.total_overlap_score: - self.rotate_subtree(neighbour, atom_2, -angle, atom_2.draw.position) + self.rotate_subtree( + neighbour, atom_2, -angle, atom_2.draw.position + ) else: self.total_overlap_score = new_overlap_score @@ -2024,32 +2566,68 @@ def process_structure(self) -> None: neighbour_1 = neighbours_2[0] neighbour_2 = neighbours_2[1] - if len(neighbour_1.draw.rings) == 1 and len(neighbour_2.draw.rings) == 1: + if ( + len(neighbour_1.draw.rings) == 1 + and len(neighbour_2.draw.rings) == 1 + ): # If the neighbours are in different rings, or in rings at all, do nothing - if neighbour_1.draw.rings[0] != neighbour_2.draw.rings[0]: + if ( + neighbour_1.draw.rings[0] + != neighbour_2.draw.rings[0] + ): continue elif neighbour_1.draw.rings or neighbour_2.draw.rings: continue else: - angle_1 = neighbour_1.draw.position.get_rotation_away_from_vector(atom_1.draw.position, atom_2.draw.position, math.radians(120)) - angle_2 = neighbour_2.draw.position.get_rotation_away_from_vector(atom_1.draw.position, atom_2.draw.position, math.radians(120)) - - self.rotate_subtree(neighbour_1, atom_2, angle_1, atom_2.draw.position) - self.rotate_subtree(neighbour_2, atom_2, angle_2, atom_2.draw.position) + angle_1 = neighbour_1.draw.position.get_rotation_away_from_vector( + atom_1.draw.position, + atom_2.draw.position, + math.radians(120), + ) + angle_2 = neighbour_2.draw.position.get_rotation_away_from_vector( + atom_1.draw.position, + atom_2.draw.position, + math.radians(120), + ) + + self.rotate_subtree( + neighbour_1, atom_2, angle_1, atom_2.draw.position + ) + self.rotate_subtree( + neighbour_2, atom_2, angle_2, atom_2.draw.position + ) new_overlap_score, _, _ = self.get_overlap_score() if new_overlap_score > self.total_overlap_score: - self.rotate_subtree(neighbour_1, atom_2, -angle_1, atom_2.draw.position) - self.rotate_subtree(neighbour_2, atom_2, -angle_2, atom_2.draw.position) + self.rotate_subtree( + neighbour_1, + atom_2, + -angle_1, + atom_2.draw.position, + ) + self.rotate_subtree( + neighbour_2, + atom_2, + -angle_2, + atom_2.draw.position, + ) else: self.total_overlap_score = new_overlap_score - self.total_overlap_score, sorted_overlap_scores, atom_to_scores = self.get_overlap_score() + ( + self.total_overlap_score, + sorted_overlap_scores, + atom_to_scores, + ) = self.get_overlap_score() if self.options.finetune: self.finetune_overlap_resolution() - self.total_overlap_score, sorted_overlap_scores, atom_to_scores = self.get_overlap_score() + ( + self.total_overlap_score, + sorted_overlap_scores, + atom_to_scores, + ) = self.get_overlap_score() for i in range(self.options.overlap_resolution_iterations): @@ -2077,8 +2655,14 @@ def position(self) -> None: self.create_next_bond(start_atom, None, 0.0) - def create_next_bond(self, atom, previous_atom=None, angle=0.0, - previous_branch_shortest=False, skip_positioning=False): + def create_next_bond( + self, + atom, + previous_atom=None, + angle=0.0, + previous_branch_shortest=False, + skip_positioning=False, + ): if atom.draw.positioned and not skip_positioning: return @@ -2106,11 +2690,16 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, # If the previous atom was not part of a bridged ring and the previous atom was part of more than one ring - if previous_atom.draw.bridged_ring is None and len(previous_atom.draw.rings) > 1: + if ( + previous_atom.draw.bridged_ring is None + and len(previous_atom.draw.rings) > 1 + ): # Find the vertex adjoining the current bridged ring that is also in both ring systems. This is the # joined vertex. for neighbour in neighbours: - if len(set(neighbour.draw.rings) & set(previous_atom.draw.rings)) == len(previous_atom.draw.rings): + if len( + set(neighbour.draw.rings) & set(previous_atom.draw.rings) + ) == len(previous_atom.draw.rings): joined_vertex = neighbour break @@ -2121,8 +2710,14 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, # for neighbour in neighbours: - if neighbour.draw.positioned and self.atoms_are_in_same_ring(neighbour, previous_atom): - position.add(Vector.subtract_vectors(neighbour.draw.position, previous_atom.draw.position)) + if neighbour.draw.positioned and self.atoms_are_in_same_ring( + neighbour, previous_atom + ): + position.add( + Vector.subtract_vectors( + neighbour.draw.position, previous_atom.draw.position + ) + ) position.invert() position.normalise() @@ -2152,10 +2747,14 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, next_ring = self.id_to_ring[atom.draw.bridged_ring] if not next_ring.positioned: - next_center = Vector.subtract_vectors(atom.draw.previous_position, atom.draw.position) + next_center = Vector.subtract_vectors( + atom.draw.previous_position, atom.draw.position + ) next_center.invert() next_center.normalise() - scalar = Polygon.find_polygon_radius(self.options.bond_length, len(next_ring.members)) + scalar = Polygon.find_polygon_radius( + self.options.bond_length, len(next_ring.members) + ) next_center.multiply_by_scalar(scalar) next_center.add(atom.draw.position) @@ -2167,11 +2766,15 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, next_ring = self.id_to_ring[atom.draw.rings[0]] if not next_ring.positioned: - next_center = Vector.subtract_vectors(atom.draw.previous_position, atom.draw.position) + next_center = Vector.subtract_vectors( + atom.draw.previous_position, atom.draw.position + ) next_center.invert() next_center.normalise() - radius = Polygon.find_polygon_radius(self.options.bond_length, len(next_ring.members)) + radius = Polygon.find_polygon_radius( + self.options.bond_length, len(next_ring.members) + ) next_center.multiply_by_scalar(radius) next_center.add(atom.draw.position) @@ -2197,12 +2800,20 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, if previous_atom: previous_bond = self.structure.bond_lookup[previous_atom][atom] - if current_bond.type == 'triple' or (previous_bond and previous_bond.type == 'triple') or \ - (current_bond.type == 'double' and previous_bond and previous_bond.type == 'double' and - previous_atom and len(previous_atom.draw.rings) == 0 and - len(atom.neighbours) == 2): - - if current_bond.type == 'double' and previous_bond.type == 'double': + if ( + current_bond.type == "triple" + or (previous_bond and previous_bond.type == "triple") + or ( + current_bond.type == "double" + and previous_bond + and previous_bond.type == "double" + and previous_atom + and len(previous_atom.draw.rings) == 0 + and len(atom.neighbours) == 2 + ) + ): + + if current_bond.type == "double" and previous_bond.type == "double": atom.draw.draw_explicit = True @@ -2211,12 +2822,18 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, current_bond.draw.center = True - if current_bond.type == 'double' or current_bond.type == 'triple' or (previous_atom and previous_bond.type == 'triple'): + if ( + current_bond.type == "double" + or current_bond.type == "triple" + or (previous_atom and previous_bond.type == "triple") + ): next_atom.draw.angle = 0.0 # next_atom.draw.draw_explicit = True - self.create_next_bond(next_atom, atom, previous_angle + next_atom.draw.angle) + self.create_next_bond( + next_atom, atom, previous_angle + next_atom.draw.angle + ) elif previous_atom and len(previous_atom.draw.rings) > 0: @@ -2241,7 +2858,9 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, else: next_atom.draw.angle = proposed_angle_1 - self.create_next_bond(next_atom, atom, previous_angle + next_atom.draw.angle) + self.create_next_bond( + next_atom, atom, previous_angle + next_atom.draw.angle + ) else: @@ -2267,15 +2886,17 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, if previous_atom: bond = self.structure.bond_lookup[previous_atom][atom] - if bond.type == 'double' and bond.chiral: + if bond.type == "double" and bond.chiral: rotatable = False previous_previous_atom = previous_atom.draw.previous_atom if previous_previous_atom: - configuration = bond.chiral_dict[previous_previous_atom][next_atom] - if configuration == 'cis': + configuration = bond.chiral_dict[ + previous_previous_atom + ][next_atom] + if configuration == "cis": a = -a @@ -2288,12 +2909,16 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, else: next_atom.draw.angle = -a - if round(math.degrees(next_atom.draw.angle), 0) == 360 or \ - round(math.degrees(next_atom.draw.angle), 0) == -360 or \ - round(math.degrees(next_atom.draw.angle), 0) == 0: + if ( + round(math.degrees(next_atom.draw.angle), 0) == 360 + or round(math.degrees(next_atom.draw.angle), 0) == -360 + or round(math.degrees(next_atom.draw.angle), 0) == 0 + ): atom.draw.draw_explicit = True - self.create_next_bond(next_atom, atom, previous_angle + next_atom.draw.angle) + self.create_next_bond( + next_atom, atom, previous_angle + next_atom.draw.angle + ) elif len(neighbours) == 2: a = atom.draw.angle @@ -2314,11 +2939,21 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, cis_atom_index = 0 trans_atom_index = 1 - if neighbour_2.type == 'C' and neighbour_1.type != 'C' and subgraph_2_size > 1 and subgraph_1_size < 5: + if ( + neighbour_2.type == "C" + and neighbour_1.type != "C" + and subgraph_2_size > 1 + and subgraph_1_size < 5 + ): cis_atom_index = 1 trans_atom_index = 0 - elif neighbour_2.type != 'C' and neighbour_1.type == 'C' and subgraph_1_size > 1 and subgraph_2_size < 5: + elif ( + neighbour_2.type != "C" + and neighbour_1.type == "C" + and subgraph_1_size > 1 + and subgraph_2_size < 5 + ): cis_atom_index = 0 trans_atom_index = 1 @@ -2331,7 +2966,10 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, previous_branch_shortest = False - if subgraph_3_size < subgraph_2_size and subgraph_3_size < subgraph_1_size: + if ( + subgraph_3_size < subgraph_2_size + and subgraph_3_size < subgraph_1_size + ): previous_branch_shortest = True trans_atom.draw.angle = a @@ -2342,22 +2980,34 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, # if the cis bond and trans bond are single bonds it can be adjacent to a # chiral double bond and may have to be placed in a specific orientation - if cis_bond.type == 'single' and trans_bond.type == 'single': + if cis_bond.type == "single" and trans_bond.type == "single": if previous_atom: previous_bond = self.structure.bond_lookup[atom][previous_atom] # checks if the previous bond was a chiral double bond # TODO: make sure chiral bonds aren't drawn first! - if previous_bond.type == 'double' and previous_bond.chiral: + if previous_bond.type == "double" and previous_bond.chiral: if previous_atom.draw.previous_atom: - configuration_cis_atom = previous_bond.chiral_dict[previous_atom.draw.previous_atom][cis_atom] - if configuration_cis_atom == 'cis': + configuration_cis_atom = previous_bond.chiral_dict[ + previous_atom.draw.previous_atom + ][cis_atom] + if configuration_cis_atom == "cis": trans_atom.draw.angle = -a cis_atom.draw.angle = a - self.create_next_bond(trans_atom, atom, previous_angle + trans_atom.draw.angle, previous_branch_shortest) - self.create_next_bond(cis_atom, atom, previous_angle + cis_atom.draw.angle, previous_branch_shortest) + self.create_next_bond( + trans_atom, + atom, + previous_angle + trans_atom.draw.angle, + previous_branch_shortest, + ) + self.create_next_bond( + cis_atom, + atom, + previous_angle + cis_atom.draw.angle, + previous_branch_shortest, + ) elif len(neighbours) == 3: subgraph_1_size = self.get_subgraph_size(neighbours[0], {atom}) @@ -2368,23 +3018,32 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, left_atom = neighbours[1] right_atom = neighbours[2] - if subgraph_2_size > subgraph_1_size and subgraph_2_size > subgraph_3_size: + if ( + subgraph_2_size > subgraph_1_size + and subgraph_2_size > subgraph_3_size + ): straight_atom = neighbours[1] left_atom = neighbours[0] right_atom = neighbours[2] - elif subgraph_3_size > subgraph_1_size and subgraph_3_size > subgraph_2_size: + elif ( + subgraph_3_size > subgraph_1_size + and subgraph_3_size > subgraph_2_size + ): straight_atom = neighbours[2] left_atom = neighbours[0] right_atom = neighbours[1] - if previous_atom and len(previous_atom.draw.rings) < 1\ - and len(straight_atom.draw.rings) < 1\ - and len(left_atom.draw.rings) < 1\ - and len(right_atom.draw.rings) < 1\ - and self.get_subgraph_size(left_atom, {atom}) == 1\ - and self.get_subgraph_size(right_atom, {atom}) == 1\ - and self.get_subgraph_size(straight_atom, {atom}) > 1: + if ( + previous_atom + and len(previous_atom.draw.rings) < 1 + and len(straight_atom.draw.rings) < 1 + and len(left_atom.draw.rings) < 1 + and len(right_atom.draw.rings) < 1 + and self.get_subgraph_size(left_atom, {atom}) == 1 + and self.get_subgraph_size(right_atom, {atom}) == 1 + and self.get_subgraph_size(straight_atom, {atom}) > 1 + ): straight_atom.draw.angle = atom.draw.angle * -1 if atom.draw.angle >= 0: left_atom.draw.angle = math.radians(30) @@ -2398,9 +3057,15 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, left_atom.draw.angle = math.radians(90) right_atom.draw.angle = math.radians(-90) - self.create_next_bond(straight_atom, atom, previous_angle + straight_atom.draw.angle) - self.create_next_bond(left_atom, atom, previous_angle + left_atom.draw.angle) - self.create_next_bond(right_atom, atom, previous_angle + right_atom.draw.angle) + self.create_next_bond( + straight_atom, atom, previous_angle + straight_atom.draw.angle + ) + self.create_next_bond( + left_atom, atom, previous_angle + left_atom.draw.angle + ) + self.create_next_bond( + right_atom, atom, previous_angle + right_atom.draw.angle + ) elif len(neighbours) == 4: subgraph_1_size = self.get_subgraph_size(neighbours[0], {atom}) @@ -2413,19 +3078,28 @@ def create_next_bond(self, atom, previous_atom=None, angle=0.0, atom_3 = neighbours[2] atom_4 = neighbours[3] - if subgraph_2_size > subgraph_1_size and subgraph_2_size > subgraph_3_size\ - and subgraph_2_size > subgraph_4_size: + if ( + subgraph_2_size > subgraph_1_size + and subgraph_2_size > subgraph_3_size + and subgraph_2_size > subgraph_4_size + ): atom_1 = neighbours[1] atom_2 = neighbours[0] - elif subgraph_3_size > subgraph_1_size and subgraph_3_size > subgraph_2_size\ - and subgraph_3_size > subgraph_4_size: + elif ( + subgraph_3_size > subgraph_1_size + and subgraph_3_size > subgraph_2_size + and subgraph_3_size > subgraph_4_size + ): atom_1 = neighbours[2] atom_2 = neighbours[0] atom_3 = neighbours[1] - elif subgraph_4_size > subgraph_1_size and subgraph_4_size > subgraph_2_size\ - and subgraph_4_size > subgraph_3_size: + elif ( + subgraph_4_size > subgraph_1_size + and subgraph_4_size > subgraph_2_size + and subgraph_4_size > subgraph_3_size + ): atom_1 = neighbours[3] atom_2 = neighbours[0] atom_3 = neighbours[1] @@ -2466,16 +3140,24 @@ def restore_ring_information(self) -> None: @staticmethod def bond_is_rotatable(bond: Bond) -> bool: - if bond.atom_1.draw.rings and \ - bond.atom_2.draw.rings and \ - len(set(bond.atom_1.draw.rings).intersection(set(bond.atom_2.draw.rings))) > 0: + if ( + bond.atom_1.draw.rings + and bond.atom_2.draw.rings + and len( + set(bond.atom_1.draw.rings).intersection(set(bond.atom_2.draw.rings)) + ) + > 0 + ): return False - - if bond.type != 'single': + + if bond.type != "single": if bond.chiral: return False - if len(bond.atom_1.drawn_neighbours) > 1 and len(bond.atom_2.drawn_neighbours) > 1: + if ( + len(bond.atom_1.drawn_neighbours) > 1 + and len(bond.atom_2.drawn_neighbours) > 1 + ): return False chiral = False @@ -2491,28 +3173,36 @@ def bond_is_rotatable(bond: Bond) -> bool: if chiral: return False - + if bond.chiral_symbol: return False - + return True @staticmethod def can_rotate_around_bond(bond: Bond) -> bool: - if bond.type != 'single': + if bond.type != "single": return False # If bond is terminal, don't bother rotating. - if len(bond.atom_1.drawn_neighbours) == 1 or len(bond.atom_2.drawn_neighbours) == 1: + if ( + len(bond.atom_1.drawn_neighbours) == 1 + or len(bond.atom_2.drawn_neighbours) == 1 + ): return False # Added this, needs extensive checking - if bond.atom_1.draw.rings and \ - bond.atom_2.draw.rings and \ - len(set(bond.atom_1.draw.rings).intersection(set(bond.atom_2.draw.rings))) > 0: + if ( + bond.atom_1.draw.rings + and bond.atom_2.draw.rings + and len( + set(bond.atom_1.draw.rings).intersection(set(bond.atom_2.draw.rings)) + ) + > 0 + ): return False return True @@ -2535,15 +3225,21 @@ def resolve_primary_overlaps(self) -> None: non_ring_neighbours = self.get_non_ring_neighbours(atom) - if len(non_ring_neighbours) > 1 or (len(non_ring_neighbours) == 1 and len(atom.draw.rings) == 2): - overlaps.append({'common': atom, - 'rings': atom.draw.rings, - 'vertices': non_ring_neighbours}) + if len(non_ring_neighbours) > 1 or ( + len(non_ring_neighbours) == 1 and len(atom.draw.rings) == 2 + ): + overlaps.append( + { + "common": atom, + "rings": atom.draw.rings, + "vertices": non_ring_neighbours, + } + ) for overlap in overlaps: - branches_to_adjust = overlap['vertices'] - rings = overlap['rings'] - root = overlap['common'] + branches_to_adjust = overlap["vertices"] + rings = overlap["rings"] + root = overlap["common"] if len(branches_to_adjust) == 2: @@ -2558,16 +3254,24 @@ def resolve_primary_overlaps(self) -> None: self.rotate_subtree(atom_2, root, -angle, root.draw.position) total, sorted_scores, atom_to_score = self.get_overlap_score() - subtree_overlap_atom_1_1, _ = self.get_subtree_overlap_score(atom_1, root, atom_to_score) - subtree_overlap_atom_2_1, _ = self.get_subtree_overlap_score(atom_2, root, atom_to_score) + subtree_overlap_atom_1_1, _ = self.get_subtree_overlap_score( + atom_1, root, atom_to_score + ) + subtree_overlap_atom_2_1, _ = self.get_subtree_overlap_score( + atom_2, root, atom_to_score + ) total_score = subtree_overlap_atom_1_1 + subtree_overlap_atom_2_1 self.rotate_subtree(atom_1, root, -2.0 * angle, root.draw.position) self.rotate_subtree(atom_2, root, 2.0 * angle, root.draw.position) total, sorted_scores, atom_to_score = self.get_overlap_score() - subtree_overlap_atom_1_2, _ = self.get_subtree_overlap_score(atom_1, root, atom_to_score) - subtree_overlap_atom_2_2, _ = self.get_subtree_overlap_score(atom_2, root, atom_to_score) + subtree_overlap_atom_1_2, _ = self.get_subtree_overlap_score( + atom_1, root, atom_to_score + ) + subtree_overlap_atom_2_2, _ = self.get_subtree_overlap_score( + atom_2, root, atom_to_score + ) total_score_2 = subtree_overlap_atom_1_2 + subtree_overlap_atom_2_2 if total_score_2 > total_score: @@ -2578,11 +3282,16 @@ def resolve_primary_overlaps(self) -> None: if len(rings) == 2: pass - def resolve_secondary_overlaps(self, sorted_scores: List[Tuple[float, Atom]]) -> None: + def resolve_secondary_overlaps( + self, sorted_scores: List[Tuple[float, Atom]] + ) -> None: for score, atom in sorted_scores: if score > self.options.overlap_sensitivity: if len(atom.drawn_neighbours) <= 1: - if atom.drawn_neighbours and atom.drawn_neighbours[0].adjacent_to_stereobond(): + if ( + atom.drawn_neighbours + and atom.drawn_neighbours[0].adjacent_to_stereobond() + ): continue closest_atom = self.get_closest_atom(atom) @@ -2606,16 +3315,18 @@ def resolve_secondary_overlaps(self, sorted_scores: List[Tuple[float, Atom]]) -> else: atom_previous_position = atom.draw.previous_position - atom.draw.position.rotate_away_from_vector(closest_position, atom_previous_position, - math.radians(20)) + atom.draw.position.rotate_away_from_vector( + closest_position, atom_previous_position, math.radians(20) + ) def get_atom_nr_to_atom(self) -> None: self.atom_nr_to_atom = {} for atom in self.structure.graph: self.atom_nr_to_atom[atom.nr] = atom - def get_subtree_overlap_score(self, root: Atom, root_parent: Atom, - atom_to_score: Dict[Atom, float]) -> Tuple[float, Vector]: + def get_subtree_overlap_score( + self, root: Atom, root_parent: Atom, atom_to_score: Dict[Atom, float] + ) -> Tuple[float, Vector]: score = 0.0 center = Vector(0, 0) @@ -2640,7 +3351,9 @@ def get_subtree_overlap_score(self, root: Atom, root_parent: Atom, return score / count, center - def get_overlap_score(self) -> Tuple[float, List[Tuple[float, Atom]], Dict[Atom, float]]: + def get_overlap_score( + self, + ) -> Tuple[float, List[Tuple[float, Atom]], Dict[Atom, float]]: total = 0.0 overlap_scores = {} @@ -2650,9 +3363,13 @@ def get_overlap_score(self) -> Tuple[float, List[Tuple[float, Atom]], Dict[Atom, for i, atom_1 in enumerate(self.drawn_atoms): for j in range(i + 1, len(self.drawn_atoms)): atom_2 = self.drawn_atoms[j] - distance = Vector.subtract_vectors(atom_1.draw.position, atom_2.draw.position).get_squared_length() + distance = Vector.subtract_vectors( + atom_1.draw.position, atom_2.draw.position + ).get_squared_length() if distance < self.options.bond_length_squared: - weight = (self.options.bond_length - math.sqrt(distance)) / self.options.bond_length + weight = ( + self.options.bond_length - math.sqrt(distance) + ) / self.options.bond_length total += weight overlap_scores[atom_1] += weight overlap_scores[atom_2] += weight @@ -2671,13 +3388,17 @@ def get_non_ring_neighbours(atom: Atom) -> List[Atom]: non_ring_neighbours = [] for neighbour in atom.drawn_neighbours: - nr_overlapping_rings = len(set(atom.draw.rings).intersection(set(neighbour.draw.rings))) + nr_overlapping_rings = len( + set(atom.draw.rings).intersection(set(neighbour.draw.rings)) + ) if nr_overlapping_rings == 0 and not neighbour.draw.is_bridge: non_ring_neighbours.append(neighbour) return non_ring_neighbours - def rotate_subtree(self, root: Atom, root_parent: Atom, angle: float, center: Vector): + def rotate_subtree( + self, root: Atom, root_parent: Atom, angle: float, center: Vector + ): for atom in self.traverse_substructure(root, {root_parent}): atom.draw.position.rotate_around_vector(angle, center) @@ -2685,8 +3406,14 @@ def rotate_subtree(self, root: Atom, root_parent: Atom, angle: float, center: Ve if anchored_ring.center: anchored_ring.center.rotate_around_vector(angle, center) - def rotate_subtree_independent(self, root: Atom, root_parent: Atom, masked_atoms: List[Atom], - angle: float, center: Vector) -> None: + def rotate_subtree_independent( + self, + root: Atom, + root_parent: Atom, + masked_atoms: List[Atom], + angle: float, + center: Vector, + ) -> None: masked_atoms.append(root_parent) masked_atoms = set(masked_atoms) @@ -2697,7 +3424,9 @@ def rotate_subtree_independent(self, root: Atom, root_parent: Atom, masked_atoms if anchored_ring.center: anchored_ring.center.rotate_around_vector(angle, center) - def traverse_substructure(self, atom: Atom, visited: Set[Atom]) -> Generator[Atom, None, None]: + def traverse_substructure( + self, atom: Atom, visited: Set[Atom] + ) -> Generator[Atom, None, None]: yield atom visited.add(atom) for neighbour in atom.drawn_neighbours: @@ -2737,7 +3466,9 @@ def create_ring(self, ring, center=None, start_atom=None, previous_atom=None): starting_angle = 0 if start_atom: - starting_angle = Vector.subtract_vectors(start_atom.draw.position, center).angle() + starting_angle = Vector.subtract_vectors( + start_atom.draw.position, center + ).angle() ring_size = len(ring.members) @@ -2752,10 +3483,18 @@ def create_ring(self, ring, center=None, start_atom=None, previous_atom=None): start_atom = ring.members[0] if ring.bridged: - KKLayout(self.structure, ring.members, center, start_atom, - self.options.bond_length, self.options.kk_threshold, self.options.kk_inner_threshold, - self.options.kk_max_iteration, self.options.kk_max_inner_iteration, - self.options.kk_max_energy) + KKLayout( + self.structure, + ring.members, + center, + start_atom, + self.options.bond_length, + self.options.kk_threshold, + self.options.kk_inner_threshold, + self.options.kk_max_iteration, + self.options.kk_max_inner_iteration, + self.options.kk_max_energy, + ) ring.positioned = True self.set_ring_center(ring) @@ -2764,7 +3503,15 @@ def create_ring(self, ring, center=None, start_atom=None, previous_atom=None): for subring in ring.subrings: self.set_ring_center(subring) else: - ring.set_member_positions(self.structure, start_atom, previous_atom, center, starting_angle, radius, angle) + ring.set_member_positions( + self.structure, + start_atom, + previous_atom, + center, + starting_angle, + radius, + angle, + ) ring.positioned = True ring.center = center @@ -2774,7 +3521,9 @@ def create_ring(self, ring, center=None, start_atom=None, previous_atom=None): if neighbour.positioned: continue - atoms = list(RingOverlap.get_vertices(self.ring_overlaps, ring.id, neighbour.id)) + atoms = list( + RingOverlap.get_vertices(self.ring_overlaps, ring.id, neighbour.id) + ) if len(atoms) == 2: @@ -2785,14 +3534,17 @@ def create_ring(self, ring, center=None, start_atom=None, previous_atom=None): atom_1 = atoms[0] atom_2 = atoms[1] - midpoint = Vector.get_midpoint(atom_1.draw.position, atom_2.draw.position) + midpoint = Vector.get_midpoint( + atom_1.draw.position, atom_2.draw.position + ) normals = Vector.get_normals(atom_1.draw.position, atom_2.draw.position) normals[0].normalise() normals[1].normalise() - apothem = Polygon.get_apothem_from_side_length(self.options.bond_length, - len(neighbour.members)) + apothem = Polygon.get_apothem_from_side_length( + self.options.bond_length, len(neighbour.members) + ) normals[0].multiply_by_scalar(apothem) normals[1].multiply_by_scalar(apothem) @@ -2802,8 +3554,12 @@ def create_ring(self, ring, center=None, start_atom=None, previous_atom=None): next_center = normals[0] - distance_to_center_1 = Vector.subtract_vectors(center, normals[0]).get_squared_length() - distance_to_center_2 = Vector.subtract_vectors(center, normals[1]).get_squared_length() + distance_to_center_1 = Vector.subtract_vectors( + center, normals[0] + ).get_squared_length() + distance_to_center_2 = Vector.subtract_vectors( + center, normals[1] + ).get_squared_length() if distance_to_center_2 > distance_to_center_1: next_center = normals[1] @@ -2811,7 +3567,7 @@ def create_ring(self, ring, center=None, start_atom=None, previous_atom=None): position_1 = Vector.subtract_vectors(atom_1.draw.position, next_center) position_2 = Vector.subtract_vectors(atom_2.draw.position, next_center) - if position_1.get_clockwise_orientation(position_2) == 'clockwise': + if position_1.get_clockwise_orientation(position_2) == "clockwise": if not neighbour.positioned: self.create_ring(neighbour, next_center, atom_1, atom_2) @@ -2830,7 +3586,9 @@ def create_ring(self, ring, center=None, start_atom=None, previous_atom=None): next_center.invert() next_center.normalise() - distance_to_center = Polygon.find_polygon_radius(self.options.bond_length, len(neighbour.members)) + distance_to_center = Polygon.find_polygon_radius( + self.options.bond_length, len(neighbour.members) + ) next_center.multiply_by_scalar(distance_to_center) next_center.add(atom.draw.position) @@ -2891,7 +3649,7 @@ def define_rings(self): atom.draw.rings.append(ring.id) for i, ring_1 in enumerate(self.rings[:-1]): - for ring_2 in self.rings[i + 1:]: + for ring_2 in self.rings[i + 1 :]: ring_overlap = RingOverlap(ring_1, ring_2) if len(ring_overlap.atoms) > 0: @@ -2948,7 +3706,7 @@ def hide_hydrogens(self): self.structure.refresh_structure() for atom in self.structure.graph: - if atom.type != 'H': + if atom.type != "H": continue elif atom.charge != 0: @@ -2959,8 +3717,12 @@ def hide_hydrogens(self): atom.draw.is_drawn = False hidden.append(atom) - if len(neighbour.draw.rings) < 2 and neighbour.draw.bridged_ring is None and \ - neighbour.draw.bridged_ring is not None and len(neighbour.draw.original_rings) < 2: + if ( + len(neighbour.draw.rings) < 2 + and neighbour.draw.bridged_ring is None + and neighbour.draw.bridged_ring is not None + and len(neighbour.draw.original_rings) < 2 + ): atom.draw.is_drawn = False neighbour.draw.has_hydrogen = True @@ -2970,7 +3732,7 @@ def hide_hydrogens(self): for atom in self.structure.graph: atom.set_drawn_neighbours() - if atom.type == 'O': + if atom.type == "O": pass self.drawn_bonds = [] @@ -2994,8 +3756,11 @@ def get_bridged_ring_subrings(self, ring_id, involved_ring_ids): ring = self.id_to_ring[ring_id] for neighbour_id in ring.neighbouring_rings: - if neighbour_id not in involved_ring_ids and neighbour_id != ring_id and \ - rings_connected_by_bridge(self.ring_overlaps, ring_id, neighbour_id): + if ( + neighbour_id not in involved_ring_ids + and neighbour_id != ring_id + and rings_connected_by_bridge(self.ring_overlaps, ring_id, neighbour_id) + ): self.get_bridged_ring_subrings(neighbour_id, involved_ring_ids) def create_bridged_ring(self, involved_ring_ids): @@ -3027,8 +3792,9 @@ def create_bridged_ring(self, involved_ring_ids): is_on_ring = False for bond in atom.bonds: - bond_associated_rings = min(len(bond.atom_1.draw.rings), - len(bond.atom_2.draw.rings)) + bond_associated_rings = min( + len(bond.atom_1.draw.rings), len(bond.atom_2.draw.rings) + ) if bond_associated_rings == 1: is_on_ring = True @@ -3059,7 +3825,7 @@ def create_bridged_ring(self, involved_ring_ids): involved_ring_ids = list(involved_ring_ids) for i, ring_id_1 in enumerate(involved_ring_ids): - for ring_id_2 in involved_ring_ids[i + 1:]: + for ring_id_2 in involved_ring_ids[i + 1 :]: self.remove_ring_overlaps_between(ring_id_1, ring_id_2) for neighbour_id in neighbours: @@ -3118,8 +3884,13 @@ def get_ring_overlaps(self, ring_id, ring_ids): for ring_overlap in self.ring_overlaps: for ring_id_2 in ring_ids: - if (ring_overlap.ring_id_1 == ring_id and ring_overlap.ring_id_2 == ring_id_2) or\ - (ring_overlap.ring_id_2 == ring_id and ring_overlap.ring_id_1 == ring_id_2): + if ( + ring_overlap.ring_id_1 == ring_id + and ring_overlap.ring_id_2 == ring_id_2 + ) or ( + ring_overlap.ring_id_2 == ring_id + and ring_overlap.ring_id_1 == ring_id_2 + ): ring_overlaps.append(ring_overlap) return ring_overlaps @@ -3128,8 +3899,13 @@ def remove_ring_overlaps_between(self, ring_id_1, ring_id_2): to_remove = [] for ring_overlap in self.ring_overlaps: - if (ring_overlap.ring_id_1 == ring_id_1 and ring_overlap.ring_id_2 == ring_id_2) or\ - (ring_overlap.ring_id_2 == ring_id_1 and ring_overlap.ring_id_1 == ring_id_2): + if ( + ring_overlap.ring_id_1 == ring_id_1 + and ring_overlap.ring_id_2 == ring_id_2 + ) or ( + ring_overlap.ring_id_2 == ring_id_1 + and ring_overlap.ring_id_1 == ring_id_2 + ): to_remove.append(ring_overlap) for ring_overlap in to_remove: @@ -3150,7 +3926,9 @@ def get_closest_atom(self, atom): if atom == atom_2: continue - squared_distance = atom.draw.position.get_squared_distance(atom_2.draw.position) + squared_distance = atom.draw.position.get_squared_distance( + atom_2.draw.position + ) if squared_distance < minimal_distance: minimal_distance = squared_distance @@ -3166,7 +3944,9 @@ def get_sorted_distances_from_list(atom, atom_list): if atom == atom_2: continue - squared_distance = atom.draw.position.get_squared_distance(atom_2.draw.position) + squared_distance = atom.draw.position.get_squared_distance( + atom_2.draw.position + ) atom_distances.append((squared_distance, atom_2)) atom_distances.sort(key=lambda x: x[0]) @@ -3174,7 +3954,11 @@ def get_sorted_distances_from_list(atom, atom_list): return atom_distances -def draw_multiple(structure: Structure, coords_only: bool = False, options: Union[None, Options] = None) -> Drawer: +def draw_multiple( + structure: Structure, + coords_only: bool = False, + options: Union[None, Options] = None, +) -> Drawer: if not options: options = Options() options_main = Options() @@ -3211,4 +3995,3 @@ def draw_multiple(structure: Structure, coords_only: bool = False, options: Unio drawer.draw_structure() return drawer - diff --git a/pikachu/drawing/rings.py b/pikachu/drawing/rings.py index f6710d9..e927988 100644 --- a/pikachu/drawing/rings.py +++ b/pikachu/drawing/rings.py @@ -3,6 +3,7 @@ from pikachu.math_functions import Vector import math + def ring_groups_have_overlap(group_1, group_2, ring_overlaps): for ring_1 in group_1: for ring_2 in group_2: @@ -28,7 +29,9 @@ def get_ring_groups(rings, ring_overlaps): ring_group_1_found = False for j, ring_group_2 in enumerate(ring_groups): if i != j: - if ring_groups_have_overlap(ring_group_1, ring_group_2, ring_overlaps): + if ring_groups_have_overlap( + ring_group_1, ring_group_2, ring_overlaps + ): indices = [i, j] new_group = list(set(ring_group_1 + ring_group_2)) ring_group_1_found = True @@ -51,7 +54,10 @@ def get_group_overlap_nr(ring_group, ring_overlaps): overlaps = 0 ring_group = set(ring_group) for ring_overlap in ring_overlaps: - if ring_overlap.ring_id_1 in ring_group and ring_overlap.ring_id_2 in ring_group: + if ( + ring_overlap.ring_id_1 in ring_group + and ring_overlap.ring_id_2 in ring_group + ): overlaps += 1 return overlaps @@ -93,8 +99,8 @@ def __eq__(self, other): return self.id == other.id def __repr__(self): - return str(self.id) + ' ' + '-'.join([atom.__repr__() for atom in self.members]) - + return str(self.id) + " " + "-".join([atom.__repr__() for atom in self.members]) + def get_angle(self): return math.pi - self.central_angle @@ -105,12 +111,16 @@ def get_ordered_neighbours(self, ring_overlaps): atoms = RingOverlap.get_vertices(ring_overlaps, self.id, neighbour_id) ordered_neighbours_and_atom_nrs.append((len(atoms), neighbour_id)) - ordered_neighbours_and_atom_nrs = sorted(ordered_neighbours_and_atom_nrs, key=lambda x: x[0], reverse=True) + ordered_neighbours_and_atom_nrs = sorted( + ordered_neighbours_and_atom_nrs, key=lambda x: x[0], reverse=True + ) ordered_neighbour_ids = [x[1] for x in ordered_neighbours_and_atom_nrs] return ordered_neighbour_ids - def set_member_positions(self, structure, start_atom, previous_atom, center, a, radius, angle): + def set_member_positions( + self, structure, start_atom, previous_atom, center, a, radius, angle + ): current_atom = start_atom iteration = 0 @@ -177,17 +187,19 @@ def is_bridge(self): return False - @staticmethod def get_vertices(ring_overlaps, ring_id_1, ring_id_2): for ring_overlap in ring_overlaps: - if (ring_overlap.ring_id_1 == ring_id_1 and ring_overlap.ring_id_2 == ring_id_2) or\ - (ring_overlap.ring_id_1 == ring_id_2 and ring_overlap.ring_id_2 == ring_id_1): + if ( + ring_overlap.ring_id_1 == ring_id_1 + and ring_overlap.ring_id_2 == ring_id_2 + ) or ( + ring_overlap.ring_id_1 == ring_id_2 + and ring_overlap.ring_id_2 == ring_id_1 + ): return ring_overlap.atoms - - def find_neighbouring_rings(ring_overlaps, ring_id): neighbouring_rings = [] @@ -199,6 +211,7 @@ def find_neighbouring_rings(ring_overlaps, ring_id): return neighbouring_rings + def rings_connected_by_bridge(ring_overlaps, ring_id_1, ring_id_2): for ring_overlap in ring_overlaps: if ring_id_1 == ring_overlap.ring_id_1 and ring_id_2 == ring_overlap.ring_id_2: @@ -207,5 +220,3 @@ def rings_connected_by_bridge(ring_overlaps, ring_id_1, ring_id_2): return ring_overlap.is_bridge() return False - - diff --git a/pikachu/drawing/sssr.py b/pikachu/drawing/sssr.py index 8157756..6c815ba 100644 --- a/pikachu/drawing/sssr.py +++ b/pikachu/drawing/sssr.py @@ -52,10 +52,14 @@ def get_rings(self): rings.append(component) continue - d, pe, pe_prime = self.get_path_included_distance_matrices(cc_adjacency_matrix) + d, pe, pe_prime = self.get_path_included_distance_matrices( + cc_adjacency_matrix + ) ring_candidates = self.get_ring_candidates(d, pe, pe_prime) - c_sssr = self.get_sssr(ring_candidates, cc_adjacency_matrix, bond_counts, ring_counts, sssr_nr) + c_sssr = self.get_sssr( + ring_candidates, cc_adjacency_matrix, bond_counts, ring_counts, sssr_nr + ) for ring in c_sssr: original_ring = self.get_original_ring_order(list(ring)) @@ -122,7 +126,6 @@ def get_component_adjacency_matrix(self): adjacency_matrix[bond.atom_1][bond.atom_2] = 1 adjacency_matrix[bond.atom_2][bond.atom_1] = 1 - bridges = self.get_bridges() for bond in bridges: @@ -157,9 +160,9 @@ def get_bridges(self): for atom in self.graph: visited[atom] = False - # disc[atom] = 0 + # disc[atom] = 0 parent[atom] = None - # low[atom] = 0 + # low[atom] = 0 for atom in self.graph: if not visited[atom]: @@ -191,7 +194,7 @@ def dfs_bridges(self, atom, visited, disc, low, parent, bridges): def get_path_included_distance_matrices(self, adjacency_matrix): """ - Use Floyd-Warshall algorithm to compute the shortest paths between all vertex pairs in a graph + Use Floyd-Warshall algorithm to compute the shortest paths between all vertex pairs in a graph """ atoms = list(adjacency_matrix.keys()) @@ -213,7 +216,7 @@ def get_path_included_distance_matrices(self, adjacency_matrix): if atom_1 == atom_2 or adjacency_matrix[atom_1][atom_2] == 1: d[atom_1][atom_2] = adjacency_matrix[atom_1][atom_2] else: - d[atom_1][atom_2] = float('inf') + d[atom_1][atom_2] = float("inf") # For neighbours: set the pe @@ -286,9 +289,11 @@ def get_ring_candidates(self, d, pe, pe_prime): for atom_1 in d: for atom_2 in d[atom_1]: - # If the atom distance is 0, or if there's only one shortest path and there is no + # If the atom distance is 0, or if there's only one shortest path and there is no # shortest path one longer than the shortest path - if d[atom_1][atom_2] == 0 or (len(pe[atom_1][atom_2]) == 1 and len(pe_prime[atom_1][atom_2]) == 0): + if d[atom_1][atom_2] == 0 or ( + len(pe[atom_1][atom_2]) == 1 and len(pe_prime[atom_1][atom_2]) == 0 + ): continue else: if len(pe[atom_1][atom_2]) > 1: @@ -298,14 +303,22 @@ def get_ring_candidates(self, d, pe, pe_prime): # else: # vertices_in_cycle = 2 * (d[atom_1][atom_2]) - if vertices_in_cycle != float('inf'): - candidates.append([vertices_in_cycle, pe[atom_1][atom_2], pe_prime[atom_1][atom_2]]) + if vertices_in_cycle != float("inf"): + candidates.append( + [ + vertices_in_cycle, + pe[atom_1][atom_2], + pe_prime[atom_1][atom_2], + ] + ) candidates = sorted(candidates, key=lambda x: x[0]) return candidates - def get_sssr(self, ring_candidates, cc_adjacency_matrix, bond_counts, ring_counts, sssr_nr): + def get_sssr( + self, ring_candidates, cc_adjacency_matrix, bond_counts, ring_counts, sssr_nr + ): c_sssr = [] all_bonds = set() @@ -319,8 +332,9 @@ def get_sssr(self, ring_candidates, cc_adjacency_matrix, bond_counts, ring_count atoms = self.bonds_to_atoms(bonds) bond_count = self.get_bond_count(atoms, cc_adjacency_matrix) - if bond_count == len(atoms) and not self.path_sets_contain(c_sssr, atoms, bonds, all_bonds, - bond_counts, ring_counts): + if bond_count == len(atoms) and not self.path_sets_contain( + c_sssr, atoms, bonds, all_bonds, bond_counts, ring_counts + ): c_sssr.append(atoms) for bond in bonds: all_bonds.add(bond) @@ -335,8 +349,9 @@ def get_sssr(self, ring_candidates, cc_adjacency_matrix, bond_counts, ring_count atoms = self.bonds_to_atoms(bonds) bond_count = self.get_bond_count(atoms, cc_adjacency_matrix) - if bond_count == len(atoms) and not self.path_sets_contain(c_sssr, atoms, bonds, all_bonds, - bond_counts, ring_counts): + if bond_count == len(atoms) and not self.path_sets_contain( + c_sssr, atoms, bonds, all_bonds, bond_counts, ring_counts + ): c_sssr.append(atoms) for bond in bonds: all_bonds.add(bond) @@ -380,7 +395,9 @@ def sets_equal(self, set_1, set_2): return True - def path_sets_contain(self, c_sssr, atoms, bonds, all_bonds, bond_counts, ring_counts): + def path_sets_contain( + self, c_sssr, atoms, bonds, all_bonds, bond_counts, ring_counts + ): for candidate_ring in c_sssr: if self.is_superset(atoms, candidate_ring): return True @@ -396,7 +413,7 @@ def path_sets_contain(self, c_sssr, atoms, bonds, all_bonds, bond_counts, ring_c if self.is_superset(all_bonds, bonds): all_contained = True - #special_case - see smiles drawer code + # special_case - see smiles drawer code special_case = False if all_contained: diff --git a/pikachu/errors.py b/pikachu/errors.py index fef3059..fde5e71 100644 --- a/pikachu/errors.py +++ b/pikachu/errors.py @@ -1,15 +1,18 @@ #!/usr/bin/env python + class StructureError(Exception): - error_to_message = {'chiral double bond': "Conflicting double bond stereochemistry.", - 'invalid smiles': "Invalid smiles.", - 'bond': "Incorrect bond placement.", - 'violated_bonding_laws': "Basic bonding laws have been violated.", - 'chiral centre': "Non-chiral atom defined as chiral.", - 'aromaticity': "Aromaticity incorrectly defined.", - 'sigma bond': "Can't form enough sigma bonds.", - 'pi bond': "Can't form enough pi bonds.", - 'aromatic p orbital': "Aromatic system lacks p-orbital"} + error_to_message = { + "chiral double bond": "Conflicting double bond stereochemistry.", + "invalid smiles": "Invalid smiles.", + "bond": "Incorrect bond placement.", + "violated_bonding_laws": "Basic bonding laws have been violated.", + "chiral centre": "Non-chiral atom defined as chiral.", + "aromaticity": "Aromaticity incorrectly defined.", + "sigma bond": "Can't form enough sigma bonds.", + "pi bond": "Can't form enough pi bonds.", + "aromatic p orbital": "Aromatic system lacks p-orbital", + } def __init__(self, error_type): self.message = self.error_to_message[error_type] @@ -21,8 +24,10 @@ def __init__(self, aromatic_system): class DrawingError(Exception): - error_to_message = {'chiral bond ring': "PIKAChU could not correctly draw the cis/trans stereochemistry of a double bond in a cycle.", - 'chiral center': "Too few elements attached to chiral center, including hydrogens and lone pairs."} + error_to_message = { + "chiral bond ring": "PIKAChU could not correctly draw the cis/trans stereochemistry of a double bond in a cycle.", + "chiral center": "Too few elements attached to chiral center, including hydrogens and lone pairs.", + } def __init__(self, error_type): self.message = self.error_to_message[error_type] @@ -31,7 +36,7 @@ def __init__(self, error_type): class ColourError(Exception): def __init__(self, colour): if type(colour) == str: - if colour == 'too few colours': + if colour == "too few colours": self.message = f"Pikachu has too few colours to work with." else: diff --git a/pikachu/fingerprinting/daylight.py b/pikachu/fingerprinting/daylight.py index ddcf82c..f6958ec 100644 --- a/pikachu/fingerprinting/daylight.py +++ b/pikachu/fingerprinting/daylight.py @@ -33,8 +33,8 @@ def get_hash(self): daylight_hash.update(str(attribute).encode()) # return int.from_bytes(hashlib.sha256(b"H").digest()[:4], 'little') - #print(hash(tuple(self.daylight))) - # print(hash(tuple(self.daylight))) + # print(hash(tuple(self.daylight))) + # print(hash(tuple(self.daylight))) return hash(tuple(self.daylight)) def atom_in_cycle(self): @@ -49,18 +49,18 @@ def atom_in_cycle(self): def get_heavy_neighbours(self): heavy_neighbours = 0 for atom in self.atom.neighbours: - if atom.type != 'H': + if atom.type != "H": heavy_neighbours += 1 return heavy_neighbours def get_hydrogen_number(self): hydrogen_nr = 0 for atom in self.atom.neighbours: - if atom.type == 'H': + if atom.type == "H": hydrogen_nr += 1 return hydrogen_nr def get_valence_minus_h(self): valence = self.atom.calc_bond_nr() - return valence - self.get_hydrogen_number() \ No newline at end of file + return valence - self.get_hydrogen_number() diff --git a/pikachu/fingerprinting/ecfp_4.py b/pikachu/fingerprinting/ecfp_4.py index 83b1534..701fa55 100644 --- a/pikachu/fingerprinting/ecfp_4.py +++ b/pikachu/fingerprinting/ecfp_4.py @@ -22,7 +22,7 @@ def __init__(self, structure, iterations=2): def set_initial_identifiers(self): for atom in self.structure.graph: - if atom.type != 'H' and atom.type != '*': + if atom.type != "H" and atom.type != "*": if atom.chiral: self.disambiguated_chiral[atom] = False @@ -38,7 +38,9 @@ def set_initial_identifiers(self): bonds = set(atom.get_non_hydrogen_bonds()) self.bonds[initial_identifier] = bonds - feature = tuple(sorted(list(bonds) + [atom], key=lambda x: (x.nr, x.type))) + feature = tuple( + sorted(list(bonds) + [atom], key=lambda x: (x.nr, x.type)) + ) self.features[feature] = (initial_identifier, 0, atom) self.hash_to_feature[initial_identifier] = feature @@ -88,18 +90,23 @@ def ecfp(self): neighbour_identifier = self.identifiers[neighbour][i] neighbour_identifiers_sorted.append(neighbour_identifier) - if len(neighbour_identifiers_sorted) == len(set(neighbour_identifiers_sorted)): + if len(neighbour_identifiers_sorted) == len( + set(neighbour_identifiers_sorted) + ): for neighbour in atom.neighbours: - if neighbour.type == 'H': - neighbour_identifier = 'dummy' + if neighbour.type == "H": + neighbour_identifier = "dummy" else: neighbour_identifier = self.identifiers[neighbour][i] neighbour_identifiers.append(neighbour_identifier) - chirality = find_chirality_from_nonh(neighbour_identifiers, neighbour_identifiers_sorted, - atom.chiral) - if chirality == 'clockwise': + chirality = find_chirality_from_nonh( + neighbour_identifiers, + neighbour_identifiers_sorted, + atom.chiral, + ) + if chirality == "clockwise": array.append(1) else: array.append(0) @@ -116,13 +123,18 @@ def ecfp(self): bond_set = bond_set.union(neighbouring_bonds) self.bonds[new_identifier] = bond_set - feature = tuple(sorted(list(bond_set) + list(self.seen_atoms[atom][i + 1]), key=lambda x: (x.nr, x.type))) + feature = tuple( + sorted( + list(bond_set) + list(self.seen_atoms[atom][i + 1]), + key=lambda x: (x.nr, x.type), + ) + ) if feature not in self.features: new_features.append((feature, new_identifier, atom)) new_features.sort(key=lambda x: tuple([y.nr for y in x[0]] + [x[1]])) - #new_features.sort() + # new_features.sort() previous_feature = None previous_atom = None @@ -156,14 +168,12 @@ def build_ecfp_bitvector(structures, depth=2, bits=1024): substructure_to_count[identifier] = 0 substructure_to_count[identifier] += 1 - substructures = sorted(list(substructure_to_count.items()), key=lambda x: x[1], reverse=True) + substructures = sorted( + list(substructure_to_count.items()), key=lambda x: x[1], reverse=True + ) bitvector_substructures = [x[0] for x in substructures[:bits]] bitvector_mapping = {} for substructure in bitvector_substructures: bitvector_mapping[substructure] = identifier_to_feature[substructure] return bitvector_substructures, bitvector_mapping - - - - diff --git a/pikachu/fingerprinting/hashing.py b/pikachu/fingerprinting/hashing.py index 3dcfc0d..6f20945 100644 --- a/pikachu/fingerprinting/hashing.py +++ b/pikachu/fingerprinting/hashing.py @@ -11,6 +11,6 @@ def hash_32_bit_integer(iterable): for attribute in iterable: hash.update(str(attribute).encode()) - hash_32 = int.from_bytes(hash.digest()[:4], byteorder='little') + hash_32 = int.from_bytes(hash.digest()[:4], byteorder="little") return hash_32 diff --git a/pikachu/fingerprinting/similarity.py b/pikachu/fingerprinting/similarity.py index 1e7464e..f548387 100644 --- a/pikachu/fingerprinting/similarity.py +++ b/pikachu/fingerprinting/similarity.py @@ -5,21 +5,26 @@ def get_jaccard_index(structure_1, structure_2, fingerprinting_depth=2): ecfp_1 = ECFP(structure_1, iterations=fingerprinting_depth) ecfp_2 = ECFP(structure_2, iterations=fingerprinting_depth) - jaccard_index = len(ecfp_1.fingerprint.intersection(ecfp_2.fingerprint)) / len(ecfp_1.fingerprint.union(ecfp_2.fingerprint)) + jaccard_index = len(ecfp_1.fingerprint.intersection(ecfp_2.fingerprint)) / len( + ecfp_1.fingerprint.union(ecfp_2.fingerprint) + ) return jaccard_index def get_jaccard_from_ecfp(ecfp_1, ecfp_2): jaccard_index = len(ecfp_1.fingerprint.intersection(ecfp_2.fingerprint)) / len( - ecfp_1.fingerprint.union(ecfp_2.fingerprint)) + ecfp_1.fingerprint.union(ecfp_2.fingerprint) + ) jaccard_distance = 1 - jaccard_index return jaccard_distance def get_jaccard_distance(structure_1, structure_2, fingerprinting_depth=2): - jaccard_index = get_jaccard_index(structure_1, structure_2, fingerprinting_depth=fingerprinting_depth) + jaccard_index = get_jaccard_index( + structure_1, structure_2, fingerprinting_depth=fingerprinting_depth + ) jaccard_distance = 1 - jaccard_index return jaccard_distance diff --git a/pikachu/general.py b/pikachu/general.py index ff7e716..cb1db9c 100644 --- a/pikachu/general.py +++ b/pikachu/general.py @@ -16,19 +16,19 @@ def smiles_from_file(smiles_file, read_all=False): if not read_all: - with open(smiles_file, 'r') as smiles: + with open(smiles_file, "r") as smiles: smiles_string = smiles.readline().strip() return smiles_string else: smiles_strings = [] - with open(smiles_file, 'r') as smiles: + with open(smiles_file, "r") as smiles: for line in smiles: smiles_string = line.strip() smiles_strings.append(smiles_string) return smiles_strings - + def read_smiles(smiles_string: str) -> Structure: """ @@ -49,7 +49,9 @@ def read_smiles(smiles_string: str) -> Structure: smiles = Smiles(smiles_string) structure = smiles.smiles_to_structure() if not structure: - raise ValueError(f"Could not produce structure for SMILES: {smiles_string}.") + raise ValueError( + f"Could not produce structure for SMILES: {smiles_string}." + ) return structure @@ -103,7 +105,7 @@ def position_smiles(smiles): """ structure = read_smiles(smiles) - if '.' in smiles: + if "." in smiles: drawer = draw_multiple(structure, coords_only=True) else: drawer = Drawer(structure, coords_only=True) @@ -124,13 +126,13 @@ def draw_smiles(smiles, options=None): options = Options() structure = read_smiles(smiles) - if '.' in smiles: + if "." in smiles: drawer = draw_multiple(structure, options=options) else: drawer = Drawer(structure, options=options) - + drawer.show_molecule() @@ -139,8 +141,10 @@ def smiles_to_molfile(smiles, molfile, options=None): options = Options() structure = read_smiles(smiles) - if '.' in smiles: - MolFileWriter(structure, molfile, drawing_options=options, multiple=True).write_mol_file() + if "." in smiles: + MolFileWriter( + structure, molfile, drawing_options=options, multiple=True + ).write_mol_file() else: MolFileWriter(structure, molfile, drawing_options=options).write_mol_file() @@ -223,7 +227,7 @@ def svg_from_smiles(smiles, svg_out, options=None): if not options: options = Options() - if '.' in smiles: + if "." in smiles: drawer = draw_multiple(structure, options=options) else: drawer = Drawer(structure, options=options, coords_only=True) @@ -246,12 +250,16 @@ def png_from_smiles(smiles, png_out, options=None): drawer.save_png(png_out) -def highlight_substructure(substructure_smiles, parent_smiles, search_mode='all', - colour=None, - check_chiral_centres=True, - check_bond_chirality=True, - visualisation='show', - out_file=None): +def highlight_substructure( + substructure_smiles, + parent_smiles, + search_mode="all", + colour=None, + check_chiral_centres=True, + check_bond_chirality=True, + visualisation="show", + out_file=None, +): """ Find occurrences of (a) substructure(s) in a parent structure and highlight it in a drawing @@ -274,43 +282,59 @@ def highlight_substructure(substructure_smiles, parent_smiles, search_mode='all' out_file: str, output file of png or svg drawing """ - assert search_mode in {'all', 'single', 'multiple'} + assert search_mode in {"all", "single", "multiple"} - if search_mode == 'all' or search_mode == 'single': + if search_mode == "all" or search_mode == "single": assert type(substructure_smiles) == str if colour: assert type(colour) in {str} else: colour = RASPBERRY - elif search_mode == 'multiple': + elif search_mode == "multiple": assert type(substructure_smiles) in {list, tuple, set} assert type(colour) in {list, tuple, set} - if search_mode == 'all': - highlight_subsmiles_all(substructure_smiles, parent_smiles, colour=colour, - check_chiral_centres=check_chiral_centres, - check_bond_chirality=check_bond_chirality, - visualisation=visualisation, - out_file=out_file) - elif search_mode == 'multiple': - highlight_subsmiles_multiple(substructure_smiles, parent_smiles, colours=colour, - check_chiral_centres=check_chiral_centres, - check_bond_chirality=check_bond_chirality, - visualisation=visualisation, - out_file=out_file) - elif search_mode == 'single': - highlight_subsmiles_single(substructure_smiles, parent_smiles, colour=colour, - check_chiral_centres=check_chiral_centres, - check_bond_chirality=check_bond_chirality, - visualisation=visualisation, - out_file=out_file) - - -def highlight_subsmiles_single(substructure_smiles, parent_smiles, colour=RASPBERRY, - check_chiral_centres=True, - check_bond_chirality=True, - visualisation='show', - out_file=None): + if search_mode == "all": + highlight_subsmiles_all( + substructure_smiles, + parent_smiles, + colour=colour, + check_chiral_centres=check_chiral_centres, + check_bond_chirality=check_bond_chirality, + visualisation=visualisation, + out_file=out_file, + ) + elif search_mode == "multiple": + highlight_subsmiles_multiple( + substructure_smiles, + parent_smiles, + colours=colour, + check_chiral_centres=check_chiral_centres, + check_bond_chirality=check_bond_chirality, + visualisation=visualisation, + out_file=out_file, + ) + elif search_mode == "single": + highlight_subsmiles_single( + substructure_smiles, + parent_smiles, + colour=colour, + check_chiral_centres=check_chiral_centres, + check_bond_chirality=check_bond_chirality, + visualisation=visualisation, + out_file=out_file, + ) + + +def highlight_subsmiles_single( + substructure_smiles, + parent_smiles, + colour=RASPBERRY, + check_chiral_centres=True, + check_bond_chirality=True, + visualisation="show", + out_file=None, +): """ Draw structure with a single occurrence of substructure_smiles highlighted with colour @@ -332,29 +356,36 @@ def highlight_subsmiles_single(substructure_smiles, parent_smiles, colour=RASPBE child_structure = read_smiles(substructure_smiles) parent_structure = read_smiles(parent_smiles) - if not colour.startswith('#'): + if not colour.startswith("#"): colour = get_hex(colour) - - parent_structure.colour_substructure_single(child_structure, colour=colour, - check_chiral_centres=check_chiral_centres, - check_bond_chirality=check_bond_chirality) + + parent_structure.colour_substructure_single( + child_structure, + colour=colour, + check_chiral_centres=check_chiral_centres, + check_bond_chirality=check_bond_chirality, + ) drawer = Drawer(parent_structure) - if visualisation == 'show': + if visualisation == "show": drawer.show_molecule() - elif visualisation == 'svg': + elif visualisation == "svg": assert out_file drawer.save_svg(out_file) - elif visualisation == 'png': + elif visualisation == "png": assert out_file drawer.save_png(out_file) -def highlight_subsmiles_all(substructure_smiles, parent_smiles, colour=RASPBERRY, - check_chiral_centres=True, - check_bond_chirality=True, - visualisation='show', - out_file=None): +def highlight_subsmiles_all( + substructure_smiles, + parent_smiles, + colour=RASPBERRY, + check_chiral_centres=True, + check_bond_chirality=True, + visualisation="show", + out_file=None, +): """ Draw structure with all occurrences of substructure_smiles highlighted with colour @@ -376,29 +407,36 @@ def highlight_subsmiles_all(substructure_smiles, parent_smiles, colour=RASPBERRY child_structure = read_smiles(substructure_smiles) parent_structure = read_smiles(parent_smiles) - if not colour.startswith('#'): + if not colour.startswith("#"): colour = get_hex(colour) - parent_structure.colour_substructure_all(child_structure, colour=colour, - check_chiral_centres=check_chiral_centres, - check_bond_chirality=check_bond_chirality) + parent_structure.colour_substructure_all( + child_structure, + colour=colour, + check_chiral_centres=check_chiral_centres, + check_bond_chirality=check_bond_chirality, + ) drawer = Drawer(parent_structure) - if visualisation == 'show': + if visualisation == "show": drawer.show_molecule() - elif visualisation == 'svg': + elif visualisation == "svg": assert out_file drawer.save_svg(out_file) - elif visualisation == 'png': + elif visualisation == "png": assert out_file drawer.save_png(out_file) -def highlight_subsmiles_multiple(substructure_smiles_list, parent_smiles, colours=None, - check_chiral_centres=True, - check_bond_chirality=True, - visualisation='show', - out_file=None): +def highlight_subsmiles_multiple( + substructure_smiles_list, + parent_smiles, + colours=None, + check_chiral_centres=True, + check_bond_chirality=True, + visualisation="show", + out_file=None, +): """ Draw structure with all occurrences of all substructure_smiles highlighted in different colours @@ -437,21 +475,24 @@ def highlight_subsmiles_multiple(substructure_smiles_list, parent_smiles, colour try: assert len(colour_list) == smiles_nr except AssertionError: - raise ColourError('too few colours') + raise ColourError("too few colours") for i, smiles in enumerate(substructure_smiles_list): child_structure = read_smiles(smiles) colour = colour_list[i] - parent_structure.colour_substructure_all(child_structure, colour=colour, - check_chiral_centres=check_chiral_centres, - check_bond_chirality=check_bond_chirality) + parent_structure.colour_substructure_all( + child_structure, + colour=colour, + check_chiral_centres=check_chiral_centres, + check_bond_chirality=check_bond_chirality, + ) drawer = Drawer(parent_structure) - if visualisation == 'show': + if visualisation == "show": drawer.show_molecule() - elif visualisation == 'svg': + elif visualisation == "svg": assert out_file drawer.save_svg(out_file) - elif visualisation == 'png': + elif visualisation == "png": assert out_file drawer.save_png(out_file) diff --git a/pikachu/inchi/inchi.py b/pikachu/inchi/inchi.py index 69930b3..986b920 100644 --- a/pikachu/inchi/inchi.py +++ b/pikachu/inchi/inchi.py @@ -8,17 +8,19 @@ class InChI: def __init__(self, inchi): self.inchi = inchi - self.layers = {'version': '', - 'formula': '', - 'connectivity': '', - 'hydrogens': '', - 'charge': '', - 'protonation': '', - 'double bond stereochemistry': '', - 'tetrahedral stereochemistry': '', - 'allene stereochemistry': '', - 'stereochemistry information': '', - 'isotopic layer': ''} + self.layers = { + "version": "", + "formula": "", + "connectivity": "", + "hydrogens": "", + "charge": "", + "protonation": "", + "double bond stereochemistry": "", + "tetrahedral stereochemistry": "", + "allene stereochemistry": "", + "stereochemistry information": "", + "isotopic layer": "", + } self.graph = {} self.atoms = [] @@ -48,21 +50,21 @@ def inchi_to_structure(self): print(self.atom_to_cyclic) def get_layers(self): - layers = self.inchi.split(r'/') - version = layers[0].split('InChI=')[-1] - version = version.split('S')[0] - self.layers['version'] = version - self.layers['formula'] = layers[1] + layers = self.inchi.split(r"/") + version = layers[0].split("InChI=")[-1] + version = version.split("S")[0] + self.layers["version"] = version + self.layers["formula"] = layers[1] fixed_h_layer = False for layer in layers[2:]: prefix = layer[0] - if prefix == 'c': - self.layers['connectivity'] = layer[1:] - elif prefix == 'h': - self.layers['hydrogens'] = layer[1:] + if prefix == "c": + self.layers["connectivity"] = layer[1:] + elif prefix == "h": + self.layers["hydrogens"] = layer[1:] print(self.layers) def parse_formula_layer(self): @@ -72,7 +74,7 @@ def parse_formula_layer(self): atom = [] digit = [] - formula_layer = self.layers['formula'] + formula_layer = self.layers["formula"] for i, symbol in enumerate(formula_layer): previous_symbol = None if i != 0: @@ -85,13 +87,13 @@ def parse_formula_layer(self): # Nothing to record yet if it is the first atom type of the list if previous_symbol: - atom_type = ''.join(atom) + atom_type = "".join(atom) # If there is more than one of the previous atom type if previous_symbol.isdigit(): - atom_number = int(''.join(digit)) - if atom_type != 'H': + atom_number = int("".join(digit)) + if atom_type != "H": for i in range(atom_number): atom_type_list.append(atom_type) else: @@ -100,7 +102,7 @@ def parse_formula_layer(self): # Happens if there is only one of the previous atom type else: - if atom_type != 'H': + if atom_type != "H": atom_type_list.append(atom_type) else: hydrogen_nr = 1 @@ -122,14 +124,14 @@ def parse_formula_layer(self): # If there is more than one of the previous atom type - atom_type = ''.join(atom) + atom_type = "".join(atom) # More than one of the last atom type if formula_layer[-1].isdigit(): - atom_number = int(''.join(digit)) - if atom_type != 'H': + atom_number = int("".join(digit)) + if atom_type != "H": for i in range(atom_number): atom_type_list.append(atom_type) else: @@ -138,17 +140,17 @@ def parse_formula_layer(self): # Happens if there is only one of the last atom type else: - if atom_type != 'H': + if atom_type != "H": atom_type_list.append(atom_type) else: hydrogen_nr = 1 for i in range(hydrogen_nr): - atom_type_list.append('H') + atom_type_list.append("H") for i, atom_type in enumerate(atom_type_list): atom = Atom(atom_type, i, None, 0, False) - if atom_type != 'H': + if atom_type != "H": self.atoms.append(atom) else: self.hydrogens.append(atom) @@ -156,17 +158,17 @@ def parse_formula_layer(self): def get_connectivity_components(self): components = [] digit = [] - for i, symbol in enumerate(self.layers['connectivity']): + for i, symbol in enumerate(self.layers["connectivity"]): if symbol.isdigit(): digit.append(symbol) else: - digit_int = int(''.join(digit)) + digit_int = int("".join(digit)) components.append(digit_int) components.append(symbol) digit = [] - if self.layers['connectivity'][-1].isdigit(): - digit_int = int(''.join(digit)) + if self.layers["connectivity"][-1].isdigit(): + digit_int = int("".join(digit)) components.append(digit_int) return components @@ -189,14 +191,14 @@ def parse_connectivity_layer(self): # Entering a new branch - if component == '(': + if component == "(": current_branch += 1 if current_branch not in branch_to_previous_atom: branch_to_previous_atom[current_branch] = None # Terminating a branch - elif component == ')': + elif component == ")": current_branch -= 1 elif type(component) == int: @@ -207,11 +209,11 @@ def parse_connectivity_layer(self): # Previous atom is in the current branch - if previous_component == '-' or previous_component == ')': + if previous_component == "-" or previous_component == ")": previous_atom = branch_to_previous_atom[current_branch] # Previous atom is in the previous branch - elif previous_component == ',' or previous_component == '(': + elif previous_component == "," or previous_component == "(": previous_atom = branch_to_previous_atom[current_branch - 1] # This is the first atom; there is no previous atom @@ -228,33 +230,33 @@ def parse_connectivity_layer(self): previous_component = component def get_hydrogen_components(self): - hydrogen_layer = self.layers['hydrogens'] + hydrogen_layer = self.layers["hydrogens"] components = [] component = [] between_brackets = False hydrogen_active = False for i, symbol in enumerate(hydrogen_layer): - if symbol == '(': + if symbol == "(": if component: components.append(component) component = [symbol] between_brackets = True - elif symbol == ')': + elif symbol == ")": component.append(symbol) between_brackets = False components.append(component) component = [] - elif symbol == 'H': + elif symbol == "H": hydrogen_active = True component.append(symbol) - elif symbol.isdigit() or symbol == '-': + elif symbol.isdigit() or symbol == "-": component.append(symbol) - elif symbol == ',': + elif symbol == ",": component.append(symbol) if hydrogen_active and not between_brackets: components.append(component) @@ -266,7 +268,7 @@ def get_hydrogen_components(self): components.append(component) for i, component in enumerate(components): - components[i] = ''.join(component) + components[i] = "".join(component) return components @@ -275,31 +277,31 @@ def parse_hydrogen_component(self, component): hydrogen_position = None last_symbol = component[-1] - if last_symbol == ')': - component_type = 'shared' + if last_symbol == ")": + component_type = "shared" hydrogen_position = 1 else: - component_type = 'single' - if last_symbol == 'H': + component_type = "single" + if last_symbol == "H": hydrogen_position = len(component) - 1 else: for i in range(len(component) - 1, -1, -1): symbol = component[i] - if symbol == 'H': + if symbol == "H": hydrogen_position = i break digit = [] - for symbol in component[hydrogen_position + 1:]: + for symbol in component[hydrogen_position + 1 :]: if symbol.isdigit(): digit.append(symbol) else: break if digit: - hydrogen_nr = int(''.join(digit)) + hydrogen_nr = int("".join(digit)) else: hydrogen_nr = 1 @@ -312,18 +314,18 @@ def parse_hydrogen_component(self, component): for symbol in component: if symbol.isdigit() and not hydrogen_active: atom.append(symbol) - elif symbol == '-': + elif symbol == "-": - current_atom = int(''.join(atom)) + current_atom = int("".join(atom)) atoms.append(current_atom) atom = [] range_active = True - elif symbol == 'H': + elif symbol == "H": hydrogen_active = True if atom: - current_atom = int(''.join(atom)) + current_atom = int("".join(atom)) if not range_active: atoms.append(current_atom) else: @@ -334,10 +336,10 @@ def parse_hydrogen_component(self, component): range_active = False - elif symbol == ',': + elif symbol == ",": if atom and not hydrogen_active: - current_atom = int(''.join(atom)) + current_atom = int("".join(atom)) if not range_active: atoms.append(current_atom) else: @@ -352,7 +354,7 @@ def parse_hydrogen_component(self, component): else: if atom: - current_atom = int(''.join(atom)) + current_atom = int("".join(atom)) if not range_active: atoms.append(current_atom) else: @@ -363,7 +365,7 @@ def parse_hydrogen_component(self, component): if atom: print("Shouldn't happen!") - current_atom = int(''.join(atom)) + current_atom = int("".join(atom)) atoms.append(current_atom) atom_objects = [] @@ -379,7 +381,7 @@ def add_hydrogens_to_graph(self): if hydrogen_info: component_type = hydrogen_info[0] hydrogen_nr = hydrogen_info[1] - if component_type == 'single': + if component_type == "single": for i in range(hydrogen_nr): hydrogen = self.hydrogens.pop() self.graph[atom].append(hydrogen) @@ -389,11 +391,9 @@ def add_hydrogens_to_graph(self): if hydrogen_info: component_type = hydrogen_info[0] hydrogen_nr = hydrogen_info[1] - if component_type == 'shared': + if component_type == "shared": atoms = hydrogen_info[2] - - def parse_hydrogen_layer(self): for atom in self.atoms: self.atom_to_hydrogens[atom] = [] @@ -401,29 +401,20 @@ def parse_hydrogen_layer(self): components = self.get_hydrogen_components() print(components) for component in components: - atoms, hydrogen_nr, component_type = self.parse_hydrogen_component(component) + atoms, hydrogen_nr, component_type = self.parse_hydrogen_component( + component + ) for atom in atoms: - if component_type == 'single': + if component_type == "single": self.atom_to_hydrogens[atom].append((component_type, hydrogen_nr)) else: - self.atom_to_hydrogens[atom].append((component_type, hydrogen_nr, atoms)) + self.atom_to_hydrogens[atom].append( + (component_type, hydrogen_nr, atoms) + ) pprint(self.atom_to_hydrogens) for atom, hydrogens in self.atom_to_hydrogens.items(): print(atom) print(self.graph[atom]) print(hydrogens) - print('\n') - - - - - - - - - - - - - + print("\n") diff --git a/pikachu/math_functions.py b/pikachu/math_functions.py index 4426219..2bb755c 100644 --- a/pikachu/math_functions.py +++ b/pikachu/math_functions.py @@ -1,74 +1,60 @@ #!/usr/bin/env python import math import matplotlib -#matplotlib.use('TkAgg') + +# matplotlib.use('TkAgg') from matplotlib import pyplot as plt class Permutations: - permutation_mapping = {0: {0: 0, - 1: 1, - 2: 2, - 3: 3}, - 1: {0: 0, - 1: 1, - 2: 3, - 3: 2}, - 2: {0: 0, - 1: 2, - 2: 1, - 3: 3}, - 3: {0: 0, - 1: 2, - 2: 3, - 3: 1}, - 4: {0: 0, - 1: 3, - 2: 1, - 3: 2}, - 5: {0: 0, - 1: 3, - 2: 2, - 3: 1}} - - permutation_mapping = {0: [0, 1, 2, 3], - 1: [0, 1, 3, 2], - 2: [0, 2, 1, 3], - 3: [0, 2, 3, 1], - 4: [0, 3, 1, 2], - 5: [0, 3, 2, 1]} - - triplet_mapping = {0: {0: 0, - 1: 1, - 2: 2}, - 1: {0: 1, - 1: 2, - 2: 3}, - 2: {0: 2, - 2: 3, - 3: 0}, - 3: {0: 3, - 1: 0, - 2: 1}} + permutation_mapping = { + 0: {0: 0, 1: 1, 2: 2, 3: 3}, + 1: {0: 0, 1: 1, 2: 3, 3: 2}, + 2: {0: 0, 1: 2, 2: 1, 3: 3}, + 3: {0: 0, 1: 2, 2: 3, 3: 1}, + 4: {0: 0, 1: 3, 2: 1, 3: 2}, + 5: {0: 0, 1: 3, 2: 2, 3: 1}, + } + + permutation_mapping = { + 0: [0, 1, 2, 3], + 1: [0, 1, 3, 2], + 2: [0, 2, 1, 3], + 3: [0, 2, 3, 1], + 4: [0, 3, 1, 2], + 5: [0, 3, 2, 1], + } + + triplet_mapping = { + 0: {0: 0, 1: 1, 2: 2}, + 1: {0: 1, 1: 2, 2: 3}, + 2: {0: 2, 2: 3, 3: 0}, + 3: {0: 3, 1: 0, 2: 1}, + } + def __init__(self): pass @staticmethod def get_circular_permutations_4(order): - return [(order[0], order[1], order[2], order[3]), - (order[0], order[1], order[3], order[2]), - (order[0], order[2], order[1], order[3]), - (order[0], order[2], order[3], order[1]), - (order[0], order[3], order[1], order[2]), - (order[0], order[3], order[2], order[1])] + return [ + (order[0], order[1], order[2], order[3]), + (order[0], order[1], order[3], order[2]), + (order[0], order[2], order[1], order[3]), + (order[0], order[2], order[3], order[1]), + (order[0], order[3], order[1], order[2]), + (order[0], order[3], order[2], order[1]), + ] @staticmethod def get_node_triplet_arcs(quadruplet): - return [(quadruplet[0], quadruplet[1], quadruplet[2]), - (quadruplet[1], quadruplet[2], quadruplet[3]), - (quadruplet[2], quadruplet[3], quadruplet[0]), - (quadruplet[3], quadruplet[0], quadruplet[1])] + return [ + (quadruplet[0], quadruplet[1], quadruplet[2]), + (quadruplet[1], quadruplet[2], quadruplet[3]), + (quadruplet[2], quadruplet[3], quadruplet[0]), + (quadruplet[3], quadruplet[0], quadruplet[1]), + ] class SimpleLine: @@ -107,18 +93,24 @@ def get_perpendicular_points(self, distance, point): dx = abs(math.sin(angle) * distance) dy = abs(math.cos(angle) * distance) - point_1 = Vector(dx * direction_combinations[0][0] + point.x, - dy * direction_combinations[0][1] + point.y) + point_1 = Vector( + dx * direction_combinations[0][0] + point.x, + dy * direction_combinations[0][1] + point.y, + ) - point_2 = Vector(dx * direction_combinations[1][0] + point.x, - dy * direction_combinations[1][1] + point.y) + point_2 = Vector( + dx * direction_combinations[1][0] + point.x, + dy * direction_combinations[1][1] + point.y, + ) return point_1, point_2 def get_bond_wedge_front(self, width, chiral_centre): half_width = width * 0.5 - point_1_mid, point_2_mid = self.get_perpendicular_points(half_width, self.point_2) + point_1_mid, point_2_mid = self.get_perpendicular_points( + half_width, self.point_2 + ) if self.atom == chiral_centre: return self.point_1, point_1_mid, point_2_mid else: @@ -134,8 +126,12 @@ def get_bond_wedge_back(self, width, chiral_center): widths = [] for i in range(6): - points_along_line.append(Vector(self.point_1.x + i * segment_size_x, - self.point_1.y + i * segment_size_y)) + points_along_line.append( + Vector( + self.point_1.x + i * segment_size_x, + self.point_1.y + i * segment_size_y, + ) + ) widths.append(i * segment_width_increase) @@ -151,7 +147,11 @@ def get_bond_wedge_back(self, width, chiral_center): line = SimpleLine(point_1, point_2) lines.append(line) - if self.atom.type == 'C' and not self.atom.charge and not self.atom.draw.draw_explicit: + if ( + self.atom.type == "C" + and not self.atom.charge + and not self.atom.draw.draw_explicit + ): return lines[:3] else: return [lines[2]] @@ -173,22 +173,40 @@ def get_truncated_line(self, ratio): new_point_1_y = self.point_1.y if self.point_1.x > self.point_2.x: - if self.atom.type != 'C' or self.atom.charge or self.atom.draw.draw_explicit: + if ( + self.atom.type != "C" + or self.atom.charge + or self.atom.draw.draw_explicit + ): new_point_1_x = self.point_1.x - truncation_x else: - if self.atom.type != 'C' or self.atom.charge or self.atom.draw.draw_explicit: + if ( + self.atom.type != "C" + or self.atom.charge + or self.atom.draw.draw_explicit + ): new_point_1_x = self.point_1.x + truncation_x if self.point_1.y > self.point_2.y: - if self.atom.type != 'C' or self.atom.charge or self.atom.draw.draw_explicit: + if ( + self.atom.type != "C" + or self.atom.charge + or self.atom.draw.draw_explicit + ): new_point_1_y = self.point_1.y - truncation_y else: - if self.atom.type != 'C' or self.atom.charge or self.atom.draw.draw_explicit: + if ( + self.atom.type != "C" + or self.atom.charge + or self.atom.draw.draw_explicit + ): new_point_1_y = self.point_1.y + truncation_y - truncated_line = HalfLine(Vector(new_point_1_x, new_point_1_y), self.point_2, self.atom, self.angle) + truncated_line = HalfLine( + Vector(new_point_1_x, new_point_1_y), self.point_2, self.atom, self.angle + ) return truncated_line @@ -237,11 +255,15 @@ def get_perpendicular_points(self, distance, point): dx = abs(math.sin(angle) * distance) dy = abs(math.cos(angle) * distance) - point_1 = Vector(dx * direction_combinations[0][0] + point.x, - dy * direction_combinations[0][1] + point.y) + point_1 = Vector( + dx * direction_combinations[0][0] + point.x, + dy * direction_combinations[0][1] + point.y, + ) - point_2 = Vector(dx * direction_combinations[1][0] + point.x, - dy * direction_combinations[1][1] + point.y) + point_2 = Vector( + dx * direction_combinations[1][0] + point.x, + dy * direction_combinations[1][1] + point.y, + ) return point_1, point_2 @@ -257,17 +279,25 @@ def get_perpendicular_lines(self, distance): dx = abs(math.sin(angle) * distance) dy = abs(math.cos(angle) * distance) - point_1 = Vector(dx * direction_combinations[0][0] + self.point_1.x, - dy * direction_combinations[0][1] + self.point_1.y) + point_1 = Vector( + dx * direction_combinations[0][0] + self.point_1.x, + dy * direction_combinations[0][1] + self.point_1.y, + ) - point_2 = Vector(dx * direction_combinations[1][0] + self.point_1.x, - dy * direction_combinations[1][1] + self.point_1.y) + point_2 = Vector( + dx * direction_combinations[1][0] + self.point_1.x, + dy * direction_combinations[1][1] + self.point_1.y, + ) - point_3 = Vector(dx * direction_combinations[0][0] + self.point_2.x, - dy * direction_combinations[0][1] + self.point_2.y) + point_3 = Vector( + dx * direction_combinations[0][0] + self.point_2.x, + dy * direction_combinations[0][1] + self.point_2.y, + ) - point_4 = Vector(dx * direction_combinations[1][0] + self.point_2.x, - dy * direction_combinations[1][1] + self.point_2.y) + point_4 = Vector( + dx * direction_combinations[1][0] + self.point_2.x, + dy * direction_combinations[1][1] + self.point_2.y, + ) line_1 = Line(point_1, point_3, self.atom_1, self.atom_2) line_2 = Line(point_2, point_4, self.atom_1, self.atom_2) @@ -292,8 +322,12 @@ def get_bond_triangle_back(self, width, chiral_center): widths = [] for i in range(6): - points_along_line.append(Vector(self.point_1.x + i * segment_size_x, - self.point_1.y + i * segment_size_y)) + points_along_line.append( + Vector( + self.point_1.x + i * segment_size_x, + self.point_1.y + i * segment_size_y, + ) + ) widths.append(i * segment_width_increase) @@ -412,15 +446,21 @@ def get_parallel_line(self, center, distance): y_translation = abs(math.sin(right_angle) * distance) midpoint = self.get_midpoint() - translated_midpoint_1 = Vector(direction_combinations[0][0] * x_translation + midpoint.x, - direction_combinations[0][1] * y_translation + midpoint.y) + translated_midpoint_1 = Vector( + direction_combinations[0][0] * x_translation + midpoint.x, + direction_combinations[0][1] * y_translation + midpoint.y, + ) - translated_midpoint_2 = Vector(direction_combinations[1][0] * x_translation + midpoint.x, - direction_combinations[1][1] * y_translation + midpoint.y) + translated_midpoint_2 = Vector( + direction_combinations[1][0] * x_translation + midpoint.x, + direction_combinations[1][1] * y_translation + midpoint.y, + ) directions = direction_combinations[0] - if center.get_squared_distance(translated_midpoint_1) > center.get_squared_distance(translated_midpoint_2): + if center.get_squared_distance( + translated_midpoint_1 + ) > center.get_squared_distance(translated_midpoint_2): directions = direction_combinations[1] x_translation = directions[0] * x_translation @@ -436,7 +476,7 @@ def get_parallel_line(self, center, distance): line = Line(new_point_1, new_point_2, self.atom_1, self.atom_2) - #return line.get_truncated_line(line_ratio) + # return line.get_truncated_line(line_ratio) return line def double_line_towards_center(self, center, distance, line_ratio): @@ -454,21 +494,26 @@ def double_line_towards_center(self, center, distance, line_ratio): y_translation = abs(math.sin(right_angle) * distance) midpoint = self.get_midpoint() - translated_midpoint_1 = Vector(direction_combinations[0][0] * x_translation + midpoint.x, - direction_combinations[0][1] * y_translation + midpoint.y) + translated_midpoint_1 = Vector( + direction_combinations[0][0] * x_translation + midpoint.x, + direction_combinations[0][1] * y_translation + midpoint.y, + ) - translated_midpoint_2 = Vector(direction_combinations[1][0] * x_translation + midpoint.x, - direction_combinations[1][1] * y_translation + midpoint.y) + translated_midpoint_2 = Vector( + direction_combinations[1][0] * x_translation + midpoint.x, + direction_combinations[1][1] * y_translation + midpoint.y, + ) directions = direction_combinations[0] - if center.get_squared_distance(translated_midpoint_1) > center.get_squared_distance(translated_midpoint_2): + if center.get_squared_distance( + translated_midpoint_1 + ) > center.get_squared_distance(translated_midpoint_2): directions = direction_combinations[1] x_translation = directions[0] * x_translation y_translation = directions[1] * y_translation - new_x1 = self.point_1.x + x_translation new_x2 = self.point_2.x + x_translation new_y1 = self.point_1.y + y_translation @@ -512,7 +557,12 @@ def get_truncated_line_ring(self, ratio): new_point_2_y = self.point_2.y - truncation_y new_point_1_y = self.point_1.y + truncation_y - truncated_line = Line(Vector(new_point_1_x, new_point_1_y), Vector(new_point_2_x, new_point_2_y), self.atom_1, self.atom_2) + truncated_line = Line( + Vector(new_point_1_x, new_point_1_y), + Vector(new_point_2_x, new_point_2_y), + self.atom_1, + self.atom_2, + ) return truncated_line def get_truncated_line(self, ratio): @@ -532,30 +582,67 @@ def get_truncated_line(self, ratio): new_point_2_y = self.point_2.y if self.point_1.x > self.point_2.x: - if self.atom_1.type != 'C' or self.atom_1.charge or self.atom_1.draw.draw_explicit: + if ( + self.atom_1.type != "C" + or self.atom_1.charge + or self.atom_1.draw.draw_explicit + ): new_point_1_x = self.point_1.x - truncation_x - if self.atom_2.type != 'C' or self.atom_2.charge or self.atom_2.draw.draw_explicit: + if ( + self.atom_2.type != "C" + or self.atom_2.charge + or self.atom_2.draw.draw_explicit + ): new_point_2_x = self.point_2.x + truncation_x else: - if self.atom_2.type != 'C' or self.atom_2.charge or self.atom_2.draw.draw_explicit: + if ( + self.atom_2.type != "C" + or self.atom_2.charge + or self.atom_2.draw.draw_explicit + ): new_point_2_x = self.point_2.x - truncation_x - if self.atom_1.type != 'C' or self.atom_1.charge or self.atom_1.draw.draw_explicit: + if ( + self.atom_1.type != "C" + or self.atom_1.charge + or self.atom_1.draw.draw_explicit + ): new_point_1_x = self.point_1.x + truncation_x if self.point_1.y > self.point_2.y: - if self.atom_1.type != 'C' or self.atom_1.charge or self.atom_1.draw.draw_explicit: + if ( + self.atom_1.type != "C" + or self.atom_1.charge + or self.atom_1.draw.draw_explicit + ): new_point_1_y = self.point_1.y - truncation_y - if self.atom_2.type != 'C' or self.atom_2.charge or self.atom_2.draw.draw_explicit: + if ( + self.atom_2.type != "C" + or self.atom_2.charge + or self.atom_2.draw.draw_explicit + ): new_point_2_y = self.point_2.y + truncation_y else: - if self.atom_2.type != 'C' or self.atom_2.charge or self.atom_2.draw.draw_explicit: + if ( + self.atom_2.type != "C" + or self.atom_2.charge + or self.atom_2.draw.draw_explicit + ): new_point_2_y = self.point_2.y - truncation_y - if self.atom_1.type != 'C' or self.atom_1.charge or self.atom_1.draw.draw_explicit: + if ( + self.atom_1.type != "C" + or self.atom_1.charge + or self.atom_1.draw.draw_explicit + ): new_point_1_y = self.point_1.y + truncation_y - truncated_line = Line(Vector(new_point_1_x, new_point_1_y), Vector(new_point_2_x, new_point_2_y), self.atom_1, self.atom_2) + truncated_line = Line( + Vector(new_point_1_x, new_point_1_y), + Vector(new_point_2_x, new_point_2_y), + self.atom_1, + self.atom_2, + ) return truncated_line @@ -565,7 +652,7 @@ def __init__(self, x, y): self.y = float(y) def __repr__(self): - return str(self.x) + ', ' + str(self.y) + return str(self.x) + ", " + str(self.y) def copy(self): return Vector(self.x, self.y) @@ -637,7 +724,7 @@ def get_closest_point_index(self, point_1, point_2): return 1 def get_squared_length(self): - return self.x ** 2 + self.y ** 2 + return self.x**2 + self.y**2 def get_squared_distance(self, vector): return (vector.x - self.x) ** 2 + (vector.y - self.y) ** 2 @@ -674,11 +761,11 @@ def get_clockwise_orientation(self, vector): b = self.x * vector.y if a > b: - return 'clockwise' + return "clockwise" elif a == b: - return 'neutral' + return "neutral" else: - return 'counterclockwise' + return "counterclockwise" def mirror_about_line(self, line_point_1, line_point_2): @@ -688,15 +775,25 @@ def mirror_about_line(self, line_point_1, line_point_2): a = (dx * dx - dy * dy) / (dx * dx + dy * dy) b = 2 * dx * dy / (dx * dx + dy * dy) - new_x = a * (self.x - line_point_1.x) + b * (self.y - line_point_1.y) + line_point_1.x - new_y = b * (self.x - line_point_1.x) - a * (self.y - line_point_1.y) + line_point_1.y + new_x = ( + a * (self.x - line_point_1.x) + + b * (self.y - line_point_1.y) + + line_point_1.x + ) + new_y = ( + b * (self.x - line_point_1.x) + - a * (self.y - line_point_1.y) + + line_point_1.y + ) self.x = new_x self.y = new_y @staticmethod def get_position_relative_to_line(vector_start, vector_end, vector): - d = (vector.x - vector_start.x) * (vector_end.y - vector_start.y) - (vector.y - vector_start.y) * (vector_end.x - vector_start.x) + d = (vector.x - vector_start.x) * (vector_end.y - vector_start.y) - ( + vector.y - vector_start.y + ) * (vector_end.x - vector_start.x) if d > 0: return 1 elif d < 0: @@ -707,15 +804,16 @@ def get_position_relative_to_line(vector_start, vector_end, vector): @staticmethod def get_directionality_triangle(vector_a, vector_b, vector_c): - determinant = (vector_b.x - vector_a.x) * (vector_c.y - vector_a.y) - \ - (vector_c.x - vector_a.x) * (vector_b.y - vector_a.y) + determinant = (vector_b.x - vector_a.x) * (vector_c.y - vector_a.y) - ( + vector_c.x - vector_a.x + ) * (vector_b.y - vector_a.y) if determinant < 0: - return 'clockwise' + return "clockwise" elif determinant == 0: return None else: - return 'counterclockwise' + return "counterclockwise" @staticmethod def mirror_vector_about_line(line_point_1, line_point_2, point): @@ -725,8 +823,16 @@ def mirror_vector_about_line(line_point_1, line_point_2, point): a = (dx * dx - dy * dy) / (dx * dx + dy * dy) b = 2 * dx * dy / (dx * dx + dy * dy) - x_new = a * (point.x - line_point_1.x) + b * (point.y - line_point_1.y) + line_point_1.x - y_new = b * (point.x - line_point_1.x) - a * (point.y - line_point_1.y) + line_point_1.y + x_new = ( + a * (point.x - line_point_1.x) + + b * (point.y - line_point_1.y) + + line_point_1.x + ) + y_new = ( + b * (point.x - line_point_1.x) + - a * (point.y - line_point_1.y) + + line_point_1.y + ) return Vector(x_new, y_new) @@ -774,8 +880,16 @@ def get_normals(vector_1, vector_2): @staticmethod def get_angle_between_vectors(vector_1, vector_2, origin): - return math.acos(((vector_1.x - origin.x) * (vector_2.x - origin.x) + (vector_1.y - origin.y) * (vector_2.y - origin.y)) / - (math.sqrt((vector_1.x - origin.x)**2 + (vector_1.y - origin.y)**2) * math.sqrt((vector_2.x - origin.x)**2 + (vector_2.y - origin.y)**2))) + return math.acos( + ( + (vector_1.x - origin.x) * (vector_2.x - origin.x) + + (vector_1.y - origin.y) * (vector_2.y - origin.y) + ) + / ( + math.sqrt((vector_1.x - origin.x) ** 2 + (vector_1.y - origin.y) ** 2) + * math.sqrt((vector_2.x - origin.x) ** 2 + (vector_2.y - origin.y) ** 2) + ) + ) # difference = Vector.subtract_vectors(vector_2, vector_1) # return difference.angle() @@ -785,6 +899,7 @@ class Triangle: """ The Awesome Triangle Class, dedicated to Jay """ + def __init__(self, point_1, point_2, point_3): self.point_1 = point_1 self.point_2 = point_2 @@ -797,7 +912,12 @@ def __init__(self, point_1, point_2, point_3): self.s = (self.edge_length_1 + self.edge_length_2 + self.edge_length_3) / 2.0 def get_squared_area(self): - return self.s * (self.s - self.edge_length_1) * (self.s - self.edge_length_2) * (self.s - self.edge_length_3) + return ( + self.s + * (self.s - self.edge_length_1) + * (self.s - self.edge_length_2) + * (self.s - self.edge_length_3) + ) def get_area(self): return math.sqrt(self.get_squared_area()) @@ -832,14 +952,11 @@ def get_apothem_from_side_length(length, edge_number): vector_4 = Vector(82.01933537136128, 44.58050841492202) vector_4.mirror_about_line(vector_1, vector_2) - labels = ['1', '2', '3', '4'] + labels = ["1", "2", "3", "4"] vectors = [vector_1, vector_2, vector_3, vector_4] - plt.gca().set_aspect('equal') - plt.scatter([vector.x for vector in vectors], [vector.y for vector in vectors], label=labels) + plt.gca().set_aspect("equal") + plt.scatter( + [vector.x for vector in vectors], [vector.y for vector in vectors], label=labels + ) plt.show() - - - - - diff --git a/pikachu/parsers/coconut.py b/pikachu/parsers/coconut.py index 4331d76..35c985a 100644 --- a/pikachu/parsers/coconut.py +++ b/pikachu/parsers/coconut.py @@ -3,7 +3,7 @@ def parse_coconut(coconut_file): smiles_strings = [] - with open(coconut_file, 'r') as coconut: + with open(coconut_file, "r") as coconut: for line in coconut: line = line.strip() smiles, identifier = line.split() @@ -13,7 +13,7 @@ def parse_coconut(coconut_file): def write_smiles(smiles, out_file): - with open(out_file, 'w') as out: + with open(out_file, "w") as out: for smi in smiles: out.write(f"{smi}\n") diff --git a/pikachu/parsers/np_atlas.py b/pikachu/parsers/np_atlas.py index 368d43a..823a660 100644 --- a/pikachu/parsers/np_atlas.py +++ b/pikachu/parsers/np_atlas.py @@ -1,13 +1,13 @@ def smiles_from_npatlas_tabular(npatlas_file): npaid_to_smiles = {} - with open (npatlas_file, 'r') as npatlas: + with open(npatlas_file, "r") as npatlas: npatlas.readline() for line in npatlas: line = line.strip() - compound_info = line.split('\t') + compound_info = line.split("\t") print(compound_info) npaid = compound_info[0] smiles = compound_info[10] npaid_to_smiles[npaid] = smiles - return npaid_to_smiles \ No newline at end of file + return npaid_to_smiles diff --git a/pikachu/reactions/basic_reactions.py b/pikachu/reactions/basic_reactions.py index 638dc6f..e1cf12a 100644 --- a/pikachu/reactions/basic_reactions.py +++ b/pikachu/reactions/basic_reactions.py @@ -1,5 +1,11 @@ from pikachu.general import read_smiles, draw_structure -from pikachu.reactions.functional_groups import find_bonds, BondDefiner, combine_structures, GroupDefiner, find_atoms +from pikachu.reactions.functional_groups import ( + find_bonds, + BondDefiner, + combine_structures, + GroupDefiner, + find_atoms, +) from pikachu.chem.atom_properties import ATOM_PROPERTIES @@ -9,13 +15,15 @@ def internal_condensation(structure, oh_bond, h_bond): o_neighbour = None for atom in oh_bond.neighbours: - if not o_atom and atom.type == 'O' and atom.has_neighbour('H'): + if not o_atom and atom.type == "O" and atom.has_neighbour("H"): o_atom = atom else: o_neighbour = atom if not o_atom: - raise Exception(f"The selected bond {oh_bond} does not attach to an -OH leaving group.") + raise Exception( + f"The selected bond {oh_bond} does not attach to an -OH leaving group." + ) # Ensure that the defined H-bond actually has an -H leaving group @@ -23,13 +31,15 @@ def internal_condensation(structure, oh_bond, h_bond): h_neighbour = None for atom in h_bond.neighbours: - if not h_atom and atom.type == 'H': + if not h_atom and atom.type == "H": h_atom = atom else: h_neighbour = atom if not h_atom: - raise Exception(f"The selected bond {h_bond} does not attach to an -H leaving group.") + raise Exception( + f"The selected bond {h_bond} does not attach to an -H leaving group." + ) # Break the bonds between the leaving groups and the rest of the product @@ -112,8 +122,12 @@ def hydrolysis(structure, bond): hydrolysed_structure.break_bond(water_bond) hydrolysed_structure.break_bond(bond) - hydrolysed_structure.make_bond(hydrogen, h_acceptor, hydrolysed_structure.find_next_bond_nr()) - hydrolysed_structure.make_bond(oxygen, oh_acceptor, hydrolysed_structure.find_next_bond_nr()) + hydrolysed_structure.make_bond( + hydrogen, h_acceptor, hydrolysed_structure.find_next_bond_nr() + ) + hydrolysed_structure.make_bond( + oxygen, oh_acceptor, hydrolysed_structure.find_next_bond_nr() + ) structures = hydrolysed_structure.split_disconnected_structures() if len(structures) == 1: @@ -145,13 +159,15 @@ def condensation(structure_1, structure_2, oh_bond, h_bond): o_neighbour = None for atom in oh_bond.neighbours: - if not o_atom and atom.type == 'O' and atom.has_neighbour('H'): + if not o_atom and atom.type == "O" and atom.has_neighbour("H"): o_atom = atom else: o_neighbour = atom if not o_atom: - raise Exception(f"The selected bond {oh_bond} does not attach to an -OH leaving group.") + raise Exception( + f"The selected bond {oh_bond} does not attach to an -OH leaving group." + ) # Ensure that the defined H-bond actually has an -H leaving group @@ -159,13 +175,15 @@ def condensation(structure_1, structure_2, oh_bond, h_bond): h_neighbour = None for atom in h_bond.neighbours: - if not h_atom and atom.type == 'H': + if not h_atom and atom.type == "H": h_atom = atom else: h_neighbour = atom if not h_atom: - raise Exception(f"The selected bond {h_bond} does not attach to an -H leaving group.") + raise Exception( + f"The selected bond {h_bond} does not attach to an -H leaving group." + ) # Break the bonds between the leaving groups and the rest of the product @@ -207,6 +225,7 @@ def condensation(structure_1, structure_2, oh_bond, h_bond): return [product, water] + # # if __name__ == "__main__": # smiles = "NCC(=O)NCC(=O)O" diff --git a/pikachu/reactions/functional_groups.py b/pikachu/reactions/functional_groups.py index c8e3bd5..e3c2378 100644 --- a/pikachu/reactions/functional_groups.py +++ b/pikachu/reactions/functional_groups.py @@ -4,7 +4,6 @@ class IndexTracker: - def __init__(self): self.atoms = {} self.bonds = {} @@ -24,26 +23,26 @@ def find_bonds(bond_neighbourhood, structure): locations = structure.find_substructures(bond_neighbourhood.structure) bonds = [] - + for match in locations: atom_1 = None atom_2 = None - if bond_neighbourhood.atom_1.type != 'H': + if bond_neighbourhood.atom_1.type != "H": atom_1 = match.atoms[bond_neighbourhood.atom_1] - elif bond_neighbourhood.atom_2.type != 'H': + elif bond_neighbourhood.atom_2.type != "H": atom_2 = match.atoms[bond_neighbourhood.atom_2] - if atom_2.has_neighbour('H'): - atom_1 = atom_2.get_neighbour('H') + if atom_2.has_neighbour("H"): + atom_1 = atom_2.get_neighbour("H") else: continue - if bond_neighbourhood.atom_2.type != 'H': + if bond_neighbourhood.atom_2.type != "H": atom_2 = match.atoms[bond_neighbourhood.atom_2] - elif bond_neighbourhood.atom_1.type != 'H': + elif bond_neighbourhood.atom_1.type != "H": atom_1 = match.atoms[bond_neighbourhood.atom_1] - if atom_1.has_neighbour('H'): - atom_2 = atom_1.get_neighbour('H') + if atom_1.has_neighbour("H"): + atom_2 = atom_1.get_neighbour("H") else: continue if atom_1 and atom_2: @@ -67,14 +66,14 @@ def find_atoms(atom_neighbourhood, structure): locations = structure.find_substructures(atom_neighbourhood.structure) atoms = [] for match in locations: - if atom_neighbourhood.atom_1.type != 'H': + if atom_neighbourhood.atom_1.type != "H": atom = match.atoms[atom_neighbourhood.atom_1] atoms.append(atom) else: neighbour = atom_neighbourhood.atom_1.neighbours[0] - if neighbour.type != 'H': + if neighbour.type != "H": neighbouring_atom = match.atoms[neighbour] - atom = neighbouring_atom.get_neighbour('H') + atom = neighbouring_atom.get_neighbour("H") atoms.append(atom) return list(set(atoms)) @@ -91,6 +90,7 @@ class BondDefiner: atom_1: int, index of atom 1 in the bond in the smiles string atom_2: int, index of atom 2 in the bond in the smiles string """ + def __init__(self, name, smiles, atom_nr_1, atom_nr_2): self.name = name self.smiles = Smiles(smiles) @@ -205,10 +205,10 @@ def combine_structures(structures): new_bonds.update(structure.bonds) new_structure = Structure(new_graph, new_bonds) - + for new_annotation, default in new_annotations: new_structure.add_attribute(new_annotation, default) - + new_structure.make_bond_lookup() new_structure.refresh_structure(find_cycles=True) diff --git a/pikachu/smiles/graph_to_smiles.py b/pikachu/smiles/graph_to_smiles.py index 3ed7422..9d18382 100644 --- a/pikachu/smiles/graph_to_smiles.py +++ b/pikachu/smiles/graph_to_smiles.py @@ -1,5 +1,8 @@ #!/usr/bin/env python -from pikachu.chem.chirality import get_chiral_permutations, get_chiral_permutations_lonepair +from pikachu.chem.chirality import ( + get_chiral_permutations, + get_chiral_permutations_lonepair, +) from pikachu.chem.bond_properties import BOND_PROPERTIES from pikachu.chem.rings import find_cycles from pikachu.smiles.smiles import read_smiles @@ -22,7 +25,7 @@ def get_cyclic_label(cycle_nr): """ if cycle_nr > 9: - return '%' + str(cycle_nr) + return "%" + str(cycle_nr) else: return str(cycle_nr) @@ -36,15 +39,15 @@ def determine_chirality(order, chirality): order.sort(key=lambda x: x.nr) new_order = tuple(order) if new_order in chiral_permutations: - if chirality == 'counterclockwise': - new_chirality = 'counterclockwise' + if chirality == "counterclockwise": + new_chirality = "counterclockwise" else: - new_chirality = 'clockwise' + new_chirality = "clockwise" else: - if chirality == 'counterclockwise': - new_chirality = 'clockwise' + if chirality == "counterclockwise": + new_chirality = "clockwise" else: - new_chirality = 'counterclockwise' + new_chirality = "counterclockwise" return new_chirality @@ -71,7 +74,7 @@ def __init__(self, structure): self.find_original_atom_indices() self.resolve_chiral_centres() self.add_bond_chirality() - self.smiles = ''.join(self.components) + self.smiles = "".join(self.components) def is_numerical_component(self, component): """ @@ -87,7 +90,7 @@ def is_numerical_component(self, component): """ for character in component: - if character not in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '%']: + if character not in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "%"]: return False return True @@ -102,7 +105,9 @@ def find_original_atom_indices(self): atom_to_adjust = atom current_index = i - while self.is_numerical_component(self.components[current_index]) or self.components[current_index] in ('=', '#'): + while self.is_numerical_component( + self.components[current_index] + ) or self.components[current_index] in ("=", "#"): current_index -= 1 self.atom_to_index[atom_to_adjust] = current_index @@ -117,7 +122,7 @@ def add_bond_chirality(self): cis_trans_atoms = [] options = bond.chiral_dict.keys() for atom in options: - if type(atom) == Atom and atom.type != 'H': + if type(atom) == Atom and atom.type != "H": cis_trans_atoms.append(atom) neighbours = [] @@ -162,7 +167,7 @@ def add_bond_chirality(self): atom_1_index = self.atom_to_index[atom_1] bond_1 = self.original_structure.bond_lookup[atom_1][attaching_atom] - + if bond_1 not in bond_to_direction: place_bond_1 = True @@ -176,8 +181,10 @@ def add_bond_chirality(self): placed_index = 0 for a, atom in enumerate(atoms_2): - if type(atom) == Atom and atom.type != 'H': - temp_bond = self.original_structure.bond_lookup[atom][other_atom] + if type(atom) == Atom and atom.type != "H": + temp_bond = self.original_structure.bond_lookup[atom][ + other_atom + ] if temp_bond in bond_to_direction: placed_index = a break @@ -187,9 +194,11 @@ def add_bond_chirality(self): for atom_2 in atoms_2: - if type(atom_2) == Atom and atom_2.type != 'H': + if type(atom_2) == Atom and atom_2.type != "H": - bond_2 = self.original_structure.bond_lookup[atom_2][other_atom] + bond_2 = self.original_structure.bond_lookup[atom_2][ + other_atom + ] place_bond_2 = True @@ -206,22 +215,54 @@ def add_bond_chirality(self): if place_bond_1: - if atom_pair_to_direction[atom_2][other_atom] == 'up' and bond.chiral_dict[atom_1][atom_2] == 'trans': - bond_to_direction[bond_1] = 'down' - atom_pair_to_direction[atom_1][attaching_atom] = 'down' - atom_pair_to_direction[attaching_atom][atom_1] = 'up' - elif atom_pair_to_direction[atom_2][other_atom] == 'up' and bond.chiral_dict[atom_1][atom_2] == 'cis': - bond_to_direction[bond_1] = 'up' - atom_pair_to_direction[atom_1][attaching_atom] = 'up' - atom_pair_to_direction[attaching_atom][atom_1] = 'down' - elif atom_pair_to_direction[atom_2][other_atom] == 'down' and bond.chiral_dict[atom_1][atom_2] == 'trans': - bond_to_direction[bond_1] = 'up' - atom_pair_to_direction[atom_1][attaching_atom] = 'up' - atom_pair_to_direction[attaching_atom][atom_1] = 'down' - elif atom_pair_to_direction[atom_2][other_atom] == 'down' and bond.chiral_dict[atom_1][atom_2] == 'cis': - bond_to_direction[bond_1] = 'down' - atom_pair_to_direction[atom_1][attaching_atom] = 'down' - atom_pair_to_direction[attaching_atom][atom_1] = 'up' + if ( + atom_pair_to_direction[atom_2][other_atom] + == "up" + and bond.chiral_dict[atom_1][atom_2] == "trans" + ): + bond_to_direction[bond_1] = "down" + atom_pair_to_direction[atom_1][ + attaching_atom + ] = "down" + atom_pair_to_direction[attaching_atom][ + atom_1 + ] = "up" + elif ( + atom_pair_to_direction[atom_2][other_atom] + == "up" + and bond.chiral_dict[atom_1][atom_2] == "cis" + ): + bond_to_direction[bond_1] = "up" + atom_pair_to_direction[atom_1][ + attaching_atom + ] = "up" + atom_pair_to_direction[attaching_atom][ + atom_1 + ] = "down" + elif ( + atom_pair_to_direction[atom_2][other_atom] + == "down" + and bond.chiral_dict[atom_1][atom_2] == "trans" + ): + bond_to_direction[bond_1] = "up" + atom_pair_to_direction[atom_1][ + attaching_atom + ] = "up" + atom_pair_to_direction[attaching_atom][ + atom_1 + ] = "down" + elif ( + atom_pair_to_direction[atom_2][other_atom] + == "down" + and bond.chiral_dict[atom_1][atom_2] == "cis" + ): + bond_to_direction[bond_1] = "down" + atom_pair_to_direction[atom_1][ + attaching_atom + ] = "down" + atom_pair_to_direction[attaching_atom][ + atom_1 + ] = "up" else: break @@ -229,70 +270,156 @@ def add_bond_chirality(self): if place_bond_1: - atom_pair_to_direction[atom_1][attaching_atom] = 'up' - atom_pair_to_direction[attaching_atom][atom_1] = 'down' - bond_to_direction[bond_1] = 'up' - - if bond.chiral_dict[atom_1][atom_2] == 'trans': - atom_pair_to_direction[atom_2][other_atom] = 'down' - atom_pair_to_direction[other_atom][atom_2] = 'up' - bond_to_direction[bond_2] = 'down' - elif bond.chiral_dict[atom_1][atom_2] == 'cis': - atom_pair_to_direction[atom_2][other_atom] = 'up' - atom_pair_to_direction[other_atom][atom_2] = 'down' - bond_to_direction[bond_2] = 'up' + atom_pair_to_direction[atom_1][ + attaching_atom + ] = "up" + atom_pair_to_direction[attaching_atom][ + atom_1 + ] = "down" + bond_to_direction[bond_1] = "up" + + if bond.chiral_dict[atom_1][atom_2] == "trans": + atom_pair_to_direction[atom_2][ + other_atom + ] = "down" + atom_pair_to_direction[other_atom][ + atom_2 + ] = "up" + bond_to_direction[bond_2] = "down" + elif bond.chiral_dict[atom_1][atom_2] == "cis": + atom_pair_to_direction[atom_2][ + other_atom + ] = "up" + atom_pair_to_direction[other_atom][ + atom_2 + ] = "down" + bond_to_direction[bond_2] = "up" else: - if atom_pair_to_direction[atom_1][attaching_atom] == 'up' and bond.chiral_dict[atom_1][atom_2] == 'trans': - bond_to_direction[bond_2] = 'down' - atom_pair_to_direction[atom_2][other_atom] = 'down' - atom_pair_to_direction[other_atom][atom_2] = 'up' - elif atom_pair_to_direction[atom_1][attaching_atom] == 'up' and bond.chiral_dict[atom_1][atom_2] == 'cis': - bond_to_direction[bond_2] = 'up' - atom_pair_to_direction[atom_2][other_atom] = 'up' - atom_pair_to_direction[other_atom][atom_2] = 'down' - elif atom_pair_to_direction[atom_1][attaching_atom] == 'down' and bond.chiral_dict[atom_1][atom_2] == 'trans': - bond_to_direction[bond_2] = 'up' - atom_pair_to_direction[atom_2][other_atom] = 'up' - atom_pair_to_direction[other_atom][atom_2] = 'down' - elif atom_pair_to_direction[atom_1][attaching_atom] == 'down' and bond.chiral_dict[atom_1][atom_2] == 'cis': - bond_to_direction[bond_2] = 'down' - atom_pair_to_direction[atom_2][other_atom] = 'down' - atom_pair_to_direction[other_atom][atom_2] = 'up' + if ( + atom_pair_to_direction[atom_1][attaching_atom] + == "up" + and bond.chiral_dict[atom_1][atom_2] == "trans" + ): + bond_to_direction[bond_2] = "down" + atom_pair_to_direction[atom_2][ + other_atom + ] = "down" + atom_pair_to_direction[other_atom][ + atom_2 + ] = "up" + elif ( + atom_pair_to_direction[atom_1][attaching_atom] + == "up" + and bond.chiral_dict[atom_1][atom_2] == "cis" + ): + bond_to_direction[bond_2] = "up" + atom_pair_to_direction[atom_2][ + other_atom + ] = "up" + atom_pair_to_direction[other_atom][ + atom_2 + ] = "down" + elif ( + atom_pair_to_direction[atom_1][attaching_atom] + == "down" + and bond.chiral_dict[atom_1][atom_2] == "trans" + ): + bond_to_direction[bond_2] = "up" + atom_pair_to_direction[atom_2][ + other_atom + ] = "up" + atom_pair_to_direction[other_atom][ + atom_2 + ] = "down" + elif ( + atom_pair_to_direction[atom_1][attaching_atom] + == "down" + and bond.chiral_dict[atom_1][atom_2] == "cis" + ): + bond_to_direction[bond_2] = "down" + atom_pair_to_direction[atom_2][ + other_atom + ] = "down" + atom_pair_to_direction[other_atom][ + atom_2 + ] = "up" if place_bond_1: - if not set(self.atom_to_cycle_nr[attaching_atom]).intersection(set(self.atom_to_cycle_nr[atom_1])): + if not set( + self.atom_to_cycle_nr[attaching_atom] + ).intersection(set(self.atom_to_cycle_nr[atom_1])): if attaching_atom_index > atom_1_index: insertion_points_1 = [attaching_atom_index - 1] - directions_1 = [atom_pair_to_direction[atom_1][attaching_atom]] + directions_1 = [ + atom_pair_to_direction[atom_1][ + attaching_atom + ] + ] else: insertion_points_1 = [atom_1_index - 1] - directions_1 = [atom_pair_to_direction[attaching_atom][atom_1]] + directions_1 = [ + atom_pair_to_direction[attaching_atom][ + atom_1 + ] + ] else: - cycle_nr = list(set(self.atom_to_cycle_nr[attaching_atom]).intersection( - set(self.atom_to_cycle_nr[atom_1])))[0] - - cycle_position_1 = self.atom_to_cycle_nr[atom_1].index(cycle_nr) - cycle_position_attaching = self.atom_to_cycle_nr[attaching_atom].index(cycle_nr) + cycle_nr = list( + set( + self.atom_to_cycle_nr[attaching_atom] + ).intersection( + set(self.atom_to_cycle_nr[atom_1]) + ) + )[0] + + cycle_position_1 = self.atom_to_cycle_nr[ + atom_1 + ].index(cycle_nr) + cycle_position_attaching = self.atom_to_cycle_nr[ + attaching_atom + ].index(cycle_nr) if attaching_atom_index > atom_1_index: - insertion_points_1 = [attaching_atom_index + cycle_position_attaching, atom_1_index + cycle_position_1] - directions_1 = [atom_pair_to_direction[attaching_atom][atom_1], atom_pair_to_direction[atom_1][attaching_atom]] + insertion_points_1 = [ + attaching_atom_index + + cycle_position_attaching, + atom_1_index + cycle_position_1, + ] + directions_1 = [ + atom_pair_to_direction[attaching_atom][ + atom_1 + ], + atom_pair_to_direction[atom_1][ + attaching_atom + ], + ] else: - insertion_points_1 = [atom_1_index + cycle_position_1, attaching_atom_index + cycle_position_attaching] - directions_1 = [atom_pair_to_direction[atom_1][attaching_atom], - atom_pair_to_direction[attaching_atom][atom_1]] - - for j, insertion_point_1 in enumerate(insertion_points_1): + insertion_points_1 = [ + atom_1_index + cycle_position_1, + attaching_atom_index + + cycle_position_attaching, + ] + directions_1 = [ + atom_pair_to_direction[atom_1][ + attaching_atom + ], + atom_pair_to_direction[attaching_atom][ + atom_1 + ], + ] + + for j, insertion_point_1 in enumerate( + insertion_points_1 + ): direction_1 = directions_1[j] - if direction_1 == 'up': - symbol_1 = '/' + if direction_1 == "up": + symbol_1 = "/" else: - symbol_1 = '\\' + symbol_1 = "\\" self.add_insert([symbol_1], insertion_point_1) @@ -303,37 +430,63 @@ def add_bond_chirality(self): atom_2_index = self.atom_to_index[atom_2] other_atom_index = self.atom_to_index[other_atom] - if not set(self.atom_to_cycle_nr[other_atom]).intersection( - set(self.atom_to_cycle_nr[atom_2])): + if not set( + self.atom_to_cycle_nr[other_atom] + ).intersection(set(self.atom_to_cycle_nr[atom_2])): if other_atom_index > atom_2_index: insertion_points_2 = [other_atom_index - 1] - directions_2 = [atom_pair_to_direction[atom_2][other_atom]] + directions_2 = [ + atom_pair_to_direction[atom_2][other_atom] + ] else: insertion_points_2 = [atom_2_index - 1] - directions_2 = [atom_pair_to_direction[other_atom][atom_2]] + directions_2 = [ + atom_pair_to_direction[other_atom][atom_2] + ] else: - cycle_nr = list(set(self.atom_to_cycle_nr[other_atom]).intersection( - set(self.atom_to_cycle_nr[atom_2])))[0] - - cycle_position_2 = self.atom_to_cycle_nr[atom_2].index(cycle_nr) - cycle_position_other = self.atom_to_cycle_nr[other_atom].index(cycle_nr) + cycle_nr = list( + set( + self.atom_to_cycle_nr[other_atom] + ).intersection( + set(self.atom_to_cycle_nr[atom_2]) + ) + )[0] + + cycle_position_2 = self.atom_to_cycle_nr[ + atom_2 + ].index(cycle_nr) + cycle_position_other = self.atom_to_cycle_nr[ + other_atom + ].index(cycle_nr) if other_atom_index > atom_2_index: - insertion_points_2 = [other_atom_index + cycle_position_other, atom_2_index + cycle_position_2] - directions_2 = [atom_pair_to_direction[other_atom][atom_2], - atom_pair_to_direction[atom_2][other_atom]] + insertion_points_2 = [ + other_atom_index + cycle_position_other, + atom_2_index + cycle_position_2, + ] + directions_2 = [ + atom_pair_to_direction[other_atom][atom_2], + atom_pair_to_direction[atom_2][other_atom], + ] else: - insertion_points_2 = [atom_2_index + cycle_position_other, other_atom_index + cycle_position_2] - directions_2 = [atom_pair_to_direction[atom_2][other_atom], - atom_pair_to_direction[other_atom][atom_2]] - - for j, insertion_point_2 in enumerate(insertion_points_2): + insertion_points_2 = [ + atom_2_index + cycle_position_other, + other_atom_index + cycle_position_2, + ] + directions_2 = [ + atom_pair_to_direction[atom_2][other_atom], + atom_pair_to_direction[other_atom][atom_2], + ] + + for j, insertion_point_2 in enumerate( + insertion_points_2 + ): direction_2 = directions_2[j] - if direction_2 == 'up': - symbol_2 = '/' + if direction_2 == "up": + symbol_2 = "/" else: symbol_2 = "\\" @@ -363,10 +516,12 @@ def resolve_chiral_centres(self): index = None for neighbour in neighbours: - if neighbour.type == 'H': + if neighbour.type == "H": index = self.atom_to_index[atom] else: - if not neighbour in [atom_and_cycle[0] for atom_and_cycle in cyclic_neighbours]: + if not neighbour in [ + atom_and_cycle[0] for atom_and_cycle in cyclic_neighbours + ]: index = self.atom_to_index[neighbour] else: for atom_and_cycle in cyclic_neighbours: @@ -376,21 +531,26 @@ def resolve_chiral_centres(self): for i, component in enumerate(self.components): - if component == cycle_nr and i > self.atom_to_index[atom]: + if ( + component == cycle_nr + and i > self.atom_to_index[atom] + ): index = i break indices_and_atoms.append((index, neighbour)) - atom_order = [atom for _,atom in sorted(indices_and_atoms)] + atom_order = [atom for _, atom in sorted(indices_and_atoms)] chirality = determine_chirality(atom_order, atom.chiral) - if chirality == 'counterclockwise': - chiral_symbol = '@' - elif chirality == 'clockwise': - chiral_symbol = '@@' + if chirality == "counterclockwise": + chiral_symbol = "@" + elif chirality == "clockwise": + chiral_symbol = "@@" chiral_centre_index = self.atom_to_index[atom] - self.components[chiral_centre_index] = self.components[chiral_centre_index].replace('X', chiral_symbol) + self.components[chiral_centre_index] = self.components[ + chiral_centre_index + ].replace("X", chiral_symbol) def get_branch_levels(self): atom_to_branch = [] @@ -401,9 +561,9 @@ def get_branch_levels(self): branch = 0 for i, component in enumerate(self.components): - if component == '(': + if component == "(": branch += 1 - elif component == ')': + elif component == ")": branch -= 1 else: if i in index_to_atom: @@ -430,7 +590,7 @@ def make_smiles_components(self): first_atom = list(self.branch_points)[0] # this happens when the entire graph is cyclic - else: + else: first_atom = list(self.structure.graph.keys())[0] self.atom_to_index[first_atom] = 0 @@ -453,13 +613,17 @@ def make_smiles_components(self): cyclic = True cycle_nr += 1 cyclic_label = get_cyclic_label(cycle_nr) - + bond = working_graph.bond_lookup[current_atom][next_atom] bond_symbol = BOND_PROPERTIES.bond_type_to_symbol[bond.type] - if bond.type == 'single' and current_atom.aromatic and next_atom.aromatic: - bond_symbol = '-' + if ( + bond.type == "single" + and current_atom.aromatic + and next_atom.aromatic + ): + bond_symbol = "-" if cyclic: @@ -469,7 +633,7 @@ def make_smiles_components(self): cyclic_label_idx_2 = self.atom_to_index[current_atom] self.add_insert([bond_symbol, cyclic_label], cyclic_label_idx_2) offset = 2 - # self.bonds_to_index[bond] = cyclic_label_idx_1 + 1 + # self.bonds_to_index[bond] = cyclic_label_idx_1 + 1 else: cyclic_label_idx_1 = self.atom_to_index[next_atom] self.add_insert([cyclic_label], cyclic_label_idx_1) @@ -477,7 +641,6 @@ def make_smiles_components(self): self.add_insert([cyclic_label], cyclic_label_idx_2) offset = 1 - self.atom_to_index[next_atom] += offset self.atom_to_index[current_atom] += offset @@ -490,7 +653,12 @@ def make_smiles_components(self): if is_branched[current_atom]: if bond_symbol: - insert = ["(", bond_symbol, self.representations[next_atom], ")"] + insert = [ + "(", + bond_symbol, + self.representations[next_atom], + ")", + ] offset = 3 else: insert = ["(", self.representations[next_atom], ")"] @@ -526,34 +694,37 @@ def make_smiles_components(self): break if not next_atom_found and atoms_left: - self.components.append('.') + self.components.append(".") current_atom = list(atoms_left)[0] self.atom_to_index[current_atom] = len(self.components) self.components.append(self.representations[current_atom]) atoms_added.add(current_atom) - - def get_neighbour_nr(self, node): neighbours = self.structure.graph[node] neighbour_nr = 0 for neighbour in neighbours: - if neighbour.type != 'H': + if neighbour.type != "H": neighbour_nr += 1 return neighbour_nr def remove_hydrogens(self): for atom in list(self.structure.graph.keys()): - if atom.type == 'H' and atom.charge == 0: + if atom.type == "H" and atom.charge == 0: neighbour = atom.neighbours[0] - if neighbour.type in ['B', 'C', 'N', 'O', 'P', 'S', 'F', 'Cl', 'Br', 'I'] \ - and neighbour.charge == 0 and not neighbour.chiral and not neighbour.pyrrole: + if ( + neighbour.type + in ["B", "C", "N", "O", "P", "S", "F", "Cl", "Br", "I"] + and neighbour.charge == 0 + and not neighbour.chiral + and not neighbour.pyrrole + ): bond = atom.bonds[0] del self.structure.bonds[bond.nr] del self.structure.bond_lookup[atom][neighbour] del self.structure.bond_lookup[neighbour][atom] - + self.structure.graph[neighbour].remove(atom) self.structure.graph[atom].remove(neighbour) @@ -583,7 +754,7 @@ def remove_explicit_hydrogen(self, hydrogen): def remove_bonds_and_nodes(self, last_node, next_node): bond = self.structure.bond_lookup[last_node][next_node] - + del self.structure.bonds[bond.nr] del self.structure.bond_lookup[last_node][next_node] del self.structure.bond_lookup[next_node][last_node] @@ -615,7 +786,7 @@ def make_branch_lookup(self): def make_index_dict(self): index_dict = {} - + for node in self.simplified_graph: index_dict[node] = None @@ -629,8 +800,8 @@ def adjust_atom_indices(self, insert, break_point): self.atom_to_index[atom] += index_jump def add_insert(self, insert, break_point): - half_1 = self.components[:break_point + 1] - half_2 = self.components[break_point + 1:] + half_1 = self.components[: break_point + 1] + half_2 = self.components[break_point + 1 :] self.adjust_atom_indices(insert, break_point) @@ -639,7 +810,7 @@ def add_insert(self, insert, break_point): def add_representations(self): self.representations = {} for atom in self.structure.graph: - if atom.type != 'H': + if atom.type != "H": if atom.aromatic: self.representations[atom] = atom.type.lower() else: @@ -654,31 +825,38 @@ def add_representations(self): def add_brackets(self): for atom in self.representations: - if atom.type not in {'B', 'C', 'N', 'O', 'P', 'S', 'F', 'Cl', 'Br', 'I'} and self.representations[atom][0] != '[': - self.representations[atom] = '[' + self.representations[atom] + ']' + if ( + atom.type not in {"B", "C", "N", "O", "P", "S", "F", "Cl", "Br", "I"} + and self.representations[atom][0] != "[" + ): + self.representations[atom] = "[" + self.representations[atom] + "]" def add_chiral_placeholders(self): for atom in self.structure.graph: if atom.chiral: - self.representations[atom] = f'[{self.representations[atom]}X]' + self.representations[atom] = f"[{self.representations[atom]}X]" def add_charge_representations(self): for atom in self.structure.graph: if atom.charge != 0: if atom.charge == 1: - charge_string = '+' + charge_string = "+" elif atom.charge == -1: - charge_string = '-' + charge_string = "-" elif atom.charge > 1: - charge_string = '+%d' % atom.charge + charge_string = "+%d" % atom.charge elif atom.charge < -1: - charge_string = '-%d' % atom.charge - + charge_string = "-%d" % atom.charge + representation = self.representations[atom] - if representation[-1] == ']': - self.representations[atom] = representation[:-1] + charge_string + ']' + if representation[-1] == "]": + self.representations[atom] = ( + representation[:-1] + charge_string + "]" + ) else: - self.representations[atom] = '[' + representation + charge_string + ']' + self.representations[atom] = ( + "[" + representation + charge_string + "]" + ) def count_hydrogens(self): hydrogen_counts = {} @@ -688,7 +866,7 @@ def count_hydrogens(self): hydrogen_counts[atom] = 0 atom_to_hydrogens[atom] = [] for neighbour in neighbours: - if neighbour.type == 'H': + if neighbour.type == "H": hydrogen_counts[atom] += 1 atom_to_hydrogens[atom].append(neighbour) @@ -696,19 +874,23 @@ def count_hydrogens(self): def add_hydrogen_representations(self): hydrogen_counts, atom_to_hydrogens = self.count_hydrogens() - + for atom, count in hydrogen_counts.items(): if count > 0: if count > 1: - hydrogen_string = f'H{count}' + hydrogen_string = f"H{count}" elif count == 1: - hydrogen_string = 'H' + hydrogen_string = "H" representation = self.representations[atom] - if representation[-1] == ']': - self.representations[atom] = representation[:-1] + hydrogen_string + ']' + if representation[-1] == "]": + self.representations[atom] = ( + representation[:-1] + hydrogen_string + "]" + ) else: - self.representations[atom] = '[' + representation + hydrogen_string + ']' + self.representations[atom] = ( + "[" + representation + hydrogen_string + "]" + ) explicit_hydrogens = atom_to_hydrogens[atom] for hydrogen in explicit_hydrogens: @@ -739,7 +921,7 @@ def find_cyclic_pairs(self): previous_atom = cycle[i - 1] self.cyclic_nodes.add(previous_atom) self.cyclic_nodes.add(atom) - pair = tuple(sorted((previous_atom, atom), key = lambda x: x.nr)) + pair = tuple(sorted((previous_atom, atom), key=lambda x: x.nr)) self.cyclic_pairs.add(pair) def is_terminal_node(self, node): @@ -757,18 +939,14 @@ def is_branch_point(self, node): if __name__ == "__main__": - smiles = 'CCCCCCCCCC(=O)N[C@@H](CC1=CNC2=CC=CC=C21)C(=O)N[C@@H](CC(=O)N)C(=O)N[C@@H](CC(=O)O)C(=O)N[C@H]3[C@H](OC(=O)[C@@H](NC(=O)[C@@H](NC(=O)[C@H](NC(=O)CNC(=O)[C@@H](NC(=O)[C@H](NC(=O)[C@@H](NC(=O)[C@@H](NC(=O)CNC3=O)CCCN)CC(=O)O)C)CC(=O)O)CO)[C@H](C)CC(=O)O)CC(=O)C4=CC=CC=C4N)C' - # smiles = 'c1ccc2c(c1)c(c[nH]2)C[C@@H](C(=O)O)N' + smiles = "CCCCCCCCCC(=O)N[C@@H](CC1=CNC2=CC=CC=C21)C(=O)N[C@@H](CC(=O)N)C(=O)N[C@@H](CC(=O)O)C(=O)N[C@H]3[C@H](OC(=O)[C@@H](NC(=O)[C@@H](NC(=O)[C@H](NC(=O)CNC(=O)[C@@H](NC(=O)[C@H](NC(=O)[C@@H](NC(=O)[C@@H](NC(=O)CNC3=O)CCCN)CC(=O)O)C)CC(=O)O)CO)[C@H](C)CC(=O)O)CC(=O)C4=CC=CC=C4N)C" + # smiles = 'c1ccc2c(c1)c(c[nH]2)C[C@@H](C(=O)O)N' structure = read_smiles(smiles) kekule_structure = structure.kekulise() GraphToSmiles(kekule_structure) GraphToSmiles(structure) - smiles = r'I\C(=C(/Cl)\F)\Br' + smiles = r"I\C(=C(/Cl)\F)\Br" structure = read_smiles(smiles) s = GraphToSmiles(structure) print(s.smiles) - - - - diff --git a/pikachu/smiles/smiles.py b/pikachu/smiles/smiles.py index 97f7dfe..e4d0d29 100644 --- a/pikachu/smiles/smiles.py +++ b/pikachu/smiles/smiles.py @@ -2,6 +2,7 @@ from pikachu.chem.structure import Structure from pikachu.errors import StructureError from pikachu.chem.atom import Atom + # from pikachu.chem.aromatic_system import AromaticSystem @@ -17,9 +18,9 @@ def read_smiles(smiles_string): def calc_charge(sign, value): - if sign == '+': + if sign == "+": return value - elif sign == '-': + elif sign == "-": return value * -1 else: raise Exception("Wrong character to indicate charge!") @@ -36,17 +37,19 @@ def parse_explicit(component): chirals = [] for i, character in enumerate(informative): - last = informative[i-1] + last = informative[i - 1] if len(informative) > 2 and i > 1: - second_last = informative[i-2] + second_last = informative[i - 2] else: - second_last = '' + second_last = "" if skip: skip = False continue if character.isupper(): - if character == 'H': - if not (len(informative) >= 2 and informative[1] in {'o', 'e', 'f', 's'}): + if character == "H": + if not ( + len(informative) >= 2 and informative[1] in {"o", "e", "f", "s"} + ): hydrogen = i else: element.append(i) @@ -66,20 +69,20 @@ def parse_explicit(component): elif character.islower(): element.append(i) # Add indices of R groups to the element symbol - elif character.isdigit() and informative[i-1] in ['R', 'X', 'Z']: + elif character.isdigit() and informative[i - 1] in ["R", "X", "Z"]: element.append(i) - elif (character + last).isdigit() and second_last in ['R', 'X', 'Z']: + elif (character + last).isdigit() and second_last in ["R", "X", "Z"]: element.append(i) elif character.isdigit(): numbers.append(i) - elif character == '+' or character == '-': + elif character == "+" or character == "-": charges.append(i) - elif character == '@': + elif character == "@": chirals.append(i) - elif character == '*': + elif character == "*": element.append(i) - element = ''.join([informative[x] for x in element]) + element = "".join([informative[x] for x in element]) # Parsing the charge @@ -119,27 +122,44 @@ def parse_explicit(component): # Parsing chirality if len(chirals) == 1: - chiral = 'counterclockwise' + chiral = "counterclockwise" elif len(chirals) == 2: - chiral = 'clockwise' + chiral = "clockwise" else: chiral = None # dealing with lone explicit hydrogens if not element and hydrogen == 0: - element = 'H' + element = "H" hydrogens = 0 return element, chiral, charge, hydrogens def make_character_dict() -> Dict[str, str]: - """Create dict of {character: label, ->} to label smiles characters - """ + """Create dict of {character: label, ->} to label smiles characters""" character_dict = {} - atoms = ["C", "O", "N", "S", "B", "P", "F", "I", "c", "n", "o", r'*', - 'Cl', 'Br', 'p', 'b', 'p', 's'] + atoms = [ + "C", + "O", + "N", + "S", + "B", + "P", + "F", + "I", + "c", + "n", + "o", + r"*", + "Cl", + "Br", + "p", + "b", + "p", + "s", + ] cyclic = list(range(1, 100)) for atom in atoms: @@ -150,20 +170,20 @@ def make_character_dict() -> Dict[str, str]: character_dict["="] = "double_bond" character_dict["("] = "branch_start" character_dict[")"] = "branch_end" - character_dict['\\'] = 'chiral_double_bond' - character_dict['/'] = 'chiral_double_bond' - character_dict['#'] = 'triple_bond' - character_dict['$'] = 'quadruple_bond' - character_dict['.'] = 'split' - character_dict['-'] = 'single_bond' - character_dict[':'] = 'aromatic_bond' + character_dict["\\"] = "chiral_double_bond" + character_dict["/"] = "chiral_double_bond" + character_dict["#"] = "triple_bond" + character_dict["$"] = "quadruple_bond" + character_dict["."] = "split" + character_dict["-"] = "single_bond" + character_dict[":"] = "aromatic_bond" return character_dict class Smiles: character_dict = make_character_dict() - two_atom_dict = {'B': {'r'}, 'C': {'l'}} + two_atom_dict = {"B": {"r"}, "C": {"l"}} def __init__(self, string: str) -> None: self.smiles = string @@ -175,25 +195,25 @@ def get_components(self): skip = False double_digits = False square_brackets = False - component = '' + component = "" for i, character in enumerate(self.smiles): if skip: skip = False elif square_brackets: component += character - if character == ']': + if character == "]": square_brackets = False self.components.append(component) - component = '' + component = "" elif double_digits: - assert character in {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} + assert character in {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"} component += character if len(component) == 2: self.components.append(component) double_digits = False - component = '' + component = "" else: @@ -208,23 +228,25 @@ def get_components(self): except IndexError: self.components.append(character) - elif character == '[': + elif character == "[": square_brackets = True component = character - elif character == '%': + elif character == "%": double_digits = True - component = '' + component = "" else: self.components.append(character) def smiles_to_structure(self): - bond_labels = {'single_bond', - 'double_bond', - 'triple_bond', - 'quadruple_bond', - 'chiral_double_bond', - 'aromatic_bond'} + bond_labels = { + "single_bond", + "double_bond", + "triple_bond", + "quadruple_bond", + "chiral_double_bond", + "aromatic_bond", + } structure = Structure() @@ -239,7 +261,7 @@ def smiles_to_structure(self): last_atoms_dict = {0: None} # Keeps track of the nature of the bond - bond_type = 'single' + bond_type = "single" bond_chiral_symbol = None # Keeps track of chiral centres @@ -255,8 +277,8 @@ def smiles_to_structure(self): bond_nr = -1 for i, component in enumerate(self.components): - if component[0] == '[': - label = 'atom' + if component[0] == "[": + label = "atom" explicit = True else: label = self.character_dict[component] @@ -264,17 +286,17 @@ def smiles_to_structure(self): if label in bond_labels: try: next_component = self.components[i + 1] - if next_component[0] == '[': - next_label = 'atom' + if next_component[0] == "[": + next_label = "atom" else: next_label = self.character_dict[next_component] - if next_label != 'atom' and next_label != 'cyclic': - raise StructureError('bond') + if next_label != "atom" and next_label != "cyclic": + raise StructureError("bond") except IndexError: - raise StructureError('bond') + raise StructureError("bond") - if label == 'split': + if label == "split": # Starts disconnected structure; set everything back to default branch_level = 0 last_atoms_dict = {0: None} @@ -288,16 +310,16 @@ def smiles_to_structure(self): hydrogens = 0 else: element, chiral, charge, hydrogens = parse_explicit(component) - if element == 'n' and hydrogens == 1: + if element == "n" and hydrogens == 1: pyrrole = True explicit = False if element.islower(): aromatic = True element = element.upper() - if element == 'O': + if element == "O": furan = True - elif element == 'S': + elif element == "S": thiophene = True else: aromatic = False @@ -325,8 +347,8 @@ def smiles_to_structure(self): for j in range(hydrogens): atom_nr += 1 bond_nr += 1 - hydrogen = Atom('H', atom_nr, None, 0, False) - structure.add_bond(atom_2, hydrogen, 'single', bond_nr) + hydrogen = Atom("H", atom_nr, None, 0, False) + structure.add_bond(atom_2, hydrogen, "single", bond_nr) atom_1 = self.get_last_atom(branch_level, last_atoms_dict) @@ -343,15 +365,17 @@ def smiles_to_structure(self): bond_nr += 1 if atom_1.aromatic and atom_2.aromatic: - if not bond_type == 'explicit_single': - bond_type = 'aromatic' + if not bond_type == "explicit_single": + bond_type = "aromatic" else: - bond_type = 'single' + bond_type = "single" - if bond_type == 'single_chiral': - bond_type = 'single' + if bond_type == "single_chiral": + bond_type = "single" - structure.add_bond(atom_1, atom_2, bond_type, bond_nr, bond_chiral_symbol) + structure.add_bond( + atom_1, atom_2, bond_type, bond_nr, bond_chiral_symbol + ) if atom_1.chiral: # Add current atom to list of residues @@ -363,7 +387,7 @@ def smiles_to_structure(self): if hydrogens == 1: chiral_dict[atom_2].append(hydrogen) - bond_type = 'single' + bond_type = "single" bond_chiral_symbol = None else: @@ -384,23 +408,22 @@ def smiles_to_structure(self): # aromatic_system_id += 1 # Set atom_2 as the last atom in the current branch level - self.track_last_atoms_per_branch(atom_2, branch_level, - last_atoms_dict) + self.track_last_atoms_per_branch(atom_2, branch_level, last_atoms_dict) elif label == "single_bond": - bond_type = 'explicit_single' + bond_type = "explicit_single" - elif label == 'aromatic_bond': - bond_type = 'aromatic' + elif label == "aromatic_bond": + bond_type = "aromatic" elif label == "double_bond": - bond_type = 'double' + bond_type = "double" elif label == "triple_bond": - bond_type = 'triple' + bond_type = "triple" elif label == "quadruple_bond": - bond_type = 'quadruple' + bond_type = "quadruple" # If there are brackets: go up or down a branch level @@ -425,7 +448,7 @@ def smiles_to_structure(self): if atom in chiral_dict: self.add_cycle_placeholder(chiral_dict, atom, cycle_nr) - if bond_type == 'single_chiral': + if bond_type == "single_chiral": cycle_to_chiral_symbol[cycle_nr] = bond_chiral_symbol # Otherwise look up the atom that the cycle closes on @@ -433,14 +456,16 @@ def smiles_to_structure(self): else: bond_nr += 1 - atom_1, atom_2, old_bond_type = self.end_cycle(cycle_nr, atom, cyclic_dict) + atom_1, atom_2, old_bond_type = self.end_cycle( + cycle_nr, atom, cyclic_dict + ) if atom_1.aromatic and atom_2.aromatic: - if not bond_type == 'explicit_single': - bond_type = 'aromatic' + if not bond_type == "explicit_single": + bond_type = "aromatic" # atom_1.aromatic_system.add_atom(atom_2) else: - bond_type = 'single' + bond_type = "single" # aromatic_system = AromaticSystem(aromatic_system_id) # aromatic_system.add_atom(atom_2) # aromatic_system_id += 1 @@ -450,22 +475,24 @@ def smiles_to_structure(self): # aromatic_system.add_atom(atom_2) # aromatic_system_id += 1 - if bond_type == 'single_chiral': - bond_type = 'single' + if bond_type == "single_chiral": + bond_type = "single" # We have to flip the symbol here, as the previous atom occurred before the double bond - if bond_chiral_symbol == '/': - bond_chiral_symbol = '\\' - elif bond_chiral_symbol == '\\': - bond_chiral_symbol = '/' + if bond_chiral_symbol == "/": + bond_chiral_symbol = "\\" + elif bond_chiral_symbol == "\\": + bond_chiral_symbol = "/" - structure.add_bond(atom_1, atom_2, bond_type, bond_nr, bond_chiral_symbol) + structure.add_bond( + atom_1, atom_2, bond_type, bond_nr, bond_chiral_symbol + ) bond_chiral_symbol = None else: - if old_bond_type != 'single': - if old_bond_type == 'single_chiral': + if old_bond_type != "single": + if old_bond_type == "single_chiral": bond_chiral_symbol = cycle_to_chiral_symbol[cycle_nr] @@ -474,23 +501,33 @@ def smiles_to_structure(self): # elif bond_chiral_symbol == '\\': # bond_chiral_symbol = '/' - structure.add_bond(atom_1, atom_2, 'single', bond_nr, bond_chiral_symbol) + structure.add_bond( + atom_1, + atom_2, + "single", + bond_nr, + bond_chiral_symbol, + ) bond_chiral_symbol = None else: - structure.add_bond(atom_1, atom_2, old_bond_type, bond_nr) + structure.add_bond( + atom_1, atom_2, old_bond_type, bond_nr + ) else: structure.add_bond(atom_1, atom_2, bond_type, bond_nr) if atom_1 in chiral_dict: - self.replace_cycle_placeholder(chiral_dict, atom_1, atom_2, cycle_nr) + self.replace_cycle_placeholder( + chiral_dict, atom_1, atom_2, cycle_nr + ) if atom_2 in chiral_dict: chiral_dict[atom_2].append(atom_1) - bond_type = 'single' + bond_type = "single" - elif label == 'chiral_double_bond': - bond_type = 'single_chiral' + elif label == "chiral_double_bond": + bond_type = "single_chiral" bond_chiral_symbol = component structure.refine_structure() @@ -512,8 +549,11 @@ def smiles_to_structure(self): # ========================================================================= @staticmethod - def add_chiral_atom(chiral_dict: Dict['Atom', Dict[str, Any]], last_atom: 'Atom', - current_atom: 'Atom') -> None: + def add_chiral_atom( + chiral_dict: Dict["Atom", Dict[str, Any]], + last_atom: "Atom", + current_atom: "Atom", + ) -> None: """Place current_atom in one of the four bond slots of last_atom Input: @@ -540,18 +580,20 @@ def replace_cycle_placeholder(chiral_dict, chiral_atom, current_atom, cycle_nr): @staticmethod def get_chiral_permutations(order): - permutations = [tuple(order), - (order[0], order[3], order[1], order[2]), - (order[0], order[2], order[3], order[1]), - (order[1], order[0], order[3], order[2]), - (order[1], order[2], order[0], order[3]), - (order[1], order[3], order[2], order[0]), - (order[2], order[0], order[1], order[3]), - (order[2], order[3], order[0], order[1]), - (order[2], order[1], order[3], order[0]), - (order[3], order[0], order[2], order[1]), - (order[3], order[1], order[0], order[2]), - (order[3], order[2], order[1], order[0])] + permutations = [ + tuple(order), + (order[0], order[3], order[1], order[2]), + (order[0], order[2], order[3], order[1]), + (order[1], order[0], order[3], order[2]), + (order[1], order[2], order[0], order[3]), + (order[1], order[3], order[2], order[0]), + (order[2], order[0], order[1], order[3]), + (order[2], order[3], order[0], order[1]), + (order[2], order[1], order[3], order[0]), + (order[3], order[0], order[2], order[1]), + (order[3], order[1], order[0], order[2]), + (order[3], order[2], order[1], order[0]), + ] return permutations @@ -562,7 +604,7 @@ def determine_chirality(self, order, chirality, atom): try: assert len(order) + len(lone_pairs) == 4 except AssertionError: - raise StructureError('chiral centre') + raise StructureError("chiral centre") original_order = order + lone_pairs @@ -573,20 +615,20 @@ def determine_chirality(self, order, chirality, atom): original_order.sort(key=lambda x: x.nr) new_order = tuple(original_order) if new_order in chiral_permutations: - if chirality == 'counterclockwise': - new_chirality = 'counterclockwise' + if chirality == "counterclockwise": + new_chirality = "counterclockwise" else: - new_chirality = 'clockwise' + new_chirality = "clockwise" else: - if chirality == 'counterclockwise': - new_chirality = 'clockwise' + if chirality == "counterclockwise": + new_chirality = "clockwise" else: - new_chirality = 'counterclockwise' + new_chirality = "counterclockwise" return new_chirality @staticmethod - def is_new_cycle(cyclic_dict: Dict[int, 'Atom'], cycle_nr: int) -> bool: + def is_new_cycle(cyclic_dict: Dict[int, "Atom"], cycle_nr: int) -> bool: """Return bool, True if a new cycle is recorded, False if not Input: @@ -604,8 +646,12 @@ def is_new_cycle(cyclic_dict: Dict[int, 'Atom'], cycle_nr: int) -> bool: return True @staticmethod - def start_cycle(cycle_nr: int, atom: 'Atom', - cyclic_dict: Dict[int, Tuple['Atom', str]], bond_type: str) -> None: + def start_cycle( + cycle_nr: int, + atom: "Atom", + cyclic_dict: Dict[int, Tuple["Atom", str]], + bond_type: str, + ) -> None: """Add a new atom and corresponding cycle number to cyclic dict Input: @@ -618,8 +664,9 @@ def start_cycle(cycle_nr: int, atom: 'Atom', cyclic_dict[cycle_nr] = (atom, bond_type) @staticmethod - def end_cycle(cycle_nr: int, atom: 'Atom', - cyclic_dict: Dict[int, 'Atom']) -> Tuple[Union['Atom', None], 'Atom', str]: + def end_cycle( + cycle_nr: int, atom: "Atom", cyclic_dict: Dict[int, "Atom"] + ) -> Tuple[Union["Atom", None], "Atom", str]: """Return pair of atoms that close a cycle Input: @@ -639,8 +686,11 @@ def end_cycle(cycle_nr: int, atom: 'Atom', return atom_old, atom, bond_type @staticmethod - def track_last_atoms_per_branch(new_atom: 'Atom', current_level: int, - last_atoms_dict: Dict[int, Union['Atom', None]]) -> None: + def track_last_atoms_per_branch( + new_atom: "Atom", + current_level: int, + last_atoms_dict: Dict[int, Union["Atom", None]], + ) -> None: """Update which atom was the last to occur at the current branch level Input: @@ -654,7 +704,9 @@ def track_last_atoms_per_branch(new_atom: 'Atom', current_level: int, last_atoms_dict[current_level] = new_atom @staticmethod - def get_last_atom(current_level: int, last_atoms_dict: Dict[int, Union['Atom', None]]) -> 'Atom': + def get_last_atom( + current_level: int, last_atoms_dict: Dict[int, Union["Atom", None]] + ) -> "Atom": """Return the last atom in the current branch level Input: diff --git a/setup.py b/setup.py index 20dca14..8253fd0 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ from setuptools import setup, find_packages -VERSION = '1.0.9' -DESCRIPTION = 'PIKACHU: Python-based Informatics Kit for Analysing CHemical Units' -LONG_DESCRIPTION = 'An easy-to-use cheminformatics kit with few dependencies.' +VERSION = "1.0.9" +DESCRIPTION = "PIKACHU: Python-based Informatics Kit for Analysing CHemical Units" +LONG_DESCRIPTION = "An easy-to-use cheminformatics kit with few dependencies." setup( name="pikachu-chem", @@ -12,5 +12,5 @@ description=DESCRIPTION, long_description=LONG_DESCRIPTION, packages=find_packages(), - install_requires=['matplotlib'], + install_requires=["matplotlib"], ) diff --git a/validation/drawing_validation.py b/validation/drawing_validation.py index b003a2c..6f3ea93 100644 --- a/validation/drawing_validation.py +++ b/validation/drawing_validation.py @@ -19,8 +19,8 @@ def validate_pikachu(smiles_file, finetune=True, strict_mode=False): options.finetune = finetune options.strict_mode = strict_mode - with open(smiles_file, 'r') as smiles_f: - with open('failed_smiles.smi', 'w') as failed_smiles: + with open(smiles_file, "r") as smiles_f: + with open("failed_smiles.smi", "w") as failed_smiles: for line in smiles_f: smiles = line.strip() @@ -35,13 +35,13 @@ def validate_pikachu(smiles_file, finetune=True, strict_mode=False): # pikachu_structure = read_smiles(smiles) rdkit_structure = MolFromSmiles(smiles) - MolToMolFile(rdkit_structure, 'temp_rdkit.mol') + MolToMolFile(rdkit_structure, "temp_rdkit.mol") - smiles_to_molfile(smiles, 'temp.mol', options=options) + smiles_to_molfile(smiles, "temp.mol", options=options) - #MolFileWriter(pikachu_structure, 'temp.mol', drawing_options=options).write_mol_file() + # MolFileWriter(pikachu_structure, 'temp.mol', drawing_options=options).write_mol_file() - rdkit_structure_pikachu = MolFromMolFile('temp.mol') + rdkit_structure_pikachu = MolFromMolFile("temp.mol") rdkit_smiles_original = MolToSmiles(rdkit_structure) rdkit_smiles_pikachu = MolToSmiles(rdkit_structure_pikachu) @@ -50,14 +50,16 @@ def validate_pikachu(smiles_file, finetune=True, strict_mode=False): correct += 1 else: incorrect += 1 - incorrect_smiles.append((smiles, rdkit_smiles_original, rdkit_smiles_pikachu)) + incorrect_smiles.append( + (smiles, rdkit_smiles_original, rdkit_smiles_pikachu) + ) print("Original:", smiles) print("RDKit: ", rdkit_smiles_original) print("PIKAChU: ", rdkit_smiles_pikachu) except Exception as e: print(smiles, e) - failed_smiles.write(f'{smiles}\t{e}\n') + failed_smiles.write(f"{smiles}\t{e}\n") if total == 100000: break @@ -79,9 +81,13 @@ def validate_pikachu(smiles_file, finetune=True, strict_mode=False): if len(argv) > 4: finetune = bool(int(argv[4])) - incorrect_smiles = validate_pikachu(smiles_file, finetune=finetune, strict_mode=strict_mode) + incorrect_smiles = validate_pikachu( + smiles_file, finetune=finetune, strict_mode=strict_mode + ) - with open(out_file, 'w') as out: - out.write("Original SMILES\tCanonical RDKit SMILES original\tCanonical RDKit SMILES PIKAChU\n") + with open(out_file, "w") as out: + out.write( + "Original SMILES\tCanonical RDKit SMILES original\tCanonical RDKit SMILES PIKAChU\n" + ) for smiles, rdkit_smiles_original, rdkit_smiles_pikachu in incorrect_smiles: out.write(f"{smiles}\t{rdkit_smiles_original}\t{rdkit_smiles_pikachu}\n") diff --git a/validation/speed_assessment.py b/validation/speed_assessment.py index ebd4254..5ec82d8 100644 --- a/validation/speed_assessment.py +++ b/validation/speed_assessment.py @@ -34,7 +34,9 @@ def substructure_matching_speed_pikachu(smiles, subsmiles): has_substructure.append(len(matches)) time_1 = time.time() - print(f'Time spent by PIKAChU finding {len(subsmiles)} substructures in {len(smiles)} structures: {time_1 - start_time}') + print( + f"Time spent by PIKAChU finding {len(subsmiles)} substructures in {len(smiles)} structures: {time_1 - start_time}" + ) return has_substructure @@ -58,13 +60,15 @@ def drawing_speed_pikachu(smiles, measuring_points): if drawn_smiles in measuring_points: time_1 = time.time() - print(f'Time spent by PIKAChU drawing {drawn_smiles} SMILES: {time_1 - start_time}') + print( + f"Time spent by PIKAChU drawing {drawn_smiles} SMILES: {time_1 - start_time}" + ) print(f"Failed smiles: {failed_smiles}") if drawn_smiles == measuring_points[-1]: break - print(f'Time spent by PIKAChU drawing {drawn_smiles} SMILES: {time_1 - start_time}') + print(f"Time spent by PIKAChU drawing {drawn_smiles} SMILES: {time_1 - start_time}") print(f"Failed smiles: {failed_smiles}") @@ -88,12 +92,14 @@ def reading_speed_pikachu(smiles, measuring_points): if r_smiles in measuring_points: time_1 = time.time() - print(f'Time spent by PIKAChU reading {r_smiles} SMILES: {time_1 - start_time}') + print( + f"Time spent by PIKAChU reading {r_smiles} SMILES: {time_1 - start_time}" + ) print(f"Failed smiles: {failed_smiles}") if r_smiles == measuring_points[-1]: break - print(f'Time spent by PIKAChU reading {r_smiles} SMILES: {time_1 - start_time}') + print(f"Time spent by PIKAChU reading {r_smiles} SMILES: {time_1 - start_time}") print(f"Failed smiles: {failed_smiles}") @@ -108,7 +114,9 @@ def substructure_matching_speed_rdkit(smiles, subsmiles): has_substructure.append(len(matches)) time_1 = time.time() - print(f'Time spent by RDKit finding {len(subsmiles)} substructures in {len(smiles)} structures: {time_1 - start_time}') + print( + f"Time spent by RDKit finding {len(subsmiles)} substructures in {len(smiles)} structures: {time_1 - start_time}" + ) return has_substructure @@ -123,14 +131,16 @@ def drawing_speed_rdkit(smiles, measuring_points): if drawn_smiles in measuring_points: time_1 = time.time() - print(f'Time spent by RDKit drawing {drawn_smiles} SMILES: {time_1 - start_time}') + print( + f"Time spent by RDKit drawing {drawn_smiles} SMILES: {time_1 - start_time}" + ) if drawn_smiles == measuring_points[-1]: break time_1 = time.time() - print(f'Time spent by RDKit drawing {drawn_smiles} SMILES: {time_1 - start_time}') + print(f"Time spent by RDKit drawing {drawn_smiles} SMILES: {time_1 - start_time}") def reading_speed_rdkit(smiles, measuring_points): @@ -139,7 +149,7 @@ def reading_speed_rdkit(smiles, measuring_points): x = MolFromSmiles(s) time_1 = time.time() - print(f'Time spent by RDKit reading {len(smiles)} SMILES: {time_1 - start_time}') + print(f"Time spent by RDKit reading {len(smiles)} SMILES: {time_1 - start_time}") def compare_substructure_matching_outcomes(pikachu_list, rdkit_list): @@ -160,12 +170,12 @@ def compare_substructure_matching_outcomes(pikachu_list, rdkit_list): def read_smiles_file(smiles_file): smiles_strings = [] - with open(smiles_file, 'r') as s_file: + with open(smiles_file, "r") as s_file: for line in s_file: smiles = line.strip() if smiles: smiles_strings.append(smiles) - + seed(11) shuffle(smiles_strings) return smiles_strings @@ -187,7 +197,7 @@ def read_smiles_file(smiles_file): # drawing_speed_pikachu(smiles_strings, measuring_points) print("Reading") - #reading_speed_pikachu(smiles_strings, measuring_points) + # reading_speed_pikachu(smiles_strings, measuring_points) drawing_speed_rdkit(smiles_strings, measuring_points) # drawing_speed_rdkit(smiles_strings) @@ -197,7 +207,3 @@ def read_smiles_file(smiles_file): # rdkit_list = substructure_matching_speed_pikachu(supersmiles_strings, subsmiles_strings) # # correct, incorrect, mistake_indices = compare_substructure_matching_outcomes(pikachu_list, rdkit_list) - - - - diff --git a/validation/steric_clashes.py b/validation/steric_clashes.py index ca42116..9f769a9 100644 --- a/validation/steric_clashes.py +++ b/validation/steric_clashes.py @@ -100,8 +100,9 @@ def find_clashes_rdkit(smiles): av_bond_length = find_average_bond_length(atoms, atom_positions, bond_lookup) if av_bond_length: - clashes = find_steric_clashes(atoms, atom_positions, av_bond_length, bond_lookup) - + clashes = find_steric_clashes( + atoms, atom_positions, av_bond_length, bond_lookup + ) # Only happens if there are no bonds in the structure. else: @@ -117,7 +118,9 @@ def find_clashes_pikachu(smiles): av_bond_length = find_average_bond_length(atoms, atom_positions, bond_lookup) if av_bond_length: - clashes = find_steric_clashes(atoms, atom_positions, av_bond_length, bond_lookup) + clashes = find_steric_clashes( + atoms, atom_positions, av_bond_length, bond_lookup + ) # Only happens if there are no bonds in the structure. else: @@ -137,9 +140,9 @@ def assess_tools(smiles_file, failed_out, clashes_out): failed_smiles_rdkit = 0 failed_smiles_pikachu = 0 - clashes = open(clashes_out, 'w') - failed = open(failed_out, 'w') - with open(smiles_file, 'r') as smi: + clashes = open(clashes_out, "w") + failed = open(failed_out, "w") + with open(smiles_file, "r") as smi: for i, line in enumerate(smi): smiles = line.strip() # try: @@ -174,8 +177,8 @@ def assess_tools(smiles_file, failed_out, clashes_out): print(f"Clashing structures PIKAChU: {clashing_structures_pikachu}") # print(f"Failed SMILES RDKit: {failed_smiles_rdkit}") print(f"Failed SMILES PIKAChU: {failed_smiles_pikachu}") - print('\n') - + print("\n") + if i == 100000: break @@ -192,10 +195,6 @@ def assess_tools(smiles_file, failed_out, clashes_out): if __name__ == "__main__": smiles_file = argv[1] - failed = smiles_file.split('.')[0] + '_failed_steric.txt' - clashing = smiles_file.split('.')[0] + '_clashing_steric.txt' + failed = smiles_file.split(".")[0] + "_failed_steric.txt" + clashing = smiles_file.split(".")[0] + "_clashing_steric.txt" assess_tools(smiles_file, failed, clashing) - - - - diff --git a/validation/test_drawing.py b/validation/test_drawing.py index 34aab3f..71a57e3 100644 --- a/validation/test_drawing.py +++ b/validation/test_drawing.py @@ -7,22 +7,8 @@ def test_set_r_group_indices_subscript(self): # Test that only R group indices are returned as subscript dummy_structure = read_smiles("CC") drawer = Drawer(dummy_structure) - test_str = ['Xe', - 'C', - '13C', - 'O', - 'R', - 'R1', - 'X23', - 'Z54'] - expected_str = ['Xe', - 'C', - '13C', - 'O', - 'R', - 'R₁', - 'X₂₃', - 'Z₅₄'] + test_str = ["Xe", "C", "13C", "O", "R", "R1", "X23", "Z54"] + expected_str = ["Xe", "C", "13C", "O", "R", "R₁", "X₂₃", "Z₅₄"] for index in range(len(test_str)): result = drawer.set_r_group_indices_subscript(test_str[index]) expected = expected_str[index] diff --git a/validation/test_read_molfile.py b/validation/test_read_molfile.py index 27c1ecf..45b4eb7 100644 --- a/validation/test_read_molfile.py +++ b/validation/test_read_molfile.py @@ -9,8 +9,8 @@ class TestMolFileReader: # Define test molfile_path and molefile_str test_dir = os.path.split(__file__)[0] - molfile_path = os.path.join(test_dir, 'temp.mol') - with open(molfile_path, 'r') as molfile: + molfile_path = os.path.join(test_dir, "temp.mol") + with open(molfile_path, "r") as molfile: molfile_str = molfile.read() def test_constructor(self): diff --git a/validation/writing_validation.py b/validation/writing_validation.py index acc2350..13ae22a 100644 --- a/validation/writing_validation.py +++ b/validation/writing_validation.py @@ -54,6 +54,3 @@ def assess_writing_accuracy(smiles_strings): if __name__ == "__main__": smiles = smiles_from_file(argv[1], all=True) assess_writing_accuracy(smiles) - - - From e60a3e7d754931de0d5b72fc343a5ab8ef01369a Mon Sep 17 00:00:00 2001 From: Kohulan Date: Thu, 24 Nov 2022 15:19:15 +0100 Subject: [PATCH 2/3] fix: Clean up irrelevant files in repository --- .DS_Store | Bin 10244 -> 0 bytes .idea/.gitignore | 8 -------- .idea/inspectionProfiles/profiles_settings.xml | 6 ------ .idea/misc.xml | 4 ---- .idea/modules.xml | 8 -------- .idea/pikachu.iml | 11 ----------- .idea/vcs.xml | 6 ------ __init__.py | 0 __pycache__/__init__.cpython-39.pyc | Bin 145 -> 0 bytes __pycache__/atom.cpython-39.pyc | Bin 12521 -> 0 bytes __pycache__/atom_properties.cpython-39.pyc | Bin 7532 -> 0 bytes __pycache__/bond.cpython-39.pyc | Bin 4901 -> 0 bytes __pycache__/bond_properties.cpython-39.pyc | Bin 1437 -> 0 bytes __pycache__/chirality.cpython-39.pyc | Bin 632 -> 0 bytes __pycache__/drawing.cpython-39.pyc | Bin 34161 -> 0 bytes __pycache__/electron.cpython-39.pyc | Bin 2151 -> 0 bytes __pycache__/errors.cpython-39.pyc | Bin 764 -> 0 bytes __pycache__/find_cycles.cpython-39.pyc | Bin 10487 -> 0 bytes __pycache__/graph_to_smiles.cpython-39.pyc | Bin 12167 -> 0 bytes __pycache__/kekulisation.cpython-39.pyc | Bin 7647 -> 0 bytes __pycache__/lone_pair.cpython-39.pyc | Bin 457 -> 0 bytes __pycache__/math_functions.cpython-39.pyc | Bin 10646 -> 0 bytes __pycache__/orbital.cpython-39.pyc | Bin 4837 -> 0 bytes __pycache__/pikachu.cpython-39.pyc | Bin 1080 -> 0 bytes __pycache__/ring_identification.cpython-39.pyc | Bin 1176 -> 0 bytes __pycache__/rings.cpython-39.pyc | Bin 4643 -> 0 bytes __pycache__/shell.cpython-39.pyc | Bin 5491 -> 0 bytes __pycache__/smiles.cpython-39.pyc | Bin 12061 -> 0 bytes __pycache__/sssr.cpython-39.pyc | Bin 8248 -> 0 bytes __pycache__/structure.cpython-39.pyc | Bin 37543 -> 0 bytes __pycache__/substructure_search.cpython-39.pyc | Bin 1492 -> 0 bytes pikachu/__pycache__/__init__.cpython-310.pyc | Bin 155 -> 0 bytes pikachu/__pycache__/__init__.cpython-36.pyc | Bin 145 -> 0 bytes pikachu/__pycache__/__init__.cpython-38.pyc | Bin 153 -> 0 bytes pikachu/__pycache__/__init__.cpython-39.pyc | Bin 153 -> 0 bytes pikachu/__pycache__/errors.cpython-310.pyc | Bin 2197 -> 0 bytes pikachu/__pycache__/errors.cpython-36.pyc | Bin 1614 -> 0 bytes pikachu/__pycache__/errors.cpython-38.pyc | Bin 1242 -> 0 bytes pikachu/__pycache__/errors.cpython-39.pyc | Bin 2223 -> 0 bytes pikachu/__pycache__/general.cpython-310.pyc | Bin 12296 -> 0 bytes pikachu/__pycache__/general.cpython-36.pyc | Bin 11731 -> 0 bytes pikachu/__pycache__/general.cpython-38.pyc | Bin 10921 -> 0 bytes pikachu/__pycache__/general.cpython-39.pyc | Bin 12437 -> 0 bytes .../__pycache__/math_functions.cpython-310.pyc | Bin 21963 -> 0 bytes .../__pycache__/math_functions.cpython-36.pyc | Bin 22507 -> 0 bytes .../__pycache__/math_functions.cpython-38.pyc | Bin 19098 -> 0 bytes .../__pycache__/math_functions.cpython-39.pyc | Bin 22081 -> 0 bytes 47 files changed, 43 deletions(-) delete mode 100644 .DS_Store delete mode 100644 .idea/.gitignore delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/pikachu.iml delete mode 100644 .idea/vcs.xml delete mode 100644 __init__.py delete mode 100644 __pycache__/__init__.cpython-39.pyc delete mode 100644 __pycache__/atom.cpython-39.pyc delete mode 100644 __pycache__/atom_properties.cpython-39.pyc delete mode 100644 __pycache__/bond.cpython-39.pyc delete mode 100644 __pycache__/bond_properties.cpython-39.pyc delete mode 100644 __pycache__/chirality.cpython-39.pyc delete mode 100644 __pycache__/drawing.cpython-39.pyc delete mode 100644 __pycache__/electron.cpython-39.pyc delete mode 100644 __pycache__/errors.cpython-39.pyc delete mode 100644 __pycache__/find_cycles.cpython-39.pyc delete mode 100644 __pycache__/graph_to_smiles.cpython-39.pyc delete mode 100644 __pycache__/kekulisation.cpython-39.pyc delete mode 100644 __pycache__/lone_pair.cpython-39.pyc delete mode 100644 __pycache__/math_functions.cpython-39.pyc delete mode 100644 __pycache__/orbital.cpython-39.pyc delete mode 100644 __pycache__/pikachu.cpython-39.pyc delete mode 100644 __pycache__/ring_identification.cpython-39.pyc delete mode 100644 __pycache__/rings.cpython-39.pyc delete mode 100644 __pycache__/shell.cpython-39.pyc delete mode 100644 __pycache__/smiles.cpython-39.pyc delete mode 100644 __pycache__/sssr.cpython-39.pyc delete mode 100644 __pycache__/structure.cpython-39.pyc delete mode 100644 __pycache__/substructure_search.cpython-39.pyc delete mode 100644 pikachu/__pycache__/__init__.cpython-310.pyc delete mode 100644 pikachu/__pycache__/__init__.cpython-36.pyc delete mode 100644 pikachu/__pycache__/__init__.cpython-38.pyc delete mode 100644 pikachu/__pycache__/__init__.cpython-39.pyc delete mode 100644 pikachu/__pycache__/errors.cpython-310.pyc delete mode 100644 pikachu/__pycache__/errors.cpython-36.pyc delete mode 100644 pikachu/__pycache__/errors.cpython-38.pyc delete mode 100644 pikachu/__pycache__/errors.cpython-39.pyc delete mode 100644 pikachu/__pycache__/general.cpython-310.pyc delete mode 100644 pikachu/__pycache__/general.cpython-36.pyc delete mode 100644 pikachu/__pycache__/general.cpython-38.pyc delete mode 100644 pikachu/__pycache__/general.cpython-39.pyc delete mode 100644 pikachu/__pycache__/math_functions.cpython-310.pyc delete mode 100644 pikachu/__pycache__/math_functions.cpython-36.pyc delete mode 100644 pikachu/__pycache__/math_functions.cpython-38.pyc delete mode 100644 pikachu/__pycache__/math_functions.cpython-39.pyc diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index b3513c7517452afb612dca53002799354b7b51cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10244 zcmeHMO>7%Q6n^8Tbe*(`lP09)N3t-LkQyQbj{)1SM8ng_O29YkT8tyWX*O z*GW?olsgxYKmsHX2gJ1%f)hgg9a1O2-prd>6A{bS%VR`IB2w@On%Ix5LlW)cX-hG$CJo9UPex@dU*gqT8)sAzArK)D zArK)DArK*OzaW5^Et1mX(kPD*h!BVnXdxi>ha?_BGag-csoXk{$twV87pi4KIUx$5 zNsnecy6#f6ATd`I)D>mk6@$6rs84!)8IP{J)Dd-bDKOQ@C2e8?5OK8~zyXa(-6V!`69@4AD7tn-#JnQenKlk5n9rYV zgyvmH_|e>97R432=B=i0i#0>Hopkyq7TeRc_o01?(yeqK=)RI);wyPab?SCjwJwRb zoa!tUz48UljaFGL>4pAu)J{n~tiRnk9wxvIdsRFY0EQuVi`MEZef5yxX8CaV1_z z9BMSiCnrXd6DKCuMv{%m@$^XY*u*nyYjI`x$k@s0h3e{!#w%;Dz5^AJ7;`Cr4yJeU zx`Wz!Y1vboece?1mqenec)Yu3|AF4VME}6S{=xpCp`nL|hYlTn`uv!Dp;AVyF?wqA*rtEKK zanaVC`9h-4E?L|#a@^J(otqbZtrsubFvhf_Rj18GjuDpu{!C4<*>jp@gSN@fUsNqi zH7iwD-dA-C~($B^{b5m3hvEI+c0KEhH$_1Ya3! zmORQXm}*svr!qOWB924kQEb@fX^yVZ4SIv#qnq>@-KL-E54y|tu>^a99c9PZNp^}o z%cj{&tiTqS?uy5n_I@ll4!er?9qhfFw@ibZ6>y4P20AQF2TQY#1?nKT&>q^RVnWux zuy)nQeYU*DCdRS6#wPB0dF@L15a>PhJWbOXI!iB59{DJszTPQ_D2dVhmJBnx4qqrX8% - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 8abfa05..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index fb39518..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/pikachu.iml b/.idea/pikachu.iml deleted file mode 100644 index 76598a1..0000000 --- a/.idea/pikachu.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/__pycache__/__init__.cpython-39.pyc b/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 02d2970b8c0200cc3210290666b9baa56801dfcb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 145 zcmYe~<>g`kg0&xA6G8N25P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HSerR!OQL%nf zVi6D}>bvA8m*%GCl@#j-WVq;;lb8ZCnjf<>c_`t=4F<|$LkeT-r}&y%}*)K MNwou+_Zf&80GiVw%>V!Z diff --git a/__pycache__/atom.cpython-39.pyc b/__pycache__/atom.cpython-39.pyc deleted file mode 100644 index b5270d419220527c6de800d1fcc0407d1c6ba8bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12521 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 diff --git a/__pycache__/bond_properties.cpython-39.pyc b/__pycache__/bond_properties.cpython-39.pyc deleted file mode 100644 index aa7bfd16ab76fcd87959c931563fa19193b13a77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1437 zcmbtUL2uJA6i(7M?aDTU7=oiEE{n7+62}Q9m{!oFu~jQvag7i(lUNKKs3wXR@}|c3=#DbjY6XI6sYYd(=RA4O2Y> z@y?+Rb-%&9^dg5jmrmqTujd4{7_18exGKLRSTKWgB9FRJje4|3>$E}rt9n$QX&!A} zHKGP>(G}VT`2egwU7gx1Ym=_QUhC!hzQ|}-ikwN!nIgYD$7v%NZ%~{NrI6A>@@$C4 zL~>Cml+Un83{W4uRl9P~TJnCOnX>BkY*^I*vA)O|)&id~J{;*?M0u0O=8{RA7n4m?SLI7PN&3P+Hz%aB91>@ORB;zVTQ}lt5DbZYHw(d6-w^2=8=&uPh zfHN_zI;n#Q>ZPwB9SJLaDvG{&LVj|1u6!;Cl?5z=hBQ4Yn?|&ZSOHPCh!ke8xOA@F zSJ&?Ip#Cq;To@CW&T-HxTNq~~Wf+%jj8j32#H?2_hK-V`rHL^WF~-rxO*2uVMlUGl zSzhS!@sd7^7GGB1R`hfMIj`Y&M{dA*thjy~epF1VuumlXNq8XQB8AUEh27CXsD(&W znDb909u=Wv0}{h!5nAH;HL;UlNRwDI)eeZ3TYu!a_V>qcOjoY=+xM5fwZ6xJXP@Q6 l{gdMZ+&w+%c1}MWc6#%(R;wq#U$$OW$=kDAntHAWe*x<5qVNC! diff --git a/__pycache__/chirality.cpython-39.pyc b/__pycache__/chirality.cpython-39.pyc deleted file mode 100644 index f4341c28b7cb8dcc766fecee59cb6d91d79327e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 632 zcmZvau};G<5Qfi5gId%|Y^aDAh=lM9AXJI6R!E42r80@zL{00+c7{q}f|ucyA~vSX zh=qx>tyms#a+jR{`|hku*lczRxOSiSY!U-}nIK#d1`9MbMVCWP-r*VY=n>L5Be*pz zlXr%8ho-Jj(a^#hX_1)YTOL^-TR)n^X5WZgPmk612*-dLM_l;({6bJPIa*S!e3;LwERP=>Oycu1Jtl9H}R`j7*QY}QKXkG4D zRy0)d4a>BUG%HHUs!|`X>tlDm<$CRQ)?o*86m^LX$O*al@A?_dr1^tZDX~v`qQFg1 R>&7>GkvA2;HTAyH;1Au3d&2+# diff --git a/__pycache__/drawing.cpython-39.pyc b/__pycache__/drawing.cpython-39.pyc deleted file mode 100644 index 46c19b14e641056aa7aba16c7a385e3179458235..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34161 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 OFJmuH`xU?7yZ;RmKhTB% diff --git a/__pycache__/graph_to_smiles.cpython-39.pyc b/__pycache__/graph_to_smiles.cpython-39.pyc deleted file mode 100644 index 0b07da3deb4c9a2ef536beb7fd45b2445b7cbd20..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12167 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 diff --git a/__pycache__/kekulisation.cpython-39.pyc b/__pycache__/kekulisation.cpython-39.pyc deleted file mode 100644 index 7db9305752a231f529dbbae3eed18ae59628b333..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7647 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 blbknu9^5hi(e)O1Q2b|=Q(s?QuQz@R0!!`6 diff --git a/__pycache__/lone_pair.cpython-39.pyc b/__pycache__/lone_pair.cpython-39.pyc deleted file mode 100644 index 7142b0fbd248f015c979636086f962ebb6bf6548..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 457 zcmY*U!Ab)$5S?U~x)cj4{={CI9}p>6@gk)lUiLD?O-dJcH*2zcPGA*q@X z!X=S~LKrBd5k_vINK8VSG-mg%wM$+3lKl_Cdsg2N!Wq#oR?UG=QX^`N|6 z>9TiT)if4%G}p9yjTvIqRy`uyhLi}zNW7gNW}hC;7yKi{RyP(gju9JYhMM=Mh%bY# q_a{Chhuv^TbizHs)`Y+>Wnxz^@_jzK%m4B$h}qTtiECDU6W|YSeqzV~ diff --git a/__pycache__/math_functions.cpython-39.pyc b/__pycache__/math_functions.cpython-39.pyc deleted file mode 100644 index 03bbff84818516bfd330caae64405a763d0276cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10646 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 diff --git a/__pycache__/orbital.cpython-39.pyc b/__pycache__/orbital.cpython-39.pyc deleted file mode 100644 index 99b703401d1e0a405e3620a70efdec0235f89b8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4837 zcmcgwTW=f38J*b+xumF@@3$n5<1}WFn5~?oNt!rr=nw~Rz;qrz)1n5%p&1Gk2zWba}dwIDk z@O<{qPui`sLj0SZ;$x$84<&p9L|?8Cq30+UJr_M&l^%$C z`4nG>x{SR~x zRn%p*q-v-=wX9Z9SJbLnLtRzt*!)af{Zv2P8MeC*f?>zO^4Y^S_x%_pd<*1A9tmF@ zS)lUB2DKguP_&*oUkfRQ?vdEFkEN35I9^f$>s!rJE>bVYPJ7tji@k6==yv^{j%)p7 zUw=3n1hJR)!gy5$TRXkL&j;*o(2KoJd(iIe3`cb*cEX^$rCltmOC)8G6QP$;J-^%d zEDUtmc-YqXYd7xqI|q9~Zx}XyvwgoY?DxB2W3cnM-Pt~9=wPee8TNG}-RkyW6nnnE z)7u&PKE)4fAVQX8$wHByjLv?VE9-VPvm-exeb~;mwuTOLBEBS*Uy84+2jWCF>sEnp zskw<^J|QdUk_{aUwC_`>kPC|G&6D?wH>ay`b0r^J`V5RDX(TgZVdN%8tbD}A$U7LZ z^AQIlzsyW)OzAdb8OCxDOBKtlx@+_vueAq*pr`yWGg!QyjtsJqFdtI0LtFWfUI)ih ziqI8|QH>#qkX0CAT%s!lb4(Add|{;{ZyZ@^CdBdiuJ}UkuSl^k_E+|k;J2IF1P6TsiP~g< z+SU%vtbUUE>$b_*aV1Dk>o6{-ucne$yj=V)+`x+IiIOBSP%FArh^C3@&H56LfnIwr z@cp>z`+I$L&}F;k`}+s&Zt_O2Vp)BLgt<~P{b+`*rmt$MSHDKWlcYeXZk?&kWLHmm z70>pnD6UubD&}nm(b>?YVoqcKp zveeaib>WEwXt}5^0gSlnvbuu0q^_!KsLSdF^&)Cdy`-+AuBeyQ4b)Zjin@t_I9iG}@4R`Z@phy)H=?D(q;qIGqogxJr)6Wgs5-jE6|qe{svh1#ZB~z}ks0Ss z_42~4G*gRy1q7@!PbX=LG1lxbF=Y?ibbsUS2AdI;34_Yykwa{zh}LX_&>Vx@T!mCa zR0dkk&N;ez-5bM^xSF~$5!zDb;ppgBG1HuUvGcIsQ}HV4PVZ437H$G$O#Wot8+f2^ zvo&@-<8}1dCQ5py6IzXb(OMc1i}4k`)=2(CG#KFe8Yu7;sAVV+*H`uf_VZC4nIcUc01hm{oj*W&IJ~{UZ=CW-xL-Fy&CVRDdV?<73PYd5jaZkIj_5o-xw|A0{&3BTx$NSE;Ayx3Sn< zMJ(xHum5>q_U%vXn_Uua7iP?C&4cA*e%1n^6XU$XA^0otXIN!dKC^}fn4-;VRY?)5 zmXF;8{XO$?oKAHAJAR?;%i>wZw4BSrfLRwgvGlxBy`DY>>pa4Kc>eX#_?&CjtJ)ac z1Ud1sre?-zA&C|f+RQewGidMVd0Npo@j7H-qzik+T9ei9N|t9;WpplckK#EuqnO+# z?_&XoL&Z2_#VG4TjLu$Npa2O4mNvBbi!ud;_Cpe`f4yK`LEirU-Y%swINv#w$*o zk(0q~H9_}MoxM*!> z>`$>Ariy{r*8$abZ7Dq>&uq%WJN5I1E@2DHn*xwnPMcZEaXn^(gj&B?r_slSjY1%8^X z7Ni$2hZ4@=mzZ0nwI>F`5ruWK3t~?2XDbs;dI+kZBrk6NLQz9vX-5^d$fX{L?~9lbx? zii$i>bu<}`li}esLiH^f7CByTyqug=0jfmq*AW`zP_K{>;D7}_Jsxyn6|h5lJ3~(7 zda%vYu)9h|3D_>hxgbtOLF_Hy$QpLwq4`tijcmMzZ diff --git a/__pycache__/ring_identification.cpython-39.pyc b/__pycache__/ring_identification.cpython-39.pyc deleted file mode 100644 index 496426c8dc147349cd94cb0328ec0f85b9412db8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1176 zcmY+DOKTKC5XZZFp1Wq(_#_ce;$<(c=s^h*1Y-^!f?&KX=rEb?xU-p^N6$nE^gvMY zAbyDCDELAA3=M+d#e4-%{%hiz*fU*S)l*gdsNckcL7$<`zCVq>`HcM}<9rh^9;4S+ zAtaOh1Dh}@q=S)5_XQhy3CE{J3!r%DPtfa65R$jtunqQVr4>fZ5W6SzN)<|{<(yTw zs?c!#QM#?ud2M!Hd?pa_uyqju-MFi)^^7-jq=)D=XfD2AZ++v>JVO~L#*zL5);iUs zTC48B3a&F5?uq&%tz#HJi)%0<>d+O{KO?e=N$QVFg@7OsZ8s*NsHVL z-o}~A6BX4{m1Q<8^iJBuS#6j8YEg4gD%(?;N*Z0{wRPjBn2!P*(iqjMfu@T&(j|I{ zRvngUv~Z7deU>SY=gJ0u^V)FX^M&avi2MC){IXWM9`D2&Z#>>8lKr{Ln|l0wx-o8w zBCE$`x)&$Y{jpZNancletke8WluEdzyJ^y$=}viIhsjhWd(mQJbPM%u2*zC=@;(>B zSj=y0obsEfCYq@p01)}r*XlEL9|+O>q3i)qAw6Vpt3%@$ zcUJ(Y{?-W{d{fg65?c2)ainQd6IR-R*dFz)1B^8N8`h7@QsvTKOX4g!BcY5_4IFex zQ9TlLt@;89;%3y>0}PWP_>C5jDmhufqUY==D{>W4GChR5?N3#sbU`q7Y4k-(xJ=>_ ziOVF2uTw!kxxIKw$8P#oMFh|dOxAZH7+)qRx=xr77{EiUVCFn1gpAAkLf=Hp*60+O a&N#PJw`jLs_-J0p{YgqQ={#XG^^d6 zRrjokRI?}LL={wV>hTe?eb!(wUnt@OwQw`z4ctjNCo1Kc?T;uity?u+gY8 zJb(S?lW^;6jD10a`NzRv8%^(^lT7j+>+?3}M2ntii}#pxr29FO?zw0?7?q@lk%y5h z%hLaxwM()ht9W}-JY&t;HBMqpZoE&TPMWCQ4vps@fx$MKiY3p`hi`%yeN?9j=@Lo{jk1WkX6PO^*zEaT|;A<@G2 zj_qAZ^Q9CzNO3Mem+ic7=PS0a+P-G{x^%FoAzkzJ-jMs?< zX{3y=NBjAfGTt~)nzhMPkQ@ZXTuBdOm6+1ONMlEXK2~8b2nUC~s9ibe#lvB^-;0{U zxH{?`s1kTwF$IXQmycQ>YslN$4;B7}t(~MZ>f-?2`tWF{l_p6~w}$a?*f|=tRCExM zms?h*&Ecu>gCHKnX%LigmVN^rq$W8ipjnQK*lC;0{LGVq(T8U%7!4+GY9FK@gveciA!UtkyB1Zlk*r zmYXRh=~1MXnR-DGodm(_*jTWvO;n@#8;X&^EyMdDQew%^x#V|n?Jh)q2{7mEuycNi zSqre2{N79cVBsi|n@sTxO_QjE+UNB#&-ewqWPcGSE_5Kb;zq(SDjKRFfZN69yT%RE zq`$04^-(X@X(#CqAAbv<{R@qqy){uK|9taNFX@Cm{TM{Cx9J*3r^@=pxZNZkSWKzc z@c9x8^-auCD-2a&AU}OUKl1;AZ&DZvh7lW3+4`pG7`Gp$M@9hgw}{&-SenXVl;-Ha zH^+Fzsx@6&Km`Ggnsw4AAP(6WaiPn0*z>o?PRcI?#geX*IhW4Z&BTS9mNNI8XU>7R zLv($EBFjGJO0!e5T$)clKbaV4`HCA{k6$jh^>n{@osM;T%V&rmP}FCmk(F_9al0?Cu0= zO@2)-%Y_t*5Tw3aOymO79aZCVQ-234=oNH~yQ0ch`MOx+E8^@MUt*08L85pXAf|ET9~fj}jD$6=WWHp_ zj{196k>Z%)?T@QjwTr8xKh4~(NSzBeD_!!;TW5FJxQ4kB=DfMNI_gN{F zSt%?3lmWJ^TTy3e74I5imIDrd#2WSobtFW3Q}}6QEc5N<@U!aJ0WFS&`kQ29A99s& zQso440@+@U>}v@Xt{rrHim=gKF|JhM9~9}Fqlki~aXN`M{1ovf9+CiW z_7-(i`@mI{^vo(bVGwb*=l(uYGR&NvaW9r_cZYH#)?A;{~QDn5(g-ENuGx@74TI--KJT$qDrhkd< zs}_7wB`^8TdAphyed-4|u0w|x4_$`oh1kWQsQHl336#RV0snIvyU6h+MDzOqr?Aup z;<%H!faxZ{?czc>Hj;0i+$W&s`KFx5mWquZu?!xXDY*qqMydKCINR)JoKVVUDaV;- zKX?cPQQ+A!o((M++xOO-sjZdCIUk~FDsiZUf~Dd9DzD;wwocBUM85y4dYaWUuga4a z4ZkO@6z$m!K3~0hQX4pLSx16hfL3r@Thz)HPF4`?p@(Q{Ptf!|bnp@?wPT^zK_TeC za~qOBLlK2Wp~czVdy2yC*VwCmgRVI#;>y+uv0ReKG~&UFr1v7yHbm^>3r$dGxPrx% zE3Ce-a{-0~$*GuB0b4OC`mNl5BjW^Oc32JTmGZ3>P^w?EvWI?hi?#Jt6jy2vDU583ktZv}< zA=XbeqyrnfyVs=c-qWU=hh2+*I?|NL%z9#U^(Lm4HL1jU@}p&dRZZnL;}qj4Awrqk z^U6&3E|{C@->D|s7-UqV{RFq#$x8P_?9-1#(cAGUtK~lX>-LI&lI4~Ea zKc*p-qw2Tlkp5C+oK8PVj}mEl&dD(g*%T>1K(oOFVWe&Yw�(P4GO9k_(H6EK1E{ zV;e+X`@*VQb5^~Nnith>H?MY)>bt0jVOb=4JI`$F#Hfgz6N_%*!2B{!EJ}d41)cgG zQL~&wfzaGJh*5`4t1k+k7FFk1oR&{M!r}@wp`K&@zvF|lg2l%UG$HBnD^IV^kY_mNmI+G**NtYb^l<(_k(1iZiqFFX8^z$Y-7!5j9Tj;#Vp-RYRTkAcmwB0|)bLxKlGRP}{ T$8=3JRYi5GB;H^7kze}{W!2ry diff --git a/__pycache__/shell.cpython-39.pyc b/__pycache__/shell.cpython-39.pyc deleted file mode 100644 index 08d9ff33bff66df3413db3aa6506afe0a648e578..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5491 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 W$9+Ba*Ub5^lNBr4M;QQ@?f(KfoQx*` diff --git a/__pycache__/smiles.cpython-39.pyc b/__pycache__/smiles.cpython-39.pyc deleted file mode 100644 index 45421404c5cb9ecd91233acfab9c73b7117eb8f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12061 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 T;WPdG4*wi~$}jjA7FPZTo(z|! diff --git a/__pycache__/sssr.cpython-39.pyc b/__pycache__/sssr.cpython-39.pyc deleted file mode 100644 index cd40dcf33f97a74e0ccaefe5a550a3335af53055..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8248 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 I!h_WR18^txjQ{`u diff --git a/__pycache__/structure.cpython-39.pyc b/__pycache__/structure.cpython-39.pyc deleted file mode 100644 index fdcea2434d87995740029040055da112e37fae0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37543 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

#Ej?tmchd(kn^vO;N zyfRWz!3`Lk!zI{Z29bmx-90qikp{vqDbns-!I~pBS7NGPC!ICx>I$R|^9&`7krsQ} z{|{;vFv7ADR8RYZJ4BZ_10M#tcz@2E{g8_0wv*8GC+t0`hxWOnu4`B7V>i<0WQkO_ zNirfMz%Ss2-BwWgrx>}9%MNpPFAI|l(GAQzK%Gw;ll*zDZbQw;uhngGfP#KTHbjn5 zFi-IQ99fp7NuqC}FD_7gzC*(igd}{dKn5Kr>rj;O$Xv>|JaFcr7?AICHpq^oT0@?><+vc=}TrA%jS7??5V_8yXCV7(mA>e?vE6*8=j{s)c)TGD)(KkEx?p;q_Xd2 z55H`G<<98?6|wkeg+Nq?K7ZVAKyQi92;gADc6_WT+s9ML7aA>mTVl2mfA1rqvK2xV zdOiLT0Mf-Vy6J)PzJz3NHv=JjFl69!yzE&ImGi!cJR<;a?Q%ZM)j=n7?r|XpP7S_< zq;&~iV56{~;stW3m3*FHaV}TE$>0P)tYnY79QGuh+M2ub{tlToD4qK*cR|#IWKi!{ zu;qLioaIxpCXu$-1l}o^Bpr+^ z(#3fI_c)J(2uj(J=>)3Ko)me%{|Q|yHhuH+sfK3xKOd$-MGE)B&cI670^P7OP;dS| zeutsrJd7_f7Z(<+>6SW3+%Z;v9XR^rm<&%JC1yDZR@! zY1{c4;*<#UXyq7}^jBvTBFf>P03G?_4Fdu-BG43dbj;&ZEgEV>HLj!q^@WB*l&2h% z2(?C-RZ=tp)#x^QSf#xwF9pt zvQ0vFr=1K%znj8H;I?9w*J0d1i3L0a#%C{a5w83wJ2_T3WB?Q8yiu47AhFOOurz8| z7rVAIv9i%K^JNZvPYEBenXBPzEs@bmU{Lh&{Gts$j1+!$i2x9T$3FwMhLg^}e{L%) z1>Sl%UvOo`m?rU6sjrwZOuhA`VT6PcM-~s1To%_v(%)40A85L$^yixJHS5zyX!HI% z!1FC!3L>VBX;z1ueK)vj#fFFUNx5Wc2=T%_97lwZ4H&;mtgsMGMH<^nB|D!$Vw&uL z{*rf)H9!Uk&jj`-u}w)1CRzR7)7rg2TWRGO_e zXi-cId_RmMX7YGQdN7MT6;^G-SYuTsi6w5+l16;IdN1{iu$?y1Lu3)KU_;}E^iseC z)Fuca%NR>$Y{Xfp&~`z##j;AC-9!n4zlcb#gL-ka5Gw^*$%wAC`ap{m_d zwYs!iYnmqmRUA}2TdjIjMR^qldl}rx;FS#SV!+j_KMMDH#;9r1r_R#ZPJh+p5yl>8 z@FoT|28uzQ!372$gCz#b3@$Oa%wUDVDuYD^V)lF+V^1@92ZMJq_;v>GV(^^|KEU9+ z8GIXq4>FLWE54txk23fH2G25hH-k2V27?6#^9-5{S`6OH;Aa_JWbpG09%Ud~=>7v^ zzr^5I82lQ8-(c`L2DdZ#JcHk5&}H!J4E~UTn3(>AF|j-RDP#Ye!JjeszYM<2;BOfG zJ%cYY0EeFuDM%O-LCGFbfoB?Oh`KC<8 zxxwMzsI%1>aYl>VoPtww#+-4dho8Ki!|oODH`c z|F=17$dfM?9oCY=)2x##-sBuE?nEq$d`0J`;%&|=QHpP$k#Iu79Ns>JRt!2jojoW! z=)Bt5?v$nd_=LTfkZg1Ty4Mw)sv%4PzQ~3TIr76g9!X2LP{*g@V5TR2)9Z@VMsF0s zYQkY4#!pVZ4Czrb1shKCIA6bB^JeGd)MPog#GR0DQ$g7P<;HpWvSYg?U$4{Ox*}wE z@gaJAco`~nABR;n8w-t&wu8m2WHcRJ=crzH_h2IRm%gbk(BBo212`av^P7gn?EeQY Cza2#Y diff --git a/__pycache__/substructure_search.cpython-39.pyc b/__pycache__/substructure_search.cpython-39.pyc deleted file mode 100644 index d27f3205c640ecec7a218982cc63a16f0f2a70ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1492 zcmZuxPmdcl6!-IZCX;kGO}i_~R?CVLRU|~V6#@xC1!Y^I3ayZK5h5yzX2&L(&HPDh zCsHcUr77IF@c|ZYmH056IQYN?3285U08TtRNxLBI@$-An_8a^C-h0XRc8g%6zduY~ zGzj^_30E6M;bR=?T_l`v$VmZx0LRmu7C}EiKH%&*=`$Yk2x@?l(Ld2-Wm!$NJp7W^1(#NVx|+FRi#SZFE@=L+CaJDuPtDGh zma|mPq!=iX$aM71WMQwTRWV6W<$oqr=-zY_2>}K%b_R@gkVaHGP*QMf>(<0M#^D|D zBQlySNevndSpvG_!4q^s9lVqeQD${$sA1@e15MFc;gZS&0~dj2+OG`h5|;22qB%X7 zw!Ov3IBPfShKVi$)8Md2{Q46e>c~V(;FJeH&>DH%*Q#T5di)WYKIa5o%#JVJrB4yw zh7@!3oai=epoF$fQKIK$qJ@8)4HC|6sArR0*hp1U3vOASDcxc#;ao;I_iwCSIndQWilUl}{`S9)^sjsI)%q)JrE{s>*?^Md`39IBTu`qgAqYhW zItWb*Zqtay(4ifq@-a)V%&>FdFsAap0k7My0lAg# zzyuf6co-0f&k>52*C2Fj=cA12M!utaqz*O4y@LT^{RM5XI(oXl(ji5FZee1Msrvpg zc?MsR@-`znT>K2gbscd$`wYG&B|Ri2G|~IcVgo}15OQ)Jqs18S`I~M!ooUR8{FSp4 z!ow3%?q1RU=`?=D0UsS(`0Uc<&C~5wPQRIAnn-c_sN1rwQe?x?Sv5llnO;l~@nkX) zCAUE?O54h-Qmheff1)tqTp{pvLb_rgou_TIvMW0%bYrxjs{(U)HQ%$L2fDv2rn79G zpjkceJ6uRv=2&~}N|}*wIGa2FQ-A&&A-m~Fq~n1~3b8I%nO@w%)9Mfjp$vm#(_;*e zExLtaLt!7bpiN`!TgWjCA4la|c;*RdU%8R5Og?bq W4~vS=a&g}!oN}>;zX{@Yy!#If)No$_ diff --git a/pikachu/__pycache__/__init__.cpython-310.pyc b/pikachu/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 975db8db36216847d5e6abe35a85008f15794f74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155 zcmd1j<>g`kf}kgo$sqbMh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6vGKeRZts8~NK zu?UC~^k8j-ZhZ{2p)q77+?f49Dul(1xTbY1T$zd`mJOr0tq9CUv~PT#i>Qb`bmjJ zK$NKOlAm0fo0?ZrtRIl!qF<7qpHr+~keQvBoKXs=g`kg1hfs6G8N25P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HWerR!OQL%nf zVi6D}>bvA8m*%GCl@#j-WVq;;lb8ZCnjfg`kf}kgo$sqbMh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6vGKeRZts8~NK zu?UC~^X~+Cw`-Nw#D=E6wZ*j9~IC06NS~Q z1i~JW*$4K$$35=92NK@9aNvU+@DSwC$pOeEUIw}BRFVx4$@PaNYG4VB21L;9;8fZEts9ZF-<5F z6Oq~||KdApBy-E?H=$>SjOZ!279u65QYF+19_7#aDt*C5QqzP`t0shtp-crQ)R6gd zl#^FlO{kSj+8L3{PbRaaZInOh(ez78hDuL}NL6++B1WD}sQW$y`uMhfDwoQqOZUXg zV9yf>mW>UWzEJvDTAFm~`Sv2hmGtCWaxEK036nN>u-saVB5Xazt>>Ey2Mm&<#bAS8 z?u+JrPB9@c_z_5y`MZt zOGcTtL|TM4C^xhmDq(D+PFR#IfLsy6X|^_Zm3gptRf=FjFx5@J<~RK9u=H=fvy#P? zl6?kND=w}}whM}zRUnYM8N|2B^SO_5oiJk+9Q?$zN1fgi9Q6$7=IJBI$hc@Z42iu+5 zY2|{*8*l_3fLH44zQvxXa!*hvVuk1#mtC%Mb@lgE`RVp{|MJ7Z>8Dl@{2MeL(1Sx5 z_8Ewm0S`F2gdu{D(UgE5@(A?E>mleZ-Uhwx^@w*~1;cI%Cx1L0SRKN!4?u`uN_a30 zIl(x1mUq6irxEY+-eoXt@eMfNuQp%JWiIRsW3(wBjkTKPGIdhDrCb-MxuB<7acZ3~ zLZ_LSOY6)kE_aXBIm;zj{PDQFd#qAzj7VL5Vv(~{%!P7s`S34hW$L#vZqAmLX6zh7 z3!&(_)H!p4$L0NrR!`DQ8kSS$^qg`rlS**PEUmZWVY|G&_GqM6?#OG|R4q(;x_=cq zK^0z|;H%5+b$}F-gM3543Ll(tjA7UTh>PG|a6#_EoB)sj7MTpgY9J7)#OY)%tYvRS z)m<;Z*s>y@RsFTgY9YP_rVF9|I~cvT!r0L%Gw^4lBb^oqjvbw3Mq1l`pA$MLC?ME)&#BJ9368d$TC1o>6wKYNdjW zbd}~J{!N-|fUrM@p=rZpu(xjFgaF=xaYe~~^8=fl7$nYYqCiN00sl6*?7*=2{vH9? zap&i-?}4QWJHQRF1Kh1@bF6b+7?1X~&yV2D7>12OIH>WD#F4YmJiZ_t=X*Sanh(_u zk1oh(xOToqsQVshPs&{=S2`0*AP3{ya(Gf#E;O~JH;kaur8Z}DDP0Cv!Ii7Dv`{k! zmxpA=8$G#oNLoIRX}{m_P2pZDEL;+_f9z;h7)PbUW~0DyZIoKY!>DRGXuB$cTgo)( zt2%Y$pD}m`p}@Ki$QA*(qjIl-T!Y5eO&b5Vfsnm@42$j2BL?*eP_psjpHwUyr0P4# b^XiR_ynI7qI#utW?%qF;FkcLV%|Z7&p&5-l diff --git a/pikachu/__pycache__/errors.cpython-38.pyc b/pikachu/__pycache__/errors.cpython-38.pyc deleted file mode 100644 index 333cd97b54e32281221ec1a56974fadf6fef4b9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1242 zcmZ`&&1xGl5SDh=&c@CkB|QWR6{VEA(DkjSP(qzhaxsCBizzJ1O3tcTuUttRH;Zq{ zrO-EM`v85Vz4k3i51m=tWs}ggHPUECqmOSsoo#M*2z*b!e&AnxLVlss`~q+eVA$ti z-V;Fty@eskjX$E`d%_3buYFGhr=-_PAoI@G2JZkyT|<%)6=dWIif2V2TB7~EHS)!p z2ye+K5bIE*Q*}=#DwFoe7;Vaz1D%gEl{l4OFrka{OtN#G3uc`(QYWdLDC^91RBj*V zS3FY?sgFnH-f^C2V`SnMC#D%s3AZoM&-;fuDUcxBKS>Y!PV3C}r|Oa?Y4LBatJr9IU4?P1a^>O} zWn^p6wA^hpFIpuGdo9e)`9#KX)rsSY7Da~ZjX3^P@N5w=ph99oGzhO@*3hM3FvD(v z38}aG{b{%B*aPt)qys##3#KVP5SuT2;4Y}T1D)x@)M9%;Jl;0I0udMowBJxiFFo^W zP6f_Sga>W#@-5-d={F*RA3pkZTUx_%8yI9`Ib%tEhb?<23o=1#mfZ`2&t}?OvYB!z zD0G#oEGzOc2Tdz(>L7EeT$W_zn}~HfjobvDOJPB~taI#GS{TQa!~Qds!yybRoku;t z3LGe*^5O9qeCa*Uv|y^*3$XXe$F6L{u(%*}gN8Kp%iRWhjcx~K9g+;{!gSI6x2GEr s#dg;638_JU2;kO_J|}YO0BYUZdcOgwmIsQW3ilUUdRr?4JHFHX3#eN>5dZ)H diff --git a/pikachu/__pycache__/errors.cpython-39.pyc b/pikachu/__pycache__/errors.cpython-39.pyc deleted file mode 100644 index ffe29532c9ac03c940d3fa801ca1edf6f190ad95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2223 zcma)7&2HO95ay5mEdMD^+`vtdEOKb8AfawO1%*>ND2xEAQ8yR$K&*FH)>b4}*CSKlj-04bS@nh2>QP zVIRmG0Q=tK9{1k?3GZz<^g#}I2y*D;0OT64gIsrV2y%lrL2f#^##_f;ca4Fkm4y-P z1DU75gg5lLHw?IszR@50;4Qw!+wa$gAz$Yk@4R7+Z-TF_a_iXYoY`E9ms%@beBD>s zD3Q#{?38eopCp2usEiY1g%*m9MJkQev#8iU%+6>cL6gcvn5fu0%$U+zFgt%^nouTE zk=dyD@;hoIbC1z)LeC8u(=%`_L`Ke}N~je)DjpA1_Kc0CrU{`|rG$%-%mgRYkon`N zATP8^sg+FH8Ijc|lUZdO6%YF~`^u7$(kT&{%1_6{$kUX%??a#uZtAD9Qa-BO6ElN7 zrw}Zg7&3jP^og`I>8=$!iwKv}Q*6nVY?L)j+HkkpT8tuWJ;tpUn+pdFlB30NonGyz zXkUd|G#6nNJCy~Ql0_UWfMQn*?~(dIrUUH4`^CHP_uv{sHynGH{@^kwI|6GQTNS6m z7QX!obwZJFmnf zPo#kkRGFKZPgjEb7yfVFhX5)8(+@6!L26Qt{Fzz^w=G|zd?QVm+kG^82y8h`JbWKO zq8x?+!ZcvF&2r;F({pHjm#gCEqr+D(`s3GxeN4duG;o5!lGw2zOq!n6G&9#>ho}*u zE4U;xc)(^X5mE8OPfC%II42@;z+(VJEc7J+h*b*!AvknGKSk(Ba-2yro^h>C1$>PH z&?PDpWJ;wrQ8y?$i&1qLsKI(=c!mCS@vo1e#?)y%6Lb^VRrk8a#iBj{s*1Y1oVu%e z>dn=6ofqc-`}RIMz#ej)|4$>-_>=*ytpC&N>wT4|T;Ja1xRpMTiGW!^o43C8CxL!) z;d8ux!LjFqY>S6D0lfAhMA^+ieNb!zaJX7A_YG5YkLDNx2ZMoftthd4=Sok=xwKL2ibb)T=Don0&zAPE8_O-Vyi60kB1=rc}eSrW|;Tebu;1Z>%pkIi6b0IayX zv+9{8P1N8L%7+|O@gX-W{KJD8oVEBHOEe!ug|8;bHv$`pSUWZuFPZmEjmDW2L>9QjopmGfF( zcMO$aTCdVK9h1{~uiCd9t6y_!oHu&){){uj=}NEBpLJ&YO{dwPbLJRk_U5;!jcV^i z|DC6vv3Cs20Ad)YhbokHrY zx8R-rP;t%yvgj=V@+G9scxRD1@166$gtDgqIqy9M$kRwY?LC9kGf3Iq1*B}GE_#=c zx`5QP-g8J@tUX^-Ys>gssj1#8Udwv{&`bWa-ZK7HJawnyec4+D?70%`E8ZGl#k#M0 z>)wliEqm&w(t7Pbh~675H8D5)-JTz|w4}KiiBTsS3IDnffktk0@LjLh9r#Hlj6`>snC=j8uVp$jArrN|gLylQ*LHio#FXGHvUtj-j=!G?=%x5DJL7#_nf~b2DFGFppO||~Hq0j%9p&MF5weZ(awTFOJ=6_>U#3Ind zNxTwM_+X4d2lANzbfgSCyk57EAX-OQtFReqo*L<~@<9EG@>A`B`qyf#gbR`3aXP9{ z$$=WHhbGn%>k>YXGT{G2@r^2hVpfqN@j@d+p_)z077Dr`pikyAO_MbeCP3q zX8CAyd7lq4T|wGt^0 zlmnHh9;=@Ek;d8;niY1B)K85~_`-z%V z18Fxlz<5;*OVA{s8}|Yjk)Y>yutF#zdWcI@WRiXH#UxRhYH$)BSXnV^E~&kcjHOU6Fsy+1>7*ucwblIbvv#zR9B%K(%(kgP;*Dc(Y+%z>ah z>;+SP0hDH1gYx>ogbm4av3_VUf5NhZ{tnOU82&8ji~`wIkv@jDDjO*$XvrLn3?{RkieX}nJvY%prk zIA&Z)+c9DTyjcYfzB!Hs94j`{cB+S!SS244Sd{`+4Oq3{tsly`>QJd;ICcWD)@!F)Bm(%Ynf&@LyyqbPrn-u)h)GFEDRPec3@9$7E zB<>~8krcpj-#VAftAdp65mwJs!>1%xWmX4clW~HZeh5WCWKPY~A&WmqRCJ9}j<$oH zBUayz0(%XUII0XDp*wB-QgWu+0)&O1XcM>F1Gf*K0OpE(HTeB@cTnge)gJ}ZCe|=L z$P62_b)==dOw+R`$K=k2_m8OXDQRjpYg$tkFC$YD7NTQDOLpV958@@1 z!WH?w)_OQ?Gms5sidRBwjXESczwKJ*T^wEOM9L*FQsO1mwDCtQI$=l|%f|WdAb@y< zUSFLgR0V*1vA=;#>cc<_hR{9m?us@QP)~y(8EjMY9JHX3ij}mgfB!{dRorYHFH>yp zrW)A)kw}pfliOuBOKhM>Dp(sz39CrEJeDvPxM^ZGHx@G<@z2l@8)&O0%@PbWNgBD> zB`z%ye-))iiO+^vYA=`(1R9Fk>vtz}&TB;{`_W`h8SeMxW<#2q+%p%iVV>}Z*hBmy zqzmSLDsji`%;Thp6N|qjE(IUNp8%HV2#std2KoIRTFUH4=^7rk{l7wjjKN*(b-t}2 zo&bKngGd9u`W;O?Cl?@60bAs9U0ehVwmnk!sMZC74xWn0uRMoZ25P;T*3u&*s_f~& zdX=!!dSmFD;~vGW2zyp|TzYK4n=s?5SNW(4vsB%r@lmU8RGq*u3m7z75m^%$c^lMD z%X0q#9LAz}qYJ|lbUGu%AF#g<$+oSyEjt`-=R2O2utm1VrC03 z$nN5w&{8vb)D#xlOzr$M8Kxl=yI-|$-nQYrL|j1IUET!w2uaaU&|%pfXHCl^{lZR$ zhYxdh1DwnSo=sNA{Agm0)@B+Cd`p8Q8;2u>o@06M>(l}g2 zd?@Vh4lo&;B268R&DtPP32%*CkcBCM_WU1a*d35<)$aQCr5*mQW(8o2dwX44ufk-p zj2OkXKb|SYxhB>+AMxax%Q49D5E-xeJ1!!_mt~-A<@S}$Ti>{T`}PlPif73+TDNK1 zeK+dtVVbMTb>*GwTU*!LFEeUJ#~e@2c+c#(p6w2VK=@%6-O0l@ zyzbmQk1D6cw(Hqi;1qKVDV^Vcj+?oq_=~Cm(IK!-G3~TrY^(BHD&HhPWWy&9J$amE`=a+ z`G?&g>4&+&ha?|XeOU?}!Ez7^M-gva?QV#e-v^^EqBOGQtR!u;=;mX&(G0bMcFVf+ z4INfVyg;wZcp<*ex`s32^{lK_NlYmX$pX0P2%l!q@%cn3{%$l1w$v{T7~FrWSy8R;w_902o=uz*F!Q69Xy@B{^| zD2NqX@9S|DAuBisaJO+l85yyKl3HA0H)SQR9acC*W5Qjiu)FZp*bHA}2cvdq#wPD^ zWoZSj!VQ#4_oH@L9hZ8*XjR=`fB{))P|$$vVH%`mWeh9C>{xIgSdkP|e7Yb-4KIzi zWU&7&4}phRVu4i7N{)dXTu}>iJvo&B3uBh6+uSyF2iv1=FUriKaKqs?0?tzRPm|rp zqHa@r_P7*o+T9Zy^9NyLeyv{mM4wcWUxPY=4!^sb=&(&2EsOVRi46ILB>hQs*N@up z{hV`BfLO(63Zk7FD$;Kp%;udl@9;jUi*FN@j!`DNeT!gZWRf|!T0NWsW(_9unRsUq z4iru3aPc)$12y>bN2)>i zY7-l%DJJUwsS1=rUh>bM=s@!NpZM8-Kz_E^fSik)^dikCZqe&c@yaxxxJ{5vyppA9 z>JIxg86YIRGuCxti?ZdjD)CLgT<9qA9u-oqq<`iCpvcr!JYgMpsXK^MB0hD3TSyKe z&gZNjX=jy#+F%j)kZS4OqdHDB>u?BX;0PM=4BtIE9oNR#`*k`udVt!9WU*WLo-&v* zl&Et5brq-ca4AiBi{_p}ai#|Ph0Y)xo1edeV4(Z(COzLvVQWK>QP#UMDjd3a8DyzEx zjF9y44(U-%JQ?Sh=8z&d;dOU*u>D1BORjG5C_O#XJ?hoAX~T2*cZf_)ddF+_JN{r7 z=lT5+!bAv!AVev!>(BkhXwMzc#t>Ucw@1-%3M%zv9c&xZu}U4A*~epJwvTjw5zL_3 zFwV3H+iwx%2`Tt@G|CfZVV-8V_rbphv6!Xh# z#RyNlY>Ht$iR9#Ao5vE#+1Q`MRZ4ruzn4kvG;`CMm43Dk8F%I=*1U7(IO&icIi2)Pjh1{51x_su;tT{`^r73k zELiNvJ9Q|DwuB~XSDxd1mztY$dLzDtf{$R`IJ9D&wKM|Pg)O)|?ELV(PV04?s^H4q8(4KE z{42aDe%8<$h^jRZRin*WL#^v4=j%)LQx?6KtQoxL>vcQ}+I$vk)71KJwAZs-{WEQK JTDtk!e*?eJc|!mI diff --git a/pikachu/__pycache__/general.cpython-36.pyc b/pikachu/__pycache__/general.cpython-36.pyc deleted file mode 100644 index e46ddc83526b48b0a4844a0bdca19a89dd95514a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11731 zcmeHN+ix6K8K2wEUVLrhT$`ptN}Fu!+HIN^nwHYEP9U^(N^F#N1uWw|WAEC#v)ePX zNn?%sP!|ay70M$jpsH^?@PNb|sn)XL6_p_0H5m%t;nkF=%x3!vj>NOqjM%(C^HB+ZN zvu$+>wL-_P+5B#`i=9%f#OXr2+^N(mor&56zuWD}RZSE{={>DBCCZ|L=d^fCOp2-Z z^xBM=7c*iOsabKKIE1qM#N*@F3J0m<;wkZv_$*Q9pEjP&MKSmAWitSH=@BciY{*4bS(a+g|A1 zj%U-6WVge^P0?D2i!^;wKWfPPkjfTp;e|4I!K+9_ntc8C94&iSPnW!7&>}4@G`1>o zS=V;JZQCb9Q|OWQAviEHH?0q~A6vURILlnYwPU_xzM=WX8~k3iwtrP|@VoAXed#+` zZ@+<*=XBRLy+-J)W3JB1Ygb>ryh4Q_MuqEN^?UvBA}5DPb&+b#k85$QA7)KfMu%yw zJB_ZCG~&=;&Mh}^daevSG4CvQeatItdh%8)@ObR)8D=N0g1I?ow#_pao#kpl8lYJ1 za5A^BThCS|iId_&4{HhIV#f;tcf+e%avIr^b|fxmbz|M*omaP6Dx4q=s;hqIkP%R> zR)`3W2A3#B10PtycZ`C{9w%5Q`p%M{Bo}}9eDz^m# z1xo`s$QGmsRZ?| z-@Xk}lJv=AbR$wTWveDLprZ0A$|bPRiTq5j8?;CzC4Bb^iWOz2N#sG<)5ysfk61dR z>;=3elqD)cOYImT=_wP8Xx7cpiU88gh*o567IKVO<3;boh(p+rBXJ&*_a|yqXHHtK z+Bt#{D=IEy(%g?%;3y%Y%4IGj29)E7G@-bU1@l{|6ini#l|djQVZR6Qo)M{n7h*w{ z&R-%CDmXMkFdb>DD#>Xafo>8H3^h}2AUQONaFMy+yGQniEzv9OnGg_Y2i;r28s2hu}| zt80~42yI&dvT8*#Zzy4}xHs{v+OZY3I-Wd))#tW>A)pkvH@$jrbE8^OI;n_sz1t6K zm4HU7Q_Gr7_xh6waPrIfB=afWq~zs7@_c%fvyV`(1Z)XsbGIVEgaQIrja3>reBWn< zOXNeS+o0{F-w^*lf(!;;Y$Etw0q=m{Aq7O*1bRPWjt60`8o&dVOq_9h(~(k%+_?WV>C7K2-=Xq zeOeyLhS--!VkOk5n2u7NDxS$)k6 z1d`|cb0RQvFjSc0VOjl8lQ`qCJ>J*8Bu+T&ALrunBqmsg@xaExS)>&QA3)*(8#9T2 zGls;EA}==Gc00Dn2yQToBafb$_x}gHz<4ii*}-I}y$ge0=vzi`3QuFpkY9m}emgEg$?8`xgP`aSqiVTpohAR zMjsBc-|zxbh3DL=6ZF?IcjH`8B~|5;CyawjYI|pq>h#-TtB17HmCji((t>jpQ|0pc9399Va1cs>)<^e_@Iu5M;Jvhb z*jFGjt;6Nv7^reo;SQ;13^$-F-m|v6LGBQJRHJNicWRjpN?qn@9Lzgz5VSUYwuTT2 zX>dHv0fC0=(=BMW5kzH|4GqoI|; zmJIrNW^>Re2tDa_8%?hR-j=tsl@fQ*_NZbtmYU6T)Qlq{^D5)uaiq85N`Z(eF-|tb z$k0l#cs6RdWSe=>Yh7O?D{qCj@5-WS$lz`*nFmt~zS#=;E|A70hdh&i=n9g4Xc#<5 z@}bVpDxssiEDMGGkYle-D?s$?t$qtJ3)OR4l60E)3b5VihI&E2!`LYwgEm6km=Pu} zrL0*k#I|Dpcn<8dhcVOK;hl@8hL#tX(mA2KqUtV?Phdd#B;C%_?J3-rUm(Mpry?Jx z6xn>$N<4ijK){+Pr9$vlc)RDtCRjNxXVMp21l(~U>;pGZ=!NmI4A|->wKY^p4Ni{`;IxM`;* zjakHHEqxBLh!WznM-2+x82qPP*u80a2`w9BTGEe9L9(h}#ua=Ax5T0@;t3aar$oUW zid+O|5T!tL0ztFVrWqB^+oiCuN9Iu-i@(?CrwkHg$fP37OA&N9Hwv{TDp>$lorsu-mk^wvC? zp#L~wzAJ#6XtcWpxyf+%w9J2nmiepx5W>_WfH)5r0$$(Th)rmxP7EXlF5^2IqqD+g6?){_{}WjJ$sMs6CgX@7H%2G^ zpW?7Wnjtsg9uSlBanIcUZ!-5^0$PoSVoA24T*57dVtJWzuHY6Q8;8HJsQ)?em0zL( zzD&0<-{j=;D0uc$V=UFqFqTd=&}UG#cjwSYV&~$U8GnO=y-SuZ&Y6_HZJf|{%Kj4@ zxWA3i){^#(qdOHumn*P!Ct%51(F9qmZ_VG(qVk{x&a%MH-9--w)QPb0u6{%FPvI;} z0cTm>)$v6h7PC!X`rg&wX46iQRn~W%icwfi7nw z`vX0&lk&;L_NFsWMfVp7v+r+v@5OtL8XRDIBN8K8>+A6K5y(?~lmCjIeB#~PNuB(- zi+@KLZqyN2a9;KN4SdS)^br$7)CVzGi4aF-B8N@aCodR&v)iVqKSj{msttVPWUdOy zQ!8NKPXKqc^bxtC)o`70;lxi7(F!Qixi_R0q%o~*Rhj$9@jt9jAfkrl3{>?V!nC{o z1}fc|9@zk6lsxYhSGea;P{8h9*eZ)r-IcJ_^7ahQD?01mtyF3cQfk#HG_mzX4%9$R7Xz diff --git a/pikachu/__pycache__/general.cpython-38.pyc b/pikachu/__pycache__/general.cpython-38.pyc deleted file mode 100644 index 50fb6e8bf0aecb72df6e5b3187250bbbeb41212e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10921 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⪚HD^8O_)ZY* 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 RG%lzcUsP&?=^X2e{{~U{n+N~^ diff --git a/pikachu/__pycache__/math_functions.cpython-310.pyc b/pikachu/__pycache__/math_functions.cpython-310.pyc deleted file mode 100644 index 8a88404b7d8598f965e40a950eae02c69fa25fe0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21963 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 diff --git a/pikachu/__pycache__/math_functions.cpython-36.pyc b/pikachu/__pycache__/math_functions.cpython-36.pyc deleted file mode 100644 index 3d5d9c293f38b6a975314b6de97658f010964028..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22507 zcmdsfYmg+_RbIZTD(lfT{hH~X=eAa()%5OaXSa9Ju9neiv^xT^h+$zL!i7XpU76EU zHPuzUH>+o+%heWEuMF5cq%amQ0m2V#3H%C!?ZAY=fWuhEzl3`f`z#y{5KpALmX zfy4g3b2D#dR#)!`0TB*Ycb>fOy!YI5&OPVcoB8y@LhafIKeTP{S=N8Ga=#M7*KtM5 zwq*rYU=OU8zS}Juac5xfIxW|>zSRocz;7qUwB+_beF)0Tv!r6h#f z5|UPu5b_bqw@~>4>+q=z@oon`Lb_e1p^q(3&t0n&mi4?H*{uq~GHVF?!6{&6F>bsP zs@?r@XWSbNBL&*A-WK36>nOdHg`Zf!_%|e^*On#bRTIljI{PE7~ zC0r332rTr>zWF79*o(r!EmcLTfUn)S`g#}^Mo=8g+x=AvT zZhjGgDG2KwqmA}6A3%Yq0bn`j?DTJPrGU0~ zs9wNi1+54sUqfI9U&NHFN3Zb)NE|UOj~!rN(llPoT-YK{%d8 z!D)_RI10k{SoK(--BI1>Mf5ZB0W4?L=HKMuV*@`{>E>pYv^MPQhV6D-Yqxht!Tx~Z zM!St340KLwC5PztZf9?=H{5BRN*|vjD55@YMC8@(ZaBU@3RDwG>J))t5gp=WDzY!T z9JLs59Gib-fe*enoSL2fElOQoGY=cK?>4IPoGu<=e&nd@xS~~nyf}T!f*^D;J6>FS zwYLlGeylePyX+``ytD30xS}fnQ+sN)t*HZOPwl=lb^6X7PhFN2oeNPd;$qW{D|@5f zaNK?>4Q$2seaIbhtvXF`h9JX~AcL94QIL3nr_>ekUh zP(Ad9UeG-B#%#@-xfuy0fn)hbRPf9hakJI;vt$> zgonv6o4&?i>0U1Yz8AW;d#W>NccD|MuvNPaxzd)bRy~of?cH9GMz$C^8IP;P;~ZEVgYZnbqvn9)*0R59B7d^&@!WMuR|j{pWqKz z0?ztwlI8)o4)RJn)?pb}<^H#fp`ahxH>^)Sf9Oxk{Sr8ZHSDQBbyqELl+;JeW9-PG zNinVTOGqh^H*M?97e8*Httv`ZFgvdLJo0^-9QSXS)}KS`;HtCMVGXmxdmZJg)7rEy zv(`9V7}uta=>oX5Ijy4ornLRVkFy7rem(H+IHNz3aHC-*p@n|4VZp>|Osg=gN}qvN z2zsan<+Iif%pIBgN;>!6^@N94ufI*@J+@!csvxD=<|$1Pfd zaWe{csKU3S-Xv__pNSvDXVQEjLn!WD6?UR9UJ`nY+MU5@xYHg$kBG}sB#P}`Tw+2L zFBB@P$MF1&Tw{-gW|@CwhG@4t-8(;kdeNf*mgs)IfG^{xYe~>K}igoMI9+M2XjDXGi&N>$bCrQKljWkcv zT&(Y?aR@>u`9fr1eP=5m`Z9z`DG66}J0MD_E5wU-5yLZ}TKx(_rG7bZ@3_&=OSqEU z{eD&b2dU*u%jlUud^A_~D@j>>FO*$FS*geVVtKpcfpE`dbw6uI4PD!WP#tNREk&KSzaW#%SFk+1==uKI~vgPd=OvUDcdIYz5 zETKeZKF8b-5Ij%t%>-N!G>3@xQBNeQ;&Kx=MuVVzzdZ$qgCK!?is zTlH)HLxS%va?Fp+bRAdp69C`1Doux0tEydDty-`YEF)eIR)SUB8^P(|4DJhbQ{mnW z&IadjKc(GKa9~{wE+M`YJQQr;z8t(Oco_GU;N2j$%dvN|dM;}}RdP#Ia_dPF|B>A2SaDpJ*J1leTCJkbLBU0e~ZD{VlfMwD0pkdBiD4nEWWQ{hML#|S^NpB8K! zp%!>Y7T8sbI|??MX`>9-b zYi3wjzY^Secpt$N1VSi6 z=ta5^G5d!ok(ev~d~@ZZp^UkvO3J9@aYG`5MC}HNh(QbE*GY`u<0O*i%p(z)&4&(* zUssG@H!*&pXeY+6Hix0fm=r4MFpqP_FI8lix-fclsy1kuS``DB<}zz!25`PcW&&q5 zXfn?}qc-;$wd5K8oM&V;^3OrPzL)T;y{fE!wX2`#@P8K=Ixr(20P z@0qx)g>`a2CaBja*NW833eo?Nm|sCi7Vf?yW}OR79Ksz7w3nFbSjHjpUoqvh(XPv3DbwZBWuT;Sml^D4K}c6- zno{a1f|Eo*ESM2f8m?qKZ0Ju%AD*5@1Lje=3WkTc4wiBbNmFv_b?=iJn^UcIiK1`@z0 zq;=ZBbe{;#$ITB%Fq7$i`iN2^HP@#2CQjh{3B;}SEJHHss|e+-W_68OA0&8z;6nu0 z2|i5lA_1vV-5{W%lcS4|Fj`z6;xlBM;!M*py-3>NYNRdQRTu*AZ0kg+qUdqlE{CWD9y3F7=b!XPVbW4fjZY^OWVi*myoi3x98NOD~ zRd45Zvf=RELxr5w48-a66RUcvXn8dH^N3qngfD`SBM0=>D-jf zdk||zq8Zr=u_ilZ*+O2N$GAa>0e`PJ9zMZQh(RP?u*s5{(zJx>K9xvb6i-5|S@+@0 zvd3^?P3ll$8sf3lObaFkY_Z?!b(GmQ z^k+Bi$tlxWUBiee;=$;-L)2kc|Acz(%bA9Y)?mD1o5uq|L$u`dTBbk@nH2c!sO7?? zFS;w8$c%H9gK(^ZFcCex(X)B+!T4+POeQ;{57Me1t zbINYbExXkc|Kk0p(yBVTRfrxGa}SESY2CD`BsYnsb-#Fixm zs*y>d9$rCNe+*4JwWdWuf*&2$v`&NNDoT*whqM4MSEY1fNf$~dHg2JGO}B#WF1ufc z7arCT?zJOZ{Qn+uQ46`);KFk#%gP8R&xG8AC5&ydGGvWd#7US^q|vWUvE_~JGDwvq zd6kv1d>ya`;7UxnVK3Z`GsS%?heq`02kvA(MZ(x349}S1PX3Xq;*1bz`4Oos_&hs) zj@(F9=QuZ}TkSqPv3i#x8rC{E%5p2;FegU5z;9$4oA(9ND0rw4if%n1cokb82Jyr+vBzaIf-!2XfgnA8QX-EN6#RwQ4ECTr4h; z?VFrAk?kV>UuE<)0BlRs(ox?^@KJ(qBY2tM+X+5K@Ers<0rES-TkZSmJCCIus8?9H zNF?e5CezA|ewj8o zag#sfz+((NN`Nyva}GIt8M#6fn0OLJEh+hX60Su0&La+gfD|DmA*@g2%6>H|tMB@7 z0*N6{cu5a%nBfuRl=>AD(&e|1dIE9caKtZ39T@}a-8L=gkZxftEo3B|j0EL>3OO}- zq7JdVqqu*Zk@@#ane+)?F*S5ZKY^H(8o(7 zKWm0vT1>8@3bpsyvl--`jHCXTe58JywaGo|vjp>ahUXW`AnAw>+9J>J zKoCz-@B~FePEl;&&hrz?gt+81g`|L&GW(&(OEL;=UF< z7CesodU!T?U+~0Ru%%k(5WX5bDdF=7KNW0A_yWRD2hT|OBEsJkykEkX5PmjzPQni% z{DI(k32z|$&A~MZzYF0H1}{kX;ow79C9aG5ZwyBw5hRv*&>%nIG2__}gt$G5ay@Tg-&#WgSXS!Ly*dH@$E{;>2+{MlxwjG|s%-|pnJrRo;3esHDC6?eN6G6d*!CSOoNExe)>9gb# zgLLNV!+i0zn*5|R)-Ty{Nlp~GDJ_+B_c>yHL_ri3y28>UHL{u8XZ{_6nOQBX5nfjb zKg=Qd$hVvgdjs!SOcoL>>wGh)cfj+rGkBKxJK$OUwK@H9J3;Vm1^m{Mjv%8zUF?aV zmYW?!9nWKe%^0@1nLy?fG$rQ<#r6;Y_kiKF6R57-8{!b{_&c($G%}p%yaG;+^=1m4 zH|hlg)pQ8i9*E`6b(OKg0ga@aFRF{$Pw3+z#5?2W?Fs-^bo?$QHDd$=L)e$IF?8 zi58tf5glBKTy$pSA~tKaTqMzqWy3ZRQZ{0VNP>`(4d5(=?x{zm)QaJ?%p8Eq3c)Vr{QX)lT# z1CGuoIGR_ZfF(17PYRY~?SQa`cUH#Ckw}aq^zJ~~PC-`FbgNm`vk_{_spHt+8$~@H zEJzQ77(iuT z2>JrK2|l?rXArq61#UNDhAu*qYQpJIZpi65KF{;me8obx7>XN{pJMk&2#|Lva}l@2 zQU$?pdbGhFLET`lczgyD2+9T-M8{6-T_OSItcHU1Pv`oS5_7&!DLKvWRY4p>(@1x5 zUCn$V24!wf?uFh~P9O7`Tz6J-GneKYCvFA!Fvy|LgNT7E75fT63#7!6gV%LDtq$Rs z2vP%_vVx>6=9F>xqXd4tF?GRjcpWb}++j}mSm{Xs>$jj&{sf++ex5*vRnfYvAXnUa zlrAlIyt@}Kr?Z-N9SnIq?HFTTB=SX?@U^whz0Lv8Z0#mO=!b|NA{}mVIAm%NMG2W- z&LQi>>IQqZIVB#90oFb$CFG40jaN|v=HkxDp{+a* zZR(fM5B0YRX4yTJt~1#9Jh7ogGS9GSTo{Hwp^_nFu3Kn8ud8$&2oAth*ohKX4cQH0 zD#k8fI;H@VFkZtGK?aIRG{CqDY1F-Q4@ym|G7R^zuQG@iyI4N39b>YvGH-HtqM0N| z(n&h8-HGk(*-@WO#IA0(JyJbv?#MZlFLK+BaxrcX^Bp+pF^LP}CR3)tB&0IoU9ycCC`^)zGwv{_D{VN!)`nv=(RJOF+<1xK< zXM41dcOtO={>z5G1kW4x1?P%ASx>Nhthm80F>A2~=;75Icrc{={rZ=1#~9Y-q?XdCL`Si0u=mXQgoHJ|#|J zLXas+`D2`loZ{m^IZepT>>Tm9;I)OnPgMQ`g2xEnNALuJ;k{Prej7W)tiENqf0~!kq!b&Wj^Jei-AJw%Roi< z&TIv4%;57CDKR}|LR&dR6zNFOhwvZScdomZoE|aCb?Vqjzh&^?zG5OwUlD#_U)4Xx zL)AYa$c9nuVO)p*BkjR|p6Tgwg56w^`JH?ek4O=k9zjH0gn1IrcQzH}$0*4AI(n>z zc-Vi-dPBaN>NOEH`!g*0VTp%GvMR2sv*y zKMso(helzi9ntIJq@jcjcF>gk;DpHVOEQUFnDU^NiYDZ!Rqep*)4_}I_p{)^Tt`)% z;v*^jKR8OQ=HMu{4a)`w+9aKoTtPD{Byl5G9!Eqx7g%g?JjNMw;o&(4nBmx{lo-Qx z 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 To(8_W3i12SegEA9 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 Date: Thu, 24 Nov 2022 15:20:43 +0100 Subject: [PATCH 3/3] fix: Remove pikachu_chem.egg-info --- pikachu_chem.egg-info/PKG-INFO | 9 ---- pikachu_chem.egg-info/SOURCES.txt | 55 ---------------------- pikachu_chem.egg-info/dependency_links.txt | 1 - pikachu_chem.egg-info/requires.txt | 1 - pikachu_chem.egg-info/top_level.txt | 1 - 5 files changed, 67 deletions(-) delete mode 100644 pikachu_chem.egg-info/PKG-INFO delete mode 100644 pikachu_chem.egg-info/SOURCES.txt delete mode 100644 pikachu_chem.egg-info/dependency_links.txt delete mode 100644 pikachu_chem.egg-info/requires.txt delete mode 100644 pikachu_chem.egg-info/top_level.txt diff --git a/pikachu_chem.egg-info/PKG-INFO b/pikachu_chem.egg-info/PKG-INFO deleted file mode 100644 index f040fb7..0000000 --- a/pikachu_chem.egg-info/PKG-INFO +++ /dev/null @@ -1,9 +0,0 @@ -Metadata-Version: 2.1 -Name: pikachu-chem -Version: 1.0.7 -Summary: PIKACHU: Python-based Informatics Kit for Analysing CHemical Units -Author: Barbara Terlouw -Author-email: barbara.terlouw@wur.nl -License-File: LICENSE.txt - -An easy-to-use cheminformatics kit with few dependencies. diff --git a/pikachu_chem.egg-info/SOURCES.txt b/pikachu_chem.egg-info/SOURCES.txt deleted file mode 100644 index 6500fe6..0000000 --- a/pikachu_chem.egg-info/SOURCES.txt +++ /dev/null @@ -1,55 +0,0 @@ -LICENSE.txt -README.md -pyproject.toml -setup.py -pikachu/__init__.py -pikachu/errors.py -pikachu/general.py -pikachu/math_functions.py -pikachu/chem/__init__.py -pikachu/chem/aromatic_system.py -pikachu/chem/atom.py -pikachu/chem/atom_properties.py -pikachu/chem/bond.py -pikachu/chem/bond_properties.py -pikachu/chem/chirality.py -pikachu/chem/electron.py -pikachu/chem/kekulisation.py -pikachu/chem/lone_pair.py -pikachu/chem/orbital.py -pikachu/chem/shell.py -pikachu/chem/structure.py -pikachu/chem/substructure_matching.py -pikachu/chem/molfile/__init__.py -pikachu/chem/molfile/read_molfile.py -pikachu/chem/molfile/write_molfile.py -pikachu/chem/rings/__init__.py -pikachu/chem/rings/find_cycles.py -pikachu/chem/rings/ring_identification.py -pikachu/drawing/__init__.py -pikachu/drawing/colours.py -pikachu/drawing/drawing.py -pikachu/drawing/rings.py -pikachu/drawing/sssr.py -pikachu/fingerprinting/__init__.py -pikachu/fingerprinting/daylight.py -pikachu/fingerprinting/ecfp_4.py -pikachu/fingerprinting/hashing.py -pikachu/fingerprinting/map_4.py -pikachu/fingerprinting/similarity.py -pikachu/inchi/__init__.py -pikachu/inchi/inchi.py -pikachu/parsers/__init__.py -pikachu/parsers/coconut.py -pikachu/parsers/np_atlas.py -pikachu/reactions/__init__.py -pikachu/reactions/basic_reactions.py -pikachu/reactions/functional_groups.py -pikachu/smiles/__init__.py -pikachu/smiles/graph_to_smiles.py -pikachu/smiles/smiles.py -pikachu_chem.egg-info/PKG-INFO -pikachu_chem.egg-info/SOURCES.txt -pikachu_chem.egg-info/dependency_links.txt -pikachu_chem.egg-info/requires.txt -pikachu_chem.egg-info/top_level.txt \ No newline at end of file diff --git a/pikachu_chem.egg-info/dependency_links.txt b/pikachu_chem.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/pikachu_chem.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/pikachu_chem.egg-info/requires.txt b/pikachu_chem.egg-info/requires.txt deleted file mode 100644 index 6ccafc3..0000000 --- a/pikachu_chem.egg-info/requires.txt +++ /dev/null @@ -1 +0,0 @@ -matplotlib diff --git a/pikachu_chem.egg-info/top_level.txt b/pikachu_chem.egg-info/top_level.txt deleted file mode 100644 index c3be790..0000000 --- a/pikachu_chem.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -pikachu