diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index ef17395..0000000 Binary files a/.DS_Store and /dev/null differ 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/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ 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 02d2970..0000000 Binary files a/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/atom.cpython-39.pyc b/__pycache__/atom.cpython-39.pyc deleted file mode 100644 index b5270d4..0000000 Binary files a/__pycache__/atom.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/atom_properties.cpython-39.pyc b/__pycache__/atom_properties.cpython-39.pyc deleted file mode 100644 index ba8fc68..0000000 Binary files a/__pycache__/atom_properties.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/bond.cpython-39.pyc b/__pycache__/bond.cpython-39.pyc deleted file mode 100644 index d92a79c..0000000 Binary files a/__pycache__/bond.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/bond_properties.cpython-39.pyc b/__pycache__/bond_properties.cpython-39.pyc deleted file mode 100644 index aa7bfd1..0000000 Binary files a/__pycache__/bond_properties.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/chirality.cpython-39.pyc b/__pycache__/chirality.cpython-39.pyc deleted file mode 100644 index f4341c2..0000000 Binary files a/__pycache__/chirality.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/drawing.cpython-39.pyc b/__pycache__/drawing.cpython-39.pyc deleted file mode 100644 index 46c19b1..0000000 Binary files a/__pycache__/drawing.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/electron.cpython-39.pyc b/__pycache__/electron.cpython-39.pyc deleted file mode 100644 index b6c6679..0000000 Binary files a/__pycache__/electron.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/errors.cpython-39.pyc b/__pycache__/errors.cpython-39.pyc deleted file mode 100644 index 0c61381..0000000 Binary files a/__pycache__/errors.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/find_cycles.cpython-39.pyc b/__pycache__/find_cycles.cpython-39.pyc deleted file mode 100644 index d343357..0000000 Binary files a/__pycache__/find_cycles.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/graph_to_smiles.cpython-39.pyc b/__pycache__/graph_to_smiles.cpython-39.pyc deleted file mode 100644 index 0b07da3..0000000 Binary files a/__pycache__/graph_to_smiles.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/kekulisation.cpython-39.pyc b/__pycache__/kekulisation.cpython-39.pyc deleted file mode 100644 index 7db9305..0000000 Binary files a/__pycache__/kekulisation.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/lone_pair.cpython-39.pyc b/__pycache__/lone_pair.cpython-39.pyc deleted file mode 100644 index 7142b0f..0000000 Binary files a/__pycache__/lone_pair.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/math_functions.cpython-39.pyc b/__pycache__/math_functions.cpython-39.pyc deleted file mode 100644 index 03bbff8..0000000 Binary files a/__pycache__/math_functions.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/orbital.cpython-39.pyc b/__pycache__/orbital.cpython-39.pyc deleted file mode 100644 index 99b7034..0000000 Binary files a/__pycache__/orbital.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/pikachu.cpython-39.pyc b/__pycache__/pikachu.cpython-39.pyc deleted file mode 100644 index ce2bdf0..0000000 Binary files a/__pycache__/pikachu.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/ring_identification.cpython-39.pyc b/__pycache__/ring_identification.cpython-39.pyc deleted file mode 100644 index 496426c..0000000 Binary files a/__pycache__/ring_identification.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/rings.cpython-39.pyc b/__pycache__/rings.cpython-39.pyc deleted file mode 100644 index 4c67846..0000000 Binary files a/__pycache__/rings.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/shell.cpython-39.pyc b/__pycache__/shell.cpython-39.pyc deleted file mode 100644 index 08d9ff3..0000000 Binary files a/__pycache__/shell.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/smiles.cpython-39.pyc b/__pycache__/smiles.cpython-39.pyc deleted file mode 100644 index 4542140..0000000 Binary files a/__pycache__/smiles.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/sssr.cpython-39.pyc b/__pycache__/sssr.cpython-39.pyc deleted file mode 100644 index cd40dcf..0000000 Binary files a/__pycache__/sssr.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/structure.cpython-39.pyc b/__pycache__/structure.cpython-39.pyc deleted file mode 100644 index fdcea24..0000000 Binary files a/__pycache__/structure.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/substructure_search.cpython-39.pyc b/__pycache__/substructure_search.cpython-39.pyc deleted file mode 100644 index d27f320..0000000 Binary files a/__pycache__/substructure_search.cpython-39.pyc and /dev/null differ 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/__pycache__/__init__.cpython-310.pyc b/pikachu/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 975db8d..0000000 Binary files a/pikachu/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/pikachu/__pycache__/__init__.cpython-36.pyc b/pikachu/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index face3d3..0000000 Binary files a/pikachu/__pycache__/__init__.cpython-36.pyc and /dev/null differ diff --git a/pikachu/__pycache__/__init__.cpython-38.pyc b/pikachu/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 2716d10..0000000 Binary files a/pikachu/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/pikachu/__pycache__/__init__.cpython-39.pyc b/pikachu/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index fb75ce2..0000000 Binary files a/pikachu/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/pikachu/__pycache__/errors.cpython-310.pyc b/pikachu/__pycache__/errors.cpython-310.pyc deleted file mode 100644 index fc955c4..0000000 Binary files a/pikachu/__pycache__/errors.cpython-310.pyc and /dev/null differ diff --git a/pikachu/__pycache__/errors.cpython-36.pyc b/pikachu/__pycache__/errors.cpython-36.pyc deleted file mode 100644 index 7f49248..0000000 Binary files a/pikachu/__pycache__/errors.cpython-36.pyc and /dev/null differ diff --git a/pikachu/__pycache__/errors.cpython-38.pyc b/pikachu/__pycache__/errors.cpython-38.pyc deleted file mode 100644 index 333cd97..0000000 Binary files a/pikachu/__pycache__/errors.cpython-38.pyc and /dev/null differ diff --git a/pikachu/__pycache__/errors.cpython-39.pyc b/pikachu/__pycache__/errors.cpython-39.pyc deleted file mode 100644 index ffe2953..0000000 Binary files a/pikachu/__pycache__/errors.cpython-39.pyc and /dev/null differ diff --git a/pikachu/__pycache__/general.cpython-310.pyc b/pikachu/__pycache__/general.cpython-310.pyc deleted file mode 100644 index 2ae3279..0000000 Binary files a/pikachu/__pycache__/general.cpython-310.pyc and /dev/null differ diff --git a/pikachu/__pycache__/general.cpython-36.pyc b/pikachu/__pycache__/general.cpython-36.pyc deleted file mode 100644 index e46ddc8..0000000 Binary files a/pikachu/__pycache__/general.cpython-36.pyc and /dev/null differ diff --git a/pikachu/__pycache__/general.cpython-38.pyc b/pikachu/__pycache__/general.cpython-38.pyc deleted file mode 100644 index 50fb6e8..0000000 Binary files a/pikachu/__pycache__/general.cpython-38.pyc and /dev/null differ diff --git a/pikachu/__pycache__/general.cpython-39.pyc b/pikachu/__pycache__/general.cpython-39.pyc deleted file mode 100644 index c1393d8..0000000 Binary files a/pikachu/__pycache__/general.cpython-39.pyc and /dev/null differ diff --git a/pikachu/__pycache__/math_functions.cpython-310.pyc b/pikachu/__pycache__/math_functions.cpython-310.pyc deleted file mode 100644 index 8a88404..0000000 Binary files a/pikachu/__pycache__/math_functions.cpython-310.pyc and /dev/null differ diff --git a/pikachu/__pycache__/math_functions.cpython-36.pyc b/pikachu/__pycache__/math_functions.cpython-36.pyc deleted file mode 100644 index 3d5d9c2..0000000 Binary files a/pikachu/__pycache__/math_functions.cpython-36.pyc and /dev/null differ diff --git a/pikachu/__pycache__/math_functions.cpython-38.pyc b/pikachu/__pycache__/math_functions.cpython-38.pyc deleted file mode 100644 index c5b505d..0000000 Binary files a/pikachu/__pycache__/math_functions.cpython-38.pyc and /dev/null differ diff --git a/pikachu/__pycache__/math_functions.cpython-39.pyc b/pikachu/__pycache__/math_functions.cpython-39.pyc deleted file mode 100644 index 0840930..0000000 Binary files a/pikachu/__pycache__/math_functions.cpython-39.pyc and /dev/null differ 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/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 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) - - -