From 5916a7b9a2631cc2f8184d442d2328707f72747b Mon Sep 17 00:00:00 2001 From: walderhu Date: Wed, 18 Dec 2024 13:31:59 +0000 Subject: [PATCH 1/5] rewrite clean2d algorithm on Python --- chython/algorithms/calculate2d/Calculate2d.py | 2642 +++++++++++++++++ chython/algorithms/calculate2d/KKLayout.py | 410 +++ chython/algorithms/calculate2d/MathHelper.py | 689 +++++ chython/algorithms/calculate2d/Properties.py | 611 ++++ chython/algorithms/calculate2d/__init__.py | 206 +- chython/algorithms/calculate2d/clean2d.js | 1 - clean2d/README | 3 - clean2d/package-lock.json | 2521 ---------------- clean2d/package.json | 23 - clean2d/src/index.js | 20 - clean2d/webpack.config.js | 12 - pyproject.toml | 1 - 12 files changed, 4353 insertions(+), 2786 deletions(-) create mode 100644 chython/algorithms/calculate2d/Calculate2d.py create mode 100644 chython/algorithms/calculate2d/KKLayout.py create mode 100644 chython/algorithms/calculate2d/MathHelper.py create mode 100644 chython/algorithms/calculate2d/Properties.py delete mode 100644 chython/algorithms/calculate2d/clean2d.js delete mode 100644 clean2d/README delete mode 100644 clean2d/package-lock.json delete mode 100644 clean2d/package.json delete mode 100644 clean2d/src/index.js delete mode 100644 clean2d/webpack.config.js diff --git a/chython/algorithms/calculate2d/Calculate2d.py b/chython/algorithms/calculate2d/Calculate2d.py new file mode 100644 index 00000000..54d776c0 --- /dev/null +++ b/chython/algorithms/calculate2d/Calculate2d.py @@ -0,0 +1,2642 @@ +""" +Class for calculating the 2D layout of a molecular graph, returning the coordinates of atom +vertices in a molecular container. + +This class provides methods for calculating and optimizing the 2D structure of a molecule, +including determining atom coordinates, handling collisions, and defining properties of rings +and bonds between atoms. Key functions include: + +- Calculating the initial positions of atoms and their subsequent adjustment to minimize +overlaps. +- Defining and classifying rings within the molecule, including handling bridged, spiro-fused, +and condensed rings. +- Handling cis-trans isomerism and atom configurations. +- Working with various types of bonds (single, double, triple) and their impact on atom +orientation. +- Calculating atom positions in ring structures and aromatic compounds. +- Handling collisions between atoms to improve molecule visualization. + +The class uses auxiliary functions for vector operations, determining atom neighbors, +calculating angles, and handling specific cases such as atoms with one, two, three, or four +neighbors. It also includes methods for working with ring structures, including determining the +ring center and positioning atoms within the ring. + +Utilizes AtomProperties, BondProperties, RingProperties, RingOverlap classes to represent atoms, +bonds, and rings, respectively. Additionally, the KKLayout class, which implements the +Kamada-Kawai algorithm, is used to minimize the system's energy by representing atoms as masses +connected by springs with a certain stiffness (used for calculating coordinates in bridged +cyclic molecules). +""" +from typing import List, Dict, Optional, Set, Tuple, Union, Generator, TYPE_CHECKING +from .MathHelper import Vector, Polygon +from .KKLayout import KKLayout +from .Properties import * +import math + +if TYPE_CHECKING: + from ...containers import MoleculeContainer + + +class Calculate2d: + """ + Class for calculating the 2D layout of a molecular graph, returning the coordinates of atom + vertices in a molecular container. + """ + + def __init__(self) -> None: + """ + The initial attributes initialization of the class includes: + bond_length: int: The bond length between atoms in the molecular graph. Used to determine + the distance between atoms when calculating their coordinates. + overlap_sensitivity: float: Sensitivity to atom overlap. Used to determine how close atoms + can be to each other before they are considered to overlap. + overlap_resolution_iterations: int: The number of iterations for resolving atom overlaps. + Indicates how many times the algorithm will attempt to improve atom positioning to + minimize overlaps. + ring_overlaps: List['RingOverlap']: A list of RingOverlap objects representing overlaps + between rings in the molecule. Used for identifying and handling ring overlaps. + total_overlap_score: float: The total overlap score in the molecule. Used for evaluating the + quality of atom positioning and minimizing overlaps. + finetune: bool: A flag indicating the need for detailed adjustment (finetuning) of atom + positions after the main calculation. Currently always set to True, but can be changed + to disable detailed adjustment. + ring_overlap_id_tracker: int: A counter for assigning unique identifiers to RingOverlap + objects. Used for the unique identification of ring overlaps. + ring_id_tracker: int: A counter for assigning unique identifiers to rings. Simplifies ring + management in the structure. + rings: List['RingProperties']: A list of RingProperties objects representing all rings in + the molecule. Used for working with ring structures and their attributes. + id_to_ring: Dict[int, 'RingProperties']: A dictionary mapping ring identifiers to their + RingProperties objects. Facilitates access to ring properties by their identifiers. + """ + + self.bond_length: int = 15 + self.overlap_sensitivity: float = 0.10 + self.overlap_resolution_iterations: int = 5 + self.ring_overlaps: List['RingOverlap'] = [] + self.total_overlap_score: float = 0.0 + # self.finetune: bool = True # используется в ветвлении, но всегда тру, бесполезная вещь + self.ring_overlap_id_tracker: int = 0 + self.ring_id_tracker: int = 0 + self.rings: List['RingProperties'] = [] + self.id_to_ring: Dict[int, 'RingProperties'] = {} + + + + def _calculate2d_coord(self, order: List[int], mc: 'MoleculeContainer') -> List[List[float]]: + """ + Calculates the coordinates of the vertices of the atoms of the graph and returns the + coordinates as + a two-dimensional array. + + This method computes the 2D coordinates for each atom in the molecular graph based on + the provided order and molecular container. It initializes the properties of the + molecular container, + defines the rings within the molecule, performs an initial approximation of atom + positions, handles collisions between atoms, and finally returns the calculated + coordinates. + + Parameters: + :param order: List[int]: + A list of integers representing the order in which atoms should be processed. This + order determines the sequence for calculating and adjusting atom positions to + minimize overlaps. + :param mc: MoleculeContainer: + An instance of MoleculeContainer that holds the molecular graph, including atoms, + bonds, and rings information necessary for the 2D layout calculation. + + Returns List[List[float]]: + A two-dimensional list where each inner list contains the x and y coordinates of an + atom in the 2D space. The order of coordinates corresponds to the order of atoms as + processed. + """ + self.create_property_attributes(mc) + self.define_rings() + + self.initial_approximation() ##main + self.collision_handling() + return self.get_coord(order) + + + + + def create_property_attributes(self, mc: 'MoleculeContainer') -> None: + """ + Initializes the property attributes for the molecular container, including atoms, bonds, and their + relationships. + + This method sets up the initial properties for the molecular container by creating dictionaries for + atoms and bonds based on the adjacency information provided by the molecular container. It also + refreshes the neighbours list for each atom to ensure accurate representation of the molecular + graph. + + Parameters + :param mc: MoleculeContainer: + An instance of MoleculeContainer that holds the molecular graph, including atoms, bonds, and + rings information necessary for the 2D layout calculation. + + Notes: + - Initializes the `atoms` dictionary with atom indices as keys and AtomProperties instances as + values, where each AtomProperties instance is created with the atom index and its corresponding + symbol. + - Refreshes the neighbours list for each atom based on the adjacency information from the molecular + container. + - Initializes the `bonds` dictionary with tuples of atom indices as keys and BondProperties + instances as values, representing the bonds between atoms. + - Sets up the `graph` dictionary to map each atom to its list of neighbouring atoms, facilitating + the representation of the molecular structure. + + The method is crucial for preparing the molecular container for further calculations by establishing + the basic properties and relationships between atoms and bonds, which are essential for the 2D + layout calculation. + """ + self.mc: 'MoleculeContainer' = mc + + self.atoms: Dict[int, AtomProperties] = {} + for atom in self.mc.int_adjacency: + symbol = self.get_symbol(atom) + self.atoms[atom] = AtomProperties(atom, symbol) + self.refresh_neighbours(self.mc.int_adjacency) + + self.graph: Dict['AtomProperties', List['AtomProperties']] = {} + for atom in self.atoms.values(): + self.graph[atom] = atom.neighbours + + self.bonds: Dict[Tuple[int], 'BondProperties'] = {} + for n, m, bond in self.mc.bonds(): + self.bonds[(n, m)] = BondProperties(self.atoms[n], self.atoms[m], bond) + + + ## creating and refreshing property attributes + + def refresh_neighbours(self, graph: Dict[int, Dict[int, int]]) -> None: + """ + Refreshes the neighbours list for each atom based on the provided graph adjacency information. + + This method updates the neighbours list for each atom in the molecular graph to ensure an accurate + representation of the molecular structure. It iterates through the graph, which is a dictionary + mapping atom indices to their adjacent atom indices, and assigns the corresponding AtomProperties + instances to the neighbours list of each atom. + + Parameters + :param graph: Dict[int, Dict[int, int]]: + A dictionary where keys are atom indices and values are dictionaries mapping to adjacent atom + indices. This structure represents the adjacency information of the molecular graph, indicating + which atoms are directly connected. + """ + for atom_index, neighbor_indexes in graph.items(): + neighbours: List['AtomProperties'] = [] + for neighbour_index in neighbor_indexes: + neighbours.append(self.atoms[neighbour_index]) + self.atoms[atom_index].neighbours = neighbours + + + def get_symbol(self, atom_index: int) -> str: + """ + Returns the atomic symbol of the atom corresponding to the given index. + + This method retrieves the atomic symbol of an atom in the molecular container based on its index. It + is a utility function used to identify the type of atom by its atomic symbol, which is essential for + various calculations and representations in the molecular graph. + + Parameters + :param atom_index: int: + The index of the atom for which the atomic symbol is to be retrieved. This index is used to + access the atom within the molecular container. + + Returns str: + The atomic symbol of the atom as a string, representing the element type of the atom (e.g., 'C' + for Carbon, 'H' for Hydrogen, etc.). + + """ + return self.mc.atom(atom_index).atomic_symbol + + + + def bond_lookup(self, atom: 'AtomProperties', next_atom: 'AtomProperties') -> Optional['BondProperties']: + """ + Возвращает связь, которая находится между этими двумя атомами + """ + return self.bonds.get((atom.id, next_atom.id)) or self.bonds.get((next_atom.id, atom.id)) + + + def get_configuration(self, atom1: 'AtomProperties', atom2: 'AtomProperties') -> Optional[str]: + """ + Проверяет есть ли конфигурация между этими атомами, + в случае отсутствия возвращает None, в ином случае + возвращает строку 'cis' или 'trans' + + self._cis_trans_stereo = {(2, 3): False} это словарь, хранящий значения + о конфигурациях молекулы, его ключами являются тюплы атомов, между которыми + есть могут быть конформации, сверху условие что эта связь обязательно должна + быть двойной, значениями является булевое значение, True если Цис конфигурация, + False если Транс, если конфигурации не предусмотренно вообще, то словарь будет пустым. + """ + if (atom1.id, atom2.id) not in list(self.mc._cis_trans_stereo.keys()): + return None + else: + configuration = self.mc._cis_trans_stereo[(atom1.id, atom2.id)] + return 'cis' if configuration else 'trans' + + +# поиск в базе колец и их классификация +# в структуре кайтона не обрабатываются случаи с мостиковыми кольцами + def define_rings(self) -> None: + """ + Defines the rings within the molecule, identifies ring overlaps, and handles bridged ring systems. + + This method performs several key steps in the process of analyzing the molecular structure: + 1. It initializes the rings present in the molecule by converting the simple cycle list (SSSR) from + the molecular container into RingProperties objects. + 2. Identifies overlaps between rings and creates RingOverlap objects for them. + 3. Finds and processes bridged ring systems, creating a unified representation for interconnected + rings that share atoms. + + Notes + ----- + - Initially, it retrieves the simple cycle list (SSSR) from the molecular container and converts + each cycle into a RingProperties object, adding them to the class's ring list. + - It then iterates through all pairs of rings to identify overlaps, creating RingOverlap objects for + those that share atoms and adding them to the class's ring overlaps list. + - For each ring, it updates the list of neighbouring rings based on identified overlaps, enhancing + the representation of the molecular structure's connectivity. + - The method also handles bridged ring systems by identifying rings that are part of a larger, + interconnected system and merges them into a single RingProperties object, ensuring a coherent + representation of complex cyclic structures. + - This process involves finding all rings involved in a bridged system, removing the original rings + from the list, and adding a new RingProperties object that represents the bridged system. + - Finally, it iterates through all rings to find any bridged systems not yet processed and repeats + the merging process, ensuring that all interconnected rings are represented as unified entities. + """ + rings = self.mc.sssr + if not rings: + return + + for neighbor_indexes in rings: + members_ring: List['AtomProperties'] = [self.atoms[atom_index] for atom_index in neighbor_indexes] + ring = RingProperties(members_ring) + self.add_ring(ring) + + for i, ring_1 in enumerate(self.rings[:-1]): + for ring_2 in self.rings[i + 1:]: + ring_overlap = RingOverlap(ring_1, ring_2) + if len(ring_overlap.atoms) > 0: + self.add_ring_overlap(ring_overlap) + + for ring in self.rings: + neighbouring_rings = self.find_neighbouring_rings(self.ring_overlaps, ring.id) + ring.neighbouring_rings = neighbouring_rings + + while True: + ring_id: int = -1 + for ring in self.rings: + if self.is_part_of_bridged_ring(ring.id) and not ring.bridged: + ring_id: int = ring.id + if ring_id == -1: + break + ring: 'RingProperties' = self.id_to_ring[ring_id] + + involved_ring_ids: Union[list[int], Set[int]] = [] + self.get_bridged_ring_subrings(ring.id, involved_ring_ids) + involved_ring_ids = set(involved_ring_ids) + + self.has_bridged_ring = True + self.create_bridged_ring(involved_ring_ids) + + for involved_ring_id in involved_ring_ids: + involved_ring = self.id_to_ring[involved_ring_id] + self.remove_ring(involved_ring) + + + bridged_systems = self.find_bridged_systems(self.rings, self.ring_overlaps) + if bridged_systems and not self.has_bridged_ring: + self.has_bridged_ring = True + for bridged_system in bridged_systems: + involved_ring_ids = set(bridged_system) + self.create_bridged_ring(involved_ring_ids) + for involved_ring_id in involved_ring_ids: + involved_ring = self.id_to_ring[involved_ring_id] + self.remove_ring(involved_ring) + + + + def add_ring_overlap(self, ring_overlap: 'RingOverlap') -> None: + """ + Adds a new ring overlap to the list of ring overlaps and assigns it a unique identifier. + + This method assigns a unique identifier to the given ring overlap and appends it to the class's list + of ring overlaps. It ensures that each ring overlap is uniquely identifiable and can be tracked + throughout the calculation process. + + Parameters + :param ring_overlap: RingOverlap + The ring overlap to be added to the list of overlaps. This object represents the intersection + between two rings in the molecule, which may need special handling during the layout calculation + to avoid visual clutter or incorrect representation. + """ + ring_overlap.id = self.ring_overlap_id_tracker + self.ring_overlaps.append(ring_overlap) + self.ring_overlap_id_tracker += 1 + + + + def is_part_of_bridged_ring(self, ring_id: int) -> bool: + """ + Determines if a given ring is part of a bridged ring system. + + This method checks if a ring, identified by its ID, is involved in a bridged ring system by + examining the list of ring overlaps. It returns True if the ring is part of a bridged system, + indicating that it is connected to another ring through a bridge, and False otherwise. + + Parameters + :param ring_id: int: + The identifier of the ring to check for involvement in a bridged ring system. + + Returns bool: + True if the ring is part of a bridged ring system, indicating that it is interconnected with + another ring through a bridge, and False otherwise. + """ + return any(ring_overlap.involves_ring(ring_id) and ring_overlap.is_bridge() \ + for ring_overlap in self.ring_overlaps) + + + + def get_bridged_ring_subrings(self, ring_id: int, involved_ring_ids: List[int]) -> None: + """ + Recursively identifies and collects the IDs of all rings involved in a bridged ring system starting + from a given ring ID. + + This method is used to find all rings that are interconnected as part of a bridged ring system, + starting from a specified ring ID. It recursively explores neighboring rings to identify all rings + that are connected through bridges, adding their IDs to a list of involved ring IDs. + + Parameters + :param ring_id: int + The identifier of the starting ring from which to begin the search for interconnected rings in a + bridged system. + :param involved_ring_ids: List[int] + A list to which the IDs of rings involved in the bridged system are appended. This list is + populated with the IDs of all rings found to be part of the bridged ring system. + """ + involved_ring_ids.append(ring_id) + 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 \ + self.rings_connected_by_bridge(self.ring_overlaps, ring_id, neighbour_id): + self.get_bridged_ring_subrings(neighbour_id, involved_ring_ids) + + + + @staticmethod + def rings_connected_by_bridge(ring_overlaps: List['RingOverlap'], ring_id_1: int, ring_id_2: int): + """ + Determines if two rings are connected by a bridge based on the list of ring overlaps. + + This method checks if two rings, identified by their IDs, are connected through a bridge by + examining the list of ring overlaps. It returns True if a bridge connection is found between the + specified rings, and False otherwise. + + Parameters + :param ring_overlaps: List['RingOverlap'] + A list of RingOverlap objects representing overlaps between rings in the molecule. Each + RingOverlap object contains information about the rings involved in the overlap and whether it + constitutes a bridge. + :param ring_id_1: int + The identifier of the first ring to check for a bridge connection. + :param ring_id_2: int + The identifier of the second ring to check for a bridge connection. + + Returns bool: + True if the specified rings are connected by a bridge, indicating a direct connection that forms + part of a bridged ring system, and False otherwise. + """ + for ring_overlap in ring_overlaps: + if ring_id_1 == ring_overlap.ring_id_1 and ring_id_2 == ring_overlap.ring_id_2: + return ring_overlap.is_bridge() + if ring_id_2 == ring_overlap.ring_id_1 and ring_id_1 == ring_overlap.ring_id_2: + return ring_overlap.is_bridge() + return False + + + + + + def create_bridged_ring(self, involved_ring_ids: Set[int]) -> None: + """ + Creates a unified representation for a bridged ring system by merging the specified rings into a + single RingProperties object. + + This method processes a set of ring IDs that are part of a bridged ring system, creating a new + RingProperties object that represents the interconnected rings as a single entity. It involves + identifying all atoms and neighbours involved in the bridged system, determining their roles (e.g., + bridge atoms), and updating the molecular structure to reflect this unified representation. + + Parameters + : param involved_ring_ids: Set[int] + A set of ring IDs that are part of a bridged ring system to be merged into a single + RingProperties object. + + Notes + - Initializes sets for atoms and neighbours involved in the bridged ring system. + - Iterates through each ring ID in the provided set, marking each as part of a subring of the ridged + system and collecting all member atoms and their neighbouring rings. + - Identifies atoms that are part of the bridged system and classifies them based on their nvolvement + in the ring system, distinguishing between those that are bridge atoms and those that are part of he + bridged ring itself. + - Creates a new RingProperties object for the bridged ring, adding it to the class's list of rings + and updating its attributes to reflect its bridged nature and interconnectedness. + - Updates the molecular structure to incorporate the new bridged ring, including updating atom + memberships and removing overlaps between the original rings that are now part of the bridged + system. + - This process is crucial for accurately representing complex cyclic structures within the molecule, + where rings are interconnected in a way that they share atoms, forming a bridged system. It ensures + that the molecular graph accurately reflects the topology of such systems, which is essential for + the correct calculation of atom positions and the overall layout in 2D space. + """ + atoms: Set['AtomProperties'] = set() + neighbours: Set[int] = set() + for ring_id in involved_ring_ids: + ring: 'RingProperties' = self.id_to_ring[ring_id] + ring.subring_of_bridged = True + for atom in ring.members: + atoms.add(atom) + for neighbour_id in ring.neighbouring_rings: + neighbours.add(neighbour_id) + leftovers: Set['AtomProperties'] = set() + ring_members: Set['AtomProperties'] = set() + for atom in atoms: + atom_rings_members_id: Set[int] = {ring.id for ring in atom.rings} + intersect = involved_ring_ids.intersection(atom_rings_members_id) + if len(atom.rings) == 1 or len(intersect) == 1: + ring_members.add(atom) + else: + leftovers.add(atom) + for atom in leftovers: + is_on_ring = False + for bond in self.get_bonds_of_atom(atom): + bond_associated_rings = min(len(bond.atom1.rings), len(bond.atom2.rings)) + if bond_associated_rings == 1: + is_on_ring = True + if is_on_ring: + atom.is_bridge_atom = True + ring_members.add(atom) + else: + atom.is_bridge = True + ring_members.add(atom) + bridged_ring = RingProperties(list(ring_members)) + self.add_ring(bridged_ring) + bridged_ring.bridged = True + bridged_ring.neighbouring_rings = list(neighbours) + for ring_id in involved_ring_ids: + ring = self.id_to_ring[ring_id] + bridged_ring.subrings.append(ring.copy()) + for atom in ring_members: + atom.bridged_ring = bridged_ring.id + for ring_id in involved_ring_ids: + if self.id_to_ring[ring_id] in atom.rings: + atom.rings.remove(self.id_to_ring[ring_id]) + atom.rings.append(bridged_ring) + involved_ring_ids: List[int] = list(involved_ring_ids) + for i, ring_id_1 in enumerate(involved_ring_ids): + 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: + ring_overlaps: List['RingOverlap'] = self.get_ring_overlaps(neighbour_id, involved_ring_ids) + for ring_overlap in ring_overlaps: + + ring_overlap.update_other(bridged_ring.id, neighbour_id) + neighbour = self.id_to_ring[neighbour_id] + neighbour.neighbouring_rings.append(bridged_ring.id) + + + + def remove_ring_overlaps_between(self, ring_id_1: int, ring_id_2: int) -> None: + """ + Removes ring overlaps between two specified rings from the list of ring overlaps. + + This method identifies and removes any ring overlaps between two rings, specified by their IDs, from + the class's list of ring overlaps. It ensures that once rings are merged or otherwise processed in a + way that eliminates their overlap, the record of their previous overlap is removed to maintain an + accurate representation of the molecular structure. + + Parameters + :param ring_id_1: int + The identifier of the first ring for which overlaps should be removed. + :param ring_id_2: int + The identifier of the second ring for which overlaps should be removed. + """ + 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): + to_remove.append(ring_overlap) + for ring_overlap in to_remove: + self.ring_overlaps.remove(ring_overlap) + + + + def get_ring_overlaps(self, ring_id: int, ring_ids: List[int]) -> List['RingOverlap']: + """ + Retrieves a list of ring overlaps involving a specified ring and a list of other ring IDs. + + Parameters + :param ring_id: int + The identifier of the ring for which overlaps with other rings are to be found. + :param ring_ids: List[int] + A list of ring identifiers to check for overlaps with the specified ring. + + Returns List['RingOverlap'] + A list of RingOverlap objects representing the overlaps between the specified ring and any of + the rings identified by the IDs in the ring_ids list. Each RingOverlap object contains + information about the rings involved in the overlap and the nature of their intersection. + """ + ring_overlaps: List['RingOverlap'] = [] + 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): + ring_overlaps.append(ring_overlap) + return ring_overlaps + + + + def remove_ring(self, ring: 'RingProperties') -> None: + """ + Removes a specified ring from the list of rings and updates the list of ring overlaps accordingly. + + Parameters + :param ring: RingProperties + The RingProperties object to be removed from the list of rings. + """ + self.rings.remove(ring) + overlaps_to_remove = [] + for ring_overlap in self.ring_overlaps: + if ring_overlap.ring_id_1 == ring.id or ring_overlap.ring_id_2 == ring.id: + overlaps_to_remove.append(ring_overlap) + for ring_overlap in overlaps_to_remove: + self.ring_overlaps.remove(ring_overlap) + for neighbouring_ring in self.rings: + if ring.id in neighbouring_ring.neighbouring_rings: + neighbouring_ring.neighbouring_rings.remove(ring.id) + + + + def find_bridged_systems(self, rings: List['RingProperties'], ring_overlaps: 'RingOverlap') -> List: + """ + Identifies bridged ring systems within the molecule based on the provided rings and their overlaps. + + Parameters + :param rings : List['RingProperties'] + A list of RingProperties objects representing the rings within the molecule to be analyzed. + :param ring_overlaps : List['RingOverlap'] + A list of RingOverlap objects representing overlaps between rings, which is used to determine + the interconnectedness of the rings. + + Returns List[List[int]] + A list of ring groups, where each group is represented as a list of ring IDs. Each group is + identified as a bridged system based on the criteria that the number of overlaps is at least as + great as the number of rings in the group, indicating a high likelihood of forming a bridged + ring system. + """ + bridged_systems: List = [] + ring_groups = self.get_ring_groups(rings, ring_overlaps) + for ring_group in ring_groups: + ring_nr: int = len(ring_group) + overlap_nr: int = self.get_group_overlap_nr(ring_group, ring_overlaps) + if overlap_nr >= ring_nr: + bridged_systems.append(ring_group) + return bridged_systems + + + # @TODO: непонятный докстринг, переписать + + def get_ring_groups(self, rings: List['RingProperties'], ring_overlaps: List['RingOverlap']) -> List: + """ + Organizes rings into groups based on their overlaps, identifying interconnected ring systems within + the molecule. + + Parameters + :param rings: List['RingProperties'] + A list of RingProperties objects representing the rings within the molecule to be analyzed. + :param ring_overlaps: List['RingOverlap'] + A list of RingOverlap objects representing overlaps between rings, which is used to determine + the interconnectedness of the rings. + + Returns List[List[int]] + A list of ring groups, where each group is represented as a list of ring IDs. Rings within a + group are interconnected, either directly or through a series of overlaps, indicating potential + bridged or fused ring systems. + + Notes + - Initializes a list of ring groups, starting with each ring as a separate group. + - Iteratively merges groups that have overlaps, indicating a structural relationship between rings, + until no more merges are possible. This is determined by comparing the number of groups before and + after attempting merges. + - Uses a helper method, ring_groups_have_overlap, to identify if two groups share an overlap, + suggesting they should be merged into a single group. + - Merging is done by creating a union of the two groups and removing the original groups from the + list, then adding the merged group. This process simplifies the representation of the molecule's + ring structure by consolidating interconnected rings. + - The merging process continues until the number of groups stabilizes, indicating that all + interconnected rings have been grouped together. + - This method is crucial for simplifying the analysis of molecular structures with complex cyclic + components, as it reduces the complexity of the ring structure by grouping interconnected rings. + This simplification aids in the identification of bridged and fused ring systems, which are + important for accurate layout calculations and visualization. + - By organizing rings into groups, it provides a basis for further analysis, such as identifying + bridged ring systems or resolving the layout of rings in a way that reflects their + interconnectedness, which is essential for the accurate representation of molecular topology in 2D + space. + - The final list of ring groups represents a simplified view of the molecule's cyclic structure, + where each group may correspond to a bridged, fused, or independent ring system, depending on the + overlaps between rings. + """ + ring_groups = [] + for ring in rings: + ring_groups.append([ring.id]) + + current_ring_nr = 0 + previous_ring_nr = -1 + while current_ring_nr != previous_ring_nr: + previous_ring_nr = current_ring_nr + indices = None + new_group = None + for i, ring_group_1 in enumerate(ring_groups): + ring_group_1_found = False + for j, ring_group_2 in enumerate(ring_groups): + if i != j: + if self.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 + break + if ring_group_1_found: + break + + if new_group: + indices.sort(reverse=True) + for index in indices: + ring_groups.pop(index) + ring_groups.append(new_group) + + current_ring_nr = len(ring_groups) + return ring_groups + + + + def ring_groups_have_overlap(self, group_1: List[int], group_2: List[int], \ + ring_overlaps: List['RingOverlap']) -> bool: + """ + Determines if two ring groups have an overlap based on the list of ring overlaps. + + Parameters + :param group_1: List[int] + The first group of ring IDs to check for overlaps. + :param group_2: List[int] + The second group of ring IDs to check for overlaps. + :param ring_overlaps: List['RingOverlap'] + A list of RingOverlap objects representing overlaps between rings in the molecule. + Each RingOverlap object contains information about the rings involved in the overlap. + + Returns bool + True if an overlap is found between any rings from the two groups, indicating a structural + relationship, and False otherwise. + """ + # for ring_1 in group_1: + # for ring_2 in group_2: + # if ring_1 in self.find_neighbouring_rings(ring_overlaps, ring_2): + # return True + # return False + # @TODO: ниже моя версия + return any(ring_1 in self.find_neighbouring_rings(ring_overlaps, ring_2) \ + for ring_1 in group_1 for ring_2 in group_2) + + + @staticmethod + def get_group_overlap_nr(ring_group, ring_overlaps: List['RingOverlap']) -> int: + """ + Calculates the number of overlaps within a group of rings based on a list of ring overlaps. + + Parameters: + :param ring_group List[int] + A list of ring identifiers (IDs) representing a group of rings to check for overlaps among. + :param ring_overlaps List['RingOverlap']: + A list of `RingOverlap` objects, where each object represents an overlap between two rings, + identified by their IDs (`ring_id_1` and `ring_id_2`). + + Returns int The total number of overlaps found within the `ring_group`, where an overlap is + counted if both rings involved are members of the group. + """ + # 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: + # overlaps += 1 + # return overlaps + # @TODO: моя версия укороченная версиянадо потестировать + ring_group_set = set(ring_group) + return sum(overlap.ring_id_1 in ring_group_set and overlap.ring_id_2 in ring_group_set\ + for overlap in ring_overlaps) + + + def get_bonds_of_atom(self, atom: 'AtomProperties') -> List['BondProperties']: + """ + Retrieves all bonds associated with a specified atom within the molecular graph. + + This method searches the molecular graph for bonds that involve a given atom, returning + a list of all bonds connected to it. Each bond is represented by a BondProperties + object, which encapsulates details about the bond type and the atoms it connects. By + iterating through the collection of all bonds in the graph and checking if the specified + atom is involved in each bond, the method accurately identifies all connections of the + atom, regardless of the atom's role (whether as the starting or ending atom of the + bond). + + Parameters: + :param atom AtomProperties: + The atom whose bonds are to be retrieved. This atom is identified by its unique + identifier within the molecular graph. + + Returns List[BondProperties]: + A list of BondProperties objects representing all bonds connected to the specified + atom. Each entry in the list corresponds to a distinct bond involving the atom, + providing comprehensive information about the atom's connectivity within the + molecular structure. + """ + # bonds: List['BondProperties'] = [] + # for (atom1_id, atom2_id), bond in self.bonds.items(): + # if atom.id in (atom1_id, atom2_id): + # bonds.append(bond) + # return bonds + # @TODO: моя сокращенная версия + return [bond for (atom1_id, atom2_id), bond in self.bonds.items() \ + if atom.id in (atom1_id, atom2_id)] + + + + def add_ring(self, ring: 'RingProperties') -> None: + """ + Adds a new ring to the class's collection and updates the internal tracking of ring + identifiers. + + Parameters: + :param ring 'RingProperties': + The `RingProperties` object representing the ring to be added. This + object encapsulates the properties and characteristics of the ring, such as its member atoms, size, and type (e.g., aromatic, bridged). + """ + ring.id = self.ring_id_tracker + self.rings.append(ring) + self.id_to_ring[ring.id] = ring + self.ring_id_tracker += 1 + + + ##первое приближение + def initial_approximation(self) -> None: + """ + Determines the initial atom from which to start the layout calculation process for a molecular + graph in 2D space. + + This method iterates through the molecular graph to find an appropriate starting atom based on + several criteria: + 1. Prefers an atom that is part of a bridged ring system, indicating complex cyclic structures + that require careful handling. + 2. If no such atom is found, it looks for an atom that belongs to a bridged ring, which suggests + a connection between rings that might need special attention during layout. + 3. If still no suitable atom is found, and if there are rings defined, it selects the first + member of the first ring in the class's ring list. + 4. If no rings are defined or none of the above conditions are met, it selects a terminal atom, + which is an atom with no more than one bond, simplifying the starting conditions. + 5. As a last resort, if no terminal atom is found, it defaults to the first atom in the graph. + + After selecting the starting atom, it initiates the bond creation process by calling + `create_next_bond` with the selected atom, setting the stage for further layout calculations. + """ + start_atom = None + + for atom in self.graph: + if atom.bridged_ring is not None: + start_atom = atom + break + + if start_atom is None: + for ring in self.rings: + if ring.bridged: + start_atom = ring.members[0] + + if start_atom is None: + if len(self.rings) > 0: + start_ring: 'RingProperties' = self.id_to_ring[0] + start_atom = start_ring.members[0] + + if start_atom is None: + for atom in self.graph: + if atom.is_terminal(): + start_atom = atom + break + + if start_atom is None: + start_atom = self.graph[0] + self.create_next_bond(start_atom, None, 0.0) + + + + def create_next_bond(self, atom: 'AtomProperties', previous_atom: Optional['AtomProperties']=None, \ + angle: float=0.0, previous_branch_shortest: bool = False) -> None: + """ + Creates the next bond for an atom in the molecular structure, updating its position + based on the previous atom and angle. + + Parameters: + :param atom: AtomProperties: + The atom for which the next bond is being created. + :param previous_atom: Optional[AtomProperties]: + The previous atom connected to the current atom. If None, it is assumed that the + current atom is the first in the molecular structure. + :param angle: float: + The angle between the previous atom and the current atom in radians. Default is 0.0. + :param previous_branch_shortest: bool: + A flag indicating if the previous branch is the shortest. Default is False. + + Logic: + 1. If the atom is already positioned, the method ends without changes. + 2. If there is no previous atom, a special method for the first atom is used. + 3. If the previous atom is connected to one or more rings, a method for calculating the + atom's position in ring structures is used. + 4. Otherwise, if the previous atom is not connected to rings, a method for calculating + the atom's position without considering rings is used. + 5. If the atom has connected rings, a method for atoms in ring structures is applied. + 6. Depending on the number of neighbors the atom has (from 1 to 4), the corresponding + method is chosen to calculate its position, considering various neighbor configurations. + """ + if atom.positioned: + return + if previous_atom is None: + self.calculate_first_atom(atom) + elif len(previous_atom.rings) > 0: + self.calculate_rings(previous_atom, atom) + else: + self.calculate_NOT_first_atom(atom, previous_atom, angle) + + if len(atom.rings) > 0: + self.calculate_some_rings(atom) + else: + neighbours: List['AtomProperties'] = atom.neighbours[:] + if previous_atom and previous_atom in neighbours: + neighbours.remove(previous_atom) + previous_angle: float = atom.get_angle() + if len(neighbours) == 1: + self.calculate_1_neighbours(neighbours, atom, previous_atom, \ + previous_angle, previous_branch_shortest) + elif len(neighbours) == 2: + self.calculate_2_neighbours(atom, neighbours, previous_atom, previous_angle) + elif len(neighbours) == 3: + self.calculate_3_neighbours(atom, neighbours, previous_atom, previous_angle) + elif len(neighbours) == 4: + self.calculate_4_neighbours(atom , neighbours, previous_angle) + + + def calculate_first_atom(self, atom: 'AtomProperties') -> None: + """ + Calculates the initial position for the first atom in a molecule. + + This method sets the initial position for the first atom in the molecular structure. It + assigns a default position based on the class's bond length and rotates it to a standard + orientation. The atom is marked as positioned if it is not part of a bridged ring + system, ensuring it's ready for further calculations in the molecular layout process. + + Parameters: + :param atom AtomProperties: + The first atom in the molecule to calculate the initial position for. + """ + dummy: Vector = Vector(self.bond_length, 0) + dummy.rotate(math.radians(-60.0)) + atom.previous_position = dummy + atom.previous_atom = None + atom.set_position(Vector(self.bond_length, 0)) + atom.angle = math.radians(-60.0) + if atom.bridged_ring is None: + atom.positioned = True + + + # @TODO: Дать нормальное название + def calculate_NOT_first_atom(self, atom: 'AtomProperties', + previous_atom: 'AtomProperties', angle: float) -> None: + """ + Calculates the position for an atom that is not the first in the molecule, based on its + previous atom and a given angle. + + Parameters: + :param atom AtomProperties: + The atom for which the position is being calculated. + :param previous_atom AtomProperties: + The atom preceding the current atom in the molecular structure, used as a reference + for positioning. + :param angle float: + The angle in radians by which the position vector should be rotated to align the + atom correctly relative to the previous atom. + """ + position: Vector = Vector(self.bond_length, 0) + position.rotate(angle) + position.add(previous_atom.position) + atom.set_position(position) + atom.set_previous_position(previous_atom) + atom.positioned = True + + + + # @TODO: разбить конкретную функцию на несколько логически обоснованных частей + # например отдельно 2-3 связи отдельно кольца и отдельно остальные случаи + def calculate_1_neighbours(self, neighbours: List[int], atom: 'AtomProperties', \ + previous_atom: 'AtomProperties', previous_angle: float, \ + previous_branch_shortest: bool) -> None: + """ + Calculates the position for an atom with exactly one neighbor in the molecular + structure, considering various bonding scenarios and configurations. + + This method is designed to handle the placement of an atom that has only one neighbor + within the molecular structure, taking into account the type of bonds it forms with its + previous atom and the presence of any rings. It adjusts the atom's position based on the + bond type (single, double, or triple) and the configuration of the molecule, including + handling cis and trans isomerism in specific scenarios. The method also considers the + angle of the previous bond and the shortest branch condition to correctly orient the + atom in space. + + Parameters: + :param neighbours List[int]: + A list of atom indices representing the neighbors of the current atom. Since the + atom has only one neighbor, this list should contain a single element. + :param atom AtomProperties: + The atom for which the position is being calculated. + :param previous_atom AtomProperties: + The atom preceding the current atom in the molecular structure, used as a reference + for positioning. + :param previous_angle float: + The angle in radians between the previous atom and the current atom. + :param previous_branch_shortest bool: + Indicates if the previous branch is the shortest, affecting the orientation of the next bond. + """ + next_atom: 'AtomProperties' = neighbours[0] + current_bond: Optional['BondProperties'] = self.bond_lookup(atom, next_atom) + previous_bond: Optional['BondProperties'] = None + if previous_atom: + previous_bond = self.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.rings) == 0 and len(atom.neighbours) == 2): + if current_bond.type == 'double' and previous_bond.type == 'double': + atom.draw_explicit = True + if current_bond.type == 'triple': + atom.draw_explicit = True + next_atom.draw_explicit = True + if current_bond.type == 'double' or current_bond.type == 'triple' or \ + (previous_atom and previous_bond.type == 'triple'): + next_atom.angle = math.radians(0) + angle_ = previous_angle + next_atom.angle + self.create_next_bond(next_atom, atom, angle_) + elif previous_atom and len(previous_atom.rings) > 0: + proposed_angle_1: float = math.radians(60.0) + proposed_angle_2: float = proposed_angle_1 * -1 + + proposed_vector_1: 'Vector' = Vector(self.bond_length, 0) + proposed_vector_2: 'Vector' = Vector(self.bond_length, 0) + proposed_vector_1.rotate(proposed_angle_1 + atom.get_angle()) + proposed_vector_2.rotate(proposed_angle_2 + atom.get_angle()) + proposed_vector_1.add(atom.position) + proposed_vector_2.add(atom.position) + centre_of_mass: Vector = self.get_current_centre_of_mass() + distance_1: float = proposed_vector_1.get_squared_distance(centre_of_mass) + distance_2: float = proposed_vector_2.get_squared_distance(centre_of_mass) + if distance_1 < distance_2: + previous_atom.angle = proposed_angle_2 + else: + previous_atom.angle = proposed_angle_1 + angle_: float = previous_angle + previous_atom.angle + self.create_next_bond(next_atom, atom, angle_) + else: + proposed_angle: float = atom.angle + + if previous_atom and len(previous_atom.neighbours) > 3: + if round(proposed_angle, 2) > 0.00: + proposed_angle: float = min([math.radians(60), proposed_angle]) + elif round(proposed_angle, 2) < 0.00: + proposed_angle: float = max([-math.radians(60), proposed_angle]) + else: + proposed_angle: float = math.radians(60) + elif proposed_angle in (0, None): + last_angled_atom: 'AtomProperties' = self.get_last_atom_with_angle(atom) + proposed_angle: float = last_angled_atom.angle + if proposed_angle is None: + proposed_angle: float = math.radians(60) + + rotatable: bool = True + if previous_atom: + bond: 'BondProperties' = self.bond_lookup(previous_atom, atom) + # This handles cases where there are no second explicit atoms in the + # configuration # of carbons between which cis and trans isomerism can occur + # For example smile = "F/C=C/F" or "F/C=C\F". + if bond.type == 'double': + rotatable: bool = False + previous_previous_atom: 'AtomProperties' = previous_atom.previous_atom + if previous_previous_atom: + if (configuration := self.get_configuration(previous_atom, atom)) is not None: + if configuration == 'cis': + proposed_angle = -proposed_angle + if rotatable: + next_atom.angle = proposed_angle if previous_branch_shortest else -proposed_angle + else: + next_atom.angle = -proposed_angle + self.create_next_bond(next_atom, atom, previous_angle + next_atom.angle) + + + + def calculate_2_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomProperties'], \ + previous_atom: 'AtomProperties', previous_angle: float) -> None: + """ + Calculates the positions for an atom with exactly two neighbours in the molecular + structure, considering cis and trans isomerism and the shortest branch condition. + + This method is responsible for determining the positions of an atom that has exactly two + neighbours within the molecular structure. It takes into account the possibility of cis + and trans isomerism and adjusts the atom's orientation based on the shortest branch + condition to ensure correct spatial arrangement. The method first checks for the + presence of a proposed angle for the atom; if none is found, a default angle is + assigned. It then handles cis and trans isomerism by adjusting the angles of the atom + and its neighbours accordingly. The method also determines whether the previous branch + is the shortest by comparing the sizes of subgraphs involving the previous atom and the + neighbours, which influences the orientation of the new bonds created. Finally, it + creates the next bonds for the atom with its neighbours, incorporating the calculated + angles and the shortest branch condition. + + Parameters: + :param atom AtomProperties: + The atom for which the positions are being calculated. + :param neighbours List[AtomProperties]: + A list of the atom's neighbours, which should contain exactly two elements. + :param previous_atom AtomProperties: + The atom preceding the current atom in the molecular structure, used as a reference + for positioning. + :param previous_angle float: + The angle in radians between the previous atom and the current atom. + """ + proposed_angle = atom.angle + if not proposed_angle: + proposed_angle = math.radians(60) + + self.handle_cis_trans_isomery(atom, neighbours, previous_atom, proposed_angle) + + if previous_atom: + subgraph_3_size: int = self.get_subgraph_size(previous_atom, {atom}) + else: + subgraph_3_size: int = 0 + + previous_branch_shortest = False + if subgraph_3_size < self.get_subgraph_size(neighbours[0], {atom}) and \ + subgraph_3_size < self.get_subgraph_size(neighbours[1], {atom}): + previous_branch_shortest = True + + self.create_next_bond(neighbours[0], atom, previous_angle + neighbours[0].angle, previous_branch_shortest) + self.create_next_bond(neighbours[1], atom, previous_angle + neighbours[1].angle, previous_branch_shortest) + + + + def handle_cis_trans_isomery(self, atom: 'AtomProperties', neighbours: List['AtomProperties'], \ + previous_atom: 'AtomProperties', proposed_angle: float) -> None: + """ + Handles the case of cis and trans isomerism for an atom with two neighbours, adjusting + their angles based on the isomeric configuration. + + This method addresses the specific scenario of cis and trans isomerism for an atom + connected to two neighbours, determining the correct spatial orientation based on the + isomeric configuration and the types of bonds involved. It calculates the subgraph sizes + for each neighbour relative to the atom to identify the cis and trans positions, + adjusting their angles accordingly. The method also considers the bond types between the + atom and its neighbours to further refine the orientation in cases where both bonds are + single, potentially adjusting angles based on the configuration of the previous atom in + the molecular structure. + + Parameters: + :param atom AtomProperties: + The central atom for which cis and trans isomerism is being evaluated. + :param neighbours List[AtomProperties]: + A list containing exactly two neighbours of the atom, between which cis and trans + isomerism is considered. + :param previous_atom AtomProperties: + The atom preceding the current atom in the molecular structure, used for additional + configuration checks. + :param proposed_angle: float: The initial proposed angle for orientation, in radians. + """ + neighbour_1, neighbour_2 = neighbours + subgraph_1_size: int = self.get_subgraph_size(neighbour_1, {atom}) + subgraph_2_size: int = self.get_subgraph_size(neighbour_2, {atom}) + + cis_atom_index: int = 0 + trans_atom_index: int = 1 + + if neighbour_2.symbol == 'C' and neighbour_1.symbol != 'C' and subgraph_2_size > 1 and subgraph_1_size < 5: + cis_atom_index = 1 + trans_atom_index = 0 + elif neighbour_2.symbol != 'C' and neighbour_1.symbol == 'C' and subgraph_1_size > 1 and subgraph_2_size < 5: + cis_atom_index = 0 + trans_atom_index = 1 + elif subgraph_2_size > subgraph_1_size: + cis_atom_index = 1 + trans_atom_index = 0 + + cis_atom: 'AtomProperties' = neighbours[cis_atom_index] + trans_atom: 'AtomProperties' = neighbours[trans_atom_index] + + trans_atom.angle = proposed_angle + cis_atom.angle = -proposed_angle + + cis_bond: 'BondProperties' = self.bond_lookup(atom, cis_atom) + trans_bond: 'BondProperties' = self.bond_lookup(atom, trans_atom) + + if cis_bond.type == 'single' and trans_bond.type == 'single': + if previous_atom: + previous_bond: 'BondProperties' = self.bond_lookup(atom, previous_atom) + if previous_bond.type == 'double': + if previous_atom.previous_atom: + atom1, atom2 = previous_atom, atom + configuration_cis_atom: Optional[str] = self.get_configuration(atom1, atom2) + if configuration_cis_atom == 'trans': + trans_atom.angle = -proposed_angle + cis_atom.angle = proposed_angle + + + + def calculate_3_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomProperties'], \ + previous_atom: 'AtomProperties', previous_angle: float) -> None: + """ + Calculates the positions for an atom with exactly three neighbours in the molecular + structure, adjusting angles based on subgraph sizes and ring involvement. + + This method is designed to handle the placement of an atom that has exactly three + neighbours within the molecular structure. It determines the orientation of these + neighbours based on the sizes of their subgraphs relative to the central atom and + adjusts their angles accordingly. The method identifies a 'straight' atom, which is + considered the primary direction of extension from the central atom, and two side atoms. + The orientation of these atoms is adjusted based on whether they are involved in any + rings and the overall structure of the molecule, ensuring a correct spatial arrangement + that minimizes overlaps and maintains the integrity of the molecular geometry. + + Parameters: + :param atom: AtomProperties + The central atom for which the positions of its neighbours are being calculated. + :param neighbours List[AtomProperties]: + A list of the atom's neighbours, which should contain exactly three elements. + :param previous_atom AtomProperties: + The atom preceding the current atom in the molecular structure, used as a reference + for positioning. + :param previous_angle float: + The angle in radians between the previous atom and the current atom, influencing + the orientation of the neighbours. + """ + subgraph_1_size = self.get_subgraph_size(neighbours[0], {atom}) + subgraph_2_size = self.get_subgraph_size(neighbours[1], {atom}) + subgraph_3_size = self.get_subgraph_size(neighbours[2], {atom}) + straight_atom: 'AtomProperties' = neighbours[0] + left_atom: 'AtomProperties' = neighbours[1] + right_atom: 'AtomProperties' = neighbours[2] + 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: + straight_atom = neighbours[2] + left_atom = neighbours[0] + right_atom = neighbours[1] + if previous_atom and len(previous_atom.rings) < 1\ + and len(straight_atom.rings) < 1\ + and len(left_atom.rings) < 1\ + and len(right_atom.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.angle = atom.angle * -1 #maybe bug + if atom.angle >= 0: + left_atom.angle = math.radians(30) + right_atom.angle = math.radians(90) + else: + left_atom.angle = math.radians(-30) + right_atom.angle = math.radians(-90) + else: + straight_atom.angle = math.radians(0) + left_atom.angle = math.radians(90) + right_atom.angle = math.radians(-90) + self.create_next_bond(straight_atom, atom, previous_angle + straight_atom.angle) + self.create_next_bond(left_atom, atom, previous_angle + left_atom.angle) + self.create_next_bond(right_atom, atom, previous_angle + right_atom.angle) + + + + def calculate_4_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomProperties'], \ + previous_angle: float) -> None: + """ + Handles the case when an atom has exactly four neighbours, adjusting their positions and + angles for correct spatial arrangement. + + This method is responsible for calculating the positions and angles of an atom that is + connected to four neighbours within the molecular structure. It determines the optimal + arrangement of these neighbours based on the sizes of their subgraphs relative to the + central atom, ensuring a non-overlapping and structurally sound configuration. The + method assigns specific angles to each neighbour to maintain a consistent and clear + representation of the molecular geometry, especially in complex molecular structures + where an atom is central to four other atoms. + + Parameters: + :param atom AtomProperties: + The central atom around which the neighbours are positioned. + :param neighbours List[AtomProperties]: + A list of the atom's neighbours, which should contain exactly four elements. + :param previous_angle float: + The angle in radians between the previous atom and the current atom, used as a + reference for positioning the neighbours. + """ + subgraph_1_size = self.get_subgraph_size(neighbours[0], {atom}) + subgraph_2_size = self.get_subgraph_size(neighbours[1], {atom}) + subgraph_3_size = self.get_subgraph_size(neighbours[2], {atom}) + subgraph_4_size = self.get_subgraph_size(neighbours[3], {atom}) + atom_1: 'AtomProperties' = neighbours[0] + atom_2: 'AtomProperties' = neighbours[1] + atom_3: 'AtomProperties' = neighbours[2] + atom_4: 'AtomProperties' = neighbours[3] + 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: + 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: + atom_1 = neighbours[3] + atom_2 = neighbours[0] + atom_3 = neighbours[1] + atom_4 = neighbours[2] + + atom_1.angle = math.radians(-36) + atom_2.angle = math.radians(36) + atom_3.angle = math.radians(-108) + atom_4.angle = math.radians(108) + self.create_next_bond(atom_1, atom, previous_angle + atom_1.angle) + self.create_next_bond(atom_2, atom, previous_angle + atom_2.angle) + self.create_next_bond(atom_3, atom, previous_angle + atom_3.angle) + self.create_next_bond(atom_4, atom, previous_angle + atom_4.angle) + + + + def get_subgraph_size(self, atom: 'AtomProperties', \ + masked_atoms: Set['AtomProperties']) -> int: + """ + Calculates and returns the size of a subtree rooted at a given atom, excluding bonds adjacent to atoms specified in masked_atoms. + + This method computes the size of a subtree within a molecular graph, starting from a + specified atom and excluding any bonds connected to atoms listed in the `masked_atoms` + set. It recursively explores the molecular structure, adding each visited atom to the + `masked_atoms` set to avoid revisiting, and counts the total number of unique atoms in + the subtree. The size of the subtree is determined by the number of atoms it contains, + excluding the initial atom itself. + + Parameters: + :param atom AtomProperties: + The root atom from which the subtree size is calculated. + :param masked_atoms Set[AtomProperties]: + A set of atoms to be excluded from the subtree calculation, typically used to avoid + counting atoms that have already been considered in previous calculations or are not + relevant to the current analysis. + """ + masked_atoms.add(atom) + for neighbour in atom.neighbours: + if neighbour not in masked_atoms: + self.get_subgraph_size(neighbour, masked_atoms) + return len(masked_atoms) - 1 + + + ## rings calculated + # @TODO: Дать нормальное название + def calculate_rings(self, previous_atom: 'AtomProperties', \ + atom: 'AtomProperties') -> None: + """ + Calculates the positions for atoms within rings and aromatic systems, treating the + calculation as if it's performed from within the ring itself. + + This method is designed to determine the coordinates of atoms that are part of ring + structures or aromatic systems within a molecular graph. It operates under the + assumption that the calculation is being performed from the perspective of being inside + the ring, allowing for accurate positioning of atoms based on their connectivity and the + geometry of the ring. The method takes into account whether the previous atom is part of + a bridged ring and adjusts the position of the current atom accordingly, ensuring that + the ring's integrity and aromaticity are preserved in the molecular representation. It + involves identifying a 'joined vertex' if the previous atom is part of multiple rings, + adjusting the position based on the relative positions of neighbours, and setting the + atom's position to maintain the ring's structure. + + Parameters: + :param previous_atom AtomProperties: + The atom preceding the current atom in the ring, used as a reference for + calculating the current atom's position. + :param atom AtomProperties: + The atom for which the position is being calculated, ensuring it fits correctly + within the ring structure. + """ + neighbours: List['AtomProperties'] = previous_atom.neighbours + joined_vertex: Optional['AtomProperties'] = None + position: Vector = Vector(0, 0) + if previous_atom.bridged_ring is None and len(previous_atom.rings) > 1: + for neighbour in neighbours: + if len(set(neighbour.rings).intersection(set(previous_atom.rings))) == len(previous_atom.rings): + joined_vertex: 'AtomProperties' = neighbour + break + + + if not joined_vertex: + for neighbour in neighbours: + if neighbour.positioned and self.atoms_are_in_same_ring(neighbour, previous_atom): + position.add(Vector.subtract_vectors(neighbour.position, previous_atom.position)) + position.invert() + position.normalise() + position.multiply_by_scalar(self.bond_length) + position.add(previous_atom.position) + else: + position = joined_vertex.position.copy() + position.rotate_around_vector(math.pi, previous_atom.position) + atom.set_previous_position(previous_atom) + atom.set_position(position) + atom.positioned = True + + + # @TODO: Дать нормальное название + def calculate_some_rings(self, atom: 'AtomProperties') -> None: + """ + Calculates the coordinates for an atom connected to a ring, handling both bridged and + non-bridged ring scenarios. + + This method is responsible for determining the coordinates of an atom that is part of a + ring structure within a molecule. It distinguishes between atoms connected to bridged + rings and those that are part of regular rings, adjusting the calculation accordingly to + ensure accurate positioning within the molecular structure. For atoms connected to + bridged rings, it retrieves the specific ring properties to handle the complexity of + bridged systems, while for atoms in regular rings, it defaults to the first ring in the + atom's ring list. The method then calculates the center position of the ring based on + the atom's previous position and the ring's geometry, ensuring the atom is correctly + placed relative to the ring's center. This involves inverting the vector from the atom's + previous position to its current position, normalizing it, and scaling it according to + the ring's radius to find the new center. Finally, it creates the ring with the + calculated center, ensuring the atom's position is accurately represented within the + ring structure. + + Parameters: + :param atom AtomProperties: + The atom for which the ring coordinates are being calculated. This atom is assumed to be part of a ring structure, either directly or through a bridged connection. + """ + if atom.bridged_ring: + next_ring: 'RingProperties' = self.id_to_ring[atom.bridged_ring] + else: + next_ring: 'RingProperties' = atom.rings[0] + + if not next_ring.positioned: + next_center = Vector.subtract_vectors(atom.previous_position, atom.position) + next_center.invert() + next_center.normalise() + radius: float = Polygon.find_polygon_radius(self.bond_length, len(next_ring.members)) + next_center.multiply_by_scalar(radius) + next_center.add(atom.position) + self.create_ring(next_ring, next_center, atom) + + + + def create_ring(self, ring: 'RingProperties', center: Optional[Vector] = None, + start_atom: Optional['AtomProperties'] = None, + previous_atom: Optional['AtomProperties'] = None) -> None: + """ + Creates a ring within a molecular structure, considering its geometry and interaction + with other rings. + + This method is responsible for creating and positioning atoms in ring structures of a + molecule. It takes into account whether the ring is bridged, determines its center, and + arranges atoms according to the geometry of the ring, as well as handles interactions + with other rings through common vertices. For bridged rings, a special algorithm is used + to correctly position atoms relative to the ring center. For non-bridged rings, atoms + are positioned based on a given angle and radius calculated from the bond length and + number of members in the ring. The method also handles cases where ring atoms intersect + with other rings, ensuring correct connections and preventing overlaps. + + Parameters: + :param ring RingProperties: + Properties of the ring for which atom coordinates are being created. + :param center Optional[Vector]: + Center of the ring. If not specified, the origin (0, 0) is used. + :param start_atom Optional[AtomProperties]: + Starting atom for calculating positions of other atoms in the ring. + :param previous_atom Optional[AtomProperties]: + Previous atom used to determine the starting angle. + """ + if ring.positioned: + return + + if center is None: + center = Vector(0, 0) + ordered_neighbour_ids: List[int] = self.get_ordered_neighbours(ring, self.ring_overlaps) + starting_angle: float = 0 + if start_atom: + starting_angle = Vector.subtract_vectors(start_atom.position, center).angle() + ring_size: int = len(ring.members) + radius: float = Polygon.find_polygon_radius(self.bond_length, ring_size) + angle: float = Polygon.get_central_angle(ring_size) + ring.central_angle = angle + if start_atom not in ring.members: + if start_atom: + start_atom.positioned = False + start_atom = ring.members[0] + + + if ring.bridged: + KKLayout(structure=self, + atoms=ring.members, + center=center, + start_atom=start_atom, + bond_length=self.bond_length) + + + ring.positioned = True + self.set_ring_center(ring) + center = ring.center + for subring in ring.subrings: + self.set_ring_center(subring) + else: + self.set_member_positions(ring, start_atom, previous_atom, center, starting_angle, radius, angle) + ring.positioned = True + ring.center = center + + for neighbour_id in ordered_neighbour_ids: + neighbour: 'RingProperties' = self.id_to_ring[neighbour_id] + if neighbour.positioned: + continue + atoms: Optional[List['AtomProperties']] = self.get_vertices(self.ring_overlaps, ring.id, neighbour.id) + if len(atoms) == 2: + self.handle_fused_rings(ring, neighbour, atoms, center) + elif len(atoms) == 1: + self.handle_spiro_rings(ring, neighbour, atoms[0], center) + + for atom in ring.members: + for neighbour in atom.neighbours: + if neighbour.positioned: + continue + atom.connected_to_ring = True + self.create_next_bond(neighbour, atom, 0.0) + + + + def handle_fused_rings(self, ring: 'RingProperties', neighbour: 'RingProperties', + atoms: List['AtomProperties'], center: Vector) -> None: + """ + Handles the processing of fused cyclic systems within molecular structures, such as + decalin ('C12CCCCC1CCCC2'). + + This method addresses the specific challenges of dealing with fused ring systems in + molecular structures, where two rings share common atoms, creating complex cyclic + compounds like decalin. It marks both rings involved as fused, calculates the midpoint + between two shared atoms, determines the normals at this midpoint, and adjusts these + normals based on the apothem of the neighboring ring to find potential centers for the + next ring positions. By comparing distances from a given center to these adjusted + normals, it selects the most suitable center for creating a new ring configuration that + accommodates the fused nature of the system. Depending on the orientation of the shared + atoms, it then creates a new ring with the selected center, ensuring the integrity of + the molecular structure is maintained during the fusion process. + + Parameters: + :param ring RingProperties: + One of the rings involved in the fusion. + :param neighbour RingProperties: + The neighboring ring involved in the fusion. + :param atoms List[AtomProperties]: + A list of atoms shared between the fused rings, typically two atoms common to both + rings. + :param center Vector: + The center point around which the fusion is considered, influencing the orientation + of the newly formed ring structure. + """ + ring.fused = True + neighbour.fused = True + atom_1: 'AtomProperties' = atoms[0] + atom_2: 'AtomProperties' = atoms[1] + midpoint: 'Vector' = Vector.get_midpoint(atom_1.position, atom_2.position) + normals: List['Vector'] = Vector.get_normals(atom_1.position, atom_2.position) + normals[0].normalise() + normals[1].normalise() + + apothem: float = Polygon.get_apothem_from_side_length(self.bond_length, len(neighbour.members)) + normals[0].multiply_by_scalar(apothem) + normals[1].multiply_by_scalar(apothem) + normals[0].add(midpoint) + normals[1].add(midpoint) + next_center: 'Vector' = 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() + if distance_to_center_2 > distance_to_center_1: + next_center = normals[1] + position_1: 'Vector' = Vector.subtract_vectors(atom_1.position, next_center) + position_2: 'Vector' = Vector.subtract_vectors(atom_2.position, next_center) + if position_1.get_clockwise_orientation(position_2) == 'clockwise': + if not neighbour.positioned: + self.create_ring(neighbour, next_center, atom_1, atom_2) + else: + if not neighbour.positioned: + self.create_ring(neighbour, next_center, atom_2, atom_1) + + + + def handle_spiro_rings(self, ring: 'RingProperties', neighbour: 'RingProperties', + atom: 'AtomProperties', center: Vector) -> None: + """ + Handles spirocyclic systems within molecular structures, such as 'C1CCCC11CC1'. + + Spirocyclic systems are characterized by two rings that share a single common atom, + forming a spiro junction. This method marks both rings as spirocyclic, calculates a new + center for the neighboring ring based on the position of the shared atom and the center + of the current ring, and adjusts the position of the neighboring ring accordingly to + maintain the integrity of the spirocyclic structure. + + Parameters: + :param ring RingProperties: + The current ring involved in the spirocyclic system. + :param neighbour RingProperties: + The neighboring ring involved in the spirocyclic system. + :param atom AtomProperties: + The atom shared by both rings at the spiro junction. + :param center Vector: + The center of the current ring, used as a reference for calculating the new center + for the neighboring ring. + """ + ring.spiro = True + neighbour.spiro = True + next_center: 'Vector' = Vector.subtract_vectors(center, atom.position) + next_center.invert() + next_center.normalise() + distance_to_center: float = Polygon.find_polygon_radius(self.bond_length, len(neighbour.members)) + next_center.multiply_by_scalar(distance_to_center) + next_center.add(atom.position) + if not neighbour.positioned: + self.create_ring(neighbour, next_center, atom) + + + ##auxiliary functions for rings calculated + def set_ring_center(self, ring: 'RingProperties') -> None: + """ + Calculates and sets the geometric center of a ring within a molecular structure. + + This method computes the center of a ring by averaging the positions of all atoms that + are members of the ring. It iterates through each atom in the ring, summing their + positions vectorially, and then divides the total by the number of atoms to find the + average position, which represents the ring's center. This center is then assigned to + the ring's `center` attribute, providing a reference point for further calculations and + manipulations involving the ring. + + Parameters: + :param ring RingProperties: + The ring for which the center is to be calculated. This object represents a cyclic + structure within the molecule, containing a list of atoms that are part of the ring. + """ + total: Vector = Vector(0, 0) + for atom in ring.members: + total.add(atom.position) + total.divide(len(ring.members)) + ring.center = total + + + + def atoms_are_in_same_ring(self, atom_1: 'AtomProperties', atom_2: 'AtomProperties') -> bool: + """ + Determines if two atoms are part of the same ring within a molecular structure. + + This method checks if two given atoms are part of the same ring by comparing their ring + memberships. It iterates through the rings of the first atom and checks if any of these + rings are also present in the list of rings for the second atom. If any ring is common + between the two atoms, it indicates that they are part of the same ring structure. + + Parameters: + :param atom_1 AtomProperties: + The first atom to check for ring membership. + :param atom_2 AtomProperties: + The second atom to check for ring membership. + + Returns bool: + True if the atoms are in the same ring, False otherwise. + """ + return any(ring_id_1 == ring_id_2 for ring_id_1 in atom_1.rings \ + for ring_id_2 in atom_2.rings) + + + + def get_vertices(self, ring_overlaps: List['RingOverlap'], ring_id_1: int, \ + ring_id_2: int) -> Optional[List['AtomProperties']]: + """ + Searches for and returns atoms that are in the overlap between two rings identified by ring_id_1 and ring_id_2. + + This method looks for atoms that are located in the overlap between two specified rings. If an overlap is found, it returns a list of atoms that are part of this overlap. If no overlap is found, the method concludes without returning any value, implying a default return of None. + + Parameters: + :param ring_overlaps List[RingOverlap]: + A list of RingOverlap objects representing overlaps between rings in the molecule. + :param ring_id_1 int: + The identifier of the first ring to check for overlaps. + :param ring_id_2 int: + The identifier of the second ring to check for overlaps. + + Returns Optional[List[AtomProperties]: + A list of AtomProperties objects representing atoms in the overlap between the two + rings, or None if no overlap is found. + """ + for ring_overlap in ring_overlaps: + if set((ring_overlap.ring_id_1, ring_overlap.ring_id_2)) == set((ring_id_1, ring_id_2)): + return [atom for atom in ring_overlap.atoms] + + + + def get_ordered_neighbours(self, ring: 'RingProperties', \ + ring_overlaps: List['RingOverlap']) -> List[int]: + """ + Retrieves an ordered list of neighboring rings based on the number of atoms they share in common with the specified ring. + + This method is designed to obtain an ordered list of neighboring rings (or structures) + based on the number of atoms they have in intersection with the given ring. It returns a + list of identifiers for the neighboring rings, sorted by the number of shared atoms in + descending order, allowing for the identification of the most closely related rings in + terms of atomic overlap. + + Parameters: + :param ring RingProperties: + The ring for which neighboring rings are to be identified and ordered. + :param ring_overlaps List[RingOverlap]: + A list of RingOverlap objects representing overlaps between rings in the molecule, + used to determine the intersection of atoms between rings. + + Returns List[int]: + A list of identifiers for neighboring rings, ordered by the number of shared atoms + in descending order. Rings with more shared atoms are listed first. + """ + ordered_neighbours_and_atom_nrs = [] + for neighbour_id in ring.neighbouring_rings: + atoms: Optional[List['AtomProperties']] = self.get_vertices(ring_overlaps, ring.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_neighbour_ids = [x[1] for x in ordered_neighbours_and_atom_nrs] + return ordered_neighbour_ids + + + + def set_member_positions(self, ring: 'RingProperties', start_atom: 'AtomProperties', \ + previous_atom: Optional['AtomProperties'], center: 'Vector', \ + starting_angle: float, radius: float, angle: float) -> None: + """ + Positions atoms within a ring structure using polar coordinates (center, radius, angle) + and incrementally increases the angle between atoms. + + The primary goal of this method is to arrange atoms in a ring structure by utilizing + polar coordinates, where each atom's position is determined relative to a central point, + at a specified radius, and with a progressively increasing angle to ensure even + distribution around the center. This method iterates through the atoms of the ring, + setting their positions based on these polar coordinates until all atoms are positioned + or a maximum iteration limit is reached. + + Parameters: + :param ring RingProperties: + The ring whose member atoms are to be positioned. + :param start_atom AtomProperties: + The starting atom for positioning within the ring. + :param previous_atom Optional[AtomProperties]: + The atom preceding the current atom in the positioning sequence. Used as a reference + for the first atom's position if it hasn't been positioned yet. + :param center Vector: + The central point around which atoms are positioned. + :param starting_angle float: + The initial angle in radians for the first atom's position relative to the center. + :param radius float: + The distance from the center to the atom's position. + :param angle float: + The angular increment in radians between consecutive atoms in the ring. + """ + current_atom = start_atom + iteration = 0 + while current_atom != None and iteration < 100: + previous = current_atom + if not previous.positioned: + x = center.x + math.cos(starting_angle) * radius + y = center.y + math.sin(starting_angle) * radius + previous.set_position(Vector(x, y)) + starting_angle += angle + + if len(ring.subrings) < 3: + previous.angle = starting_angle + previous.positioned = True + + current_atom = self.get_next_in_ring(ring, current_atom, previous_atom) + previous_atom = previous + + if current_atom == start_atom: + current_atom = None + iteration += 1 + + + + def get_next_in_ring(self, ring: 'RingProperties', current_atom: 'AtomProperties', \ + previous_atom: 'AtomProperties') -> Optional['AtomProperties']: + """ + Searches for the next atom in the ring, excluding the previous atom. + + This method iterates through the neighbors of the current atom to find the next atom + that is a member of the specified ring, excluding the atom that was previously + considered. It checks each neighbor to see if it belongs to the ring and is not the same + as the previous atom. If a suitable atom is found, it is returned; otherwise, None is + returned. This is useful for traversing the atoms in a ring structure while ensuring + that the traversal does not immediately return to the previous atom, allowing for a + continuous loop through the ring's members. + + Parameters: + :param ring RingProperties: + The ring within which to search for the next atom. + :param current_atom AtomProperties: + The current atom from which to start the search. + :param previous_atom AtomProperties: + The atom to exclude from the search, typically the atom preceding the current atom + in the traversal. + + Returns Optional[AtomProperties]: + The next atom in the ring that is different from the previous atom, or None if no + such atom is found. + """ + neighbours: List[int] = current_atom.neighbours + for neighbour in neighbours: + for member in ring.members: + if neighbour == member: + if previous_atom != neighbour: + return neighbour + + + @staticmethod + def find_neighbouring_rings(ring_overlaps: List['RingOverlap'], ring_id: int) -> List[int]: + """ + Finds and returns a list of identifiers for rings neighboring the specified ring. + + This method searches through a list of ring overlaps to identify rings that are adjacent + to a given ring, identified by its ID. It examines each overlap to determine if the + specified ring is involved and collects the identifiers of neighboring rings, excluding + the specified ring itself. This is useful for understanding the connectivity and + structure of rings within a molecular graph, especially in complex molecules where rings + may overlap or share atoms. + + Parameters: + :param ring_overlaps List[RingOverlap]: + A list of RingOverlap objects, each representing an overlap between two rings in the + molecule. These objects contain information about which rings are involved in each + overlap. + :param ring_id int: + The identifier of the ring for which neighboring rings are to be found. + + Returns List[int]: + A list of identifiers for rings that are neighbors to the specified ring, based on + the overlaps. Each identifier represents a ring that shares at least one atom with + the specified ring, indicating a direct connection or overlap. + """ + neighbouring_rings = [] + for ring_overlap in ring_overlaps: + if ring_overlap.ring_id_1 == ring_id: + neighbouring_rings.append(ring_overlap.ring_id_2) + elif ring_overlap.ring_id_2 == ring_id: + neighbouring_rings.append(ring_overlap.ring_id_1) + return neighbouring_rings + + + + def get_current_centre_of_mass(self) -> Vector: + """ + Calculates and returns the current center of mass of the molecular graph. + + This method computes the center of mass of the molecular graph by summing the positions + of all positioned atoms and dividing by the number of positioned atoms. It iterates + through each atom in the graph, adding the position of each atom to a total if the atom + is positioned, and then divides this total by the count of positioned atoms to find the + average position, which represents the center of mass. This center of mass can be used + as a reference point for various calculations and manipulations within the molecular + structure, such as aligning or repositioning atoms relative to the overall structure. + + Returns Vector: + A Vector object representing the coordinates of the center of mass of the molecular + graph, based on the positions of all positioned atoms. + """ + total = Vector(0, 0) + count = 0 + for atom in self.graph: + if atom.positioned: + total.add(atom.position) + count += 1 + total.divide(count) + return total + + + + def get_last_atom_with_angle(self, atom: 'AtomProperties') -> Optional['AtomProperties']: + """ + Retrieves the last atom in a chain that has a defined angle relative to the initial atom. + + This method traverses backwards from a given atom through its predecessors until it + finds an atom with a defined angle or reaches an atom without a predecessor. It starts + with the initial atom's immediate predecessor and continues to trace back through the + chain of previous atoms until it encounters an atom with a non-zero angle or reaches the + beginning of the chain. + + Parameters: + :param atom AtomProperties: + The starting atom from which to begin the search for an atom with a defined angle. + + Returns Optional[AtomProperties]: + The last atom in the chain that has a defined angle, or None if no such atom is + found before reaching the start of the chain. + """ + parent_atom: Optional['AtomProperties'] = atom.previous_atom + angle: float = parent_atom.angle + while parent_atom and not angle: + parent_atom = parent_atom.previous_atom + angle = parent_atom.angle + return parent_atom + + + # упаковка и возвращение координат + def get_coord(self, order: List[int]) -> List[List[float]]: + """ + Packs and returns the coordinates of atoms in a two-dimensional array based on the + specified order. + + Parameters: + :param order List[int]: + A list of integers representing the order in which atom coordinates should be + packed. Each integer corresponds to an atom index in the class's atom dictionary. + + Returns List[List[float]]: + A two-dimensional list where each inner list contains the x and y coordinates of an + atom, following the order specified in the input list. + """ + xy: List[List[float]] = [] + for ord in order: + vector = self.atoms[ord].position + xy.append([vector.x, vector.y]) + return xy + + + # обработка коллизий + def collision_handling(self) -> None: + """ + Handles the positioning of all atoms and resolves any overlaps within the molecular structure. + + This method is responsible for adjusting the positions of atoms to minimize overlaps and + ensure a visually coherent representation of the molecule. It begins by resolving + primary overlaps through a preliminary adjustment phase, followed by an iterative + process to refine the positions of atoms based on their bonds and connectivity. The + process involves calculating the total overlap score, identifying atoms that can be + rotated around their bonds, and adjusting their positions to reduce overlaps. It also + considers the complexity of the subgraphs connected to each atom, preferring to rotate + smaller subgraphs to minimize disruption. Special handling is given to atoms connected + by double bonds, which are less flexible. The method iteratively adjusts the positions + of atoms based on their overlap scores, sensitivity thresholds, and the ability to + rotate around bonds, aiming to find a configuration that minimizes the total overlap + score. Additionally, it accounts for atoms within rings and their specific constraints, + applying rotations to subtrees of atoms to resolve overlaps while maintaining the + integrity of the molecular structure. The process is repeated for a set number of + iterations to gradually improve the layout, with an option for fine-tuning overlaps if + enabled. + + The collision handling process includes: + - Resolving primary overlaps to ensure atoms do not occupy the same space. + - Calculating the total overlap score and identifying atoms that can be rotated to + reduce overlaps. + - Adjusting atom positions based on their connectivity and the presence of double bonds, + which restrict rotation. + - Considering the depth of subgraphs connected to each atom to decide which atom to + rotate in cases of overlap. + - Applying rotations to subtrees to resolve overlaps, with specific logic for atoms + connected by single or double bonds. + - Repeatedly recalculating overlap scores and adjusting positions until a satisfactory + layout is achieved or the maximum number of iterations is reached. + - Optionally, fine-tuning the positions for further refinement if the `finetune` flag is + set. + """ + self.resolve_primary_overlaps() + + self.total_overlap_score, sorted_overlap_scores, atom_to_scores = self.get_overlap_score() + for i in range(self.overlap_resolution_iterations): + for (atom1_index, atom2_index), bond in self.bonds.items(): + n: 'AtomProperties' = self.atoms[atom1_index] + m: 'AtomProperties' = self.atoms[atom2_index] + if self.can_rotate_around_bond(bond): + tree_depth_1: int = self.get_subgraph_size(n, {m}) + tree_depth_2: int = self.get_subgraph_size(m, {n}) + atom_1_rotatable: bool = True + atom_2_rotatable: bool = True + for neighbouring_bond in self.get_bonds_of_atom(n): + if neighbouring_bond.type == 'double': + atom_1_rotatable = False + for neighbouring_bond in self.get_bonds_of_atom(m): + if neighbouring_bond.type == 'double': + atom_2_rotatable = False + if not atom_1_rotatable and not atom_2_rotatable: + continue + elif atom_1_rotatable and not atom_2_rotatable: + atom_2: 'AtomProperties' = n + atom_1: 'AtomProperties' = m + elif atom_2_rotatable and not atom_1_rotatable: + atom_1: 'AtomProperties' = n + atom_2: 'AtomProperties' = m + else: + atom_1: 'AtomProperties' = m + atom_2: 'AtomProperties' = n + if tree_depth_1 > tree_depth_2: + atom_1: 'AtomProperties' = n + atom_2: 'AtomProperties' = m + subtree_overlap_score, _ = self.get_subtree_overlap_score(atom_2, atom_1, atom_to_scores) + if subtree_overlap_score > self.overlap_sensitivity: + + neighbours_2 = atom_2.neighbours[:] + neighbours_2.remove(atom_1) + + if len(neighbours_2) == 1: + neighbour = neighbours_2[0] + angle = neighbour.position.get_rotation_away_from_vector(atom_1.position, atom_2.position, math.radians(120)) + self.rotate_subtree(neighbour, atom_2, angle, atom_2.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.position) + else: + self.total_overlap_score = new_overlap_score + elif len(neighbours_2) == 2: + if atom_2.rings and atom_1.rings: + continue + neighbour_1: 'AtomProperties' = neighbours_2[0] + neighbour_2: 'AtomProperties' = neighbours_2[1] + if len(neighbour_1.rings) == 1 and len(neighbour_2.rings) == 1: + if neighbour_1.rings[0] != neighbour_2.rings[0]: + continue + elif neighbour_1.rings or neighbour_2.rings: + continue + else: + angle_1 = neighbour_1.position.get_rotation_away_from_vector(atom_1.position, atom_2.position, math.radians(120)) + angle_2 = neighbour_2.position.get_rotation_away_from_vector(atom_1.position, atom_2.position, math.radians(120)) + self.rotate_subtree(neighbour_1, atom_2, angle_1, atom_2.position) + self.rotate_subtree(neighbour_2, atom_2, angle_2, atom_2.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.position) + self.rotate_subtree(neighbour_2, atom_2, -angle_2, atom_2.position) + else: + self.total_overlap_score = new_overlap_score + self.total_overlap_score, sorted_overlap_scores, atom_to_scores = self.get_overlap_score() + for _ in range(self.overlap_resolution_iterations): + self._finetune_overlap_resolution() + self.total_overlap_score, sorted_overlap_scores, atom_to_scores = self.get_overlap_score() + for i in range(self.overlap_resolution_iterations): + self.resolve_secondary_overlaps(sorted_overlap_scores) + + + ## вспомогательные функции для collision_handling + def resolve_primary_overlaps(self) -> None: + """ + Resolves initial overlaps in the molecular structure, focusing on cases where a ring has + two outgoing edges. + + This method addresses the issue of overlaps that occur when a ring within the molecular + structure has two edges extending outwards, which can lead to collisions in the layout, + especially noticeable in representations of cyclohexane in a quarter-staggered + conformation. It identifies atoms that are part of such overlaps by examining each ring + and its members, and then resolves these overlaps by adjusting the positions of the + involved atoms. The resolution process involves calculating the angle of rotation needed + to minimize the overlap and applying this rotation to the subtrees connected to the + overlapping atoms. + """ + overlaps: List = [] + resolved_atoms: Dict[int, bool] = {atom.id: False for atom in self.graph} + + for ring in self.rings: + for atom in ring.members: + if resolved_atoms[atom.id]: + continue + resolved_atoms[atom.id] = True + non_ring_neighbours: List['AtomProperties'] = self.get_non_ring_neighbours(atom) + if len(non_ring_neighbours) > 1 or (len(non_ring_neighbours) == 1 and len(atom.rings) == 2): + overlaps.append({'common': atom, 'rings': atom.rings, 'vertices': non_ring_neighbours}) + for overlap in overlaps: + branches_to_adjust: List['AtomProperties'] = overlap['vertices'] + rings: List['RingProperties'] = overlap['rings'] + root: 'AtomProperties' = overlap['common'] + if len(branches_to_adjust) == 2: + atom_1, atom_2 = branches_to_adjust + angle = (2 * math.pi - rings[0].get_angle()) / 6.0 + self.rotate_subtree(atom_1, root, angle, root.position) + self.rotate_subtree(atom_2, root, -angle, root.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) + total_score = subtree_overlap_atom_1_1 + subtree_overlap_atom_2_1 + self.rotate_subtree(atom_1, root, -2.0 * angle, root.position) + self.rotate_subtree(atom_2, root, 2.0 * angle, root.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) + total_score_2 = subtree_overlap_atom_1_2 + subtree_overlap_atom_2_2 + if total_score_2 > total_score: + self.rotate_subtree(atom_1, root, 2.0 * angle, root.position) + self.rotate_subtree(atom_2, root, -2.0 * angle, root.position) + + ## вспомогательные функции для resolve_primary_overlaps + @staticmethod + def get_non_ring_neighbours(atom: 'AtomProperties') -> List['AtomProperties']: + """ + Identifies and returns a list of neighbours of the specified atom that are not part of + any ring it belongs to. + + This method examines the neighbours of a given atom and filters out those that are part + of the same rings as the atom, focusing on neighbours that are not involved in any ring + structure with the atom. It is useful for understanding the connectivity of an atom + within the molecular graph, especially in contexts where the atom's interactions outside + of cyclic structures are of interest. By comparing the ring memberships of the atom and + its neighbours, it identifies neighbours that do not share any rings with the atom and + are not considered bridge atoms, providing insight into the atom's connections to other + parts of the molecule that are not part of its immediate cyclic environment. + + Parameters: + :param atom AtomProperties: + The atom for which non-ring neighbours are to be identified. + + Returns: List[AtomProperties]: + A list of AtomProperties objects representing neighbours of the specified atom that + are not part of any ring the atom is a member of, excluding bridge atoms. + """ + non_ring_neighbours: List['AtomProperties'] = [] + for neighbour in atom.neighbours: + nr_overlapping_rings = len(set(atom.ring_indexes).intersection(set(neighbour.ring_indexes))) + if nr_overlapping_rings == 0 and not neighbour.is_bridge: + non_ring_neighbours.append(neighbour) + return non_ring_neighbours + + ## вспомогательные функции для resolve_primary_overlaps + def rotate_subtree(self, root: 'AtomProperties', root_parent: 'AtomProperties', \ + angle: float, center: 'Vector') -> None: + """ + Rotates a subtree of the molecular structure around a specified center by a given angle. + + This method rotates a subtree within the molecular graph, starting from a root atom, + around a specified center point by a given angle. It is used to adjust the positions of + atoms and their associated structures, such as anchored rings, to resolve overlaps or to + achieve a desired orientation. The rotation is applied to the root atom and all atoms + connected to it within the subtree, excluding the root's parent to maintain the + integrity of the molecular structure. This is particularly useful in the layout process + to minimize overlaps or to align parts of the molecule according to specific + requirements. The rotation affects not only the positions of atoms in the subtree but + also updates the centers of any anchored rings associated with the atoms, ensuring that + the entire subtree is cohesively repositioned. + + Parameters: + :param root AtomProperties: + The root atom of the subtree to be rotated. This atom serves as the starting point + for the rotation. + :param root_parent AtomProperties: + The parent atom of the root, which is excluded from the rotation to maintain the + connection to the rest of the molecular structure. + :param angle float: + The angle in radians by which the subtree will be rotated around the center. + :param center Vector: + The point around which the rotation is performed. This center is typically the + position of a pivotal atom or a calculated point that serves as the axis of rotation. + """ + for atom in self.traverse_substructure(root, {root_parent}): + atom.position.rotate_around_vector(angle, center) + for anchored_ring in atom.anchored_rings: + if anchored_ring.center: + anchored_ring.center.rotate_around_vector(angle, center) + + + ## вспомогательные функции для rotate_subtree + def traverse_substructure(self, atom: 'AtomProperties', visited: Set['AtomProperties']) \ + -> Generator['AtomProperties', None, None]: + """ + Traverses a substructure of the molecular graph starting from a given atom, yielding + atoms in a depth-first manner. + + This method performs a depth-first traversal of the molecular graph, starting from a + specified atom, and yields atoms that are reachable from it, excluding those already + visited to avoid cycles. It is a generator function that explores the molecular + structure by recursively visiting each atom's neighbours, ensuring that each atom is + visited only once. + + Parameters: + :param atom AtomProperties: + The starting atom for the traversal. The traversal begins from this atom and + explores the connected substructure. + :param visited Set[AtomProperties]: + A set of atoms that have already been visited during the traversal. This is used to + avoid revisiting atoms and to ensure that each atom is processed only once. + + Yields AtomProperties: + Atoms in the connected substructure of the starting atom, yielded one at a time. + This allows for iterative processing or analysis of the substructure without the + need to construct and return a complete list of atoms upfront, which can be + beneficial for large molecular graphs. + """ + yield atom + visited.add(atom) + for neighbour in atom.neighbours: + if neighbour not in visited: + yield from self.traverse_substructure(neighbour, visited) + + + ## вспомогательные функции для resolve_primary_overlaps + def get_overlap_score(self) -> Tuple[float, List[Tuple[float, 'AtomProperties']], Dict[int, float]]: + """ + Calculates the total overlap score and returns a sorted list of atoms by their overlap + scores along with the score dictionary. + + This method computes the total overlap score for the molecular graph represented by the + class and returns a tuple containing the total overlap score, a list of atoms sorted by + their overlap scores in descending order, and a dictionary mapping atom IDs to their + individual overlap scores. The overlap score is a measure of how much atoms overlap with + each other, indicating the compactness or congestion within the molecular structure. It + is calculated based on the distances between atoms, with closer atoms contributing more + significantly to the score. The method iterates through all pairs of atoms, calculates + the overlap score for each pair based on their distance, and aggregates these scores to + determine the total overlap and individual atom overlap scores. The atoms are then + sorted by their scores to identify those with the highest overlap, which can be critical + for resolving spatial conflicts in the molecular layout. + + Returns Tuple[float, List[Tuple[float, 'AtomProperties'], Dict[int, float]]: + A tuple containing: + - The total overlap score for the molecular graph, representing the overall + compactness or congestion. + - A list of tuples, each containing an atom's overlap score and the atom itself, + sorted by the score in descending order. This list helps identify atoms that are + most affected by overlaps and may require adjustment. + - A dictionary mapping atom IDs to their individual overlap scores, providing + detailed insights into the distribution of overlaps across the structure. + """ + total: float = 0.0 + overlap_scores : Dict[int, float]= {} + for atom in self.graph: + overlap_scores[atom.id] = 0.0 + + atoms: List['AtomProperties'] = list(self.graph.keys()) + for i, atom_1 in enumerate(atoms): + for j, atom_2 in enumerate(atoms[i+1:], start=i+1): + distance: float = Vector.subtract_vectors(atom_1.position, atom_2.position).get_squared_length() + if distance < (self.bond_length ** 2): + weight = (self.bond_length - math.sqrt(distance)) / self.bond_length + total += weight + overlap_scores[atom_1.id] += weight + overlap_scores[atom_2.id] += weight + sorted_overlaps: List[Tuple[float, 'AtomProperties']] = [] + for atom in atoms: + sorted_overlaps.append((overlap_scores[atom.id], atom)) + sorted_overlaps.sort(key=lambda x: x[0], reverse=True) + return total, sorted_overlaps, overlap_scores + + + ## вспомогательные функции для resolve_primary_overlaps + def get_subtree_overlap_score(self, root: 'AtomProperties', root_parent: 'AtomProperties', + atom_to_score: Dict[int, float]) -> Tuple[float, Vector]: + """ + Calculates the weighted center and total overlap score for a subtree rooted at a given + atom, excluding its parent. + + This method computes the total overlap score and the weighted center position for a + subtree within the molecular graph, starting from a specified root atom and excluding + its parent. The subtree's overlap score is a measure of how much the atoms within the + subtree overlap with others, indicating the compactness or congestion of the subtree's + layout. The weighted center is calculated based on the positions of atoms that + contribute significantly to the overlap, providing a central point that can be used for + adjusting the subtree's position to reduce overlaps. The method iterates through the + subtree, accumulating the overlap scores of atoms that exceed a sensitivity threshold + and adjusting their positions relative to this score to find a central point of gravity. + This central point, along with the total score, can guide the repositioning of the + subtree to minimize spatial conflicts within the molecular structure. + + Parameters: + :param root AtomProperties: + The root atom of the subtree for which the overlap score and weighted center are + calculated. This atom serves as the starting point for the traversal and score + calculation. + :param root_parent AtomProperties: + The parent atom of the root, which is excluded from the subtree to maintain the + integrity of the molecular graph's structure during calculations. + :param atom_to_score Dict[int, float]: + A dictionary mapping atom IDs to their individual overlap scores, used to determine + the contribution of each atom in the subtree to the total overlap score. + + Returns Tuple[float, Vector]: A tuple containing: + - The average overlap score for the subtree, calculated as the sum of individual + atom scores divided by the number of contributing atoms. This score indicates the + subtree's overall compactness or the extent of overlap among its atoms. + - The weighted center position (Vector) of the subtree, derived from the positions + of atoms with significant overlap scores. This center is calculated by summing the + positions of contributing atoms, each weighted by its overlap score, and then + dividing by the total score to find a central point for potential repositioning. + """ + score = 0.0 + center = Vector(0, 0) + count = 0 + for atom in self.traverse_substructure(root, {root_parent}): + subscore = atom_to_score[atom.id] + if subscore > self.overlap_sensitivity: + score += subscore + count += 1 + position = atom.position.copy() + position.multiply_by_scalar(subscore) + center.add(position) + if score: + center.divide(score) + if count == 0: + count = 1 + return score / count, center + + + @staticmethod + def can_rotate_around_bond(bond: 'BondProperties') -> bool: + """ + Determines whether a bond can be rotated to adjust the molecular structure without + breaking its integrity. + + This method evaluates whether a given bond within a molecular structure can be rotated + as part of layout adjustments, such as resolving overlaps or optimizing the spatial + arrangement of atoms. Rotation around a bond is a common operation in molecular graph + manipulation, but it's constrained by several factors to maintain the molecule's + structural integrity. The method checks the type of the bond, the number of neighbours + each atom involved in the bond has, and their involvement in ring structures. + Specifically, it assesses whether the bond is a single bond (allowing for rotation), + whether either atom has only one neighbour (which would prevent meaningful rotation), + and whether both atoms are part of the same ring (which could disrupt the ring's + geometry if rotated). + + Parameters: + :param bond BondProperties: + The bond to evaluate for potential rotation. This object contains information about + the bond type and the atoms it connects. + + Returns bool: + True if the bond can be safely rotated, indicating that it is a single bond not + involving atoms that are solely connected through this bond or are part of the same + ring, thereby allowing for adjustments that maintain the molecule's integrity. False + otherwise, indicating constraints that prevent rotation to avoid disrupting the + molecular structure. + """ + if bond.type != 'single': + return False + if len(bond.atom1.neighbours) == 1 or len(bond.atom2.neighbours) == 1: + return False + if bond.atom1.rings and bond.atom2.rings and len(set(bond.atom1.rings).intersection(set(bond.atom2.rings))) > 0: + return False + return True + + + def _finetune_overlap_resolution(self) -> None: + """ + Fine-tunes the resolution of overlaps between atoms in the molecular structure by + iteratively adjusting the positions of atoms to minimize the total overlap score. + + This method is designed to refine the positioning of atoms within the molecular + structure to reduce overlaps, focusing on atoms that are too close to each other. It + operates by identifying pairs of clashing atoms, determining the shortest path between + them, and then attempting to rotate the atoms around their bonds to find a configuration + that minimizes the overlap. The process involves several steps: + + 1. Identifies pairs of atoms that are clashing, i.e., too close to each other based on a + predefined sensitivity threshold. + 2. For each pair of clashing atoms, it finds the shortest path connecting them in the + molecular graph. + 3. Along this path, it identifies bonds that are rotatable (excluding double bonds) and + calculates a distance metric for each bond based on its position in the path. + 4. Selects the bond with the smallest distance metric as the best candidate for rotation + to reduce overlap. + 5. Rotates the subtree of atoms around the best bond in increments, evaluating the + overlap score after each rotation to find the optimal rotation angle. + 6. Applies the optimal rotation to minimize the total overlap score. + + The method iteratively adjusts the positions of atoms connected by rotatable bonds to + resolve overlaps, with a preference for rotating smaller subtrees to minimize structural + disruption. It uses a scoring system to evaluate the effectiveness of each rotation and + selects the rotation that results in the lowest overlap score. This process is repeated + for all identified clashing atom pairs until the total overlap score is below a + sensitivity threshold or no further improvement can be made. + """ + if self.total_overlap_score > self.overlap_sensitivity: + clashing_atoms: List[Tuple['AtomProperties', 'AtomProperties']] = self._find_clashing_atoms() + best_bonds: List['BondProperties'] = [] + for atom_1, atom_2 in clashing_atoms: + if self.is_connected(atom_1, atom_2): + shortest_path: List[Union['BondProperties', 'AtomProperties']] = self.find_shortest_path(atom_1, atom_2) + rotatable_bonds: List['BondProperties'] = [] + distances: List[float] = [] + for i, bond in enumerate(shortest_path): + distance_1: int = i + distance_2: int = len(shortest_path) - i + average_distance = len(shortest_path) / 2 + distance_metric = abs(average_distance - distance_1) + abs(average_distance - distance_2) + if self.bond_is_rotatable(bond): # я не дореализовал #fix it + rotatable_bonds.append(bond) + distances.append(distance_metric) + best_bond: Optional['BondProperties'] = None + optimal_distance: float = float('inf') + for i, distance in enumerate(distances): + if distance < optimal_distance: + best_bond: 'BondProperties' = rotatable_bonds[i] + optimal_distance: float = distance + if best_bond is not None: + best_bonds.append(best_bond) + best_bonds = list(set(best_bonds)) + for best_bond in best_bonds: + if self.total_overlap_score > self.overlap_sensitivity: + atom_1, atom_2 = best_bond.atom1, best_bond.atom2 + subtree_size_1: int = self.get_subgraph_size(atom_1, {atom_2}) + subtree_size_2: int = self.get_subgraph_size(atom_2, {atom_1}) + if subtree_size_1 < subtree_size_2: + rotating_atom = atom_1 + parent_atom = atom_2 + else: + rotating_atom = atom_2 + parent_atom = atom_1 + overlap_score, _, _ = self.get_overlap_score() + scores: List[float] = [overlap_score] + # Attempt 12 rotations + for i in range(12): + self.rotate_subtree(rotating_atom, parent_atom, math.radians(30), parent_atom.position) + new_overlap_score, _, _ = self.get_overlap_score() + scores.append(new_overlap_score) + assert len(scores) == 13 + scores = scores[:12] + best_i = 0 + best_score = scores[0] + for i, score in enumerate(scores): + if score < best_score: + best_score = score + best_i = i + self.total_overlap_score = best_score + self.rotate_subtree(rotating_atom, parent_atom, math.radians(30 * best_i + 1), parent_atom.position) + + + + def _find_clashing_atoms(self) -> List[Tuple['AtomProperties', 'AtomProperties']]: + """ + Identifies and returns a list of atom pairs that are clashing, i.e., positioned too + close to each other based on a distance threshold. + + This method scans through all pairs of atoms in the molecular graph to find those that + are closer than a specified distance threshold, indicating a clash or overlap. It + calculates the squared distance between each pair of atoms and compares it against a + threshold value to determine if they are clashing. The threshold is defined as 80% of + the squared bond length, aiming to identify atoms that are significantly closer than + they should be, considering the typical bond length in the molecular structure. + + Returns List[Tuple['AtomProperties', 'AtomProperties']: + A list of tuples, where each tuple contains two 'AtomProperties' objects + representing a pair of atoms that are clashing. Each tuple indicates a pair of atoms + that are positioned too close to each other, based on the distance threshold. + """ + clashing_atoms: List[Tuple['AtomProperties', 'AtomProperties']] = [] + atoms: List['AtomProperties'] = list(self.graph.keys()) + for i, atom_1 in enumerate(atoms): + for j, atom_2 in enumerate(atoms[i+1:], start=i+1): + if self.bond_lookup(atom_1, atom_2) is None: + distance = Vector.subtract_vectors(atom_1.position, atom_2.position).get_squared_length() + if distance < 0.8 * (self.bond_length**2): + clashing_atoms.append((atom_1, atom_2)) + return clashing_atoms + + + + def is_connected(self, atom_1, atom_2) -> bool: + """ + Determines if two atoms are connected within the molecular graph, i.e., part of the same + molecular structure. + + Parameters: + atom_1 AtomProperties: The first atom to check for connectivity. + atom_2 AtomProperties: The second atom to check for connectivity. + + Returns bool: + True if both atoms are present in the molecular graph, indicating they are part of + the same molecular structure, and False otherwise. + """ + return atom_1 in self.graph and atom_2 in self.graph + + + + def bond_is_rotatable(self, bond: 'BondProperties') -> bool: + """ + Determines if a bond can be rotated in the molecular structure drawing, based on its + type and the atoms it connects. + + This method evaluates whether a given bond is rotatable, which is crucial for adjusting the molecular layout to resolve overlaps or achieve a more accurate representation. A bond is considered rotatable if it is not constrained by stereochemical considerations, such as being part of a ring or having a specific type that restricts rotation (e.g., double or triple bonds). The method checks if the bond connects atoms that are part of the same ring, which would prevent rotation, and if the bond type is not a single bond, it further checks the number of neighbors each atom has to determine if rotation is possible. Additionally, it considers chiral centers and specific stereochemical markers that might restrict rotation. The presence of these conditions indicates that the bond is not rotatable, and the method returns False. Otherwise, it returns True, indicating the bond can be rotated to adjust the molecular structure. + + Parameters + :param bond BondProperties: The bond to evaluate for rotatability. + + Returns bool: + True if the bond is rotatable, meaning it can be rotated in the drawing to adjust the molecular structure without violating stereochemical constraints; False if the bond is fixed in place due to being part of a ring, being a non-single bond, or involving chiral centers. + """ + atom_1, atom_2 = bond.atom1, bond.atom2 + if atom_1.rings and atom_2.rings and len(set(atom_1.rings).intersection(set(atom_2.rings))) > 0: + return False + if bond.type != 'single': + if len(atom_1.neighbours) > 1 and len(atom_2.neighbours) > 1: + return False + chiral = False + self.get_bonds_of_atom(atom_1) + # for bond_1 in self.get_bonds_of_atom(atom_1): + # if self.chiral[bond_1]: + # chiral = True + # break + # for bond_2 in self.get_bonds_of_atom(atom_2): + # if self.chiral[bond_2]: + # chiral = True + # break + if chiral: + return False + # if self.chiral_symbol[bond]: + # return False + return True + + + + def find_shortest_path(self, atom_1: 'AtomProperties', atom_2: 'AtomProperties', \ + path_type: str = 'bond') -> List[Union['BondProperties', 'AtomProperties']]: + """ + Finds the shortest path between two atoms in the molecular graph, returning either the + sequence of bonds or atoms along the path. + + This method implements a shortest path algorithm to determine the most direct route + between two specified atoms within the molecular structure. It can return the path as a + list of either the bonds connecting the atoms or the atoms themselves, depending on the + `path_type` parameter. The algorithm initializes by setting the distance to all atoms as + infinite, except for the starting atom, which is set to zero. It then iteratively + selects the atom with the smallest distance that has not been visited, updates the + distances to its neighbors, and marks it as visited. This process continues until the + destination atom is reached or all atoms have been visited. Finally, it constructs the + path from the destination atom back to the starting atom using the recorded previous + hops. + + Parameters + :param atom_1 AtomProperties: + The starting atom from which to find the shortest path. + :param atom_2 AtomProperties: + The destination atom to which to find the shortest path. + :param path_type str: + Specifies the type of elements to return in the path. 'bond' returns the bonds along the path, 'atom' returns the atoms. Default is 'bond'. + + Returns List[Union['BondProperties', 'AtomProperties']: + A list representing the shortest path between `atom_1` and `atom_2`. If `path_type` + is 'bond', the list contains `BondProperties` objects; if 'atom', it contains + `AtomProperties` objects. + + Raises ValueError: + If `path_type` is neither 'bond' nor 'atom'. + """ + distances: Dict['AtomProperties', float] = {} + previous_hop: Dict['AtomProperties', Optional['AtomProperties']] = {} + unvisited: Set['AtomProperties'] = set() + for atom in self.graph: + distances[atom] = float('inf') + previous_hop[atom] = None + unvisited.add(atom) + distances[atom_1] = 0.0 + while unvisited: + current_atom: Optional['AtomProperties'] = None + minimum: float = float('inf') + # Find the atom with the smallest distance value that has not yet been visited + for atom in unvisited: + dist: float = distances[atom] + if dist < minimum: + current_atom: 'AtomProperties' = atom + minimum = dist + if current_atom is None: + break + if current_atom == atom_2: + break + unvisited.remove(current_atom) + # If there exists a shorter path between the source atom and the neighbours, update distance + for neighbour in self.graph[current_atom]: + if neighbour in unvisited: + alternative_distance: float = distances[current_atom] + 1.0 + + if alternative_distance < distances[neighbour]: + distances[neighbour] = alternative_distance + previous_hop[neighbour] = current_atom + # Construct the path of atoms + path_atoms: List['AtomProperties'] = [] + current_atom: Optional['AtomProperties'] = atom_2 + if previous_hop[current_atom] or current_atom == atom_1: + while current_atom: + path_atoms.insert(0, current_atom) + current_atom = previous_hop[current_atom] + if path_type == 'bond': + path: List[Union['BondProperties', 'AtomProperties']] = [] + for i in range(1, len(path_atoms)): + atom_1 = path_atoms[i - 1] + atom_2 = path_atoms[i] + bond = self.bond_lookup(atom_1, atom_2) + path.append(bond) + return path + elif path_type == 'atom': + return path_atoms + else: + raise ValueError("Path type must be 'bond' or 'atom'.") + + + + def resolve_secondary_overlaps(self, sorted_scores: List[Tuple[float, 'AtomProperties']]) -> None: + """ + Resolves secondary overlaps in the molecular structure by adjusting the positions of + atoms based on their overlap scores. + + This method addresses secondary overlaps in the molecular layout by iteratively + adjusting the positions of atoms that have been identified as overlapping beyond a + specified sensitivity threshold. It processes a list of atoms sorted by their overlap + scores, focusing on atoms with scores higher than a predefined sensitivity level. For + each atom, it determines the appropriate action based on the atom's connectivity and + proximity to other atoms to minimize overlaps. The process involves finding the closest + atom if the target atom has only one neighbor or is isolated, calculating a new position + that reduces overlap, and then rotating the atom to this new position. The rotation is + designed to move the atom away from its closest neighbor or a specified position to + alleviate the overlap. + + Parameters + :param sorted_scores List[Tuple[float, AtomProperties]]: + A list of tuples, where each tuple contains an overlap score and an `AtomProperties` + object. The list is sorted by score, with the highest scores (indicating more + significant overlaps) first. + + The steps involved are as follows: + 1. Iterate through atoms sorted by their overlap scores, focusing on those with scores + exceeding the sensitivity threshold. + 2. For atoms with one or no neighbors, find the closest atom in the structure to + determine a direction for rotation. + 3. Calculate a new position for the atom based on the closest atom's position or a + specified reference point, considering the previous positions of both the atom and its + closest neighbor. + 4. Rotate the atom to the new position to reduce overlap, using a predefined angle to + ensure minimal disruption to the molecular structure. + """ + for score, atom in sorted_scores: + if score > self.overlap_sensitivity: + if len(atom.neighbours) <= 1: + if atom.neighbours: + continue + closest_atom: 'AtomProperties' = self.get_closest_atom(atom) + neighbours = closest_atom.neighbours + if len(neighbours) <= 1: + if not closest_atom.previous_position: + closest_position: float = atom.neighbours[0].position + else: + closest_position: float = closest_atom.previous_position + else: + if not closest_atom.previous_position: + closest_position: float = atom.neighbours[0].position + else: + closest_position: float = closest_atom.position + if not atom.previous_position: + atom_previous_position: float = atom.neighbours[0].position + else: + atom_previous_position: float = atom.previous_position + atom.position.rotate_away_from_vector(closest_position, \ + atom_previous_position, math.radians(20)) + + + + def get_closest_atom(self, atom: 'AtomProperties') -> 'AtomProperties': + """ + Identifies and returns the atom closest to a given atom within the molecular graph. + + This method calculates the distance between a specified atom and all other atoms in the + molecular graph to find the closest one. It iterates through each atom in the graph, + comparing their distances to the given atom and updates the closest atom found so far + based on the smallest squared distance. The squared distance is used for efficiency, + avoiding the computational cost of square root calculations. The method is useful for + determining the spatial relationship between atoms, which can be crucial for layout + adjustments, overlap resolution, or identifying nearby atoms for various analyses. + + Parameters + :param atom AtomProperties: + The reference atom from which to find the closest atom in the molecular graph. + + Returns AtomProperties: + The atom determined to be closest to the specified atom based on the shortest + distance. If no closer atom is found (e.g., if the graph only contains the specified + atom), the method returns None. + """ + minimal_distance = float('inf') + closest_atom: Optional['AtomProperties'] = None + for atom_2 in self.graph: + if atom == atom_2: + continue + squared_distance: float = atom.position.get_squared_distance(atom_2.position) + if squared_distance < minimal_distance: + minimal_distance: float = squared_distance + closest_atom: 'AtomProperties' = atom_2 + return closest_atom + +__all__ = ['Calculate2d'] \ No newline at end of file diff --git a/chython/algorithms/calculate2d/KKLayout.py b/chython/algorithms/calculate2d/KKLayout.py new file mode 100644 index 00000000..eb7ec6b3 --- /dev/null +++ b/chython/algorithms/calculate2d/KKLayout.py @@ -0,0 +1,410 @@ +""" +Defines the KKLayout class, utilized for arranging molecular structures using the Kamada-Kawai +algorithm. + +Description: +The KKLayout class is designed to optimize the layout of molecular structures through the +application of the Kamada-Kawai algorithm, a graph drawing method that models atoms as physical +bodies connected by springs, aiming to find a low-energy configuration. This approach allows for +the visualization of molecular structures in a two-dimensional plane, where atoms are positioned +according to calculated coordinates to reflect their interconnections and minimize the system's +total energy. The class is initialized with essential details about the molecular structure, +enabling the creation of matrices for storing atomic distances, bond stiffness, and interaction +energies. These matrices support the iterative process of finding an optimal layout that +minimizes energy, thereby enhancing the stability and clarity of the molecular representation. + +Outcome: +Achieves a stable molecular layout where atomic positions are optimized for minimal energy, +enhancing the interpretability of complex molecular structures. This optimized arrangement not +only reflects the underlying chemistry but also simplifies the visual analysis of molecular +architecture, making it invaluable for scientific and educational purposes. +""" +from typing import Dict, List, TYPE_CHECKING, Tuple +from .MathHelper import Vector, Polygon +import math +if TYPE_CHECKING: + from .Calculate2d import Calculate2d + from .Properties import * + +class KKLayout: + """ + Class for calculating the optimal arrangement of atoms in a molecular structure + using the Kamada-Kawai algorithm. + """ + def __init__(self, structure: 'Calculate2d', atoms: List['AtomProperties'], \ + center: 'Vector', start_atom: 'AtomProperties', bond_length: float, \ + threshold: float=0.1, inner_threshold: float=0.1, max_iteration: int=2000, + max_inner_iteration: int=50, max_energy: int=1e9): + """ + Initializes the KKLayout object with the necessary parameters to calculate the optimal + arrangement of atoms in a molecular structure using the Kamada-Kawai algorithm. + + Parameters: + :param structure Calculate2d: + An instance of the Calculate2d class used for performing 2D space calculations. + :param atoms List[AtomProperties]: + A list containing instances of the AtomProperties class representing the atoms + in the molecular structure. + :param center Vector: + An instance of the Vector class specifying the geometric center of the molecular + structure. + :param start_atom AtomProperties: + An instance of the AtomProperties class designating the starting atom for + constructing the layout. + :param bond_length float: + The specified length of bonds between atoms in the molecular structure. + :param threshold Optional[float]: + The energy threshold value used to determine when to stop iterations during the + calculation. Defaults to 0.1. + :param inner_threshold Optional[float]: + The inner energy threshold value used to determine when to stop internal iterations + during the calculation. Defaults to 0.1. + :param max_iteration Optional[int]: + The maximum number of iterations allowed for the calculation process. Defaults to + 2000. + :param max_inner_iteration Optional[int]: + The maximum number of inner iterations allowed for the calculation process. + Defaults to 50. + :param max_energy Optional[int]: + The maximum allowable energy level for the system being calculated. Defaults to 1e9. + + Attributes: + self.structure: Stores the Calculate2d instance passed as a parameter. + self.atoms: Stores the list of AtomProperties instances representing the atoms in the + structure. + self.center: Stores the Vector instance representing the center of the molecular + structure. + self.start_atom: Stores the AtomProperties instance representing the starting atom for + the layout construction. + self.edge_strength: Stores the bond length between atoms. + self.threshold: Stores the energy threshold value for stopping iterations. + self.inner_threshold: Stores the inner energy threshold value for internal iterations. + self.max_iteration: Stores the maximum number of iterations allowed. + self.max_inner_iteration: Stores the maximum number of inner iterations allowed. + self.max_energy: Stores the maximum allowable energy level for the system. + + Additional Attributes: + self.x_positions, self.y_positions: Dictionaries storing the X and Y coordinates of each + atom in the structure. + self.positioned: A dictionary indicating whether each atom has been positioned. + self.length_matrix: A matrix storing the lengths of bonds between pairs of atoms. + self.distance_matrix: A matrix storing the distances between pairs of atoms. + self.spring_strengths: A matrix storing the stiffness values of links between pairs of + atoms. + self.energy_matrix: A matrix storing the interaction energy between pairs of atoms. + self.energy_sums_x, self.energy_sums_y: Dictionaries storing the summations of energies + along the X and Y axes for each atom. + """ + self.structure: 'Calculate2d' = structure + self.atoms: List['AtomProperties'] = atoms + self.center: 'Vector' = center + self.start_atom: 'AtomProperties' = start_atom + self.edge_strength: int = bond_length + self.threshold: float = threshold + self.inner_threshold: float = inner_threshold + self.max_iteration: int = max_iteration + self.max_inner_iteration: int = max_inner_iteration + self.max_energy: int = max_energy + + self.x_positions: Dict['AtomProperties', float] = {} + self.y_positions: Dict['AtomProperties', float] = {} + self.positioned: Dict['AtomProperties', bool] = {} + self.length_matrix: Dict['AtomProperties', Dict['AtomProperties', float]] = {} + self.distance_matrix: Dict[int, Dict[int, float]] = {} + self.spring_strengths: Dict['AtomProperties', Dict['AtomProperties', float]] = {} + self.energy_matrix: Dict['AtomProperties', Dict['AtomProperties', Optional[float]]] = {} + self.energy_sums_x: Dict['AtomProperties', Dict['AtomProperties', Optional[float]]] = {} + self.energy_sums_y: Dict['AtomProperties', Dict['AtomProperties', Optional[float]]] = {} + + self.initialise_matrices() + self.get_kk_layout() + + + def initialise_matrices(self) -> None: + """ + Initializes various matrices required for calculating the layout of a molecular + structure. + + This method computes the initial positions of atoms based on the center of the molecule + and bond length. It creates matrices to store bond lengths, link stiffnesses, + interaction energies, + and sums of energy along the X and Y axes. The initialization process involves + calculating the distance matrix, determining initial atom positions, + and preparing matrices for further calculations in the Kamada-Kawai algorithm. + + Steps involved: + 1. Compute the distance matrix to understand the connectivity and distances between + atoms. + 2. Determine initial positions for atoms based on a circular layout around the + molecule's center, considering unpositioned atoms. + 3. Initialize matrices for bond lengths, spring strengths, and interaction energies + between atoms. + 4. Calculate the initial energy matrix based on atom positions and bond lengths. + """ + self.distance_matrix = self.get_subgraph_distance_matrix(self.atoms) + length = len(self.atoms) + radius = Polygon.find_polygon_radius(500, length) + angle = Polygon.get_central_angle(length) + a: float = 0.0 + for atom in self.atoms: + if not atom.positioned: + self.x_positions[atom] = self.center.x + math.cos(a) * radius + self.y_positions[atom] = self.center.y + math.sin(a) * radius + else: + self.x_positions[atom] = atom.position.x + self.y_positions[atom] = atom.position.y + self.positioned[atom] = atom.positioned + a += angle + for atom_1 in self.atoms: + self.length_matrix[atom_1] = {} + self.spring_strengths[atom_1] = {} + self.energy_matrix[atom_1] = {} + self.energy_sums_x[atom_1] = None + 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.energy_matrix[atom_1][atom_2] = None + for atom_1 in self.atoms: + ux = self.x_positions[atom_1] + uy = self.y_positions[atom_1] + d_ex = 0.0 + d_ey = 0.0 + for atom_2 in self.atoms: + if atom_1 == atom_2: + continue + vx = self.x_positions[atom_2] + vy = self.y_positions[atom_2] + 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_2][atom_1] = self.energy_matrix[atom_1][atom_2] + d_ex += self.energy_matrix[atom_1][atom_2][0] + d_ey += self.energy_matrix[atom_1][atom_2][1] + self.energy_sums_x[atom_1] = d_ex + self.energy_sums_y[atom_1] = d_ey + + + def get_kk_layout(self) -> None: + """ + Initiates the iterative process to find the optimal arrangement of atoms in a molecular + structure using the Kamada-Kawai algorithm. + + This method performs iterations until the system's energy falls below a threshold value + or the maximum number of iterations is reached. At each iteration, + the `update()` method is called to move atoms with the highest energy, aiming to + minimize the overall system energy through gradual adjustments. + + Description: + The Kamada-Kawai algorithm is employed to iteratively refine the positions of atoms + within a molecular structure, seeking a configuration that minimizes the system's + energy. This method orchestrates the iterative process, + adjusting atom positions based on their energy states until a satisfactory layout is + achieved or predefined limits are met. It operates by repeatedly identifying atoms with + the highest energy contributions + and adjusting their positions to reduce strain within the molecular structure, thereby + optimizing the layout towards a state of lower potential energy. + + Process: + - Iterations continue until either the system's energy drops below a specified + threshold, indicating an acceptable level of stability, or the maximum iteration count + is reached, preventing infinite loops. + - At each iteration, the atom contributing most significantly to the system's energy is + identified, and its position is adjusted to decrease overall energy. + - Inner iterations within each main iteration further refine the position of the most + energetic atom, stopping once the change in energy falls below an inner threshold or the + maximum number of inner iterations is reached, + ensuring fine-tuning of atomic positions for optimal placement. + - After concluding iterations, final positions are assigned to atoms, marking them as + positioned and forcing their placement to prevent further adjustments. + + Outcome: + - Achieves a stable molecular layout where atomic positions are optimized to minimize + energy, reflecting the algorithm's goal of balance and stability. + - Marks atoms as positioned and forcibly placed, indicating completion and preventing + further adjustments, ensuring structural integrity. + """ + iteration = 0 + max_energy = self.max_energy + while max_energy > self.threshold and self.max_iteration > iteration: + iteration += 1 + max_energy_atom, max_energy, d_ex, d_ey = self.highest_energy() + delta = max_energy + inner_iteration = 0 + 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) + for atom in self.atoms: + atom.position.x = self.x_positions[atom] + atom.position.y = self.y_positions[atom] + atom.positioned = True + atom.force_positioned = True + + + def energy(self, atom: 'AtomProperties') -> List[float]: + """ + Calculates the energy of the system for a given atom. + + The energy is defined as the sum of the squares of the energy components along the X and + Y axes, as well as the components themselves. This allows for an evaluation of the + atom's overall state within the system and its contribution to the total energy. + + Parameters: + :param atom AtomProperties: + The atom for which the energy is calculated. + + Returns List[float]: + A list containing: + - Total energy (sum of the squares of the components), + - Energy along the X-axis, + - Energy along the Y-axis. + """ + energy: List[float] = [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) -> Tuple['AtomProperties', float, float, float]: + """ + Identifies the atom with the highest energy among those not yet positioned. + + This method scans through all atoms in the molecular structure to find the one with the + greatest energy contribution that has not been positioned yet. It is crucial for the + iterative process of optimizing the layout according to the Kamada-Kawai algorithm, as + it targets atoms requiring adjustment to minimize overall system energy. + + Returns Tuple[AtomProperties, float, float, float]: + A tuple containing: + - AtomProperties: The atom identified as having the highest energy among those not yet positioned. + - float: The maximum energy value associated with this atom. + - float: The energy component along the X-axis for the atom with the highest energy. + - float: The energy component along the Y-axis for the atom with the highest energy. + """ + max_energy = 0.0 + max_energy_atom = None + max_d_ex = 0.0 + max_d_ey = 0.0 + for atom in self.atoms: + delta, d_ex, d_ey = self.energy(atom) + if delta > max_energy and not self.positioned[atom]: + max_energy = delta + max_energy_atom = atom + max_d_ex = d_ex + max_d_ey = d_ey + return max_energy_atom, max_energy, max_d_ex, max_d_ey + + + def update(self, atom: 'AtomProperties', d_ex: float, d_ey: float) -> None: + """ + Updates the position of a specified atom based on its energy. + + Parameters: + :param atom AtomProperties: + The atom whose position needs to be updated. + :param d_ex float: + Energy along the X-axis for the atom. + :param d_ey float: + Energy along the Y-axis for the atom. + + Description: + This method recalculates and adjusts the position of a given atom within the molecular structure based on its current energy state, aiming to minimize the overall system energy through iterative refinement. It incorporates the Kamada-Kawai algorithm principles, adjusting atomic positions to reduce strain and achieve a stable configuration. By considering the energies along the X and Y axes, the method computes new coordinates that reflect a balance between the atom's interactions with other atoms, effectively reducing its contribution to the system's total energy. The process involves calculating forces acting on the atom due to its connections, represented by springs with specific strengths, and updating its position accordingly. The adjustments are made in both X and Y directions, aiming to move the atom towards a state of lower potential energy, thereby contributing to the optimization of the entire molecular layout. + """ + dxx = 0.0 + dyy = 0.0 + dxy = 0.0 + ux = self.x_positions[atom] + uy = self.y_positions[atom] + lengths_array = self.length_matrix[atom] + strengths_array = self.spring_strengths[atom] + for atom_2 in self.atoms: + if atom == atom_2: + continue + vx = self.x_positions[atom_2] + vy = self.y_positions[atom_2] + length = lengths_array[atom_2] + strength = strengths_array[atom_2] + squared_xdiff = (ux - vx) ** 2 + squared_ydiff = (uy - vy) ** 2 + denom = 1.0 / (squared_xdiff + squared_ydiff) ** 1.5 + dxx += strength * (1 - length * squared_ydiff * denom) + dyy += strength * (1 - length * squared_xdiff * denom) + dxy += strength * (length * (ux - vx) * (uy - vy) * denom) + if dxx == 0: + dxx = 0.1 + if dyy == 0: + dyy = 0.1 + if dxy == 0: + dxy = 0.1 + dy = (d_ex / dxx + d_ey / dxy) / (dxy / dxx - dyy / dxy) + dx = -(dxy * dy + d_ex) / dxx + self.x_positions[atom] += dx + self.y_positions[atom] += dy + d_ex = 0.0 + d_ey = 0.0 + ux = self.x_positions[atom] + uy = self.y_positions[atom] + for atom_2 in self.atoms: + if atom == atom_2: + continue + vx = self.x_positions[atom_2] + vy = self.y_positions[atom_2] + previous_ex = self.energy_matrix[atom][atom_2][0] + 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) + self.energy_matrix[atom][atom_2] = [dx, dy] + d_ex += dx + d_ey += dy + self.energy_sums_x[atom_2] += dx - previous_ex + self.energy_sums_y[atom_2] += dy - previous_ey + self.energy_sums_x[atom] = d_ex + self.energy_sums_y[atom] = d_ey + + + def get_subgraph_distance_matrix(self, atoms: List['AtomProperties']) \ + -> Dict[int, Dict[int, float]]: + """ + Computes the distance matrix between atoms in a subgraph. + + Parameters: + :param atoms List[AtomProperties]: + Specifies the subset of atoms to analyze, focusing the calculation on relevant + components of the molecular structure. + + Returns Dict[int, Dict[int, float]]: + Provides a comprehensive view of atomic distances, where keys represent atoms and + values are dictionaries mapping to other atoms with corresponding shortest path + distances. This structured output facilitates targeted adjustments, ensuring that + atomic placements minimize overall system energy. + + Description: + This method calculates the shortest path distances between all pairs of atoms within a + given subset of a molecular structure, forming a subgraph. It initializes the distance + matrix with infinite distances for all atom pairs except those directly connected, which + are set to 1, indicating a bond exists. It then applies a variation of the + Floyd-Warshall algorithm to find the shortest paths between all atoms, updating the + matrix to reflect the shortest distances found. This process is crucial for + understanding the connectivity and spatial relationships within the molecular structure, + aiding in layout optimization by identifying the most efficient paths between atoms. The + resulting matrix provides insights into the molecular topology, guiding the arrangement + of atoms to minimize overall energy and enhance structural stability. + """ + distance_matrix: Dict[int, Dict[int, float]] = {} + for atom_1 in atoms: + if atom_1 not in distance_matrix: + distance_matrix[atom_1] = {} + + for atom_2 in atoms: + if self.structure.bond_lookup(atom_1, atom_2): + distance_matrix[atom_1][atom_2] = 1 + else: + distance_matrix[atom_1][atom_2] = float('inf') + + 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] + return distance_matrix \ No newline at end of file diff --git a/chython/algorithms/calculate2d/MathHelper.py b/chython/algorithms/calculate2d/MathHelper.py new file mode 100644 index 00000000..32f433ba --- /dev/null +++ b/chython/algorithms/calculate2d/MathHelper.py @@ -0,0 +1,689 @@ +""" +This module introduces the `Vector` and `Polygon` classes, designed to perform mathematical +calculations relevant to two-dimensional Cartesian coordinate systems and regular polygons. + +The `Vector` class facilitates operations with coordinates, including vector arithmetic +(addition, subtraction, multiplication/division by scalars), normalization, rotation, and +distance calculations. It also supports methods for determining the angle of a vector, its +length, and whether it lies in a certain quadrant. Additionally, it includes functions for +reflecting vectors about lines, finding the closest atom or point, and rotating vectors around +other vectors or points. + +The `Polygon` class focuses on properties and calculations related to regular polygons, such as +finding the circumradius, calculating central angles, determining the apothem, and identifying +the type of polygon based on its number of sides. It also includes static methods for +calculating normals to lines defined by two vectors and for adding, averaging, and mirroring +vectors. + +Together, these classes provide a comprehensive toolkit for performing geometric computations +essential in fields such as computer graphics, physics simulations, and computational chemistry, +where precise manipulation and analysis of spatial relationships are required. +""" +import math +from typing import Union, TYPE_CHECKING, List + +if TYPE_CHECKING: + from .Properties import AtomProperties + + +class Vector: + """ + The `Vector` class facilitates operations with coordinates, including vector arithmetic + (addition, subtraction, multiplication/division by scalars), normalization, rotation, and + distance calculations. It also supports methods for determining the angle of a vector, its + length, and whether it lies in a certain quadrant. Additionally, it includes functions for + reflecting vectors about lines, finding the closest atom or point, and rotating vectors around + other vectors or points. + """ + def __init__(self, x: Union[int, float], y: Union[int, float]) -> None: + """ + Constructor of the vector class + + Parameters: + :param x Union[int, float]: + The coordinate of the vector along the abscissa axis + :param y Union[int, float]: + The coordinate of the vector along the ordinate axis + + Attributes: + x: the coordinate of the vector along the abscissa axis + y: the coordinate of the vector along the ordinate axis + """ + self.x: float = float(x) + self.y: float = float(y) + + + def __repr__(self) -> str: + """ + The method needed for debugging the code + + Returns a string containing the coordinates of the point + """ + return str(self.x) + ', ' + str(self.y) + + + def copy(self) -> 'Vector': + """ + Creates a copy of the current class object + + Returns a copy of the object + """ + return Vector(self.x, self.y) + + + def subtract(self, vector: 'Vector'): + """ + A method for the operation of subtraction between vectors + + Parameters: + :param vector 'Vector': + Another object of the current class + """ + self.x -= vector.x + self.y -= vector.y + + + def rotate(self, angle: float) -> None: + """ + A method that rotates the vector by the appropriate angle from the signature + of the function and updates the coordinates of the current class object + + Parameters: + :param angle float: + The angle by which the vector should be rotated + """ + new_x: float = self.x * math.cos(angle) - self.y * math.sin(angle) + new_y: float = self.x * math.sin(angle) + self.y * math.cos(angle) + + self.x = new_x + self.y = new_y + + + def add(self, vector: 'Vector') -> None: + """ + A class method that adds vectors and updates the coordinates of the current class object + + Parameters: + :param vector 'Vector': + Another object of the current class + """ + self.x += vector.x + self.y += vector.y + + + def invert(self) -> None: + """ + A class method that inverts the current coordinates of objects of the class + """ + self.x = self.x * -1 + self.y = self.y * -1 + + + def divide(self, scalar: float) -> None: + """ + A class method that divides the coordinates of the current class object + vectors for an arbitrary number + + Parameters: + :param scalar float: + Number divider + """ + self.x = self.x / scalar + self.y = self.y / scalar + + + def normalise(self) -> None: + """ + Normalization of coordinates (dividing them by the length of the vector itself) + """ + if self.length() != 0: + self.divide(self.length()) + + + def angle(self) -> float: + """ + A method that calculates the angle of inclination of the current vector + + Returns float the angle of inclination of the vector + """ + return math.atan2(self.y, self.x) + + + def length(self) -> float: + """ + Calculates the length of the current vector + + Returns float + """ + return math.sqrt((self.x**2) + (self.y**2)) + + + def multiply_by_scalar(self, scalar: float) -> None: + """ + Multiplies the coordinates of the current vector by an arbitrary real number + + Parameters: + :param scalar float + """ + self.x = self.x * scalar + self.y = self.y * scalar + + + def rotate_around_vector(self, angle: float, vector: 'Vector') -> None: + """ + Rotates a point (or vector) around a given vector by a specified angle. + + Parameters: + :param angle float: + The angle by which to rotate the point, typically measured in radians. + :param vector 'Vector': + The vector around which the rotation occurs. This vector serves as the reference + point. + """ + self.x -= vector.x + self.y -= vector.y + + x = self.x * math.cos(angle) - self.y * math.sin(angle) + y = self.x * math.sin(angle) + self.y * math.cos(angle) + + self.x = x + vector.x + self.y = y + vector.y + + + def get_closest_atom(self, atom_1: 'AtomProperties', atom_2: 'AtomProperties') -> 'AtomProperties': + """ + This method determines which of the two atoms (represented by the objects atom_1 and atom_2) + is closer to the current object (represented by self). + + Parameters: + :param atom_1: 'AtomProperties': + The first atom to compare. + :param atom_2: 'AtomProperties': + The second atom to compare. + + Returns 'AtomProperties': + The closest atom. + """ + distance_1 = self.get_squared_distance(atom_1.position) + distance_2 = self.get_squared_distance(atom_2.position) + return atom_1 if distance_1 < distance_2 else atom_2 + + + def get_closest_point_index(self, point_1: 'Vector', point_2: 'Vector') -> int: + """ + The method is designed to determine which of the two specified coordinates (point_1: 'Vector', point_2: 'Vector') + closer to the current point. + + Parameters + :param point_1 'Vector': + The first point to be compared with. It can be a tuple, a list, or an object + representing coordinates. + :param point_2 'Vector': + The second point to compare with. Similarly, it can be a tuple, a list, or an + object. + + Returns int: + The index of the nearest point: 0 for point_1 and 1 for point_2. + """ + distance_1 = self.get_squared_distance(point_1) + distance_2 = self.get_squared_distance(point_2) + return 0 if distance_1 < distance_2 else 1 + + + def get_squared_length(self) -> float: + """ + Calculates the length squared + + Returns float: + Vector length squared + """ + return self.x ** 2 + self.y ** 2 + + + def get_squared_distance(self, vector: 'Vector') -> float: + """ + The method is designed to calculate the square of the distance between the current vector + (represented by self) and the specified vector (or point) represented by the vector object. + + Parameters + :param vector: 'Vector': + An object representing a vector or point from which to calculate the distance. + + Returns float: + The square of the distance + """ + return (vector.x - self.x) ** 2 + (vector.y - self.y) ** 2 + + + def get_distance(self, vector: 'Vector') -> float: + """ + The method is designed to calculate the distance between the current vector (represented by self) and + the specified vector (or point) represented by the vector object. + + Parameters + :param vector: 'Vector': + An object representing a vector or point from which to calculate the distance. + + Returns float: + The distance between the coordinates of the current vector and the passed parameter + """ + return math.sqrt(self.get_squared_distance(vector)) + + + def get_rotation_away_from_vector(self, vector: 'Vector', center: 'Vector', angle: float) -> float: + """ + The method is designed to determine how much the angle of rotation (in a positive or negative direction) + from a given vector measures the distance to this vector. + + Parameters + :param vector 'Vector': + The vector to "move away from". It can be a point or a direction, relative to which + the rotation is taking place. + :param center 'Vector': + The center of rotation around which the object (represented by self) rotates. + :param angle float: + The angle at which the rotation occurs. This value can be positive or negative. + + Returns returns the rotation angle that minimizes the distance to the vector, + either in a positive or negative direction. + """ + tmp = self.copy() + + tmp.rotate_around_vector(angle, center) + squared_distance_1 = tmp.get_squared_distance(vector) + + tmp.rotate_around_vector(-2.0 * angle, center) + squared_distance_2 = tmp.get_squared_distance(vector) + return angle if squared_distance_2 < squared_distance_1 else -angle + + + def rotate_away_from_vector(self, vector: 'Vector', center: 'Vector', angle: float) -> None: + """ + The method is designed to rotate the current object (represented by self) around a given + one center in such a way as to minimize the distance to the specified vector. + If rotation in one direction leads to a decrease in the distance, the function corrects + the rotation,to ensure maximum distance from the vector. + + Parameters + :param vector 'Vector': + The vector to "move away from". It can be a point or a direction, relative to which + the rotation is taking place. + :param center 'Vector': + The center of rotation around which the object rotates. + :param angle float: + The angle at which the rotation occurs. This value can be positive or negative. + """ + self.rotate_around_vector(angle, center) + squared_distance_1 = self.get_squared_distance(vector) + self.rotate_around_vector(-2.0 * angle, center) + squared_distance_2 = self.get_squared_distance(vector) + + if squared_distance_2 < squared_distance_1: + self.rotate_around_vector(2.0 * angle, center) + + + def get_clockwise_orientation(self, vector: 'Vector') -> str: + """ + The method is designed to determine the orientation (positive or negative) between + the current object (represented by self) and the specified vector (represented by + the vector object). + + Parameters + :param vector 'Vector': + The vector relative to which the orientation is determined. + + Returns str: + A string indicating whether the orientation is "clockwise", "counterclockwise" + or "neutral". + """ + a: float = self.y * vector.x + b: float = self.x * vector.y + + if a > b: + return 'clockwise' + elif a == b: + return 'neutral' + else: + return 'counterclockwise' + + + def mirror_about_line(self, line_point_1: 'Vector', line_point_2: 'Vector') -> None: + """ + The method is designed to reflect the current object (represented by self) relative to a + given line, defined by two points (line_point_1 and line_point_2). After performing this + function, the coordinates of the object will be changed so that it is on the opposite + side of the line, keeping the same distance to the line. + + Parameters + :param line_point_1: 'Vector': + The first point defining the line. + :param line_point_2: 'Vector': + The second point defining the line. + """ + dx = line_point_2.x - line_point_1.x + dy = line_point_2.y - line_point_1.y + + 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 + + self.x = new_x + self.y = new_y + + + @staticmethod + def get_position_relative_to_line(vector_start: 'Vector', vector_end: 'Vector', vector: 'Vector') -> int: + """ + Determines the position of a vector relative to a line defined by two points. + + Parameters: + :param vector_start 'Vector': + The start point of the line. + :param vector_end 'Vector': + The end point of the line. + :param vector 'Vector': + The vector whose position relative to the line is to be determined. + + Returns int: + 1 if the vector is to the left of the line, -1 if the vector is to the right of the + line, 0 if the vector lies on the line. + """ + 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: + return -1 + else: + return 0 + + + @staticmethod + def get_directionality_triangle(vector_a: 'Vector', vector_b: 'Vector', vector_c: 'Vector') -> str: + """ + Determines the directionality of the triangle formed by three vectors (or points). + + Parameters: + :param vector_a 'Vector': + The first vertex of the triangle. + :param vector_b 'Vector': + The second vertex of the triangle. + :param vector_c 'Vector': + The third vertex of the triangle. + + Returns str: + - 'clockwise' if the triangle is oriented in a clockwise direction. + - 'counterclockwise' if the triangle is oriented in a counterclockwise direction. + - None if the three points are collinear (lie on the same line). + """ + 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' + elif determinant == 0: + return None + else: + return 'counterclockwise' + + + @staticmethod + def mirror_vector_about_line(line_point_1: 'Vector', line_point_2: 'Vector', point: 'Vector')-> 'Vector': + """ + Mirrors a point (or vector) across a line defined by two points. + + Parameters: + :param line_point_1 'Vector': + The first point defining the line. + :param line_point_2 'Vector': + The second point defining the line. + :param point 'Vector': + The point to be mirrored across the line. + + Returns Vector: + A new Vector representing the mirrored point across the line. + """ + dx = line_point_2.x - line_point_1.x + dy = line_point_2.y - line_point_1.y + + 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 + return Vector(x_new, y_new) + + + @staticmethod + def get_line_angle(point_1: 'Vector', point_2: 'Vector') -> float: + """ + Calculates the angle of a line defined by two points with respect to the positive x-axis. + + Parameters: + point_1 'Vector': + The first point defining the line. + point_2 'Vector': + The second point defining the line. + + Returns float: + The angle of the line in radians, in the range [-π, π]. + """ + difference = Vector.subtract_vectors(point_2, point_1) + return difference.angle() + + + @staticmethod + def subtract_vectors(vector_1: 'Vector', vector_2: 'Vector')-> 'Vector': + """ + Subtracts one vector from another. + + Parameters: + vector_1 'Vector': + The vector from which to subtract. + vector_2 'Vector': + The vector to subtract. + + Returns Vector: + A new Vector representing the result of the subtraction (vector_1 - vector_2). + """ + x = vector_1.x - vector_2.x + y = vector_1.y - vector_2.y + return Vector(x, y) + + + @staticmethod + def add_vectors(vector_1: 'Vector', vector_2: 'Vector') -> 'Vector': + """ + Adds two vectors together. + + Parameters: + vector_1 'Vector': + The first vector to add. + vector_2 'Vector': + The second vector to add. + + Returns Vector: + A new Vector representing the result of the addition (vector_1 + vector_2). + """ + x = vector_1.x + vector_2.x + y = vector_1.y + vector_2.y + return Vector(x, y) + + + @staticmethod + def get_midpoint(vector_1: 'Vector', vector_2: 'Vector') -> 'Vector': + """ + Calculates the midpoint between two vectors. + + Parameters: + vector_1 'Vector': + The first vector. + vector_2 'Vector': + The second vector. + + Returns Vector: + A new Vector representing the midpoint between vector_1 and vector_2. + """ + x = (vector_1.x + vector_2.x) / 2 + y = (vector_1.y + vector_2.y) / 2 + return Vector(x, y) + + + @staticmethod + def get_average(vectors: List['Vector']) -> 'Vector': + """ + Calculates the average of a list of vectors. + + Parameters: + :param vectors List[Vector]: + A list of vectors for which the average is to be calculated. + + Returns: + Vector: A new Vector representing the average of the input vectors. + """ + average_x = 0.0 + average_y = 0.0 + for vector in vectors: + average_x += vector.x + average_y += vector.y + return Vector(average_x / len(vectors), average_y / len(vectors)) + + + @staticmethod + def get_normals(vector_1: 'Vector', vector_2: 'Vector') -> List['Vector']: + """ + Calculates the normal vectors to the line defined by two vectors. + + Parameters: + :param vector_1 'Vector': + The first vector defining the line. + :param vector_2 'Vector': + The second vector defining the line. + + Returns List[Vector]: + A list containing two normal vectors to the line defined by vector_1 and vector_2. + """ + delta = Vector.subtract_vectors(vector_2, vector_1) + return [Vector(-delta.y, delta.x), Vector(delta.y, -delta.x)] + + + @staticmethod + def get_angle_between_vectors(vector_1: 'Vector', vector_2: 'Vector', origin: 'Vector') -> float: + """ + Calculates the angle between two vectors relative to a given origin point. + + Parameters: + :param vector_1 'Vector': + The first vector. + :param vector_2 'Vector': + The second vector. + :param origin 'Vector': + The origin point relative to which the angle is calculated. + + Returns: + float: The angle between vector_1 and vector_2 in radians, in the range [0, π]. + """ + v1_x_diff: float = vector_1.x - origin.x + v1_y_diff: float = vector_1.y - origin.y + v2_x_diff: float = vector_2.x - origin.x + v2_y_diff: float = vector_2.y - origin.y + + dot_product: float = v1_x_diff * v2_x_diff + v1_y_diff * v2_y_diff + length_v1: float = math.sqrt(v1_x_diff ** 2 + v1_y_diff ** 2) + length_v2: float = math.sqrt(v2_x_diff ** 2 + v2_y_diff ** 2) + + cos_angle = dot_product / (length_v1 * length_v2) + return math.acos(cos_angle) + + + + + + + + + + + + + +class Polygon: + """ + The `Polygon` class focuses on properties and calculations related to regular polygons, such as + finding the circumradius, calculating central angles, determining the apothem, and identifying + the type of polygon based on its number of sides. It also includes static methods for + calculating normals to lines defined by two vectors and for adding, averaging, and mirroring + vectors. + """ + def __init__(self, edge_number: float) -> None: + """ + Initializes a Polygon with a specified number of edges. + + Parameters: + :param edge_number float: + The number of edges (sides) of the polygon. + """ + self.edge_number: float = edge_number + + @staticmethod + def find_polygon_radius(edge_length: float, edge_number: float) -> float: + """ + Calculates the radius of the circumcircle of a regular polygon. + + Parameters: + :param edge_length float: + The length of one edge of the polygon. + :param edge_number float: + The number of edges (sides) of the polygon. + + Returns float: + The radius of the circumcircle. + """ + return edge_length / (2 * math.sin(math.pi / edge_number)) + + @staticmethod + def get_central_angle(edge_number: float) -> float: + """ + Calculates the central angle of a regular polygon. + + Parameters: + :param edge_number float: + The number of edges (sides) of the polygon. + + Returns float: + The central angle in radians. + """ + return math.radians(float(360) / edge_number) + + @staticmethod + def get_apothem(radius: float, edge_number: float) -> float: + """ + Calculates the apothem of a regular polygon. + + Parameters: + :param radius float: + The radius of the circumcircle of the polygon. + :param edge_number float: + The number of edges (sides) of the polygon. + + Returns float: + The length of the apothem. + """ + return radius * math.cos(math.pi / edge_number) + + @staticmethod + def get_apothem_from_side_length(length: float, edge_number: float) -> float: + """ + Calculates the apothem of a regular polygon given the side length. + + Parameters: + :param length float: + The length of one edge of the polygon. + :param edge_number float: + The number of edges (sides) of the polygon. + + Returns float: + The length of the apothem. + """ + radius: float = Polygon.find_polygon_radius(length, edge_number) + return Polygon.get_apothem(radius, edge_number) \ No newline at end of file diff --git a/chython/algorithms/calculate2d/Properties.py b/chython/algorithms/calculate2d/Properties.py new file mode 100644 index 00000000..d8f16930 --- /dev/null +++ b/chython/algorithms/calculate2d/Properties.py @@ -0,0 +1,611 @@ +""" +This module defines classes that extend the properties of rings, atoms, and bonds within the +Kaiton structure, focusing on attributes and methods necessary for coordinate calculations. + +The classes contained herein serve as supplements to the existing Kaiton structure, offering +additional functionalities tailored for computational chemistry applications. They are designed +to facilitate the calculation of molecular geometries by providing detailed attributes and +methods specific to rings, atoms, and bonds, thereby enhancing the structure's utility in +algorithms that require precise spatial information. These classes include RingProperties, +AtomProperties, BondProperties, and RingOverlap, each tailored to represent different aspects of +molecular structures with attributes and methods that aid in determining spatial relationships +and characteristics inherent to chemical compounds. + +Classes: +- RingProperties: Represents the properties of rings within a molecule, including identifiers, + member atoms, positioning status, geometric center, presence of subrings, and types of rings + (e.g., bridged, spiro, fused). +- AtomProperties: Encapsulates atomic properties crucial for molecular geometry calculations, + such as atomic symbols, positions, and connectivity. +- BondProperties: Details the computational parameters of chemical bonds, including atom + references and bond types. +- RingOverlap: Handles overlaps between rings, identifying shared atoms and determining + structural characteristics like bridging. + +These classes are integral for algorithms that necessitate a deep understanding of molecular +topology and geometry, offering a structured approach to manipulating and analyzing chemical +structures programmatically. They facilitate the representation of complex molecular features +such as ring systems, atomic configurations, and bond characteristics, making them indispensable +for cheminformatics and computational chemistry applications. +""" + +from typing import List, Optional, Tuple +from .MathHelper import Vector +import math + +class RingProperties: + """ + A class on computing parameters of rings + """ + def __init__(self: 'RingProperties', ring: List['AtomProperties']) -> None: + """ + Constructor of the class that complements information about rings in the Kaiton + structure, creating new properties or converting existing ones into a more convenient + form for use in coordinate calculation algorithms. + + Parameters: + :param ring List['AtomProperties']: + A list of AtomProperties objects forming the current ring. + + Attributes: + - id Optional[int]: Contains information about the ring identifier, its sequential + number. + - members List['AtomProperties']: A list of references to corresponding AtomProperties + objects which are participants of this ring. + - members_id List[int]: A list of identifiers for AtomProperties objects which are + participants of this ring. + - positioned bool: A boolean value corresponding to the state of the ring calculation, + returns True if all atoms of this ring have received their coordinates. + - center 'Vector': The center of the ring in coordinates. + - subrings List: A boolean value indicating whether the ring contains subrings. + - bridged bool: A boolean value indicating whether the ring is a bridge ring. + - spiro bool: A boolean value indicating whether the ring is a spirocycle. + - fused bool: A boolean value indicating whether it is part of a condensed cyclic system. + - subring_of_bridged bool: A boolean value indicating whether the subrings are bridged. + - central_angle float: The central angle of the ring. + - neighbouring_rings List[int]: A list of identifiers of neighboring rings to the + current one. + """ + self.id: Optional[int] = None + self.members: List['AtomProperties'] = ring + self.members_id: List[int] = [atom.id for atom in self.members] + self.positioned: bool = False + self.center: 'Vector' = Vector(0, 0) + self.subrings: List = [] + self.bridged = False + self.spiro: bool = False + self.fused: bool = False + self.subring_of_bridged = False + self.central_angle: float = 0.0 + self.neighbouring_rings: List[int] = [] + + # добавляем в свойства атомов то что они находятся в этом кольце + for atom in self.members: + atom.ring_indexes.append(self.id) + atom.rings.append(self) + + + def __eq__(self, other: 'RingProperties') -> bool: + """ + Compares two RingProperties instances for equality based on their identifiers. + + This method checks if the identifiers of the two RingProperties instances being compared + are equal, returning True if they match and False otherwise. It serves as a quick way to + determine if two rings refer to the same entity in terms of their unique identifier. + + Parameters: + :param other RingProperties: + The instance to compare with the current instance. + + Returns bool: + True if the identifiers of the two instances are equal, indicating they represent + the same ring. False otherwise. + """ + return False if other is None else self.id == other.id + + + def __hash__(self) -> int: + """ + Returns the hash value of the current object, which is the unique identifier of the ring. + + This method is used when the object needs to be inserted into a hash-based collection + such as a set or dictionary. + The hash value is derived from the ring's unique identifier, allowing for efficient + storage and retrieval of ring objects + in collections that rely on hashing. + + Returns int: + The unique identifier of the current object of the class, used as the hash value. + """ + return self.id + + + def get_angle(self) -> float: + """ + Calculates the exterior angle of the polygon formed by the ring in radians. + + The exterior angle is determined by subtracting the central angle of the ring from π + (pi), providing a measure + of the angle formed outside the ring by extending one of its sides. This method is + particularly useful for + understanding the geometry of the ring within the context of its surrounding environment. + + Returns float: + The exterior angle of the polygon formed by the ring in radians. + """ + return math.pi - self.central_angle + + + def __repr__(self) -> str: + """ + Provides a human-readable representation of the RingProperties object, primarily + intended for debugging purposes. + + The representation includes the ring's identifier followed by the identifiers of the + atoms that are part of the ring, separated by hyphens. This format offers a concise yet + informative overview of the ring's composition, aiding in the identification and + analysis of rings during development and debugging sessions. + + Returns str: + A string combining the ring's identifier and the identifiers of the atoms that make + up the ring, separated by hyphens. + """ + members: str = '-'.join(str(member) for member in self.members) + return f'{self.id} {members}' + + + def copy(self) -> 'RingProperties': + """ + Creates a deep copy of the current RingProperties instance, duplicating all its + attributes and relationships. + + This method constructs a new RingProperties object that mirrors the current + instance exactly, including the list of member atoms, their identifiers, the + ring's position status, geometric center, presence of subrings, + and various boolean flags indicating the ring's characteristics (e.g., whether it + is bridged, spiro, fused). + Additionally, it copies over the list of neighboring rings and any subrings + associated with the ring. + + Returns RingProperties: + A new instance of the RingProperties class that is a deep copy of the current + instance, complete with all attributes and relationships duplicated. + """ + new_members: List['AtomProperties'] = [] + for atom in self.members: + new_members.append(atom.copy()) + + new_ring = RingProperties(new_members) + new_ring.id = self.id + for ring_id in self.neighbouring_rings: + new_ring.neighbouring_rings.append(ring_id) + + new_ring.positioned = self.positioned + for subring in self.subrings: + new_ring.subrings.append(subring) + new_ring.bridged = self.bridged + new_ring.subring_of_bridged = self.subring_of_bridged + new_ring.spiro = self.spiro + new_ring.fused = self.fused + new_ring.central_angle = self.central_angle + return new_ring + + + + + + + + + + + + + + + + + + + + + + + + +class BondProperties: + """ + A class about computational parameters of links + """ + def __init__(self: 'BondProperties', atom1: 'AtomProperties', \ + atom2: 'AtomProperties', bond) -> None: + """ + Constructor of the class that complements information about bonds in the Kaiton + structure, creating new properties or converting existing ones into a more + convenient form for use in coordinate calculation algorithms. + + Parameters: + :param atom1 'AtomProperties': + Reference to the object of the class of the first atom forming this bond. + :param atom2 'AtomProperties': + Reference to the object of the class of the second atom forming this bond. + :param bond: + Reference to the original Kaiton bond class. + + Attributes: + - id Tuple['AtomProperties']: Identifier of the current bond, which is a tuple of + atom identifiers between which this bond exists. + - n int: Identifier of the first atom of this bond. + - m int: Identifier of the second atom of this bond. + - atom1 'AtomProperties': Reference to the object of the class of the first atom of + this bond. + - atom2 'AtomProperties': Reference to the object of the class of the second atom of + this bond. + - type str: String that characterizes the type of bond, primary, secondary, or + tertiary. + + # center (bool): Placeholder for future expansion. + # chiral (bool): Placeholder for future expansion. + # chiral_symbol (Optional[str]): Placeholder for future expansion. + + The constructor initializes the bond properties based on the provided atoms and + determines its type (single, double, triple) based on the order of the bond. + + """ + self.id: Tuple['AtomProperties'] = (atom1.id, atom2.id) + self.n: int = atom1.id #atom1 index + self.m: int = atom2.id #atom2 index + + self.atom1: 'AtomProperties' = atom1 + self.atom2: 'AtomProperties' = atom2 + + # self.center: bool = False # рудименты кода + # self.chiral: bool = False # рудименты кода + # self.chiral_symbol: Optional[str] = None # # рудименты кода + + self.type = Optional[None] + if bond.order == 1: + self.type = 'single' + elif bond.order == 2: + self.type = 'double' + elif bond.order == 3: + self.type = 'triple' + + + + + + + + + + + + + + + + + + + + + + + + + + +class AtomProperties: + """ + A class about computing parameters of atoms + """ + def __init__(self: 'AtomProperties', atom_index: int, symbol: str) -> None: + """ + Initializes an instance of the AtomProperties class with data about an atom. + + Parameters: + :param atom_index int: + The index of the current atom within the molecular structure. + :param symbol str: + Symbol representing the element according to the periodic table. + + Attributes: + - id int: Unique identifier for the atom. + - symbol str: String characterizing the name of the element according to the periodic + table. + - ring_indexes List[int]: List of identifiers for rings in which the atom is a + participant. + - rings List['RingProperties']: List of references to RingProperties objects + representing the rings in which the atom is involved. + - is_bridge_atom bool: Flag indicating whether the atom is a bridging atom. + - is_bridge bool: Flag indicating whether the atom is a bridging atom. + - bridged_ring Optional['RingProperties']: RingProperties object representing the ring + through which a bridge passes. + - positioned bool: Flag indicating whether the coordinates for the current atom have + been calculated. + - previous_position 'Vector': Coordinates of the preceding atom. + - position 'Vector': Current coordinates of the atom. + - angle Optional[float]: Angle between the current and preceding atom. + - force_positioned bool: Flag indicating whether the atom's position was calculated + forcibly. + - connected_to_ring bool: Flag indicating whether the atom is connected to a ring. + - draw_explicit bool: Flag indicating whether the atom should be drawn explicitly. + - neighbours List['AtomProperties']: List of AtomProperties objects representing atoms + with which the current atom forms bonds. + - previous_atom Optional['AtomProperties']: Reference to an AtomProperties object + representing the preceding atom in the chain. + """ + + self.id: int = atom_index + self.symbol: str = symbol + self.ring_indexes: List[int] = [] + self.rings: List['RingProperties'] = [] + + # self.original_rings: List['RingProperties'] = [] + self.anchored_rings: List['RingProperties'] = [] + self.is_bridge_atom: bool = False + self.is_bridge: bool = False + + self.bridged_ring = None + self.positioned: bool = False + + self.previous_position: 'Vector' = Vector(0, 0) + self.position: 'Vector' = Vector(0, 0) + self.angle: Optional[float] = None + self.force_positioned: bool = False + self.connected_to_ring: bool = False + self.draw_explicit: bool = False + self.neighbours: List['AtomProperties'] = [] + self.previous_atom: Optional['AtomProperties'] = None + + + def __eq__(self, other: 'AtomProperties') -> bool: + """ + Compares two AtomProperties instances for equality based on their identifiers. + + Parameters: + :param other 'AtomProperties': + Another instance of the AtomProperties class. + + Returns bool: + True if both instances represent atoms with the same identifier, otherwise False. + """ + return False if other is None else self.id == other.id + + + def set_position(self, vector: 'Vector') -> None: + """ + Sets the position of the current atom to the specified vector. + + Parameters: + :param vector 'Vector': + An instance of the Vector class, whose coordinates are assigned as the position of the current atom. + """ + self.position: 'Vector' = vector + + + def __hash__(self) -> int: + """ + Returns the hash value of the current object, which is the unique identifier of the atom. + + This method is used when the object needs to be inserted into a hash-based collection + such as a set or dictionary. + + Returns int: + The unique identifier of the current object of the class, used as the hash value. + """ + return self.id + + + def __repr__(self) -> str: + """ + Provides a human-readable representation of the AtomProperties object, primarily + intended for debugging purposes. + + The representation includes the atomic symbol followed by the atomic index, adjusted by + subtracting 1 due to the indexing convention in Chython where numbering starts from 1 + instead of 0. + + Returns str: + A string combining the atomic symbol and the adjusted atomic index. + """ + return f'{self.symbol}_{self.id - 1}' + + + def get_angle(self, reference_vector: Optional['Vector']=None) -> float: + """ + Calculates the angle between the current atom and either the previous atom or a + specified reference vector. + + By default, the angle is calculated between the current atom and the previous atom. + However, if a reference_vector is provided, the angle between the current atom and the + reference_vector will be calculated instead. + + Parameters: + :param reference_vector Optional['Vector']: + An object of the Vector class representing the coordinates with which the angle will + be calculated. If None, the angle between the current atom and the previous atom is + calculated. Defaults to None. + + Returns float: + The angle between the current atom and either the previous atom or the specified + reference vector, depending on the parameter provided. + """ + vector_1: float = self.position + vector_2: float = self.previous_position if not reference_vector else reference_vector + vector = Vector.subtract_vectors(vector_1, vector_2) + return vector.angle() + + + def copy(self) -> 'AtomProperties': + """ + Creates a deep copy of the current AtomProperties instance and returns it as a new + object of the same class. + + This method duplicates all attributes of the current atom, including its position, + connections, and identifiers, ensuring that modifications to the copy do not affect the + original atom object. + + Returns AtomProperties: + A new instance of the AtomProperties class with identical properties to the original + atom, but as a separate object in memory. + """ + new_atom = AtomProperties(self.id, self.symbol) + new_atom.ring_indexes =self.ring_indexes + new_atom.rings = self.rings + # new_atom.original_rings = self.original_rings + new_atom.anchored_rings = self.anchored_rings + new_atom.is_bridge_atom = self.is_bridge_atom + new_atom.is_bridge = self.is_bridge + new_atom.positioned = self.positioned + new_atom.previous_position = self.previous_position + new_atom.position = self.position + new_atom.angle = self.angle + new_atom.force_positioned = self.force_positioned + new_atom.connected_to_ring = self.connected_to_ring + new_atom.draw_explicit = self.draw_explicit + new_atom.neighbours = self.neighbours + new_atom.previous_atom = self.previous_atom + return new_atom + + + def is_terminal(self) -> bool: + "Returns boolean whether a given atom is terminal (has no more than one bond)." + return len(self.neighbours) <= 1 + + def set_previous_position(self, previous_atom: 'AtomProperties') -> None: + "Set previous position atom" + self.previous_position = previous_atom.position + self.previous_atom = previous_atom + + + + + + + + + + + + + +class RingOverlap: + """ + Initializes an instance of the RingOverlap class, which represents the overlap between + two rings. + """ + def __init__(self, ring_1: 'RingProperties', ring_2: 'RingProperties') -> None: + """ + This class is designed to handle situations where two rings share common atoms, + indicating an overlap or intersection between them. + + Parameters: + :param ring_1 RingProperties: + An instance of the RingProperties class representing the first ring involved in the + overlap. + :param ring_2 RingProperties: + An instance of the RingProperties class representing the second ring involved in the + overlap. + + Attributes: + - id: A unique identifier for the overlap instance. Initially set to None, indicating + that the overlap ID may need to be assigned externally. + - ring_id_1 int: The identifier of the first ring participating in the overlap. + - ring_id_2 int: The identifier of the second ring participating in the overlap. + - atoms List['AtomProperties']: A list of AtomProperties instances that are common to + both rings, representing the atoms where the overlap occurs. + + The constructor identifies the common atoms between the two rings by intersecting the + members of both rings and stores their identifiers for reference. + """ + self.id = None + self.ring_id_1: int = ring_1.id + self.ring_id_2: int = ring_2.id + self.atoms: List['AtomProperties'] = set(ring_1.members).intersection(set(ring_2.members)) + + + def __repr__(self) -> str: + """ + Provides a human-readable representation of the RingOverlap object, primarily intended + for debugging purposes. + + Returns a string that includes the overlap identifier and the identifiers of the rings + involved in the overlap, offering a convenient way to inspect the state of the object + quickly. + + Returns str: + A formatted string containing the overlap's unique identifier (`id`) and the + identifiers of the two rings (`ring_id_1` and `ring_id_2`) participating in the overlap. + This representation aids in quickly identifying the instance's state, especially useful + during debugging sessions. + """ + return f'{self.id=}, {self.ring_id_1=}, {self.ring_id_2=}' + + + def is_bridge(self) -> bool: + """ + Determines whether the overlap represents a bridge between rings based on the number of + common atoms and their ring memberships. + + This method checks if the current overlap involves more than two atoms or if any atom + within the overlap participates in more than two rings, indicating a bridging structure. + + Returns bool: + True if the overlap is considered part of a single bridge ring, otherwise False. + Specifically, returns True if either the number of atoms involved in the overlap + exceeds two or if any atom in the overlap belongs to more than two rings, suggesting + a complex bridging configuration. + """ + return len(self.atoms) > 2 or any(len(atom.rings) > 2 for atom in self.atoms) + # ниже старая версия функции + # if len(self.atoms) > 2: + # return True + # for atom in self.atoms: + # if len(atom.rings) > 2: + # return True + # return False + + + def involves_ring(self, ring_id: int) -> bool: + """ + Checks if a ring identified by `ring_id` is involved in the current overlap. + + This method determines whether the specified ring identifier matches either of the two + rings participating in the overlap represented by the current instance of RingOverlap. + + Parameters: + :param ring_id int: + The identifier of the ring to check for involvement in the overlap. + + Returns bool: + True if the specified ring identifier matches either `ring_id_1` or `ring_id_2`, + indicating that the ring is part of the current overlap. False otherwise. + """ + return self.ring_id_1 == ring_id or self.ring_id_2 == ring_id + + + def update_other(self, ring_id: int, other_ring_id: int) -> None: + """ + Updates the current attributes of the class depending on the other ring. + + This method adjusts the internal identifiers of the RingOverlap instance based on the + provided ring identifiers, ensuring that the instance accurately reflects the rings + involved in the overlap. + + Parameters: + :param ring_id int: + Identifier of the first ring to be considered for updating. + :param other_ring_id int: + Identifier of another ring, used to determine which attribute (ring_id_1 or + ring_id_2) needs to be updated. + + If the current instance's ring_id_1 matches other_ring_id, then ring_id is assigned to + ring_id_2, and vice versa. This ensures that the RingOverlap instance correctly tracks + the two rings involved in the overlap. + """ + if self.ring_id_1 == other_ring_id: + self.ring_id_2 = ring_id + else: + self.ring_id_1 = ring_id \ No newline at end of file diff --git a/chython/algorithms/calculate2d/__init__.py b/chython/algorithms/calculate2d/__init__.py index c8fe17a5..9cb04b96 100644 --- a/chython/algorithms/calculate2d/__init__.py +++ b/chython/algorithms/calculate2d/__init__.py @@ -1,205 +1 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2019-2024 Ramil Nugmanov -# Copyright 2019, 2020 Dinar Batyrshin -# This file is part of chython. -# -# chython is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program; if not, see . -# -from math import sqrt -from random import random -from typing import TYPE_CHECKING, Union -from ...exceptions import ImplementationError - - -try: - from importlib.resources import files -except ImportError: # python3.8 - from importlib_resources import files - - -if TYPE_CHECKING: - from chython import ReactionContainer, MoleculeContainer - -try: - from py_mini_racer.py_mini_racer import MiniRacer, JSEvalException - - ctx = MiniRacer() - ctx.eval('const self = this') - ctx.eval(files(__package__).joinpath('clean2d.js').read_text()) -except RuntimeError: - ctx = None - - -class Calculate2DMolecule: - __slots__ = () - - def clean2d(self: Union['MoleculeContainer', 'Calculate2DMolecule']): - """ - Calculate 2d layout of graph. https://pubs.acs.org/doi/10.1021/acs.jcim.7b00425 JS implementation used. - """ - if ctx is None: - raise ImportError('py_mini_racer is not installed or broken') - plane = {} - entry = iter(sorted(self, key=lambda n: len(self._bonds[n]))) - for _ in range(min(5, len(self))): - smiles, order = self.__clean2d_prepare(next(entry)) - try: - xy = ctx.call('$.clean2d', smiles) - except JSEvalException: - continue - break - else: - raise ImplementationError - - shift_x, shift_y = xy[0] - for n, (x, y) in zip(order, xy): - plane[n] = (x - shift_x, shift_y - y) - - bonds = [] - for n, m, _ in self.bonds(): - xn, yn = plane[n] - xm, ym = plane[m] - bonds.append(sqrt((xm - xn) ** 2 + (ym - yn) ** 2)) - if bonds: - bond_reduce = sum(bonds) / len(bonds) / .825 - else: - bond_reduce = 1. - - atoms = self._atoms - for n, (x, y) in plane.items(): - a = atoms[n] - a._x = x / bond_reduce - a._y = y / bond_reduce - - if self.connected_components_count > 1: - shift_x = 0. - for c in self.connected_components: - shift_x = self._fix_plane_mean(shift_x, component=c) + .9 - self.__dict__.pop('__cached_method__repr_svg_', None) - - def _fix_plane_mean(self: 'MoleculeContainer', shift_x: float, shift_y=0., component=None) -> float: - atoms = self._atoms - if component is None: - component = atoms - - left_atom = atoms[min(component, key=lambda x: atoms[x].x)] - right_atom = atoms[max(component, key=lambda x: atoms[x].x)] - - min_x = left_atom.x - shift_x - if len(left_atom.atomic_symbol) == 2: - min_x -= .2 - - max_x = right_atom.x - min_x - min_y = min(atoms[x].y for x in component) - max_y = max(atoms[x].y for x in component) - mean_y = (max_y + min_y) / 2 - shift_y - for n in component: - a = atoms[n] - a._x -= min_x - a._y -= mean_y - - if -.18 <= right_atom.y <= .18: - factor = right_atom.implicit_hydrogens - if factor == 1: - max_x += .15 - elif factor: - max_x += .25 - return max_x - - def _fix_plane_min(self: 'MoleculeContainer', shift_x: float, shift_y=0., component=None) -> float: - atoms = self._atoms - if component is None: - component = atoms - - right_atom = atoms[max(component, key=lambda x: atoms[x].x)] - min_x = min(atoms[x].x for x in component) - shift_x - max_x = right_atom.x - min_x - min_y = min(atoms[x].y for x in component) - shift_y - - for n in component: - a = atoms[n] - a._x -= min_x - a._y -= min_y - - if shift_y - .18 <= right_atom.y <= shift_y + .18: - factor = right_atom.implicit_hydrogens - if factor == 1: - max_x += .15 - elif factor: - max_x += .25 - return max_x - - def __clean2d_prepare(self: 'MoleculeContainer', entry): - w = {n: random() for n in self._atoms} - w[entry] = -1 - smiles, order = self._smiles(w.__getitem__, random=True, charges=False, stereo=False, _return_order=True) - return ''.join(smiles).replace('~', '-'), order - - -class Calculate2DReaction: - __slots__ = () - - def clean2d(self: 'ReactionContainer'): - """ - Recalculate 2d coordinates - """ - for m in self.molecules(): - m.clean2d() - self.fix_positions() - - def fix_positions(self: 'ReactionContainer'): - """ - Fix coordinates of molecules in reaction - """ - shift_x = 0 - reactants = self.reactants - amount = len(reactants) - 1 - signs = [] - for m in reactants: - max_x = m._fix_plane_mean(shift_x) - if amount: - max_x += .2 - signs.append(max_x) - amount -= 1 - shift_x = max_x + 1 - arrow_min = shift_x - - if self.reagents: - shift_x += .4 - for m in self.reagents: - max_x = m._fix_plane_min(shift_x, .5) - shift_x = max_x + 1 - shift_x += .4 - if shift_x - arrow_min < 3: - shift_x = arrow_min + 3 - else: - shift_x += 3 - arrow_max = shift_x - 1 - - products = self.products - amount = len(products) - 1 - for m in products: - max_x = m._fix_plane_mean(shift_x) - if amount: - max_x += .2 - signs.append(max_x) - amount -= 1 - shift_x = max_x + 1 - self._arrow = (arrow_min, arrow_max) - self._signs = tuple(signs) - self.flush_cache() - - -__all__ = ['Calculate2DMolecule', 'Calculate2DReaction'] +from .clean2d import Calculate2DMolecule, Calculate2DReaction \ No newline at end of file diff --git a/chython/algorithms/calculate2d/clean2d.js b/chython/algorithms/calculate2d/clean2d.js deleted file mode 100644 index 6c60ef9b..00000000 --- a/chython/algorithms/calculate2d/clean2d.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.$=e():t.$=e()}(self,(function(){return(()=>{var t={348:t=>{class e{static clone(t){let i=Array.isArray(t)?Array():{};for(let r in t){let n=t[r];"function"==typeof n.clone?i[r]=n.clone():i[r]="object"==typeof n?e.clone(n):n}return i}static equals(t,e){if(t.length!==e.length)return!1;let i=t.slice().sort(),r=e.slice().sort();for(var n=0;n-1&&t.splice(i,1),t}static removeAll(t,e){return t.filter((function(t){return-1===e.indexOf(t)}))}static merge(t,e){let i=new Array(t.length+e.length);for(let e=0;e{const r=i(348);i(843),i(421);class n{constructor(t,e="-"){this.element=1===t.length?t.toUpperCase():t,this.drawExplicit=!1,this.ringbonds=Array(),this.rings=Array(),this.bondType=e,this.branchBond=null,this.isBridge=!1,this.isBridgeNode=!1,this.originalRings=Array(),this.bridgedRing=null,this.anchoredRings=Array(),this.bracket=null,this.plane=0,this.attachedPseudoElements={},this.hasAttachedPseudoElements=!1,this.isDrawn=!0,this.isConnectedToRing=!1,this.neighbouringElements=Array(),this.isPartOfAromaticRing=t!==this.element,this.bondCount=0,this.chirality="",this.isStereoCenter=!1,this.priority=0,this.mainChain=!1,this.hydrogenDirection="down",this.subtreeDepth=1,this.hasHydrogen=!1,this.class=void 0}addNeighbouringElement(t){this.neighbouringElements.push(t)}attachPseudoElement(t,e,i=0,r=0){null===i&&(i=0),null===r&&(r=0);let n=i+t+r;this.attachedPseudoElements[n]?this.attachedPseudoElements[n].count+=1:this.attachedPseudoElements[n]={element:t,count:1,hydrogenCount:i,previousElement:e,charge:r},this.hasAttachedPseudoElements=!0}getAttachedPseudoElements(){let t={},e=this;return Object.keys(this.attachedPseudoElements).sort().forEach((function(i){t[i]=e.attachedPseudoElements[i]})),t}getAttachedPseudoElementsCount(){return Object.keys(this.attachedPseudoElements).length}isHeteroAtom(){return"C"!==this.element&&"H"!==this.element}addAnchoredRing(t){r.contains(this.anchoredRings,{value:t})||this.anchoredRings.push(t)}getRingbondCount(){return this.ringbonds.length}backupRings(){this.originalRings=Array(this.rings.length);for(let t=0;t{const r=i(474),n=i(614),{getChargeText:s}=(i(929),i(843),i(421),i(537));t.exports=class{constructor(t,e,i){this.canvas="string"==typeof t||t instanceof String?document.getElementById(t):t,this.ctx=this.canvas.getContext("2d"),this.themeManager=e,this.opts=i,this.drawingWidth=0,this.drawingHeight=0,this.offsetX=0,this.offsetY=0,this.fontLarge=this.opts.fontSizeLarge+"pt Helvetica, Arial, sans-serif",this.fontSmall=this.opts.fontSizeSmall+"pt Helvetica, Arial, sans-serif",this.updateSize(this.opts.width,this.opts.height),this.ctx.font=this.fontLarge,this.hydrogenWidth=this.ctx.measureText("H").width,this.halfHydrogenWidth=this.hydrogenWidth/2,this.halfBondThickness=this.opts.bondThickness/2}updateSize(t,e){this.devicePixelRatio=window.devicePixelRatio||1,this.backingStoreRatio=this.ctx.webkitBackingStorePixelRatio||this.ctx.mozBackingStorePixelRatio||this.ctx.msBackingStorePixelRatio||this.ctx.oBackingStorePixelRatio||this.ctx.backingStorePixelRatio||1,this.ratio=this.devicePixelRatio/this.backingStoreRatio,1!==this.ratio?(this.canvas.width=t*this.ratio,this.canvas.height=e*this.ratio,this.canvas.style.width=t+"px",this.canvas.style.height=e+"px",this.ctx.setTransform(this.ratio,0,0,this.ratio,0,0)):(this.canvas.width=t*this.ratio,this.canvas.height=e*this.ratio)}setTheme(t){this.colors=t}scale(t){let e=-Number.MAX_VALUE,i=-Number.MAX_VALUE,r=Number.MAX_VALUE,n=Number.MAX_VALUE;for(var s=0;so.x&&(r=o.x),n>o.y&&(n=o.y)}var o=this.opts.padding;e+=o,i+=o,r-=o,n-=o,this.drawingWidth=e-r,this.drawingHeight=i-n;var h=this.canvas.offsetWidth/this.drawingWidth,a=this.canvas.offsetHeight/this.drawingHeight,l=h.5&&(e.stroke(),e.beginPath(),e.strokeStyle=this.themeManager.getColor(t.getRightElement())||this.themeManager.getColor("C"),m=!0),r.subtract(o),e.moveTo(r.x,r.y),r.add(n.multiplyScalar(o,2)),e.lineTo(r.x,r.y)}e.stroke(),e.restore()}drawDebugText(t,e,i){let r=this.ctx;r.save(),r.font="5px Droid Sans, sans-serif",r.textAlign="start",r.textBaseline="top",r.fillStyle="#ff0000",r.fillText(i,t+this.offsetX,e+this.offsetY),r.restore()}drawBall(t,e,i){let n=this.ctx;n.save(),n.beginPath(),n.arc(t+this.offsetX,e+this.offsetY,this.opts.bondLength/4.5,0,r.twoPI,!1),n.fillStyle=this.themeManager.getColor(i),n.fill(),n.restore()}drawPoint(t,e,i){let n=this.ctx,s=this.offsetX,o=this.offsetY;n.save(),n.globalCompositeOperation="destination-out",n.beginPath(),n.arc(t+s,e+o,1.5,0,r.twoPI,!0),n.closePath(),n.fill(),n.globalCompositeOperation="source-over",n.beginPath(),n.arc(t+this.offsetX,e+this.offsetY,.75,0,r.twoPI,!1),n.fillStyle=this.themeManager.getColor(i),n.fill(),n.restore()}drawText(t,e,i,n,o,h,a,l,g,d={}){let u=this.ctx,c=this.offsetX,p=this.offsetY;u.save(),u.textAlign="start",u.textBaseline="alphabetic";let f="",v=0;a&&(f=s(a),u.font=this.fontSmall,v=u.measureText(f).width);let m="0",b=0;l>0&&(m=l.toString(),u.font=this.fontSmall,b=u.measureText(m).width),1===a&&"N"===i&&d.hasOwnProperty("0O")&&d.hasOwnProperty("0O-1")&&(d={"0O":{element:"O",count:2,hydrogenCount:0,previousElement:"C",charge:""}},a=0),u.font=this.fontLarge,u.fillStyle=this.themeManager.getColor("BACKGROUND");let y=u.measureText(i);y.totalWidth=y.width+v,y.height=parseInt(this.fontLarge,10);let x=y.width>this.opts.fontSizeLarge?y.width:this.opts.fontSizeLarge;x/=1.5,u.globalCompositeOperation="destination-out",u.beginPath(),u.arc(t+c,e+p,x,0,r.twoPI,!0),u.closePath(),u.fill(),u.globalCompositeOperation="source-over";let S=-y.width/2,A=-y.width/2;u.fillStyle=this.themeManager.getColor(i),u.fillText(i,t+c+S,e+this.opts.halfFontSizeLarge+p),S+=y.width,a&&(u.font=this.fontSmall,u.fillText(f,t+c+S,e-this.opts.fifthFontSizeSmall+p),S+=v),l>0&&(u.font=this.fontSmall,u.fillText(m,t+c+A-b,e-this.opts.fifthFontSizeSmall+p),A-=b),u.font=this.fontLarge;let C=0,R=0;if(1===n){let i=t+c,r=e+p+this.opts.halfFontSizeLarge;C=this.hydrogenWidth,A-=C,"left"===o?i+=A:"right"===o||"up"===o&&h||"down"===o&&h?i+=S:"up"!==o||h?"down"!==o||h||(r+=this.opts.fontSizeLarge+this.opts.quarterFontSizeLarge,i-=this.halfHydrogenWidth):(r-=this.opts.fontSizeLarge+this.opts.quarterFontSizeLarge,i-=this.halfHydrogenWidth),u.fillText("H",i,r),S+=C}else if(n>1){let i=t+c,r=e+p+this.opts.halfFontSizeLarge;C=this.hydrogenWidth,u.font=this.fontSmall,R=u.measureText(n).width,A-=C+R,"left"===o?i+=A:"right"===o||"up"===o&&h||"down"===o&&h?i+=S:"up"!==o||h?"down"!==o||h||(r+=this.opts.fontSizeLarge+this.opts.quarterFontSizeLarge,i-=this.halfHydrogenWidth):(r-=this.opts.fontSizeLarge+this.opts.quarterFontSizeLarge,i-=this.halfHydrogenWidth),u.font=this.fontLarge,u.fillText("H",i,r),u.font=this.fontSmall,u.fillText(n,i+this.halfHydrogenWidth+R,r+this.opts.fifthFontSizeSmall),S+=C+this.halfHydrogenWidth+R}for(let i in d){if(!d.hasOwnProperty(i))continue;let r=0,n=0,h=d[i].element,a=d[i].count,l=d[i].hydrogenCount,g=d[i].charge;u.font=this.fontLarge,a>1&&l>0&&(r=u.measureText("(").width,n=u.measureText(")").width);let f=u.measureText(h).width,v=0,m="",b=0;C=0,l>0&&(C=this.hydrogenWidth),u.font=this.fontSmall,a>1&&(v=u.measureText(a).width),0!==g&&(m=s(g),b=u.measureText(m).width),R=0,l>1&&(R=u.measureText(l).width),u.font=this.fontLarge;let y=t+c,x=e+p+this.opts.halfFontSizeLarge;u.fillStyle=this.themeManager.getColor(h),a>0&&(A-=v),a>1&&l>0&&("left"===o?(A-=n,u.fillText(")",y+A,x)):(u.fillText("(",y+S,x),S+=r)),"left"===o?(A-=f,u.fillText(h,y+A,x)):(u.fillText(h,y+S,x),S+=f),l>0&&("left"===o?(A-=C+R,u.fillText("H",y+A,x),l>1&&(u.font=this.fontSmall,u.fillText(l,y+A+C,x+this.opts.fifthFontSizeSmall))):(u.fillText("H",y+S,x),S+=C,l>1&&(u.font=this.fontSmall,u.fillText(l,y+S,x+this.opts.fifthFontSizeSmall),S+=R))),u.font=this.fontLarge,a>1&&l>0&&("left"===o?(A-=r,u.fillText("(",y+A,x)):(u.fillText(")",y+S,x),S+=n)),u.font=this.fontSmall,a>1&&("left"===o?u.fillText(a,y+A+r+n+C+R+f,x+this.opts.fifthFontSizeSmall):(u.fillText(a,y+S,x+this.opts.fifthFontSizeSmall),S+=v)),0!==g&&("left"===o?u.fillText(m,y+A+r+n+C+R+f,e-this.opts.fifthFontSizeSmall+p):(u.fillText(m,y+S,e-this.opts.fifthFontSizeSmall+p),S+=b))}u.restore()}getChargeText(t){return 1===t?"+":2===t?"2+":-1===t?"-":-2===t?"2-":""}drawDebugPoint(t,e,i="",r="#f00"){this.drawCircle(t,e,2,r,!0,!0,i)}drawAromaticityRing(t){let e=this.ctx,i=r.apothemFromSideLength(this.opts.bondLength,t.getSize());e.save(),e.strokeStyle=this.themeManager.getColor("C"),e.lineWidth=this.opts.bondThickness,e.beginPath(),e.arc(t.center.x+this.offsetX,t.center.y+this.offsetY,i-this.opts.bondSpacing,0,2*Math.PI,!0),e.closePath(),e.stroke(),e.restore()}clear(){this.ctx.clearRect(0,0,this.canvas.offsetWidth,this.canvas.offsetHeight)}}},237:(t,e,i)=>{const r=i(474),n=i(348),s=i(614),o=i(929),h=(i(843),i(826)),a=i(427),l=i(421),g=i(333),d=i(841),u=i(707),c=i(473),p=i(654),f=i(207);t.exports=class{constructor(t){this.graph=null,this.doubleBondConfigCount=0,this.doubleBondConfig=null,this.ringIdCounter=0,this.ringConnectionIdCounter=0,this.canvasWrapper=null,this.totalOverlapScore=0,this.defaultOptions={width:500,height:500,scale:0,bondThickness:1,bondLength:30,shortBondLength:.8,bondSpacing:.17*30,atomVisualization:"default",isomeric:!0,debug:!1,terminalCarbons:!1,explicitHydrogens:!0,overlapSensitivity:.42,overlapResolutionIterations:1,compactDrawing:!0,fontFamily:"Arial, Helvetica, sans-serif",fontSizeLarge:11,fontSizeSmall:3,padding:10,experimentalSSSR:!1,kkThreshold:.1,kkInnerThreshold:.1,kkMaxIteration:2e4,kkMaxInnerIteration:50,kkMaxEnergy:1e9,themes:{dark:{C:"#fff",O:"#e74c3c",N:"#3498db",F:"#27ae60",CL:"#16a085",BR:"#d35400",I:"#8e44ad",P:"#d35400",S:"#f1c40f",B:"#e67e22",SI:"#e67e22",H:"#aaa",BACKGROUND:"#141414"},light:{C:"#222",O:"#e74c3c",N:"#3498db",F:"#27ae60",CL:"#16a085",BR:"#d35400",I:"#8e44ad",P:"#d35400",S:"#f1c40f",B:"#e67e22",SI:"#e67e22",H:"#666",BACKGROUND:"#fff"},oldschool:{C:"#000",O:"#000",N:"#000",F:"#000",CL:"#000",BR:"#000",I:"#000",P:"#000",S:"#000",B:"#000",SI:"#000",H:"#000",BACKGROUND:"#fff"},solarized:{C:"#586e75",O:"#dc322f",N:"#268bd2",F:"#859900",CL:"#16a085",BR:"#cb4b16",I:"#6c71c4",P:"#d33682",S:"#b58900",B:"#2aa198",SI:"#2aa198",H:"#657b83",BACKGROUND:"#fff"},"solarized-dark":{C:"#93a1a1",O:"#dc322f",N:"#268bd2",F:"#859900",CL:"#16a085",BR:"#cb4b16",I:"#6c71c4",P:"#d33682",S:"#b58900",B:"#2aa198",SI:"#2aa198",H:"#839496",BACKGROUND:"#fff"},matrix:{C:"#678c61",O:"#2fc079",N:"#4f7e7e",F:"#90d762",CL:"#82d967",BR:"#23755a",I:"#409931",P:"#c1ff8a",S:"#faff00",B:"#50b45a",SI:"#409931",H:"#426644",BACKGROUND:"#fff"},github:{C:"#24292f",O:"#cf222e",N:"#0969da",F:"#2da44e",CL:"#6fdd8b",BR:"#bc4c00",I:"#8250df",P:"#bf3989",S:"#d4a72c",B:"#fb8f44",SI:"#bc4c00",H:"#57606a",BACKGROUND:"#fff"},carbon:{C:"#161616",O:"#da1e28",N:"#0f62fe",F:"#198038",CL:"#007d79",BR:"#fa4d56",I:"#8a3ffc",P:"#ff832b",S:"#f1c21b",B:"#8a3800",SI:"#e67e22",H:"#525252",BACKGROUND:"#fff"},cyberpunk:{C:"#ea00d9",O:"#ff3131",N:"#0abdc6",F:"#00ff9f",CL:"#00fe00",BR:"#fe9f20",I:"#ff00ff",P:"#fe7f00",S:"#fcee0c",B:"#ff00ff",SI:"#ffffff",H:"#913cb1",BACKGROUND:"#fff"},gruvbox:{C:"#665c54",O:"#cc241d",N:"#458588",F:"#98971a",CL:"#79740e",BR:"#d65d0e",I:"#b16286",P:"#af3a03",S:"#d79921",B:"#689d6a",SI:"#427b58",H:"#7c6f64",BACKGROUND:"#fbf1c7"},"gruvbox-dark":{C:"#ebdbb2",O:"#cc241d",N:"#458588",F:"#98971a",CL:"#b8bb26",BR:"#d65d0e",I:"#b16286",P:"#fe8019",S:"#d79921",B:"#8ec07c",SI:"#83a598",H:"#bdae93",BACKGROUND:"#282828"},custom:{C:"#222",O:"#e74c3c",N:"#3498db",F:"#27ae60",CL:"#16a085",BR:"#d35400",I:"#8e44ad",P:"#d35400",S:"#f1c40f",B:"#e67e22",SI:"#e67e22",H:"#666",BACKGROUND:"#fff"}}},this.opts=f.extend(!0,this.defaultOptions,t),this.opts.halfBondSpacing=this.opts.bondSpacing/2,this.opts.bondLengthSq=this.opts.bondLength*this.opts.bondLength,this.opts.halfFontSizeLarge=this.opts.fontSizeLarge/2,this.opts.quarterFontSizeLarge=this.opts.fontSizeLarge/4,this.opts.fifthFontSizeSmall=this.opts.fontSizeSmall/5,this.theme=this.opts.themes.dark}draw(t,e,i="light",r=!1){this.initDraw(t,i,r),this.infoOnly||(this.themeManager=new p(this.opts.themes,i),this.canvasWrapper=new d(e,this.themeManager,this.opts)),r||(this.processGraph(),this.canvasWrapper.scale(this.graph.vertices),this.drawEdges(this.opts.debug),this.drawVertices(this.opts.debug),this.canvasWrapper.reset(),this.opts.debug&&(console.log(this.graph),console.log(this.rings),console.log(this.ringConnections)))}edgeRingCount(t){let e=this.graph.edges[t],i=this.graph.vertices[e.sourceId],r=this.graph.vertices[e.targetId];return Math.min(i.value.rings.length,r.value.rings.length)}getBridgedRings(){let t=Array();for(var e=0;ei&&(i=h,t=r,e=n)}}let o=-s.subtract(this.graph.vertices[t].position,this.graph.vertices[e].position).angle();if(!isNaN(o)){let t=o%.523599;for(t<.2617995?o-=t:o+=.523599-t,r=0;r1?t:""),i.delete("C")}if(i.has("H")){let t=i.get("H");e+="H"+(t>1?t:""),i.delete("H")}return Object.keys(a.atomicNumbers).sort().map((t=>{if(i.has(t)){let r=i.get(t);e+=t+(r>1?r:"")}})),e}getRingbondType(t,e){if(t.value.getRingbondCount()<1||e.value.getRingbondCount()<1)return null;for(var i=0;in&&(s=e.sourceId,o=e.targetId),this.getSubtreeOverlapScore(o,s,t.vertexScores).value>this.opts.overlapSensitivity){let e=this.graph.vertices[s],i=this.graph.vertices[o],n=i.getNeighbours(s);if(1===n.length){let t=this.graph.vertices[n[0]],s=t.position.getRotateAwayFromAngle(e.position,i.position,r.toRad(120));this.rotateSubtree(t.id,i.id,s,i.position);let o=this.getOverlapScore().total;o>this.totalOverlapScore?this.rotateSubtree(t.id,i.id,-s,i.position):this.totalOverlapScore=o}else if(2===n.length){if(0!==i.value.rings.length&&0!==e.value.rings.length)continue;let t=this.graph.vertices[n[0]],s=this.graph.vertices[n[1]];if(1===t.value.rings.length&&1===s.value.rings.length){if(t.value.rings[0]!==s.value.rings[0])continue}else{if(0!==t.value.rings.length||0!==s.value.rings.length)continue;{let n=t.position.getRotateAwayFromAngle(e.position,i.position,r.toRad(120)),o=s.position.getRotateAwayFromAngle(e.position,i.position,r.toRad(120));this.rotateSubtree(t.id,i.id,n,i.position),this.rotateSubtree(s.id,i.id,o,i.position);let h=this.getOverlapScore().total;h>this.totalOverlapScore?(this.rotateSubtree(t.id,i.id,-n,i.position),this.rotateSubtree(s.id,i.id,-o,i.position)):this.totalOverlapScore=h}}}t=this.getOverlapScore()}}}this.resolveSecondaryOverlaps(t.scores),this.opts.isomeric&&this.annotateStereochemistry(),this.opts.compactDrawing&&"default"===this.opts.atomVisualization&&this.initPseudoElements(),this.rotateDrawing()}initRings(){let t=new Map;for(var e=this.graph.vertices.length-1;e>=0;e--){let r=this.graph.vertices[e];if(0!==r.value.ringbonds.length)for(var i=0;i0&&this.addRingConnection(n)}for(e=0;e0;){let t=-1;for(e=0;er&&(r=e,n=t)}return n}getVerticesAt(t,e,i){let r=Array();for(var n=0;ni;){let n=this.graph.vertices[i],o=this.graph.vertices[r];if(!n.value.isDrawn||!o.value.isDrawn)continue;let h=s.subtract(n.position,o.position).lengthSq();if(hd[1]?0:1,sideCount:l,position:l[0]>l[1]?0:1,anCount:o,bnCount:h}}setRingCenter(t){let e=t.getSize(),i=new s(0,0);for(var r=0;r1||0==e.bnCount&&e.anCount>1){c[0].multiplyScalar(i.opts.halfBondSpacing),c[1].multiplyScalar(i.opts.halfBondSpacing);let t=new o(s.add(d,c[0]),s.add(u,c[0]),l,g),e=new o(s.add(d,c[1]),s.add(u,c[1]),l,g);this.canvasWrapper.drawLine(t),this.canvasWrapper.drawLine(e)}else if(e.sideCount[0]>e.sideCount[1]){c[0].multiplyScalar(i.opts.bondSpacing),c[1].multiplyScalar(i.opts.bondSpacing);let t=new o(s.add(d,c[0]),s.add(u,c[0]),l,g);t.shorten(this.opts.bondLength-this.opts.shortBondLength*this.opts.bondLength),this.canvasWrapper.drawLine(t),this.canvasWrapper.drawLine(new o(d,u,l,g))}else if(e.sideCount[0]e.totalSideCount[1]){c[0].multiplyScalar(i.opts.bondSpacing),c[1].multiplyScalar(i.opts.bondSpacing);let t=new o(s.add(d,c[0]),s.add(u,c[0]),l,g);t.shorten(this.opts.bondLength-this.opts.shortBondLength*this.opts.bondLength),this.canvasWrapper.drawLine(t),this.canvasWrapper.drawLine(new o(d,u,l,g))}else if(e.totalSideCount[0]<=e.totalSideCount[1]){c[0].multiplyScalar(i.opts.bondSpacing),c[1].multiplyScalar(i.opts.bondSpacing);let t=new o(s.add(d,c[1]),s.add(u,c[1]),l,g);t.shorten(this.opts.bondLength-this.opts.shortBondLength*this.opts.bondLength),this.canvasWrapper.drawLine(t),this.canvasWrapper.drawLine(new o(d,u,l,g))}}else if("#"===r.bondType){c[0].multiplyScalar(i.opts.bondSpacing/1.5),c[1].multiplyScalar(i.opts.bondSpacing/1.5);let t=new o(s.add(d,c[0]),s.add(u,c[0]),l,g),e=new o(s.add(d,c[1]),s.add(u,c[1]),l,g);this.canvasWrapper.drawLine(t),this.canvasWrapper.drawLine(e),this.canvasWrapper.drawLine(new o(d,u,l,g))}else if("."===r.bondType);else{let t=h.value.isStereoCenter,e=a.value.isStereoCenter;"up"===r.wedge?this.canvasWrapper.drawWedge(new o(d,u,l,g,t,e)):"down"===r.wedge?this.canvasWrapper.drawDashedWedge(new o(d,u,l,g,t,e)):this.canvasWrapper.drawLine(new o(d,u,l,g,t,e))}if(e){let e=s.midpoint(d,u);this.canvasWrapper.drawDebugText(e.x,e.y,"e: "+t)}}drawVertices(t){var e=this.graph.vertices.length;for(e=0;e0&&(t=this.graph.vertices[this.rings[0].members[0]]),null===t&&(t=this.graph.vertices[0]),this.createNextBond(t,null,0)}backupRingInformation(){this.originalRings=Array(),this.originalRingConnections=Array();for(var t=0;ts.subtract(e,l[0]).lengthSq()&&(u=l[1]);let c=s.subtract(o.position,u),p=s.subtract(h.position,u);-1===c.clockwise(p)?i.positioned||this.createRing(i,u,o,h):i.positioned||this.createRing(i,u,h,o)}else if(1===n.length){t.isSpiro=!0,i.isSpiro=!0;let o=this.graph.vertices[n[0]],h=s.subtract(e,o.position);h.invert(),h.normalize();let a=r.polyCircumradius(this.opts.bondLength,i.getSize());h.multiplyScalar(a),h.add(o.position),i.positioned||this.createRing(i,h,o)}}for(p=0;pr.opts.overlapSensitivity&&(n+=e,h++);let s=r.graph.vertices[t.id].position.clone();s.multiplyScalar(e),o.add(s)})),o.divide(n),{value:n/h,center:o}}getCurrentCenterOfMass(){let t=new s(0,0),e=0;for(var i=0;i1){let e=Array();for(var n=0;nh&&(this.rotateSubtree(t.id,e.common.id,2*r,e.common.position),this.rotateSubtree(i.id,e.common.id,-2*r,e.common.position))}else 1===e.vertices.length&&e.rings.length}}resolveSecondaryOverlaps(t){for(var e=0;ethis.opts.overlapSensitivity){let i=this.graph.vertices[t[e].id];if(i.isTerminal()){let t=this.getClosestVertex(i);if(t){let e=null;e=t.isTerminal()?0===t.id?this.graph.vertices[1].position:t.previousPosition:0===t.id?this.graph.vertices[1].position:t.position;let n=0===i.id?this.graph.vertices[1].position:i.previousPosition;i.position.rotateAwayFrom(e,n,r.toRad(20))}}}}getLastVertexWithAngle(t){let e=0,i=null;for(;!e&&t;)i=this.graph.vertices[t],e=i.angle,t=i.parentVertexId;return i}createNextBond(t,e=null,i=0,o=!1,h=!1){if(t.positioned&&!h)return;let a=!1;if(e){let i=this.graph.getEdge(t.id,e.id);"/"!==i.bondType&&"\\"!==i.bondType||++this.doubleBondConfigCount%2!=1||null===this.doubleBondConfig&&(this.doubleBondConfig=i.bondType,a=!0,null===e.parentVertexId&&t.value.branchBond&&("/"===this.doubleBondConfig?this.doubleBondConfig="\\":"\\"===this.doubleBondConfig&&(this.doubleBondConfig="/")))}if(!h)if(e)if(e.value.rings.length>0){let i=e.neighbours,r=null,o=new s(0,0);if(null===e.value.bridgedRing&&e.value.rings.length>1)for(var l=0;l0){let e=this.getRing(t.value.rings[0]);if(!e.positioned){let i=s.subtract(t.previousPosition,t.position);i.invert(),i.normalize();let n=r.polyCircumradius(this.opts.bondLength,e.getSize());i.multiplyScalar(n),i.add(t.position),this.createRing(e,i,t)}}else{t.value.isStereoCenter;let i=t.getNeighbours(),h=Array();for(l=0;l0){let e=r.toRad(60),n=-e,o=new s(this.opts.bondLength,0),h=new s(this.opts.bondLength,0);o.rotate(e).add(t.position),h.rotate(n).add(t.position);let a=this.getCurrentCenterOfMass(),l=o.distanceSq(a),d=h.distanceSq(a);i.angle=l3?r=r>0?Math.min(1.0472,r):r<0?Math.max(-1.0472,r):1.0472:r||(r=this.getLastVertexWithAngle(t.id).angle,r||(r=1.0472)),e&&!a){let e=this.graph.getEdge(t.id,i.id).bondType;"/"===e?("/"===this.doubleBondConfig||"\\"===this.doubleBondConfig&&(r=-r),this.doubleBondConfig=null):"\\"===e&&("/"===this.doubleBondConfig?r=-r:this.doubleBondConfig,this.doubleBondConfig=null)}i.angle=o?r:-r,this.createNextBond(i,t,g+i.angle)}}else if(2===h.length){let i=t.angle;i||(i=1.0472);let r=this.graph.getTreeDepth(h[0],t.id),n=this.graph.getTreeDepth(h[1],t.id),s=this.graph.vertices[h[0]],o=this.graph.vertices[h[1]];s.value.subtreeDepth=r,o.value.subtreeDepth=n;let a=this.graph.getTreeDepth(e?e.id:null,t.id);e&&(e.value.subtreeDepth=a);let l=0,d=1;"C"===o.value.element&&"C"!==s.value.element&&n>1&&r<5?(l=1,d=0):"C"!==o.value.element&&"C"===s.value.element&&r>1&&n<5?(l=0,d=1):n>r&&(l=1,d=0);let u=this.graph.vertices[h[l]],c=this.graph.vertices[h[d]],p=(this.graph.getEdge(t.id,u.id),this.graph.getEdge(t.id,c.id),!1);ai&&n>s?(o=this.graph.vertices[h[1]],a=this.graph.vertices[h[0]],l=this.graph.vertices[h[2]]):s>i&&s>n&&(o=this.graph.vertices[h[2]],a=this.graph.vertices[h[0]],l=this.graph.vertices[h[1]]),e&&e.value.rings.length<1&&o.value.rings.length<1&&a.value.rings.length<1&&l.value.rings.length<1&&1===this.graph.getTreeDepth(a.id,t.id)&&1===this.graph.getTreeDepth(l.id,t.id)&&this.graph.getTreeDepth(o.id,t.id)>1?(o.angle=-t.angle,t.angle>=0?(a.angle=r.toRad(30),l.angle=r.toRad(90)):(a.angle=-r.toRad(30),l.angle=-r.toRad(90)),this.createNextBond(o,t,g+o.angle),this.createNextBond(a,t,g+a.angle),this.createNextBond(l,t,g+l.angle)):(o.angle=0,a.angle=r.toRad(90),l.angle=-r.toRad(90),this.createNextBond(o,t,g+o.angle),this.createNextBond(a,t,g+a.angle),this.createNextBond(l,t,g+l.angle))}else if(4===h.length){let e=this.graph.getTreeDepth(h[0],t.id),i=this.graph.getTreeDepth(h[1],t.id),n=this.graph.getTreeDepth(h[2],t.id),s=this.graph.getTreeDepth(h[3],t.id),o=this.graph.vertices[h[0]],a=this.graph.vertices[h[1]],l=this.graph.vertices[h[2]],d=this.graph.vertices[h[3]];o.value.subtreeDepth=e,a.value.subtreeDepth=i,l.value.subtreeDepth=n,d.value.subtreeDepth=s,i>e&&i>n&&i>s?(o=this.graph.vertices[h[1]],a=this.graph.vertices[h[0]],l=this.graph.vertices[h[2]],d=this.graph.vertices[h[3]]):n>e&&n>i&&n>s?(o=this.graph.vertices[h[2]],a=this.graph.vertices[h[0]],l=this.graph.vertices[h[1]],d=this.graph.vertices[h[3]]):s>e&&s>i&&s>n&&(o=this.graph.vertices[h[3]],a=this.graph.vertices[h[0]],l=this.graph.vertices[h[1]],d=this.graph.vertices[h[2]]),o.angle=-r.toRad(36),a.angle=r.toRad(36),l.angle=-r.toRad(108),d.angle=r.toRad(108),this.createNextBond(o,t,g+o.angle),this.createNextBond(a,t,g+a.angle),this.createNextBond(l,t,g+l.angle),this.createNextBond(d,t,g+d.angle)}}}getCommonRingbondNeighbour(t){let e=t.neighbours;for(var i=0;i0&&i.value.rings.length>0&&this.areVerticesInSameRing(e,i))}isRingAromatic(t){for(var e=0;el&&(l=a[e][1].length),i=0;ig&&(g=a[e][1][i].length);for(e=0;ee[1][i][r])return-1;if(t[1][i][r]1&&s.value.hasHydrogen,C=s.value.hasHydrogen?1:0;for(e=0;ee[0]?-1:t[0]=0&&(i=i===y?x:y,o[d[e]]!==t);e--);this.graph.getEdge(s.id,t).wedge=i}}s.value.chirality=b}}visitStereochemistry(t,e,i,r,n,s,o=0){i[t]=1;let h=this.graph.vertices[t],a=h.value.getAtomicNumber();r.length<=s&&r.push(Array());for(var l=0;l0)continue;if("P"===i.value.element)continue;if("C"===i.value.element&&3===n.length&&"N"===n[0].value.element&&"N"===n[1].value.element&&"N"===n[2].value.element)continue;let s=0,o=0;for(e=0;e1&&o++}if(o>1||s<2)continue;let h=null;for(e=0;e1&&(h=t)}for(e=0;e1)continue;t.value.isDrawn=!1;let r=a.maxBonds[t.value.element]-t.value.bondCount,s="";t.value.bracket&&(r=t.value.bracket.hcount,s=t.value.bracket.charge||0),i.value.attachPseudoElement(t.value.element,h?h.value.element:null,r,s)}}for(t=0;t{class e{constructor(t,e,i=1){this.id=null,this.sourceId=t,this.targetId=e,this.weight=i,this.bondType="-",this.isPartOfAromaticRing=!1,this.center=!1,this.wedge=""}setBondType(t){this.bondType=t,this.weight=e.bonds[t]}static get bonds(){return{"-":1,"/":1,"\\":1,"=":2,"#":3,$:4}}}t.exports=e},707:(t,e,i)=>{const r=i(474),n=(i(614),i(843)),s=i(826),o=(i(421),i(427));class h{constructor(t,e=!1){this.vertices=Array(),this.edges=Array(),this.vertexIdsToEdgeId={},this.isomeric=e,this._time=0,this._init(t)}_init(t,e=0,i=null,r=!1){let h=new o(t.atom.element?t.atom.element:t.atom,t.bond);h.branchBond=t.branchBond,h.ringbonds=t.ringbonds,h.bracket=t.atom.element?t.atom:null,h.class=t.atom.class;let a=new n(h),l=this.vertices[i];if(this.addVertex(a),null!==i){a.setParentVertexId(i),a.value.addNeighbouringElement(l.value.element),l.addChild(a.id),l.value.addNeighbouringElement(h.element),l.spanningTreeChildren.push(a.id);let t=new s(i,a.id,1),e=null;r?(t.setBondType(a.value.branchBond||"-"),e=a.id,t.setBondType(a.value.branchBond||"-"),e=a.id):(t.setBondType(l.value.bondType||"-"),e=l.id),this.addEdge(t)}let g=t.ringbondCount+1;h.bracket&&(g+=h.bracket.hcount);let d=0;if(h.bracket&&h.bracket.chirality){h.isStereoCenter=!0,d=h.bracket.hcount;for(var u=0;ui[r][s]+i[s][n]&&(i[r][n]=i[r][s]+i[s][n]);return i}getSubgraphDistanceMatrix(t){let e=t.length,i=this.getSubgraphAdjacencyMatrix(t),r=Array(e);for(var n=0;nr[n][o]+r[o][s]&&(r[n][s]=r[n][o]+r[o][s]);return r}getAdjacencyList(){let t=this.vertices.length,e=Array(t);for(var i=0;i0;){let t=n.shift(),i=this.vertices[t];e(i);for(var s=0;sr&&(r=s)}return r+1}traverseTree(t,e,i,r=999999,n=!1,s=1,o=null){if(null===o&&(o=new Uint8Array(this.vertices.length)),s>r+1||1===o[t])return;o[t]=1;let h=this.vertices[t],a=h.getNeighbours(e);(!n||s>1)&&i(h);for(var l=0;lt&&!1===S[u]&&(t=n,e=u,i=s,r=o)}return[e,t,i,r]},D=function(t,e,i){let r=0,n=0,s=0,o=y[t],h=x[t],a=A[t],l=C[t];for(u=f;u--;){if(u===t)continue;let e=y[u],i=x[u],g=a[u],d=l[u],c=(o-e)*(o-e),p=1/Math.pow(c+(h-i)*(h-i),1.5);r+=d*(1-g*(h-i)*(h-i)*p),n+=d*(1-g*c*p),s+=d*(g*(o-e)*(h-i)*p)}0===r&&(r=.1),0===n&&(n=.1),0===s&&(s=.1);let g=e/r+i/s;g/=s/r-n/s;let d=-(s*g+e)/r;y[t]+=d,x[t]+=g;let c,p,v,m,b,S=N[t];for(e=0,i=0,o=y[t],h=x[t],u=f;u--;)t!==u&&(c=y[u],p=x[u],v=S[u][0],m=S[u][1],b=1/Math.sqrt((o-c)*(o-c)+(h-p)*(h-p)),d=l[u]*(o-c-a[u]*(o-c)*b),g=l[u]*(h-p-a[u]*(h-p)*b),S[u]=[d,g],e+=d,i+=g,O[u]+=d-v,M[u]+=g-m);O[t]=e,M[t]=i},F=0,z=0,H=0,V=0,W=0,U=0;for(;g>o&&a>W;)for(W++,[F,g,z,H]=E(),V=g,U=0;V>h&&l>U;)U++,D(F,z,H),[V,z,H]=k(F);for(u=f;u--;){let e=t[u],i=this.vertices[e];i.position.x=y[u],i.position.y=x[u],i.positioned=!0,i.forcePositioned=!0}}_bridgeDfs(t,e,i,r,n,s,o){e[t]=!0,i[t]=r[t]=++this._time;for(var h=0;hi[t]&&o.push([t,a]))}}static getConnectedComponents(t){let e=t.length,i=new Array(e),r=new Array;i.fill(!1);for(var n=0;n1&&r.push(e)}return r}static getConnectedComponentCount(t){let e=t.length,i=new Array(e),r=0;i.fill(!1);for(var n=0;n{const r=i(614);class n{constructor(t=new r(0,0),e=new r(0,0),i=null,n=null,s=!1,o=!1){this.from=t,this.to=e,this.elementFrom=i,this.elementTo=n,this.chiralFrom=s,this.chiralTo=o}clone(){return new n(this.from.clone(),this.to.clone(),this.elementFrom,this.elementTo)}getLength(){return Math.sqrt(Math.pow(this.to.x-this.from.x,2)+Math.pow(this.to.y-this.from.y,2))}getAngle(){return r.subtract(this.getRightVector(),this.getLeftVector()).angle()}getRightVector(){return this.from.x{class e{static round(t,e){return e=e||1,Number(Math.round(t+"e"+e)+"e-"+e)}static meanAngle(t){let e=0,i=0;for(var r=0;r{t.exports=class{static extend(){let t=this,e={},i=!1,r=0,n=arguments.length;"[object Boolean]"===Object.prototype.toString.call(arguments[0])&&(i=arguments[0],r++);let s=function(r){for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(i&&"[object Object]"===Object.prototype.toString.call(r[n])?e[n]=t.extend(!0,e[n],r[n]):e[n]=r[n])};for(;r{t.exports=function(){"use strict";function t(e,i,r,n){this.message=e,this.expected=i,this.found=r,this.location=n,this.name="SyntaxError","function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,t)}return function(t,e){function i(){this.constructor=t}i.prototype=e.prototype,t.prototype=new i}(t,Error),t.buildMessage=function(t,e){var i={literal:function(t){return'"'+n(t.text)+'"'},class:function(t){var e,i="";for(e=0;e0){for(e=1,r=1;ett&&(tt=J,et=[]),et.push(t))}function ht(e,i){return new t(e,null,null,i)}function at(){var t,i,r,n,s,o,a,l,g;if(J,t=J,i=function(){var t;return J,t=function(){var t,i,r,n;return J,t=J,66===e.charCodeAt(J)?(i="B",J++):(i=h,ot(b)),i!==h?(114===e.charCodeAt(J)?(r="r",J++):(r=h,ot(y)),r===h&&(r=null),r!==h?t=i=[i,r]:(J=t,t=h)):(J=t,t=h),t===h&&(t=J,67===e.charCodeAt(J)?(i="C",J++):(i=h,ot(x)),i!==h?(108===e.charCodeAt(J)?(r="l",J++):(r=h,ot(S)),r===h&&(r=null),r!==h?t=i=[i,r]:(J=t,t=h)):(J=t,t=h),t===h&&(A.test(e.charAt(J))?(t=e.charAt(J),J++):(t=h,ot(C)))),t!==h&&(t=(n=t).length>1?n.join(""):n),t}(),t===h&&(t=dt())===h&&(t=function(){var t,i,r,n,s,o,a,l,g,d;return J,t=J,91===e.charCodeAt(J)?(i="[",J++):(i=h,ot(p)),i!==h?(r=function(){var t,i,r,n;return J,t=J,O.test(e.charAt(J))?(i=e.charAt(J),J++):(i=h,ot(M)),i!==h?(k.test(e.charAt(J))?(r=e.charAt(J),J++):(r=h,ot(E)),r===h&&(r=null),r!==h?(k.test(e.charAt(J))?(n=e.charAt(J),J++):(n=h,ot(E)),n===h&&(n=null),n!==h?t=i=[i,r,n]:(J=t,t=h)):(J=t,t=h)):(J=t,t=h),t!==h&&(t=Number(t.join(""))),t}(),r===h&&(r=null),r!==h?("se"===e.substr(J,2)?(n="se",J+=2):(n=h,ot(f)),n===h&&("as"===e.substr(J,2)?(n="as",J+=2):(n=h,ot(v)),n===h&&(n=dt())===h&&(n=function(){var t,i,r;return J,t=J,B.test(e.charAt(J))?(i=e.charAt(J),J++):(i=h,ot(I)),i!==h?(P.test(e.charAt(J))?(r=e.charAt(J),J++):(r=h,ot(L)),r===h&&(r=null),r!==h?t=i=[i,r]:(J=t,t=h)):(J=t,t=h),t!==h&&(t=t.join("")),t}(),n===h&&(n=ut()))),n!==h?(s=function(){var t,i,r,n,s,o,a;return J,t=J,64===e.charCodeAt(J)?(i="@",J++):(i=h,ot(D)),i!==h?(64===e.charCodeAt(J)?(r="@",J++):(r=h,ot(D)),r===h&&(r=J,"TH"===e.substr(J,2)?(n="TH",J+=2):(n=h,ot(F)),n!==h?(z.test(e.charAt(J))?(s=e.charAt(J),J++):(s=h,ot(H)),s!==h?r=n=[n,s]:(J=r,r=h)):(J=r,r=h),r===h&&(r=J,"AL"===e.substr(J,2)?(n="AL",J+=2):(n=h,ot(V)),n!==h?(z.test(e.charAt(J))?(s=e.charAt(J),J++):(s=h,ot(H)),s!==h?r=n=[n,s]:(J=r,r=h)):(J=r,r=h),r===h&&(r=J,"SP"===e.substr(J,2)?(n="SP",J+=2):(n=h,ot(W)),n!==h?(U.test(e.charAt(J))?(s=e.charAt(J),J++):(s=h,ot(q)),s!==h?r=n=[n,s]:(J=r,r=h)):(J=r,r=h),r===h&&(r=J,"TB"===e.substr(J,2)?(n="TB",J+=2):(n=h,ot(j)),n!==h?(O.test(e.charAt(J))?(s=e.charAt(J),J++):(s=h,ot(M)),s!==h?(k.test(e.charAt(J))?(o=e.charAt(J),J++):(o=h,ot(E)),o===h&&(o=null),o!==h?r=n=[n,s,o]:(J=r,r=h)):(J=r,r=h)):(J=r,r=h),r===h&&(r=J,"OH"===e.substr(J,2)?(n="OH",J+=2):(n=h,ot(_)),n!==h?(O.test(e.charAt(J))?(s=e.charAt(J),J++):(s=h,ot(M)),s!==h?(k.test(e.charAt(J))?(o=e.charAt(J),J++):(o=h,ot(E)),o===h&&(o=null),o!==h?r=n=[n,s,o]:(J=r,r=h)):(J=r,r=h)):(J=r,r=h)))))),r===h&&(r=null),r!==h?t=i=[i,r]:(J=t,t=h)):(J=t,t=h),t!==h&&(t=(a=t)[1]?"@"==a[1]?"@@":a[1].join("").replace(",",""):"@"),t}(),s===h&&(s=null),s!==h?(o=function(){var t,i,r,n;return J,t=J,72===e.charCodeAt(J)?(i="H",J++):(i=h,ot(K)),i!==h?(k.test(e.charAt(J))?(r=e.charAt(J),J++):(r=h,ot(E)),r===h&&(r=null),r!==h?t=i=[i,r]:(J=t,t=h)):(J=t,t=h),t!==h&&(t=(n=t)[1]?Number(n[1]):1),t}(),o===h&&(o=null),o!==h?(a=function(){var t;return J,t=function(){var t,i,r,n,s,o;return J,t=J,43===e.charCodeAt(J)?(i="+",J++):(i=h,ot(G)),i!==h?(43===e.charCodeAt(J)?(r="+",J++):(r=h,ot(G)),r===h&&(r=J,O.test(e.charAt(J))?(n=e.charAt(J),J++):(n=h,ot(M)),n!==h?(k.test(e.charAt(J))?(s=e.charAt(J),J++):(s=h,ot(E)),s===h&&(s=null),s!==h?r=n=[n,s]:(J=r,r=h)):(J=r,r=h)),r===h&&(r=null),r!==h?t=i=[i,r]:(J=t,t=h)):(J=t,t=h),t!==h&&(t=(o=t)[1]?"+"!=o[1]?Number(o[1].join("")):2:1),t}(),t===h&&(t=function(){var t,i,r,n,s,o;return J,t=J,45===e.charCodeAt(J)?(i="-",J++):(i=h,ot(X)),i!==h?(45===e.charCodeAt(J)?(r="-",J++):(r=h,ot(X)),r===h&&(r=J,O.test(e.charAt(J))?(n=e.charAt(J),J++):(n=h,ot(M)),n!==h?(k.test(e.charAt(J))?(s=e.charAt(J),J++):(s=h,ot(E)),s===h&&(s=null),s!==h?r=n=[n,s]:(J=r,r=h)):(J=r,r=h)),r===h&&(r=null),r!==h?t=i=[i,r]:(J=t,t=h)):(J=t,t=h),t!==h&&(t=(o=t)[1]?"-"!=o[1]?-Number(o[1].join("")):-2:-1),t}()),t}(),a===h&&(a=null),a!==h?(l=function(){var t,i,r,n,s,o,a;if(J,t=J,58===e.charCodeAt(J)?(i=":",J++):(i=h,ot(Y)),i!==h){if(r=J,O.test(e.charAt(J))?(n=e.charAt(J),J++):(n=h,ot(M)),n!==h){for(s=[],k.test(e.charAt(J))?(o=e.charAt(J),J++):(o=h,ot(E));o!==h;)s.push(o),k.test(e.charAt(J))?(o=e.charAt(J),J++):(o=h,ot(E));s!==h?r=n=[n,s]:(J=r,r=h)}else J=r,r=h;r===h&&(Z.test(e.charAt(J))?(r=e.charAt(J),J++):(r=h,ot($))),r!==h?t=i=[i,r]:(J=t,t=h)}else J=t,t=h;return t!==h&&(a=t,t=Number(a[1][0]+a[1][1].join(""))),t}(),l===h&&(l=null),l!==h?(93===e.charCodeAt(J)?(g="]",J++):(g=h,ot(m)),g!==h?t=i=[i,r,n,s,o,a,l,g]:(J=t,t=h)):(J=t,t=h)):(J=t,t=h)):(J=t,t=h)):(J=t,t=h)):(J=t,t=h)):(J=t,t=h)):(J=t,t=h),t!==h&&(t={isotope:(d=t)[1],element:d[2],chirality:d[3],hcount:d[4],charge:d[5],class:d[6]}),t}(),t===h&&(t=ut())),t}(),i!==h){for(r=[],n=lt();n!==h;)r.push(n),n=lt();if(r!==h){for(n=[],s=J,(o=gt())===h&&(o=null),o!==h&&(a=ct())!==h?s=o=[o,a]:(J=s,s=h);s!==h;)n.push(s),s=J,(o=gt())===h&&(o=null),o!==h&&(a=ct())!==h?s=o=[o,a]:(J=s,s=h);if(n!==h){for(s=[],o=lt();o!==h;)s.push(o),o=lt();if(s!==h)if((o=gt())===h&&(o=null),o!==h)if((a=at())===h&&(a=null),a!==h){for(l=[],g=lt();g!==h;)l.push(g),g=lt();l!==h?t=i=[i,r,n,s,o,a,l]:(J=t,t=h)}else J=t,t=h;else J=t,t=h;else J=t,t=h}else J=t,t=h}else J=t,t=h}else J=t,t=h;return t!==h&&(t=function(t){for(var e=[],i=[],r=0;r{const r=i(348),n=i(614),s=(i(843),i(333));class o{constructor(t){this.id=null,this.members=t,this.edges=[],this.insiders=[],this.neighbours=[],this.positioned=!1,this.center=new n(0,0),this.rings=[],this.isBridged=!1,this.isPartOfBridged=!1,this.isSpiro=!1,this.isFused=!1,this.centralAngle=0,this.canFlip=!0}clone(){let t=new o(this.members);return t.id=this.id,t.insiders=r.clone(this.insiders),t.neighbours=r.clone(this.neighbours),t.positioned=this.positioned,t.center=this.center.clone(),t.rings=r.clone(this.rings),t.isBridged=this.isBridged,t.isPartOfBridged=this.isPartOfBridged,t.isSpiro=this.isSpiro,t.isFused=this.isFused,t.centralAngle=this.centralAngle,t.canFlip=this.canFlip,t}getSize(){return this.members.length}getPolygon(t){let e=[];for(let i=0;i{i(843),i(421),t.exports=class{constructor(t,e){this.id=null,this.firstRingId=t.id,this.secondRingId=e.id,this.vertices=new Set;for(var i=0;i2)return!0;for(let e of this.vertices)if(t[e].value.rings.length>2)return!0;return!1}static isBridge(t,e,i,r){let n=null;for(let s=0;s{const r=i(707);class n{static getRings(t,e=!1){let i=t.getComponentsAdjacencyMatrix();if(0===i.length)return null;let s=r.getConnectedComponents(i),o=Array();for(var h=0;he){if(t===e+1)for(n[a][l]=[r[a][l].length],s=r[a][l].length;s--;)for(n[a][l][s]=[r[a][l][s].length],o=r[a][l][s].length;o--;)for(n[a][l][s][o]=[r[a][l][s][o].length],h=r[a][l][s][o].length;h--;)n[a][l][s][o][h]=[r[a][l][s][o][0],r[a][l][s][o][1]];else n[a][l]=Array();for(i[a][l]=e,r[a][l]=[[]],s=r[a][g][0].length;s--;)r[a][l][0].push(r[a][g][0][s]);for(s=r[g][l][0].length;s--;)r[a][l][0].push(r[g][l][0][s])}else if(t===e){if(r[a][g].length&&r[g][l].length)if(r[a][l].length){let t=Array();for(s=r[a][g][0].length;s--;)t.push(r[a][g][0][s]);for(s=r[g][l][0].length;s--;)t.push(r[g][l][0][s]);r[a][l].push(t)}else{let t=Array();for(s=r[a][g][0].length;s--;)t.push(r[a][g][0][s]);for(s=r[g][l][0].length;s--;)t.push(r[g][l][0][s]);r[a][l][0]=t}}else if(t===e-1)if(n[a][l].length){let t=Array();for(s=r[a][g][0].length;s--;)t.push(r[a][g][0][s]);for(s=r[g][l][0].length;s--;)t.push(r[g][l][0][s]);n[a][l].push(t)}else{let t=Array();for(s=r[a][g][0].length;s--;)t.push(r[a][g][0][s]);for(s=r[g][l][0].length;s--;)t.push(r[g][l][0][s]);n[a][l][0]=t}}return{d:i,pe:r,pe_prime:n}}static getRingCandidates(t,e,i){let r=t.length,n=Array(),s=0;for(let o=0;oa)return l}else for(let r=0;ra)return l}return l}static getEdgeCount(t){let e=0,i=t.length;for(var r=i-1;r--;)for(var n=i;n--;)1===t[r][n]&&e++;return e}static getEdgeList(t){let e=t.length,i=Array();for(var r=e-1;r--;)for(var n=e;n--;)1===t[r][n]&&i.push([r,n]);return i}static bondsToAtoms(t){let e=new Set;for(var i=t.length;i--;)e.add(t[i][0]),e.add(t[i][1]);return e}static getBondCount(t,e){let i=0;for(let r of t)for(let n of t)r!==n&&(i+=e[r][n]);return i/2}static pathSetsContain(t,e,i,r,s,o){for(var h=t.length;h--;){if(n.isSupersetOf(e,t[h]))return!0;if(t[h].size===e.size&&n.areSetsEqual(t[h],e))return!0}let a=0,l=!1;for(h=i.length;h--;)for(var g=r.length;g--;)(i[h][0]===r[g][0]&&i[h][1]===r[g][1]||i[h][1]===r[g][0]&&i[h][0]===r[g][1])&&a++,a===i.length&&(l=!0);let d=!1;if(l)for(let t of e)if(o[t]{t.exports=class{constructor(t,e){this.colors=t,this.theme=this.colors[e]}getColor(t){return t&&(t=t.toUpperCase())in this.theme?this.theme[t]:this.theme.C}setTheme(t){this.colors.hasOwnProperty(t)&&(this.theme=this.colors[t])}}},537:t=>{t.exports={getChargeText:function(t){return 1===t?"+":2===t?"2+":-1===t?"-":-2===t?"2-":""}}},614:t=>{class e{constructor(t,e){0==arguments.length?(this.x=0,this.y=0):1==arguments.length?(this.x=t.x,this.y=t.y):(this.x=t,this.y=e)}clone(){return new e(this.x,this.y)}toString(){return"("+this.x+","+this.y+")"}add(t){return this.x+=t.x,this.y+=t.y,this}subtract(t){return this.x-=t.x,this.y-=t.y,this}divide(t){return this.x/=t,this.y/=t,this}multiply(t){return this.x*=t.x,this.y*=t.y,this}multiplyScalar(t){return this.x*=t,this.y*=t,this}invert(){return this.x=-this.x,this.y=-this.y,this}angle(){return Math.atan2(this.y,this.x)}distance(t){return Math.sqrt((t.x-this.x)*(t.x-this.x)+(t.y-this.y)*(t.y-this.y))}distanceSq(t){return(t.x-this.x)*(t.x-this.x)+(t.y-this.y)*(t.y-this.y)}clockwise(t){let e=this.y*t.x,i=this.x*t.y;return e>i?-1:e===i?0:1}relativeClockwise(t,e){let i=(this.y-t.y)*(e.x-t.x),r=(this.x-t.x)*(e.y-t.y);return i>r?-1:i===r?0:1}rotate(t){let i=new e(0,0),r=Math.cos(t),n=Math.sin(t);return i.x=this.x*r-this.y*n,i.y=this.x*n+this.y*r,this.x=i.x,this.y=i.y,this}rotateAround(t,e){let i=Math.sin(t),r=Math.cos(t);this.x-=e.x,this.y-=e.y;let n=this.x*r-this.y*i,s=this.x*i+this.y*r;return this.x=n+e.x,this.y=s+e.y,this}rotateTo(t,i,r=0){this.x+=.001,this.y-=.001;let n=e.subtract(this,i),s=e.subtract(t,i),o=e.angle(s,n);return this.rotateAround(o+r,i),this}rotateAwayFrom(t,e,i){this.rotateAround(i,e);let r=this.distanceSq(t);this.rotateAround(-2*i,e),this.distanceSq(t)n?i:-i}getRotateToAngle(t,i){let r=e.subtract(this,i),n=e.subtract(t,i),s=e.angle(n,r);return Number.isNaN(s)?0:s}isInPolygon(t){let e=!1;for(let i=0,r=t.length-1;ithis.y!=t[r].y>this.y&&this.x<(t[r].x-t[i].x)*(this.y-t[i].y)/(t[r].y-t[i].y)+t[i].x&&(e=!e);return e}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}lengthSq(){return this.x*this.x+this.y*this.y}normalize(){return this.divide(this.length()),this}normalized(){return e.divideScalar(this,this.length())}whichSide(t,e){return(this.x-t.x)*(e.y-t.y)-(this.y-t.y)*(e.x-t.x)}sameSideAs(t,e,i){let r=this.whichSide(t,e),n=i.whichSide(t,e);return r<0&&n<0||0==r&&0==n||r>0&&n>0}static add(t,i){return new e(t.x+i.x,t.y+i.y)}static subtract(t,i){return new e(t.x-i.x,t.y-i.y)}static multiply(t,i){return new e(t.x*i.x,t.y*i.y)}static multiplyScalar(t,i){return new e(t.x,t.y).multiplyScalar(i)}static midpoint(t,i){return new e((t.x+i.x)/2,(t.y+i.y)/2)}static normals(t,i){let r=e.subtract(i,t);return[new e(-r.y,r.x),new e(r.y,-r.x)]}static units(t,i){let r=e.subtract(i,t);return[new e(-r.y,r.x).normalize(),new e(r.y,-r.x).normalize()]}static divide(t,i){return new e(t.x/i.x,t.y/i.y)}static divideScalar(t,i){return new e(t.x/i,t.y/i)}static dot(t,e){return t.x*e.x+t.y*e.y}static angle(t,i){let r=e.dot(t,i);return Math.acos(r/(t.length()*i.length()))}static threePointangle(t,i,r){let n=e.subtract(i,t),s=e.subtract(r,i),o=t.distance(i),h=i.distance(r);return Math.acos(e.dot(n,s)/(o*h))}static scalarProjection(t,i){let r=i.normalized();return e.dot(t,r)}static averageDirection(t){let i=new e(0,0);for(var r=0;r{const r=i(474),n=i(348),s=i(614);i(427);class o{constructor(t,e=0,i=0){this.id=null,this.value=t,this.position=new s(e||0,i||0),this.previousPosition=new s(0,0),this.parentVertexId=null,this.children=Array(),this.spanningTreeChildren=Array(),this.edges=Array(),this.positioned=!1,this.angle=null,this.dir=1,this.neighbourCount=0,this.neighbours=Array(),this.neighbouringElements=Array(),this.forcePositioned=!1}setPosition(t,e){this.position.x=t,this.position.y=e}setPositionFromVector(t){this.position.x=t.x,this.position.y=t.y}addChild(t){this.children.push(t),this.neighbours.push(t),this.neighbourCount++}addRingbondChild(t,e){if(this.children.push(t),this.value.bracket){let i=1;0===this.id&&0===this.value.bracket.hcount&&(i=0),1===this.value.bracket.hcount&&0===e&&(i=2),1===this.value.bracket.hcount&&1===e&&(i=this.neighbours.length<3?2:3),null===this.value.bracket.hcount&&0===e&&(i=1),null===this.value.bracket.hcount&&1===e&&(i=this.neighbours.length<3?1:2),this.neighbours.splice(i,0,t)}else this.neighbours.push(t);this.neighbourCount++}setParentVertexId(t){this.neighbourCount++,this.parentVertexId=t,this.neighbours.push(t)}isTerminal(){return!!this.value.hasAttachedPseudoElements||null===this.parentVertexId&&this.children.length<2||0===this.children.length}clone(){let t=new o(this.value,this.position.x,this.position.y);return t.id=this.id,t.previousPosition=new s(this.previousPosition.x,this.previousPosition.y),t.parentVertexId=this.parentVertexId,t.children=n.clone(this.children),t.spanningTreeChildren=n.clone(this.spanningTreeChildren),t.edges=n.clone(this.edges),t.positioned=this.positioned,t.angle=this.angle,t.forcePositioned=this.forcePositioned,t}equals(t){return this.id===t.id}getAngle(t=null,e=!1){let i=null;return i=t?s.subtract(this.position,t):s.subtract(this.position,this.previousPosition),e?r.toDeg(i.angle()):i.angle()}getTextDirection(t){let e=this.getDrawnNeighbours(t),i=Array();if(1===t.length)return"right";for(let r=0;r{var e=t&&t.__esModule?()=>t.default:()=>t;return i.d(e,{a:e}),e},i.d=(t,e)=>{for(var r in e)i.o(e,r)&&!i.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),i.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var r={};return(()=>{"use strict";i.r(r),i.d(r,{clean2d:()=>o});var t=i(237),e=i.n(t),n=i(19),s=i.n(n);function o(t){const i=new(e())({}),r=s().parse(t);i.initDraw(r,"light",!1),i.processGraph();let n=i.graph.vertices,o=Array();for(let t=0;t Date: Wed, 18 Dec 2024 13:45:39 +0000 Subject: [PATCH 2/5] add copyright and license --- chython/algorithms/calculate2d/Calculate2d.py | 20 +++++++++++++++++++ chython/algorithms/calculate2d/KKLayout.py | 20 +++++++++++++++++++ chython/algorithms/calculate2d/MathHelper.py | 20 +++++++++++++++++++ chython/algorithms/calculate2d/Properties.py | 20 +++++++++++++++++++ chython/algorithms/calculate2d/__init__.py | 20 +++++++++++++++++++ 5 files changed, 100 insertions(+) diff --git a/chython/algorithms/calculate2d/Calculate2d.py b/chython/algorithms/calculate2d/Calculate2d.py index 54d776c0..d67bbd19 100644 --- a/chython/algorithms/calculate2d/Calculate2d.py +++ b/chython/algorithms/calculate2d/Calculate2d.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2024 Denis Lipatov +# Copyright 2024 Vyacheslav Grigorev +# Copyright 2024 Timur Gimadiev +# This file is part of chython. +# +# chython is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, see . +# """ Class for calculating the 2D layout of a molecular graph, returning the coordinates of atom vertices in a molecular container. diff --git a/chython/algorithms/calculate2d/KKLayout.py b/chython/algorithms/calculate2d/KKLayout.py index eb7ec6b3..a6f8b8e7 100644 --- a/chython/algorithms/calculate2d/KKLayout.py +++ b/chython/algorithms/calculate2d/KKLayout.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2024 Denis Lipatov +# Copyright 2024 Vyacheslav Grigorev +# Copyright 2024 Timur Gimadiev +# This file is part of chython. +# +# chython is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, see . +# """ Defines the KKLayout class, utilized for arranging molecular structures using the Kamada-Kawai algorithm. diff --git a/chython/algorithms/calculate2d/MathHelper.py b/chython/algorithms/calculate2d/MathHelper.py index 32f433ba..7e95ac03 100644 --- a/chython/algorithms/calculate2d/MathHelper.py +++ b/chython/algorithms/calculate2d/MathHelper.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2024 Denis Lipatov +# Copyright 2024 Vyacheslav Grigorev +# Copyright 2024 Timur Gimadiev +# This file is part of chython. +# +# chython is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, see . +# """ This module introduces the `Vector` and `Polygon` classes, designed to perform mathematical calculations relevant to two-dimensional Cartesian coordinate systems and regular polygons. diff --git a/chython/algorithms/calculate2d/Properties.py b/chython/algorithms/calculate2d/Properties.py index d8f16930..9ae9f31f 100644 --- a/chython/algorithms/calculate2d/Properties.py +++ b/chython/algorithms/calculate2d/Properties.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2024 Denis Lipatov +# Copyright 2024 Vyacheslav Grigorev +# Copyright 2024 Timur Gimadiev +# This file is part of chython. +# +# chython is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, see . +# """ This module defines classes that extend the properties of rings, atoms, and bonds within the Kaiton structure, focusing on attributes and methods necessary for coordinate calculations. diff --git a/chython/algorithms/calculate2d/__init__.py b/chython/algorithms/calculate2d/__init__.py index 9cb04b96..193ea427 100644 --- a/chython/algorithms/calculate2d/__init__.py +++ b/chython/algorithms/calculate2d/__init__.py @@ -1 +1,21 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2024 Denis Lipatov +# Copyright 2024 Vyacheslav Grigorev +# Copyright 2024 Timur Gimadiev +# This file is part of chython. +# +# chython is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, see . +# from .clean2d import Calculate2DMolecule, Calculate2DReaction \ No newline at end of file From f0ed48019258cfd9f1642f4e2f21e88d0f8c2941 Mon Sep 17 00:00:00 2001 From: denis lipatov Date: Mon, 23 Dec 2024 11:55:10 +0000 Subject: [PATCH 3/5] update feature cleand2d algorithm in python to branch V2 --- chython/algorithms/calculate2d/clean2d.py | 180 ++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 chython/algorithms/calculate2d/clean2d.py diff --git a/chython/algorithms/calculate2d/clean2d.py b/chython/algorithms/calculate2d/clean2d.py new file mode 100644 index 00000000..0fd77b72 --- /dev/null +++ b/chython/algorithms/calculate2d/clean2d.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2024 Denis Lipatov +# Copyright 2024 Vyacheslav Grigorev +# Copyright 2024 Timur Gimadiev +# This file is part of chython. +# +# chython is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, see . +# +from typing import TYPE_CHECKING, Union, List +from .Calculate2d import Calculate2d +from math import sqrt + +if TYPE_CHECKING: + from ...containers import ReactionContainer, MoleculeContainer + +try: + from importlib.resources import files +except ImportError: # python3.8 + from importlib_resources import files + + +class Calculate2DMolecule: + __slots__ = () + + def clean2d(self: Union['MoleculeContainer', 'Calculate2DMolecule']): + """ + Calculate 2d layout of graph. https://pubs.acs.org/doi/10.1021/acs.jcim.7b00425 JS implementation used. + """ + plane = {} + entry = iter(sorted(self, key=lambda n: len(self._bonds[n]))) + smiles, order = self.__clean2d_prepare(next(entry)) + + obj = Calculate2d() + xy: List[List[float, float]] = obj._calculate2d_coord(order, self) + + + shift_x, shift_y = xy[0] + for n, (x, y) in zip(order, xy): + plane[n] = (x - shift_x, shift_y - y) + + bonds = [] + for n, m, _ in self.bonds(): + xn, yn = plane[n] + xm, ym = plane[m] + bonds.append(sqrt((xm - xn) ** 2 + (ym - yn) ** 2)) + if bonds: + bond_reduce = sum(bonds) / len(bonds) / .825 + else: + bond_reduce = 1. + + atoms = self._atoms + for n, (x, y) in plane.items(): + a = atoms[n] + a._x = x / bond_reduce + a._y = y / bond_reduce + + if self.connected_components_count > 1: + shift_x = 0. + for c in self.connected_components: + shift_x = self._fix_plane_mean(shift_x, component=c) + .9 + self.__dict__.pop('__cached_method__repr_svg_', None) + + def _fix_plane_mean(self: 'MoleculeContainer', shift_x: float, shift_y=0., component=None) -> float: + plane = self._plane + if component is None: + component = plane + + left_atom = min(component, key=lambda x: plane[x][0]) + right_atom = max(component, key=lambda x: plane[x][0]) + + min_x = plane[left_atom][0] - shift_x + if len(self._atoms[left_atom].atomic_symbol) == 2: + min_x -= 0.2 + + max_x = plane[right_atom][0] - min_x + min_y = min(plane[x][1] for x in component) + max_y = max(plane[x][1] for x in component) + mean_y = (max_y + min_y) / 2 - shift_y + for n in component: + x, y = plane[n] + plane[n] = (x - min_x, y - mean_y) + + if -0.18 <= plane[right_atom][1] <= 0.18: + factor = self._hydrogens[right_atom] + if factor == 1: + max_x += 0.15 + elif factor: + max_x += 0.25 + return max_x + + def _fix_plane_min(self: 'MoleculeContainer', shift_x: float, shift_y=0., component=None) -> float: + plane = self._plane + if component is None: + component = plane + + right_atom = max(component, key=lambda x: plane[x][0]) + min_x = min(plane[x][0] for x in component) - shift_x + max_x = plane[right_atom][0] - min_x + min_y = min(plane[x][1] for x in component) - shift_y + + for n in component: + x, y = plane[n] + plane[n] = (x - min_x, y - min_y) + + if shift_y - 0.18 <= plane[right_atom][1] <= shift_y + 0.18: + factor = self._hydrogens[right_atom] + if factor == 1: + max_x += 0.15 + elif factor: + max_x += 0.25 + return max_x + + + def __clean2d_prepare(self: 'MoleculeContainer', entry): + w = {n: i for i, n in enumerate(self._atoms)} + w[entry] = -1 + smiles, order = self._smiles(w.__getitem__, random=True, charges=False, stereo=False, _return_order=True) + return ''.join(smiles).replace('~', '-'), order + +class Calculate2DReaction: + __slots__ = () + + def clean2d(self: 'ReactionContainer'): + for m in self.molecules(): + m.clean2d() + self.fix_positions() + + def fix_positions(self: 'ReactionContainer'): + shift_x = 0 + reactants = self.reactants + amount = len(reactants) - 1 + signs = [] + for m in reactants: + max_x = m._fix_plane_mean(shift_x) + if amount: + max_x += .2 + signs.append(max_x) + amount -= 1 + shift_x = max_x + 1 + arrow_min = shift_x + + if self.reagents: + shift_x += .4 + for m in self.reagents: + max_x = m._fix_plane_min(shift_x, .5) + shift_x = max_x + 1 + shift_x += .4 + if shift_x - arrow_min < 3: + shift_x = arrow_min + 3 + else: + shift_x += 3 + arrow_max = shift_x - 1 + + products = self.products + amount = len(products) - 1 + for m in products: + max_x = m._fix_plane_mean(shift_x) + if amount: + max_x += .2 + signs.append(max_x) + amount -= 1 + shift_x = max_x + 1 + self._arrow = (arrow_min, arrow_max) + self._signs = tuple(signs) + self.flush_cache() + + +__all__ = ['Calculate2DMolecule', 'Calculate2DReaction'] \ No newline at end of file From e31fe418dd9796fd34397542b7b9408a3db99f5e Mon Sep 17 00:00:00 2001 From: denis lipatov Date: Mon, 23 Dec 2024 11:56:38 +0000 Subject: [PATCH 4/5] fix readme to for the new version of clean2d on python --- README.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index bbe8d4e8..94262b2a 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ Features: - Produce template based reactions and molecules - Atom-to-atom mapping, checking and rule-based fixing - Perform MCS search - - 2d coordinates generation (based on `SmilesDrawer `_) + - 2d coordinates generation - 2d/3d depiction with Jupyter support - SMARTS parser with restrictions - Protective groups remover @@ -31,8 +31,6 @@ Install Only python 3.8+. -Note: for using `clean2d` install NodeJS into system. - * **stable version available through PyPI**:: pip install chython From 8a0870cca849b79200cf19b089a52ad4714048c7 Mon Sep 17 00:00:00 2001 From: walderhu Date: Fri, 31 Jan 2025 17:02:13 +0000 Subject: [PATCH 5/5] refactor clean2d on python Some property classes and properties that duplicate the existing functionality of the kayton were removed, and were made to improve the readability of the code, which allowed to optimize the algorithm The Vestor class for calculating coordinates and vectors was also improved, it was inherited from np.array and as a result a 2.5 times boost was obtained. Also some functions were improved, the changes were of the following nature - stylistic character - changes aimed at improving code readability - changes aimed at optimizing the algorithm (it was completely rewritten) - changes related to the replacement of property class analogs with the existing ones of the molecular container. - minor corrections of functions, in which calculation operations over coordinates and vectors took place, the same algorithm was rewritten, but adjusted to the optimized class already --- chython/algorithms/calculate2d/Calculate2d.py | 1102 ++++++++--------- chython/algorithms/calculate2d/KKLayout.py | 28 +- chython/algorithms/calculate2d/MathHelper.py | 709 ----------- chython/algorithms/calculate2d/Properties.py | 231 +--- chython/algorithms/calculate2d/__init__.py | 12 +- chython/algorithms/calculate2d/clean2d.py | 180 --- chython/algorithms/calculate2d/molecule.py | 45 +- chython/algorithms/calculate2d/polygon.py | 124 ++ chython/algorithms/calculate2d/reaction.py | 24 +- chython/periodictable/base/vector.py | 521 +++++++- 10 files changed, 1195 insertions(+), 1781 deletions(-) delete mode 100644 chython/algorithms/calculate2d/MathHelper.py delete mode 100644 chython/algorithms/calculate2d/clean2d.py create mode 100644 chython/algorithms/calculate2d/polygon.py diff --git a/chython/algorithms/calculate2d/Calculate2d.py b/chython/algorithms/calculate2d/Calculate2d.py index d67bbd19..40d85b64 100644 --- a/chython/algorithms/calculate2d/Calculate2d.py +++ b/chython/algorithms/calculate2d/Calculate2d.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright 2024 Denis Lipatov -# Copyright 2024 Vyacheslav Grigorev -# Copyright 2024 Timur Gimadiev +# Copyright 2024, 2025 Denis Lipatov +# Copyright 2024, 2025 Vyacheslav Grigorev +# Copyright 2024, 2025 Timur Gimadiev # This file is part of chython. # # chython is free software; you can redistribute it and/or modify @@ -18,6 +18,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, see . # + """ Class for calculating the 2D layout of a molecular graph, returning the coordinates of atom vertices in a molecular container. @@ -48,13 +49,16 @@ cyclic molecules). """ from typing import List, Dict, Optional, Set, Tuple, Union, Generator, TYPE_CHECKING -from .MathHelper import Vector, Polygon +from ...periodictable.base.vector import Vector +from .polygon import Polygon from .KKLayout import KKLayout -from .Properties import * -import math +from .Properties import * # RingProperties, AtomProperties, RingOverlap +import math # radiand, sin, cos, pi +import numpy as np if TYPE_CHECKING: from ...containers import MoleculeContainer + from ...containers import Bond class Calculate2d: @@ -95,45 +99,18 @@ def __init__(self) -> None: self.overlap_resolution_iterations: int = 5 self.ring_overlaps: List['RingOverlap'] = [] self.total_overlap_score: float = 0.0 - # self.finetune: bool = True # используется в ветвлении, но всегда тру, бесполезная вещь + self.ring_overlap_id_tracker: int = 0 - self.ring_id_tracker: int = 0 + self.ring_id_tracker: int = 0 self.rings: List['RingProperties'] = [] self.id_to_ring: Dict[int, 'RingProperties'] = {} - - - def _calculate2d_coord(self, order: List[int], mc: 'MoleculeContainer') -> List[List[float]]: - """ - Calculates the coordinates of the vertices of the atoms of the graph and returns the - coordinates as - a two-dimensional array. - - This method computes the 2D coordinates for each atom in the molecular graph based on - the provided order and molecular container. It initializes the properties of the - molecular container, - defines the rings within the molecule, performs an initial approximation of atom - positions, handles collisions between atoms, and finally returns the calculated - coordinates. - - Parameters: - :param order: List[int]: - A list of integers representing the order in which atoms should be processed. This - order determines the sequence for calculating and adjusting atom positions to - minimize overlaps. - :param mc: MoleculeContainer: - An instance of MoleculeContainer that holds the molecular graph, including atoms, - bonds, and rings information necessary for the 2D layout calculation. - - Returns List[List[float]]: - A two-dimensional list where each inner list contains the x and y coordinates of an - atom in the 2D space. The order of coordinates corresponds to the order of atoms as - processed. - """ + def _calculate2d_coord(self, order: List[int], + mc: 'MoleculeContainer') -> List[List[float]]: self.create_property_attributes(mc) self.define_rings() - self.initial_approximation() ##main + self.initial_approximation() self.collision_handling() return self.get_coord(order) @@ -174,21 +151,14 @@ def create_property_attributes(self, mc: 'MoleculeContainer') -> None: self.atoms: Dict[int, AtomProperties] = {} for atom in self.mc.int_adjacency: - symbol = self.get_symbol(atom) - self.atoms[atom] = AtomProperties(atom, symbol) + self.atoms[atom] = AtomProperties(atom) self.refresh_neighbours(self.mc.int_adjacency) self.graph: Dict['AtomProperties', List['AtomProperties']] = {} for atom in self.atoms.values(): self.graph[atom] = atom.neighbours - self.bonds: Dict[Tuple[int], 'BondProperties'] = {} - for n, m, bond in self.mc.bonds(): - self.bonds[(n, m)] = BondProperties(self.atoms[n], self.atoms[m], bond) - - ## creating and refreshing property attributes - def refresh_neighbours(self, graph: Dict[int, Dict[int, int]]) -> None: """ Refreshes the neighbours list for each atom based on the provided graph adjacency information. @@ -210,57 +180,50 @@ def refresh_neighbours(self, graph: Dict[int, Dict[int, int]]) -> None: neighbours.append(self.atoms[neighbour_index]) self.atoms[atom_index].neighbours = neighbours - - def get_symbol(self, atom_index: int) -> str: - """ - Returns the atomic symbol of the atom corresponding to the given index. - - This method retrieves the atomic symbol of an atom in the molecular container based on its index. It - is a utility function used to identify the type of atom by its atomic symbol, which is essential for - various calculations and representations in the molecular graph. - - Parameters - :param atom_index: int: - The index of the atom for which the atomic symbol is to be retrieved. This index is used to - access the atom within the molecular container. - - Returns str: - The atomic symbol of the atom as a string, representing the element type of the atom (e.g., 'C' - for Carbon, 'H' for Hydrogen, etc.). + def bond_lookup(self, atom: 'AtomProperties', next_atom: 'AtomProperties') -> Optional['Bond']: """ - return self.mc.atom(atom_index).atomic_symbol + Retrieves the bond object between two specified atoms. + This method checks if there is a bond between the given atoms and returns the corresponding + bond object if it exists. + Parameters: + atom (AtomProperties): The first atom involved in the bond. + next_atom (AtomProperties): The second atom involved in the bond. - def bond_lookup(self, atom: 'AtomProperties', next_atom: 'AtomProperties') -> Optional['BondProperties']: + Returns: + Optional[Bond]: The bond object between the two atoms if it exists; otherwise, returns None. """ - Возвращает связь, которая находится между этими двумя атомами - """ - return self.bonds.get((atom.id, next_atom.id)) or self.bonds.get((next_atom.id, atom.id)) - + if self.mc.has_bond(atom.id, next_atom.id): + return self.mc.bond(atom.id, next_atom.id) def get_configuration(self, atom1: 'AtomProperties', atom2: 'AtomProperties') -> Optional[str]: """ - Проверяет есть ли конфигурация между этими атомами, - в случае отсутствия возвращает None, в ином случае - возвращает строку 'cis' или 'trans' + Checks if there is a configuration between the specified atoms. + + If no configuration exists, it returns None. Otherwise, it returns + 'cis' if the configuration is cis or 'trans' if the configuration is trans. + + The configurations are stored in a dictionary, where the keys are tuples + of atom IDs that may have configurations. The condition is that the bond + between these atoms must be a double bond. The values in the dictionary + are boolean: True indicates a cis configuration, while False indicates + a trans configuration. If no configurations are defined, the dictionary + will be empty. + + Parameters: + atom1 (AtomProperties): The first atom to check for configuration. + atom2 (AtomProperties): The second atom to check for configuration. - self._cis_trans_stereo = {(2, 3): False} это словарь, хранящий значения - о конфигурациях молекулы, его ключами являются тюплы атомов, между которыми - есть могут быть конформации, сверху условие что эта связь обязательно должна - быть двойной, значениями является булевое значение, True если Цис конфигурация, - False если Транс, если конфигурации не предусмотренно вообще, то словарь будет пустым. + Returns: + Optional[str]: 'cis' if the configuration is cis, 'trans' if it is trans, + or None if no configuration exists. """ - if (atom1.id, atom2.id) not in list(self.mc._cis_trans_stereo.keys()): - return None - else: + if (atom1.id, atom2.id) in list(self.mc._cis_trans_stereo.keys()): configuration = self.mc._cis_trans_stereo[(atom1.id, atom2.id)] return 'cis' if configuration else 'trans' - -# поиск в базе колец и их классификация -# в структуре кайтона не обрабатываются случаи с мостиковыми кольцами def define_rings(self) -> None: """ Defines the rings within the molecule, identifies ring overlaps, and handles bridged ring systems. @@ -305,30 +268,30 @@ def define_rings(self) -> None: for ring in self.rings: neighbouring_rings = self.find_neighbouring_rings(self.ring_overlaps, ring.id) - ring.neighbouring_rings = neighbouring_rings + ring.neighbouring_rings = neighbouring_rings while True: ring_id: int = -1 for ring in self.rings: - if self.is_part_of_bridged_ring(ring.id) and not ring.bridged: - ring_id: int = ring.id + if self.is_part_of_bridged_ring(ring.id) and not ring.bridged: + ring_id: int = ring.id if ring_id == -1: break ring: 'RingProperties' = self.id_to_ring[ring_id] involved_ring_ids: Union[list[int], Set[int]] = [] - self.get_bridged_ring_subrings(ring.id, involved_ring_ids) + self.get_bridged_ring_subrings(ring.id, involved_ring_ids) involved_ring_ids = set(involved_ring_ids) self.has_bridged_ring = True self.create_bridged_ring(involved_ring_ids) - + for involved_ring_id in involved_ring_ids: involved_ring = self.id_to_ring[involved_ring_id] self.remove_ring(involved_ring) - - - bridged_systems = self.find_bridged_systems(self.rings, self.ring_overlaps) + + bridged_systems = self.find_bridged_systems( + self.rings, self.ring_overlaps) if bridged_systems and not self.has_bridged_ring: self.has_bridged_ring = True for bridged_system in bridged_systems: @@ -359,7 +322,6 @@ def add_ring_overlap(self, ring_overlap: 'RingOverlap') -> None: self.ring_overlap_id_tracker += 1 - def is_part_of_bridged_ring(self, ring_id: int) -> bool: """ Determines if a given ring is part of a bridged ring system. @@ -376,11 +338,10 @@ def is_part_of_bridged_ring(self, ring_id: int) -> bool: True if the ring is part of a bridged ring system, indicating that it is interconnected with another ring through a bridge, and False otherwise. """ - return any(ring_overlap.involves_ring(ring_id) and ring_overlap.is_bridge() \ - for ring_overlap in self.ring_overlaps) + return any(ring_overlap.involves_ring(ring_id) \ + and ring_overlap.is_bridge() for ring_overlap in self.ring_overlaps) - def get_bridged_ring_subrings(self, ring_id: int, involved_ring_ids: List[int]) -> None: """ Recursively identifies and collects the IDs of all rings involved in a bridged ring system starting @@ -401,14 +362,15 @@ def get_bridged_ring_subrings(self, ring_id: int, involved_ring_ids: List[int]) involved_ring_ids.append(ring_id) 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 \ - self.rings_connected_by_bridge(self.ring_overlaps, ring_id, neighbour_id): + is_connected_by_bridge: bool = \ + neighbour_id not in involved_ring_ids and neighbour_id != ring_id and \ + self.rings_connected_by_bridge(self.ring_overlaps, ring_id, neighbour_id) + if is_connected_by_bridge: self.get_bridged_ring_subrings(neighbour_id, involved_ring_ids) - - @staticmethod - def rings_connected_by_bridge(ring_overlaps: List['RingOverlap'], ring_id_1: int, ring_id_2: int): + def rings_connected_by_bridge(ring_overlaps: List['RingOverlap'], + ring_id_1: int, ring_id_2: int) -> bool: """ Determines if two rings are connected by a bridge based on the list of ring overlaps. @@ -437,9 +399,6 @@ def rings_connected_by_bridge(ring_overlaps: List['RingOverlap'], ring_id_1: int return ring_overlap.is_bridge() return False - - - def create_bridged_ring(self, involved_ring_ids: Set[int]) -> None: """ @@ -477,7 +436,6 @@ def create_bridged_ring(self, involved_ring_ids: Set[int]) -> None: neighbours: Set[int] = set() for ring_id in involved_ring_ids: ring: 'RingProperties' = self.id_to_ring[ring_id] - ring.subring_of_bridged = True for atom in ring.members: atoms.add(atom) for neighbour_id in ring.neighbouring_rings: @@ -493,8 +451,9 @@ def create_bridged_ring(self, involved_ring_ids: Set[int]) -> None: leftovers.add(atom) for atom in leftovers: is_on_ring = False - for bond in self.get_bonds_of_atom(atom): - bond_associated_rings = min(len(bond.atom1.rings), len(bond.atom2.rings)) + for bond, n, m in self.get_bonds_of_atom(atom): + atom1, atom2 = self.atoms[n], self.atoms[m] + bond_associated_rings = min(len(atom1.rings), len(atom2.rings)) if bond_associated_rings == 1: is_on_ring = True if is_on_ring: @@ -521,7 +480,8 @@ def create_bridged_ring(self, involved_ring_ids: Set[int]) -> None: 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: - ring_overlaps: List['RingOverlap'] = self.get_ring_overlaps(neighbour_id, involved_ring_ids) + ring_overlaps: List['RingOverlap'] = self.get_ring_overlaps( + neighbour_id, involved_ring_ids) for ring_overlap in ring_overlaps: ring_overlap.update_other(bridged_ring.id, neighbour_id) @@ -547,9 +507,12 @@ def remove_ring_overlaps_between(self, ring_id_1: int, ring_id_2: int) -> None: """ 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): + is_matching_overlap: bool = \ + (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 is_matching_overlap: to_remove.append(ring_overlap) + for ring_overlap in to_remove: self.ring_overlaps.remove(ring_overlap) @@ -573,13 +536,15 @@ def get_ring_overlaps(self, ring_id: int, ring_ids: List[int]) -> List['RingOver ring_overlaps: List['RingOverlap'] = [] 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): + is_matching_overlap: bool = \ + (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 is_matching_overlap: ring_overlaps.append(ring_overlap) return ring_overlaps - + def remove_ring(self, ring: 'RingProperties') -> None: """ Removes a specified ring from the list of rings and updates the list of ring overlaps accordingly. @@ -599,9 +564,8 @@ def remove_ring(self, ring: 'RingProperties') -> None: if ring.id in neighbouring_ring.neighbouring_rings: neighbouring_ring.neighbouring_rings.remove(ring.id) - - - def find_bridged_systems(self, rings: List['RingProperties'], ring_overlaps: 'RingOverlap') -> List: + def find_bridged_systems(self, rings: List['RingProperties'], + ring_overlaps: 'RingOverlap') -> List: """ Identifies bridged ring systems within the molecule based on the provided rings and their overlaps. @@ -622,15 +586,15 @@ def find_bridged_systems(self, rings: List['RingProperties'], ring_overlaps: 'Ri ring_groups = self.get_ring_groups(rings, ring_overlaps) for ring_group in ring_groups: ring_nr: int = len(ring_group) - overlap_nr: int = self.get_group_overlap_nr(ring_group, ring_overlaps) + overlap_nr: int = self.get_group_overlap_nr( + ring_group, ring_overlaps) if overlap_nr >= ring_nr: bridged_systems.append(ring_group) return bridged_systems - # @TODO: непонятный докстринг, переписать - - def get_ring_groups(self, rings: List['RingProperties'], ring_overlaps: List['RingOverlap']) -> List: + def get_ring_groups(self, rings: List['RingProperties'], + ring_overlaps: List['RingOverlap']) -> List: """ Organizes rings into groups based on their overlaps, identifying interconnected ring systems within the molecule. @@ -674,7 +638,6 @@ def get_ring_groups(self, rings: List['RingProperties'], ring_overlaps: List['Ri ring_groups = [] for ring in rings: ring_groups.append([ring.id]) - current_ring_nr = 0 previous_ring_nr = -1 while current_ring_nr != previous_ring_nr: @@ -685,7 +648,8 @@ def get_ring_groups(self, rings: List['RingProperties'], ring_overlaps: List['Ri ring_group_1_found = False for j, ring_group_2 in enumerate(ring_groups): if i != j: - if self.ring_groups_have_overlap(ring_group_1, ring_group_2, ring_overlaps): + if self.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 @@ -702,9 +666,7 @@ def get_ring_groups(self, rings: List['RingProperties'], ring_overlaps: List['Ri current_ring_nr = len(ring_groups) return ring_groups - - - def ring_groups_have_overlap(self, group_1: List[int], group_2: List[int], \ + def ring_groups_have_overlap(self, group_1: List[int], group_2: List[int], ring_overlaps: List['RingOverlap']) -> bool: """ Determines if two ring groups have an overlap based on the list of ring overlaps. @@ -722,18 +684,12 @@ def ring_groups_have_overlap(self, group_1: List[int], group_2: List[int], \ True if an overlap is found between any rings from the two groups, indicating a structural relationship, and False otherwise. """ - # for ring_1 in group_1: - # for ring_2 in group_2: - # if ring_1 in self.find_neighbouring_rings(ring_overlaps, ring_2): - # return True - # return False - # @TODO: ниже моя версия - return any(ring_1 in self.find_neighbouring_rings(ring_overlaps, ring_2) \ + return any(ring_1 in self.find_neighbouring_rings(ring_overlaps, ring_2) for ring_1 in group_1 for ring_2 in group_2) - @staticmethod - def get_group_overlap_nr(ring_group, ring_overlaps: List['RingOverlap']) -> int: + def get_group_overlap_nr(ring_group, + ring_overlaps: List['RingOverlap']) -> int: """ Calculates the number of overlaps within a group of rings based on a list of ring overlaps. @@ -747,19 +703,11 @@ def get_group_overlap_nr(ring_group, ring_overlaps: List['RingOverlap']) -> int: Returns int The total number of overlaps found within the `ring_group`, where an overlap is counted if both rings involved are members of the group. """ - # 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: - # overlaps += 1 - # return overlaps - # @TODO: моя версия укороченная версиянадо потестировать ring_group_set = set(ring_group) - return sum(overlap.ring_id_1 in ring_group_set and overlap.ring_id_2 in ring_group_set\ - for overlap in ring_overlaps) + return sum(overlap.ring_id_1 in ring_group_set and overlap.ring_id_2 in ring_group_set + for overlap in ring_overlaps) - - def get_bonds_of_atom(self, atom: 'AtomProperties') -> List['BondProperties']: + def get_bonds_of_atom(self, atom: 'AtomProperties') -> Set['Bond']: """ Retrieves all bonds associated with a specified atom within the molecular graph. @@ -782,17 +730,8 @@ def get_bonds_of_atom(self, atom: 'AtomProperties') -> List['BondProperties']: providing comprehensive information about the atom's connectivity within the molecular structure. """ - # bonds: List['BondProperties'] = [] - # for (atom1_id, atom2_id), bond in self.bonds.items(): - # if atom.id in (atom1_id, atom2_id): - # bonds.append(bond) - # return bonds - # @TODO: моя сокращенная версия - return [bond for (atom1_id, atom2_id), bond in self.bonds.items() \ - if atom.id in (atom1_id, atom2_id)] - + return set((bond, n, m) for n, m, bond in self.mc.bonds() if atom.id in (n, m)) - def add_ring(self, ring: 'RingProperties') -> None: """ Adds a new ring to the class's collection and updates the internal tracking of ring @@ -808,8 +747,6 @@ def add_ring(self, ring: 'RingProperties') -> None: self.id_to_ring[ring.id] = ring self.ring_id_tracker += 1 - - ##первое приближение def initial_approximation(self) -> None: """ Determines the initial atom from which to start the layout calculation process for a molecular @@ -831,17 +768,16 @@ def initial_approximation(self) -> None: `create_next_bond` with the selected atom, setting the stage for further layout calculations. """ start_atom = None - for atom in self.graph: if atom.bridged_ring is not None: start_atom = atom break - + if start_atom is None: for ring in self.rings: if ring.bridged: start_atom = ring.members[0] - + if start_atom is None: if len(self.rings) > 0: start_ring: 'RingProperties' = self.id_to_ring[0] @@ -852,15 +788,15 @@ def initial_approximation(self) -> None: if atom.is_terminal(): start_atom = atom break - + if start_atom is None: - start_atom = self.graph[0] + start_atom = self.graph[0] self.create_next_bond(start_atom, None, 0.0) - - - def create_next_bond(self, atom: 'AtomProperties', previous_atom: Optional['AtomProperties']=None, \ - angle: float=0.0, previous_branch_shortest: bool = False) -> None: + def create_next_bond(self, atom: 'AtomProperties', + previous_atom: Optional['AtomProperties'] = None, + angle: float = 0.0, + previous_branch_shortest: bool = False) -> None: """ Creates the next bond for an atom in the molecular structure, updating its position based on the previous atom and angle. @@ -889,31 +825,30 @@ def create_next_bond(self, atom: 'AtomProperties', previous_atom: Optional['Atom """ if atom.positioned: return - if previous_atom is None: + if previous_atom is None: self.calculate_first_atom(atom) - elif len(previous_atom.rings) > 0: + elif len(previous_atom.rings) > 0: self.calculate_rings(previous_atom, atom) - else: + else: self.calculate_NOT_first_atom(atom, previous_atom, angle) - if len(atom.rings) > 0: + if len(atom.rings) > 0: self.calculate_some_rings(atom) else: - neighbours: List['AtomProperties'] = atom.neighbours[:] + neighbours: List['AtomProperties'] = atom.neighbours.copy() if previous_atom and previous_atom in neighbours: neighbours.remove(previous_atom) previous_angle: float = atom.get_angle() if len(neighbours) == 1: - self.calculate_1_neighbours(neighbours, atom, previous_atom, \ - previous_angle, previous_branch_shortest) + self.calculate_1_neighbours(neighbours, atom, previous_atom, previous_angle, + previous_branch_shortest) elif len(neighbours) == 2: self.calculate_2_neighbours(atom, neighbours, previous_atom, previous_angle) elif len(neighbours) == 3: self.calculate_3_neighbours(atom, neighbours, previous_atom, previous_angle) elif len(neighbours) == 4: - self.calculate_4_neighbours(atom , neighbours, previous_angle) + self.calculate_4_neighbours(atom, neighbours, previous_angle) - def calculate_first_atom(self, atom: 'AtomProperties') -> None: """ Calculates the initial position for the first atom in a molecule. @@ -936,10 +871,8 @@ def calculate_first_atom(self, atom: 'AtomProperties') -> None: if atom.bridged_ring is None: atom.positioned = True - - # @TODO: Дать нормальное название - def calculate_NOT_first_atom(self, atom: 'AtomProperties', - previous_atom: 'AtomProperties', angle: float) -> None: + def calculate_NOT_first_atom(self, atom: 'AtomProperties', + previous_atom: 'AtomProperties', angle: float) -> None: """ Calculates the position for an atom that is not the first in the molecule, based on its previous atom and a given angle. @@ -956,18 +889,14 @@ def calculate_NOT_first_atom(self, atom: 'AtomProperties', """ position: Vector = Vector(self.bond_length, 0) position.rotate(angle) - position.add(previous_atom.position) + position += previous_atom.position atom.set_position(position) atom.set_previous_position(previous_atom) atom.positioned = True - - - # @TODO: разбить конкретную функцию на несколько логически обоснованных частей - # например отдельно 2-3 связи отдельно кольца и отдельно остальные случаи - def calculate_1_neighbours(self, neighbours: List[int], atom: 'AtomProperties', \ - previous_atom: 'AtomProperties', previous_angle: float, \ - previous_branch_shortest: bool) -> None: + def calculate_1_neighbours(self, neighbours: List[int], + atom: 'AtomProperties', previous_atom: 'AtomProperties', + previous_angle: float, previous_branch_shortest: bool) -> None: """ Calculates the position for an atom with exactly one neighbor in the molecular structure, considering various bonding scenarios and configurations. @@ -995,20 +924,21 @@ def calculate_1_neighbours(self, neighbours: List[int], atom: 'AtomProperties', Indicates if the previous branch is the shortest, affecting the orientation of the next bond. """ next_atom: 'AtomProperties' = neighbours[0] - current_bond: Optional['BondProperties'] = self.bond_lookup(atom, next_atom) - previous_bond: Optional['BondProperties'] = None + current_bond: Optional['Bond'] = self.bond_lookup(atom, next_atom) + previous_bond: Optional['Bond'] = None if previous_atom: previous_bond = self.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.rings) == 0 and len(atom.neighbours) == 2): - if current_bond.type == 'double' and previous_bond.type == 'double': + if current_bond.order == 3 or (previous_bond and previous_bond.order == 3) \ + or (current_bond.order == 2 and previous_bond + and previous_bond.order == 2 and previous_atom and \ + len(previous_atom.rings) == 0 and len(atom.neighbours) == 2): + if current_bond.order == 2 and previous_bond.order == 2: atom.draw_explicit = True - if current_bond.type == 'triple': + if current_bond.order == 3: atom.draw_explicit = True next_atom.draw_explicit = True - if current_bond.type == 'double' or current_bond.type == 'triple' or \ - (previous_atom and previous_bond.type == 'triple'): + if current_bond.order == 2 or current_bond.order == 3 or ( + previous_atom and previous_bond.order == 3): next_atom.angle = math.radians(0) angle_ = previous_angle + next_atom.angle self.create_next_bond(next_atom, atom, angle_) @@ -1016,25 +946,23 @@ def calculate_1_neighbours(self, neighbours: List[int], atom: 'AtomProperties', proposed_angle_1: float = math.radians(60.0) proposed_angle_2: float = proposed_angle_1 * -1 - proposed_vector_1: 'Vector' = Vector(self.bond_length, 0) - proposed_vector_2: 'Vector' = Vector(self.bond_length, 0) + proposed_vector_1: Vector = Vector(self.bond_length, 0) + proposed_vector_2: Vector = Vector(self.bond_length, 0) proposed_vector_1.rotate(proposed_angle_1 + atom.get_angle()) proposed_vector_2.rotate(proposed_angle_2 + atom.get_angle()) - proposed_vector_1.add(atom.position) - proposed_vector_2.add(atom.position) + proposed_vector_1 += atom.position + proposed_vector_2 += atom.position centre_of_mass: Vector = self.get_current_centre_of_mass() + distance_1: float = proposed_vector_1.get_squared_distance(centre_of_mass) distance_2: float = proposed_vector_2.get_squared_distance(centre_of_mass) - if distance_1 < distance_2: - previous_atom.angle = proposed_angle_2 - else: - previous_atom.angle = proposed_angle_1 + previous_atom.angle = proposed_angle_2 if distance_1 < distance_2 else proposed_angle_1 angle_: float = previous_angle + previous_atom.angle self.create_next_bond(next_atom, atom, angle_) else: - proposed_angle: float = atom.angle + proposed_angle: float = atom.angle - if previous_atom and len(previous_atom.neighbours) > 3: + if previous_atom and len(previous_atom.neighbours) > 3: if round(proposed_angle, 2) > 0.00: proposed_angle: float = min([math.radians(60), proposed_angle]) elif round(proposed_angle, 2) < 0.00: @@ -1049,27 +977,23 @@ def calculate_1_neighbours(self, neighbours: List[int], atom: 'AtomProperties', rotatable: bool = True if previous_atom: - bond: 'BondProperties' = self.bond_lookup(previous_atom, atom) - # This handles cases where there are no second explicit atoms in the - # configuration # of carbons between which cis and trans isomerism can occur - # For example smile = "F/C=C/F" or "F/C=C\F". - if bond.type == 'double': + bond: 'Bond' = self.bond_lookup(previous_atom, atom) + if bond.order == 2: rotatable: bool = False previous_previous_atom: 'AtomProperties' = previous_atom.previous_atom if previous_previous_atom: if (configuration := self.get_configuration(previous_atom, atom)) is not None: - if configuration == 'cis': + if configuration == 'cis': proposed_angle = -proposed_angle if rotatable: next_atom.angle = proposed_angle if previous_branch_shortest else -proposed_angle else: next_atom.angle = -proposed_angle - self.create_next_bond(next_atom, atom, previous_angle + next_atom.angle) - + self.create_next_bond(next_atom, atom, + previous_angle + next_atom.angle) - - def calculate_2_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomProperties'], \ - previous_atom: 'AtomProperties', previous_angle: float) -> None: + def calculate_2_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomProperties'], + previous_atom: 'AtomProperties', previous_angle: float) -> None: """ Calculates the positions for an atom with exactly two neighbours in the molecular structure, considering cis and trans isomerism and the shortest branch condition. @@ -1102,24 +1026,22 @@ def calculate_2_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomP proposed_angle = math.radians(60) self.handle_cis_trans_isomery(atom, neighbours, previous_atom, proposed_angle) - - if previous_atom: - subgraph_3_size: int = self.get_subgraph_size(previous_atom, {atom}) - else: - subgraph_3_size: int = 0 + subgraph_3_size: int = self.get_subgraph_size(previous_atom, {atom}) if previous_atom else 0 previous_branch_shortest = False - if subgraph_3_size < self.get_subgraph_size(neighbours[0], {atom}) and \ + if subgraph_3_size < self.get_subgraph_size(neighbours[0], {atom}) and \ subgraph_3_size < self.get_subgraph_size(neighbours[1], {atom}): previous_branch_shortest = True - self.create_next_bond(neighbours[0], atom, previous_angle + neighbours[0].angle, previous_branch_shortest) - self.create_next_bond(neighbours[1], atom, previous_angle + neighbours[1].angle, previous_branch_shortest) + self.create_next_bond(neighbours[0], atom, + previous_angle + neighbours[0].angle, + previous_branch_shortest) + self.create_next_bond(neighbours[1], atom, + previous_angle + neighbours[1].angle, + previous_branch_shortest) - - - def handle_cis_trans_isomery(self, atom: 'AtomProperties', neighbours: List['AtomProperties'], \ - previous_atom: 'AtomProperties', proposed_angle: float) -> None: + def handle_cis_trans_isomery(self, atom: 'AtomProperties', neighbours: List['AtomProperties'], + previous_atom: 'AtomProperties', proposed_angle: float) -> None: """ Handles the case of cis and trans isomerism for an atom with two neighbours, adjusting their angles based on the isomeric configuration. @@ -1147,14 +1069,15 @@ def handle_cis_trans_isomery(self, atom: 'AtomProperties', neighbours: List['Ato neighbour_1, neighbour_2 = neighbours subgraph_1_size: int = self.get_subgraph_size(neighbour_1, {atom}) subgraph_2_size: int = self.get_subgraph_size(neighbour_2, {atom}) - cis_atom_index: int = 0 trans_atom_index: int = 1 - - if neighbour_2.symbol == 'C' and neighbour_1.symbol != 'C' and subgraph_2_size > 1 and subgraph_1_size < 5: + neighbour_1_is_C: bool = ((self.mc.atom(neighbour_1.id).atomic_symbol) == 'C') + neighbour_2_is_C: bool = ((self.mc.atom(neighbour_2.id).atomic_symbol) == 'C') + + if neighbour_2_is_C and not neighbour_1_is_C and subgraph_2_size > 1 and subgraph_1_size < 5: cis_atom_index = 1 trans_atom_index = 0 - elif neighbour_2.symbol != 'C' and neighbour_1.symbol == 'C' and subgraph_1_size > 1 and subgraph_2_size < 5: + elif not neighbour_2_is_C and neighbour_1_is_C and subgraph_1_size > 1 and subgraph_2_size < 5: cis_atom_index = 0 trans_atom_index = 1 elif subgraph_2_size > subgraph_1_size: @@ -1166,14 +1089,13 @@ def handle_cis_trans_isomery(self, atom: 'AtomProperties', neighbours: List['Ato trans_atom.angle = proposed_angle cis_atom.angle = -proposed_angle + cis_bond: 'Bond' = self.bond_lookup(atom, cis_atom) + trans_bond: 'Bond' = self.bond_lookup(atom, trans_atom) - cis_bond: 'BondProperties' = self.bond_lookup(atom, cis_atom) - trans_bond: 'BondProperties' = self.bond_lookup(atom, trans_atom) - - if cis_bond.type == 'single' and trans_bond.type == 'single': + if cis_bond.order == 1 and trans_bond.order == 1: if previous_atom: - previous_bond: 'BondProperties' = self.bond_lookup(atom, previous_atom) - if previous_bond.type == 'double': + previous_bond: 'Bond' = self.bond_lookup(atom, previous_atom) + if previous_bond.order == 2: if previous_atom.previous_atom: atom1, atom2 = previous_atom, atom configuration_cis_atom: Optional[str] = self.get_configuration(atom1, atom2) @@ -1181,10 +1103,8 @@ def handle_cis_trans_isomery(self, atom: 'AtomProperties', neighbours: List['Ato trans_atom.angle = -proposed_angle cis_atom.angle = proposed_angle - - - def calculate_3_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomProperties'], \ - previous_atom: 'AtomProperties', previous_angle: float) -> None: + def calculate_3_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomProperties'], + previous_atom: 'AtomProperties', previous_angle: float) -> None: """ Calculates the positions for an atom with exactly three neighbours in the molecular structure, adjusting angles based on subgraph sizes and ring involvement. @@ -1231,7 +1151,7 @@ def calculate_3_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomP 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.angle = atom.angle * -1 #maybe bug + straight_atom.angle = atom.angle * -1 if atom.angle >= 0: left_atom.angle = math.radians(30) right_atom.angle = math.radians(90) @@ -1242,14 +1162,15 @@ def calculate_3_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomP straight_atom.angle = math.radians(0) left_atom.angle = math.radians(90) right_atom.angle = math.radians(-90) - self.create_next_bond(straight_atom, atom, previous_angle + straight_atom.angle) - self.create_next_bond(left_atom, atom, previous_angle + left_atom.angle) - self.create_next_bond(right_atom, atom, previous_angle + right_atom.angle) - + self.create_next_bond(straight_atom, atom, + previous_angle + straight_atom.angle) + self.create_next_bond(left_atom, atom, + previous_angle + left_atom.angle) + self.create_next_bond(right_atom, atom, + previous_angle + right_atom.angle) - - def calculate_4_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomProperties'], \ - previous_angle: float) -> None: + def calculate_4_neighbours(self, atom: 'AtomProperties', + neighbours: List['AtomProperties'], previous_angle: float) -> None: """ Handles the case when an atom has exactly four neighbours, adjusting their positions and angles for correct spatial arrangement. @@ -1294,7 +1215,7 @@ def calculate_4_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomP atom_2 = neighbours[0] atom_3 = neighbours[1] atom_4 = neighbours[2] - + atom_1.angle = math.radians(-36) atom_2.angle = math.radians(36) atom_3.angle = math.radians(-108) @@ -1305,9 +1226,8 @@ def calculate_4_neighbours(self, atom: 'AtomProperties', neighbours: List['AtomP self.create_next_bond(atom_4, atom, previous_angle + atom_4.angle) - - def get_subgraph_size(self, atom: 'AtomProperties', \ - masked_atoms: Set['AtomProperties']) -> int: + + def get_subgraph_size(self, atom: 'AtomProperties', masked_atoms: Set['AtomProperties']) -> int: """ Calculates and returns the size of a subtree rooted at a given atom, excluding bonds adjacent to atoms specified in masked_atoms. @@ -1326,16 +1246,14 @@ def get_subgraph_size(self, atom: 'AtomProperties', \ counting atoms that have already been considered in previous calculations or are not relevant to the current analysis. """ - masked_atoms.add(atom) + masked_atoms.add(atom) for neighbour in atom.neighbours: if neighbour not in masked_atoms: self.get_subgraph_size(neighbour, masked_atoms) return len(masked_atoms) - 1 - ## rings calculated - # @TODO: Дать нормальное название - def calculate_rings(self, previous_atom: 'AtomProperties', \ + def calculate_rings(self, previous_atom: 'AtomProperties', atom: 'AtomProperties') -> None: """ Calculates the positions for atoms within rings and aromatic systems, treating the @@ -1360,7 +1278,7 @@ def calculate_rings(self, previous_atom: 'AtomProperties', \ The atom for which the position is being calculated, ensuring it fits correctly within the ring structure. """ - neighbours: List['AtomProperties'] = previous_atom.neighbours + neighbours: List['AtomProperties'] = previous_atom.neighbours joined_vertex: Optional['AtomProperties'] = None position: Vector = Vector(0, 0) if previous_atom.bridged_ring is None and len(previous_atom.rings) > 1: @@ -1369,24 +1287,23 @@ def calculate_rings(self, previous_atom: 'AtomProperties', \ joined_vertex: 'AtomProperties' = neighbour break - if not joined_vertex: for neighbour in neighbours: if neighbour.positioned and self.atoms_are_in_same_ring(neighbour, previous_atom): - position.add(Vector.subtract_vectors(neighbour.position, previous_atom.position)) + position += (neighbour.position - previous_atom.position) position.invert() position.normalise() - position.multiply_by_scalar(self.bond_length) - position.add(previous_atom.position) + position *= self.bond_length + position += previous_atom.position else: position = joined_vertex.position.copy() position.rotate_around_vector(math.pi, previous_atom.position) - atom.set_previous_position(previous_atom) + atom.set_previous_position(previous_atom) atom.set_position(position) atom.positioned = True - # @TODO: Дать нормальное название + def calculate_some_rings(self, atom: 'AtomProperties') -> None: """ Calculates the coordinates for an atom connected to a ring, handling both bridged and @@ -1410,25 +1327,23 @@ def calculate_some_rings(self, atom: 'AtomProperties') -> None: :param atom AtomProperties: The atom for which the ring coordinates are being calculated. This atom is assumed to be part of a ring structure, either directly or through a bridged connection. """ - if atom.bridged_ring: - next_ring: 'RingProperties' = self.id_to_ring[atom.bridged_ring] - else: - next_ring: 'RingProperties' = atom.rings[0] - + next_ring: 'RingProperties' = self.id_to_ring[atom.bridged_ring] \ + if atom.bridged_ring else atom.rings[0] + if not next_ring.positioned: - next_center = Vector.subtract_vectors(atom.previous_position, atom.position) - next_center.invert() - next_center.normalise() - radius: float = Polygon.find_polygon_radius(self.bond_length, len(next_ring.members)) - next_center.multiply_by_scalar(radius) - next_center.add(atom.position) - self.create_ring(next_ring, next_center, atom) - + next_center: 'Vector' = atom.previous_position - atom.position + next_center.invert() + next_center.normalise() + radius: float = Polygon.find_polygon_radius(self.bond_length, len(next_ring.members)) + next_center *= radius + next_center += atom.position + self.create_ring(next_ring, next_center, atom) - def create_ring(self, ring: 'RingProperties', center: Optional[Vector] = None, - start_atom: Optional['AtomProperties'] = None, - previous_atom: Optional['AtomProperties'] = None) -> None: + + def create_ring(self, ring: 'RingProperties', center: Optional[Vector] = None, + start_atom: Optional['AtomProperties'] = None, + previous_atom: Optional['AtomProperties'] = None) -> None: """ Creates a ring within a molecular structure, considering its geometry and interaction with other rings. @@ -1456,60 +1371,57 @@ def create_ring(self, ring: 'RingProperties', center: Optional[Vector] = None, return if center is None: - center = Vector(0, 0) - ordered_neighbour_ids: List[int] = self.get_ordered_neighbours(ring, self.ring_overlaps) + center: Vector = Vector(0, 0) + ordered_neighbour_ids: List[int] = self.get_ordered_neighbours(ring, self.ring_overlaps) starting_angle: float = 0 if start_atom: - starting_angle = Vector.subtract_vectors(start_atom.position, center).angle() + starting_angle: float = (start_atom.position - center).angle() ring_size: int = len(ring.members) - radius: float = Polygon.find_polygon_radius(self.bond_length, ring_size) + radius: float = Polygon.find_polygon_radius( + self.bond_length, ring_size) angle: float = Polygon.get_central_angle(ring_size) ring.central_angle = angle - if start_atom not in ring.members: + if start_atom not in ring.members: if start_atom: start_atom.positioned = False start_atom = ring.members[0] - if ring.bridged: - KKLayout(structure=self, - atoms=ring.members, - center=center, - start_atom=start_atom, - bond_length=self.bond_length) - - + KKLayout(structure=self, atoms=ring.members, center=center, + start_atom=start_atom, bond_length=self.bond_length) ring.positioned = True - self.set_ring_center(ring) + self.set_ring_center(ring) center = ring.center - for subring in ring.subrings: - self.set_ring_center(subring) + for subring in ring.subrings: + self.set_ring_center(subring) else: - self.set_member_positions(ring, start_atom, previous_atom, center, starting_angle, radius, angle) + self.set_member_positions(ring, start_atom, previous_atom, + center, starting_angle, radius, angle) ring.positioned = True ring.center = center for neighbour_id in ordered_neighbour_ids: - neighbour: 'RingProperties' = self.id_to_ring[neighbour_id] - if neighbour.positioned: + neighbour: 'RingProperties' = self.id_to_ring[neighbour_id] + if neighbour.positioned: continue - atoms: Optional[List['AtomProperties']] = self.get_vertices(self.ring_overlaps, ring.id, neighbour.id) + atoms: Optional[List['AtomProperties']] = self.get_vertices( + self.ring_overlaps, ring.id, neighbour.id) if len(atoms) == 2: self.handle_fused_rings(ring, neighbour, atoms, center) elif len(atoms) == 1: self.handle_spiro_rings(ring, neighbour, atoms[0], center) for atom in ring.members: - for neighbour in atom.neighbours: + for neighbour in atom.neighbours: if neighbour.positioned: continue atom.connected_to_ring = True self.create_next_bond(neighbour, atom, 0.0) - - def handle_fused_rings(self, ring: 'RingProperties', neighbour: 'RingProperties', - atoms: List['AtomProperties'], center: Vector) -> None: + def handle_fused_rings(self, ring: 'RingProperties', + neighbour: 'RingProperties', atoms: List['AtomProperties'], + center: Vector) -> None: """ Handles the processing of fused cyclic systems within molecular structures, such as decalin ('C12CCCCC1CCCC2'). @@ -1537,38 +1449,40 @@ def handle_fused_rings(self, ring: 'RingProperties', neighbour: 'RingProperties' The center point around which the fusion is considered, influencing the orientation of the newly formed ring structure. """ - ring.fused = True - neighbour.fused = True + atom_1: 'AtomProperties' = atoms[0] atom_2: 'AtomProperties' = atoms[1] - midpoint: 'Vector' = Vector.get_midpoint(atom_1.position, atom_2.position) - normals: List['Vector'] = Vector.get_normals(atom_1.position, atom_2.position) + midpoint: Vector = Vector.get_midpoint(atom_1.position, atom_2.position) + normals: List[Vector] = Vector.get_normals(atom_1.position, atom_2.position) normals[0].normalise() normals[1].normalise() - apothem: float = Polygon.get_apothem_from_side_length(self.bond_length, len(neighbour.members)) - normals[0].multiply_by_scalar(apothem) - normals[1].multiply_by_scalar(apothem) - normals[0].add(midpoint) - normals[1].add(midpoint) - next_center: 'Vector' = 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() + apothem: float = Polygon.get_apothem_from_side_length( + self.bond_length, len(neighbour.members)) + normals[0] *= apothem + normals[0] += midpoint + normals[1] *= apothem + normals[1] += midpoint + next_center: Vector = normals[0] + + distance_to_center_1: float = (center - normals[0]).get_squared_length() + distance_to_center_2: float = (center - normals[1]).get_squared_length() + if distance_to_center_2 > distance_to_center_1: next_center = normals[1] - position_1: 'Vector' = Vector.subtract_vectors(atom_1.position, next_center) - position_2: 'Vector' = Vector.subtract_vectors(atom_2.position, next_center) + + position_1: Vector = atom_1.position - next_center + position_2: Vector = atom_2.position - next_center + if position_1.get_clockwise_orientation(position_2) == 'clockwise': if not neighbour.positioned: self.create_ring(neighbour, next_center, atom_1, atom_2) - else: - if not neighbour.positioned: - self.create_ring(neighbour, next_center, atom_2, atom_1) - - - - def handle_spiro_rings(self, ring: 'RingProperties', neighbour: 'RingProperties', - atom: 'AtomProperties', center: Vector) -> None: + elif not neighbour.positioned: + self.create_ring(neighbour, next_center, atom_2, atom_1) + + + def handle_spiro_rings(self, ring: 'RingProperties', neighbour: 'RingProperties', + atom: 'AtomProperties', center: Vector) -> None: """ Handles spirocyclic systems within molecular structures, such as 'C1CCCC11CC1'. @@ -1589,19 +1503,17 @@ def handle_spiro_rings(self, ring: 'RingProperties', neighbour: 'RingProperties' The center of the current ring, used as a reference for calculating the new center for the neighboring ring. """ - ring.spiro = True - neighbour.spiro = True - next_center: 'Vector' = Vector.subtract_vectors(center, atom.position) + next_center: Vector = center - atom.position + next_center.invert() next_center.normalise() distance_to_center: float = Polygon.find_polygon_radius(self.bond_length, len(neighbour.members)) - next_center.multiply_by_scalar(distance_to_center) - next_center.add(atom.position) + next_center *= distance_to_center + next_center += atom.position if not neighbour.positioned: self.create_ring(neighbour, next_center, atom) - - ##auxiliary functions for rings calculated + def set_ring_center(self, ring: 'RingProperties') -> None: """ Calculates and sets the geometric center of a ring within a molecular structure. @@ -1618,15 +1530,12 @@ def set_ring_center(self, ring: 'RingProperties') -> None: The ring for which the center is to be calculated. This object represents a cyclic structure within the molecule, containing a list of atoms that are part of the ring. """ - total: Vector = Vector(0, 0) - for atom in ring.members: - total.add(atom.position) - total.divide(len(ring.members)) + total: Vector = sum(atom.position for atom in ring.members) / len(ring.members) ring.center = total - - def atoms_are_in_same_ring(self, atom_1: 'AtomProperties', atom_2: 'AtomProperties') -> bool: + def atoms_are_in_same_ring(self, atom_1: 'AtomProperties', + atom_2: 'AtomProperties') -> bool: """ Determines if two atoms are part of the same ring within a molecular structure. @@ -1644,13 +1553,11 @@ def atoms_are_in_same_ring(self, atom_1: 'AtomProperties', atom_2: 'AtomProperti Returns bool: True if the atoms are in the same ring, False otherwise. """ - return any(ring_id_1 == ring_id_2 for ring_id_1 in atom_1.rings \ - for ring_id_2 in atom_2.rings) + return any(ring_id_1 == ring_id_2 for ring_id_1 in atom_1.rings + for ring_id_2 in atom_2.rings) - - - def get_vertices(self, ring_overlaps: List['RingOverlap'], ring_id_1: int, \ - ring_id_2: int) -> Optional[List['AtomProperties']]: + def get_vertices(self, ring_overlaps: List['RingOverlap'], ring_id_1: int, + ring_id_2: int) -> Optional[List['AtomProperties']]: """ Searches for and returns atoms that are in the overlap between two rings identified by ring_id_1 and ring_id_2. @@ -1673,9 +1580,8 @@ def get_vertices(self, ring_overlaps: List['RingOverlap'], ring_id_1: int, \ return [atom for atom in ring_overlap.atoms] - - def get_ordered_neighbours(self, ring: 'RingProperties', \ - ring_overlaps: List['RingOverlap']) -> List[int]: + def get_ordered_neighbours(self, ring: 'RingProperties', + ring_overlaps: List['RingOverlap']) -> List[int]: """ Retrieves an ordered list of neighboring rings based on the number of atoms they share in common with the specified ring. @@ -1698,18 +1604,19 @@ def get_ordered_neighbours(self, ring: 'RingProperties', \ """ ordered_neighbours_and_atom_nrs = [] for neighbour_id in ring.neighbouring_rings: - atoms: Optional[List['AtomProperties']] = self.get_vertices(ring_overlaps, ring.id, neighbour_id) + atoms: Optional[List['AtomProperties']] = self.get_vertices( + ring_overlaps, ring.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, ring: 'RingProperties', start_atom: 'AtomProperties', \ - previous_atom: Optional['AtomProperties'], center: 'Vector', \ - starting_angle: float, radius: float, angle: float) -> None: + def set_member_positions(self, ring: 'RingProperties', start_atom: 'AtomProperties', + previous_atom: Optional['AtomProperties'], center: Vector, + starting_angle: float, radius: float, angle: float) -> None: """ Positions atoms within a ring structure using polar coordinates (center, radius, angle) and incrementally increases the angle between atoms. @@ -1740,11 +1647,11 @@ def set_member_positions(self, ring: 'RingProperties', start_atom: 'AtomProperti """ current_atom = start_atom iteration = 0 - while current_atom != None and iteration < 100: + while current_atom is not None and iteration < 100: previous = current_atom if not previous.positioned: - x = center.x + math.cos(starting_angle) * radius - y = center.y + math.sin(starting_angle) * radius + x = center[0] + math.cos(starting_angle) * radius + y = center[1] + math.sin(starting_angle) * radius previous.set_position(Vector(x, y)) starting_angle += angle @@ -1752,7 +1659,8 @@ def set_member_positions(self, ring: 'RingProperties', start_atom: 'AtomProperti previous.angle = starting_angle previous.positioned = True - current_atom = self.get_next_in_ring(ring, current_atom, previous_atom) + current_atom = self.get_next_in_ring( + ring, current_atom, previous_atom) previous_atom = previous if current_atom == start_atom: @@ -1793,10 +1701,10 @@ def get_next_in_ring(self, ring: 'RingProperties', current_atom: 'AtomProperties if neighbour == member: if previous_atom != neighbour: return neighbour - - + @staticmethod - def find_neighbouring_rings(ring_overlaps: List['RingOverlap'], ring_id: int) -> List[int]: + def find_neighbouring_rings(ring_overlaps: List['RingOverlap'], + ring_id: int) -> List[int]: """ Finds and returns a list of identifiers for rings neighboring the specified ring. @@ -1821,7 +1729,7 @@ def find_neighbouring_rings(ring_overlaps: List['RingOverlap'], ring_id: int) -> the specified ring, indicating a direct connection or overlap. """ neighbouring_rings = [] - for ring_overlap in ring_overlaps: + for ring_overlap in ring_overlaps: if ring_overlap.ring_id_1 == ring_id: neighbouring_rings.append(ring_overlap.ring_id_2) elif ring_overlap.ring_id_2 == ring_id: @@ -1846,17 +1754,13 @@ def get_current_centre_of_mass(self) -> Vector: A Vector object representing the coordinates of the center of mass of the molecular graph, based on the positions of all positioned atoms. """ - total = Vector(0, 0) - count = 0 - for atom in self.graph: - if atom.positioned: - total.add(atom.position) - count += 1 - total.divide(count) - return total - + if positioned_atoms := [atom.position for atom in self.graph if atom.positioned]: + total = np.sum(positioned_atoms, axis=0) + count = len(positioned_atoms) + return total / count + else: + return Vector(0, 0) - def get_last_atom_with_angle(self, atom: 'AtomProperties') -> Optional['AtomProperties']: """ Retrieves the last atom in a chain that has a defined angle relative to the initial atom. @@ -1875,16 +1779,14 @@ def get_last_atom_with_angle(self, atom: 'AtomProperties') -> Optional['AtomProp The last atom in the chain that has a defined angle, or None if no such atom is found before reaching the start of the chain. """ - parent_atom: Optional['AtomProperties'] = atom.previous_atom + parent_atom: Optional['AtomProperties'] = atom.previous_atom angle: float = parent_atom.angle while parent_atom and not angle: parent_atom = parent_atom.previous_atom angle = parent_atom.angle return parent_atom - - # упаковка и возвращение координат - def get_coord(self, order: List[int]) -> List[List[float]]: + def get_coord(self, order: List[int]) -> List: """ Packs and returns the coordinates of atoms in a two-dimensional array based on the specified order. @@ -1898,14 +1800,8 @@ def get_coord(self, order: List[int]) -> List[List[float]]: A two-dimensional list where each inner list contains the x and y coordinates of an atom, following the order specified in the input list. """ - xy: List[List[float]] = [] - for ord in order: - vector = self.atoms[ord].position - xy.append([vector.x, vector.y]) - return xy + return [self.atoms[ord].position[:] for ord in order] - - # обработка коллизий def collision_handling(self) -> None: """ Handles the positioning of all atoms and resolves any overlaps within the molecular structure. @@ -1943,22 +1839,21 @@ def collision_handling(self) -> None: set. """ self.resolve_primary_overlaps() - - self.total_overlap_score, sorted_overlap_scores, atom_to_scores = self.get_overlap_score() - for i in range(self.overlap_resolution_iterations): - for (atom1_index, atom2_index), bond in self.bonds.items(): + self.total_overlap_score, sorted_overlap_scores, atom_to_scores = self.get_overlap_score() + for _ in range(self.overlap_resolution_iterations): + for atom1_index, atom2_index, bond in self.mc.bonds(): n: 'AtomProperties' = self.atoms[atom1_index] m: 'AtomProperties' = self.atoms[atom2_index] - if self.can_rotate_around_bond(bond): + if self.can_rotate_around_bond(bond, n, m): tree_depth_1: int = self.get_subgraph_size(n, {m}) tree_depth_2: int = self.get_subgraph_size(m, {n}) atom_1_rotatable: bool = True atom_2_rotatable: bool = True - for neighbouring_bond in self.get_bonds_of_atom(n): - if neighbouring_bond.type == 'double': + for neighbouring_bond, *coords in self.get_bonds_of_atom(n): + if neighbouring_bond.order == 2: atom_1_rotatable = False - for neighbouring_bond in self.get_bonds_of_atom(m): - if neighbouring_bond.type == 'double': + for neighbouring_bond, *coords in self.get_bonds_of_atom(m): + if neighbouring_bond.order == 2: atom_2_rotatable = False if not atom_1_rotatable and not atom_2_rotatable: continue @@ -1974,15 +1869,16 @@ def collision_handling(self) -> None: if tree_depth_1 > tree_depth_2: atom_1: 'AtomProperties' = n atom_2: 'AtomProperties' = m - 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.overlap_sensitivity: - - neighbours_2 = atom_2.neighbours[:] - neighbours_2.remove(atom_1) + neighbours_2 = atom_2.neighbours.copy() + neighbours_2.remove(atom_1) if len(neighbours_2) == 1: neighbour = neighbours_2[0] - angle = neighbour.position.get_rotation_away_from_vector(atom_1.position, atom_2.position, math.radians(120)) + angle = neighbour.position.get_rotation_away_from_vector( \ + atom_1.position, atom_2.position, math.radians(120)) self.rotate_subtree(neighbour, atom_2, angle, atom_2.position) new_overlap_score, _, _ = self.get_overlap_score() if new_overlap_score > self.total_overlap_score: @@ -2000,8 +1896,10 @@ def collision_handling(self) -> None: elif neighbour_1.rings or neighbour_2.rings: continue else: - angle_1 = neighbour_1.position.get_rotation_away_from_vector(atom_1.position, atom_2.position, math.radians(120)) - angle_2 = neighbour_2.position.get_rotation_away_from_vector(atom_1.position, atom_2.position, math.radians(120)) + angle_1 = neighbour_1.position.get_rotation_away_from_vector( \ + atom_1.position, atom_2.position, math.radians(120)) + angle_2 = neighbour_2.position.get_rotation_away_from_vector( \ + atom_1.position, atom_2.position, math.radians(120)) self.rotate_subtree(neighbour_1, atom_2, angle_1, atom_2.position) self.rotate_subtree(neighbour_2, atom_2, angle_2, atom_2.position) new_overlap_score, _, _ = self.get_overlap_score() @@ -2014,11 +1912,9 @@ def collision_handling(self) -> None: for _ in range(self.overlap_resolution_iterations): self._finetune_overlap_resolution() self.total_overlap_score, sorted_overlap_scores, atom_to_scores = self.get_overlap_score() - for i in range(self.overlap_resolution_iterations): + for _ in range(self.overlap_resolution_iterations): self.resolve_secondary_overlaps(sorted_overlap_scores) - - ## вспомогательные функции для collision_handling def resolve_primary_overlaps(self) -> None: """ Resolves initial overlaps in the molecular structure, focusing on cases where a ring has @@ -2041,11 +1937,14 @@ def resolve_primary_overlaps(self) -> None: if resolved_atoms[atom.id]: continue resolved_atoms[atom.id] = True - non_ring_neighbours: List['AtomProperties'] = self.get_non_ring_neighbours(atom) - if len(non_ring_neighbours) > 1 or (len(non_ring_neighbours) == 1 and len(atom.rings) == 2): - overlaps.append({'common': atom, 'rings': atom.rings, 'vertices': non_ring_neighbours}) + non_ring_neighbours: List['AtomProperties'] = self.get_non_ring_neighbours( + atom) + if len(non_ring_neighbours) > 1 or (len(non_ring_neighbours) == 1 and\ + len(atom.rings) == 2): + overlaps.append({'common': atom, 'rings': atom.rings, + 'vertices': non_ring_neighbours}) for overlap in overlaps: - branches_to_adjust: List['AtomProperties'] = overlap['vertices'] + branches_to_adjust: List['AtomProperties'] = overlap['vertices'] rings: List['RingProperties'] = overlap['rings'] root: 'AtomProperties' = overlap['common'] if len(branches_to_adjust) == 2: @@ -2054,20 +1953,23 @@ def resolve_primary_overlaps(self) -> None: self.rotate_subtree(atom_1, root, angle, root.position) self.rotate_subtree(atom_2, root, -angle, root.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.position) self.rotate_subtree(atom_2, root, 2.0 * angle, root.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: self.rotate_subtree(atom_1, root, 2.0 * angle, root.position) self.rotate_subtree(atom_2, root, -2.0 * angle, root.position) - ## вспомогательные функции для resolve_primary_overlaps @staticmethod def get_non_ring_neighbours(atom: 'AtomProperties') -> List['AtomProperties']: """ @@ -2093,14 +1995,15 @@ def get_non_ring_neighbours(atom: 'AtomProperties') -> List['AtomProperties']: """ non_ring_neighbours: List['AtomProperties'] = [] for neighbour in atom.neighbours: - nr_overlapping_rings = len(set(atom.ring_indexes).intersection(set(neighbour.ring_indexes))) + nr_overlapping_rings = len( + set(atom.ring_indexes).intersection( + set(neighbour.ring_indexes))) if nr_overlapping_rings == 0 and not neighbour.is_bridge: non_ring_neighbours.append(neighbour) return non_ring_neighbours - ## вспомогательные функции для resolve_primary_overlaps - def rotate_subtree(self, root: 'AtomProperties', root_parent: 'AtomProperties', \ - angle: float, center: 'Vector') -> None: + def rotate_subtree(self, root: 'AtomProperties', root_parent: 'AtomProperties', + angle: float, center: Vector) -> None: """ Rotates a subtree of the molecular structure around a specified center by a given angle. @@ -2129,15 +2032,13 @@ def rotate_subtree(self, root: 'AtomProperties', root_parent: 'AtomProperties', position of a pivotal atom or a calculated point that serves as the axis of rotation. """ for atom in self.traverse_substructure(root, {root_parent}): - atom.position.rotate_around_vector(angle, center) + atom.position.rotate_around_vector(angle, center) for anchored_ring in atom.anchored_rings: if anchored_ring.center: - anchored_ring.center.rotate_around_vector(angle, center) - + anchored_ring.center.rotate_around_vector(angle, center) - ## вспомогательные функции для rotate_subtree - def traverse_substructure(self, atom: 'AtomProperties', visited: Set['AtomProperties']) \ - -> Generator['AtomProperties', None, None]: + def traverse_substructure(self, atom: 'AtomProperties', + visited: Set['AtomProperties']) -> Generator['AtomProperties', None, None]: """ Traverses a substructure of the molecular graph starting from a given atom, yielding atoms in a depth-first manner. @@ -2169,7 +2070,6 @@ def traverse_substructure(self, atom: 'AtomProperties', visited: Set['AtomProper yield from self.traverse_substructure(neighbour, visited) - ## вспомогательные функции для resolve_primary_overlaps def get_overlap_score(self) -> Tuple[float, List[Tuple[float, 'AtomProperties']], Dict[int, float]]: """ Calculates the total overlap score and returns a sorted list of atoms by their overlap @@ -2198,28 +2098,29 @@ class and returns a tuple containing the total overlap score, a list of atoms so detailed insights into the distribution of overlaps across the structure. """ total: float = 0.0 - overlap_scores : Dict[int, float]= {} - for atom in self.graph: - overlap_scores[atom.id] = 0.0 - - atoms: List['AtomProperties'] = list(self.graph.keys()) - for i, atom_1 in enumerate(atoms): - for j, atom_2 in enumerate(atoms[i+1:], start=i+1): - distance: float = Vector.subtract_vectors(atom_1.position, atom_2.position).get_squared_length() - if distance < (self.bond_length ** 2): - weight = (self.bond_length - math.sqrt(distance)) / self.bond_length + overlap_scores: Dict[int, float] = {atom.id: 0.0 for atom in self.graph} + atoms: List['AtomProperties'] = list(self.graph) + positions = np.array([atom.position for atom in atoms]) + distances_squared = np.sum((positions[:, np.newaxis] - positions[np.newaxis, :]) ** 2, axis=-1) + bond_length_squared = self.bond_length ** 2 + + for i in range(len(atoms)): + for j in range(i + 1, len(atoms)): + if distances_squared[i, j] < bond_length_squared: + distance_squared = distances_squared[i, j] + weight = (self.bond_length - np.sqrt(distance_squared)) / self.bond_length total += weight - overlap_scores[atom_1.id] += weight - overlap_scores[atom_2.id] += weight - sorted_overlaps: List[Tuple[float, 'AtomProperties']] = [] - for atom in atoms: - sorted_overlaps.append((overlap_scores[atom.id], atom)) + overlap_scores[atoms[i].id] += weight + overlap_scores[atoms[j].id] += weight + + sorted_overlaps: List[Tuple[float, 'AtomProperties']] = \ + [(overlap_scores[atom.id], atom) for atom in atoms] sorted_overlaps.sort(key=lambda x: x[0], reverse=True) return total, sorted_overlaps, overlap_scores - ## вспомогательные функции для resolve_primary_overlaps - def get_subtree_overlap_score(self, root: 'AtomProperties', root_parent: 'AtomProperties', + def get_subtree_overlap_score(self, root: 'AtomProperties', + root_parent: 'AtomProperties', atom_to_score: Dict[int, float]) -> Tuple[float, Vector]: """ Calculates the weighted center and total overlap score for a subtree rooted at a given @@ -2267,17 +2168,16 @@ def get_subtree_overlap_score(self, root: 'AtomProperties', root_parent: 'AtomPr score += subscore count += 1 position = atom.position.copy() - position.multiply_by_scalar(subscore) - center.add(position) + position *= subscore + center += position if score: - center.divide(score) + center /= score if count == 0: count = 1 return score / count, center - - @staticmethod - def can_rotate_around_bond(bond: 'BondProperties') -> bool: + + def can_rotate_around_bond(self, bond: 'Bond', atom1: 'AtomProperties', atom2: 'AtomProperties') -> bool: """ Determines whether a bond can be rotated to adjust the molecular structure without breaking its integrity. @@ -2305,15 +2205,13 @@ def can_rotate_around_bond(bond: 'BondProperties') -> bool: otherwise, indicating constraints that prevent rotation to avoid disrupting the molecular structure. """ - if bond.type != 'single': - return False - if len(bond.atom1.neighbours) == 1 or len(bond.atom2.neighbours) == 1: - return False - if bond.atom1.rings and bond.atom2.rings and len(set(bond.atom1.rings).intersection(set(bond.atom2.rings))) > 0: - return False - return True + is_single_bond: bool = (bond.order == 1) + has_multiple_neighbours: bool = (len(atom1.neighbours) > 1 and len(atom2.neighbours) > 1) + are_in_same_ring: bool = (atom1.rings and atom2.rings and + len(set(atom1.rings).intersection(set(atom2.rings))) > 0) + return is_single_bond and has_multiple_neighbours and not are_in_same_ring + - def _finetune_overlap_resolution(self) -> None: """ Fine-tunes the resolution of overlaps between atoms in the molecular structure by @@ -2346,32 +2244,40 @@ def _finetune_overlap_resolution(self) -> None: """ if self.total_overlap_score > self.overlap_sensitivity: clashing_atoms: List[Tuple['AtomProperties', 'AtomProperties']] = self._find_clashing_atoms() - best_bonds: List['BondProperties'] = [] + best_connections: List[Tuple[int, int]] = [] # List to store best connections as tuples of atom indices + for atom_1, atom_2 in clashing_atoms: if self.is_connected(atom_1, atom_2): - shortest_path: List[Union['BondProperties', 'AtomProperties']] = self.find_shortest_path(atom_1, atom_2) - rotatable_bonds: List['BondProperties'] = [] - distances: List[float] = [] - for i, bond in enumerate(shortest_path): - distance_1: int = i - distance_2: int = len(shortest_path) - i - average_distance = len(shortest_path) / 2 - distance_metric = abs(average_distance - distance_1) + abs(average_distance - distance_2) - if self.bond_is_rotatable(bond): # я не дореализовал #fix it - rotatable_bonds.append(bond) - distances.append(distance_metric) - best_bond: Optional['BondProperties'] = None - optimal_distance: float = float('inf') - for i, distance in enumerate(distances): - if distance < optimal_distance: - best_bond: 'BondProperties' = rotatable_bonds[i] - optimal_distance: float = distance - if best_bond is not None: - best_bonds.append(best_bond) - best_bonds = list(set(best_bonds)) - for best_bond in best_bonds: + shortest_path: List['AtomProperties'] = self.find_shortest_path(atom_1, atom_2) + rotatable_connections: List[Tuple[int, int]] = [] # Store rotatable connections as tuples + distances: List[float] = [] + for i in range(len(shortest_path) - 1): + atom = shortest_path[i] + distance_1: int = i + distance_2: int = len(shortest_path) - i - 1 # Уменьшаем на 1 + average_distance = len(shortest_path) / 2 + distance_metric = abs(average_distance - distance_1) + abs(average_distance - distance_2) + + if atom.id in self.mc.int_adjacency.keys() and \ + any(neighbor.id in self.mc.int_adjacency[atom.id] for neighbor in shortest_path): + rotatable_connections.append((atom.id, shortest_path[i + 1].id)) # Обращаемся к следующему атома + distances.append(distance_metric) + + best_connection: Optional[Tuple[int, int]] = None + optimal_distance: float = float('inf') + for i, distance in enumerate(distances): + if distance < optimal_distance: + best_connection = rotatable_connections[i] + optimal_distance = distance + + if best_connection is not None: + best_connections.append(best_connection) + + best_connections = set(best_connections) + for best_connection in best_connections: if self.total_overlap_score > self.overlap_sensitivity: - atom_1, atom_2 = best_bond.atom1, best_bond.atom2 + n, m = best_connection # Unpack the best connection tuple + atom_1, atom_2 = self.atoms[n], self.atoms[m] subtree_size_1: int = self.get_subgraph_size(atom_1, {atom_2}) subtree_size_2: int = self.get_subgraph_size(atom_2, {atom_1}) if subtree_size_1 < subtree_size_2: @@ -2382,13 +2288,14 @@ def _finetune_overlap_resolution(self) -> None: parent_atom = atom_1 overlap_score, _, _ = self.get_overlap_score() scores: List[float] = [overlap_score] - # Attempt 12 rotations + for i in range(12): - self.rotate_subtree(rotating_atom, parent_atom, math.radians(30), parent_atom.position) + self.rotate_subtree(rotating_atom, parent_atom, + math.radians(30), parent_atom.position) new_overlap_score, _, _ = self.get_overlap_score() scores.append(new_overlap_score) assert len(scores) == 13 - scores = scores[:12] + scores = scores[:12].copy() best_i = 0 best_score = scores[0] for i, score in enumerate(scores): @@ -2396,7 +2303,8 @@ def _finetune_overlap_resolution(self) -> None: best_score = score best_i = i self.total_overlap_score = best_score - self.rotate_subtree(rotating_atom, parent_atom, math.radians(30 * best_i + 1), parent_atom.position) + self.rotate_subtree(rotating_atom, parent_atom, \ + math.radians(30 * best_i + 1), parent_atom.position) @@ -2420,15 +2328,15 @@ def _find_clashing_atoms(self) -> List[Tuple['AtomProperties', 'AtomProperties'] clashing_atoms: List[Tuple['AtomProperties', 'AtomProperties']] = [] atoms: List['AtomProperties'] = list(self.graph.keys()) for i, atom_1 in enumerate(atoms): - for j, atom_2 in enumerate(atoms[i+1:], start=i+1): + for j, atom_2 in enumerate(atoms[i + 1:], start=i + 1): if self.bond_lookup(atom_1, atom_2) is None: - distance = Vector.subtract_vectors(atom_1.position, atom_2.position).get_squared_length() + difference: 'Vector' = atom_1.position - atom_2.position + distance: float = difference.get_squared_length() if distance < 0.8 * (self.bond_length**2): clashing_atoms.append((atom_1, atom_2)) return clashing_atoms - - + def is_connected(self, atom_1, atom_2) -> bool: """ Determines if two atoms are connected within the molecular graph, i.e., part of the same @@ -2445,130 +2353,126 @@ def is_connected(self, atom_1, atom_2) -> bool: return atom_1 in self.graph and atom_2 in self.graph - - def bond_is_rotatable(self, bond: 'BondProperties') -> bool: - """ - Determines if a bond can be rotated in the molecular structure drawing, based on its - type and the atoms it connects. - - This method evaluates whether a given bond is rotatable, which is crucial for adjusting the molecular layout to resolve overlaps or achieve a more accurate representation. A bond is considered rotatable if it is not constrained by stereochemical considerations, such as being part of a ring or having a specific type that restricts rotation (e.g., double or triple bonds). The method checks if the bond connects atoms that are part of the same ring, which would prevent rotation, and if the bond type is not a single bond, it further checks the number of neighbors each atom has to determine if rotation is possible. Additionally, it considers chiral centers and specific stereochemical markers that might restrict rotation. The presence of these conditions indicates that the bond is not rotatable, and the method returns False. Otherwise, it returns True, indicating the bond can be rotated to adjust the molecular structure. - - Parameters - :param bond BondProperties: The bond to evaluate for rotatability. - - Returns bool: - True if the bond is rotatable, meaning it can be rotated in the drawing to adjust the molecular structure without violating stereochemical constraints; False if the bond is fixed in place due to being part of a ring, being a non-single bond, or involving chiral centers. - """ - atom_1, atom_2 = bond.atom1, bond.atom2 - if atom_1.rings and atom_2.rings and len(set(atom_1.rings).intersection(set(atom_2.rings))) > 0: - return False - if bond.type != 'single': - if len(atom_1.neighbours) > 1 and len(atom_2.neighbours) > 1: - return False - chiral = False - self.get_bonds_of_atom(atom_1) - # for bond_1 in self.get_bonds_of_atom(atom_1): - # if self.chiral[bond_1]: - # chiral = True - # break - # for bond_2 in self.get_bonds_of_atom(atom_2): - # if self.chiral[bond_2]: - # chiral = True - # break - if chiral: - return False - # if self.chiral_symbol[bond]: - # return False - return True - - - - def find_shortest_path(self, atom_1: 'AtomProperties', atom_2: 'AtomProperties', \ - path_type: str = 'bond') -> List[Union['BondProperties', 'AtomProperties']]: - """ - Finds the shortest path between two atoms in the molecular graph, returning either the - sequence of bonds or atoms along the path. - - This method implements a shortest path algorithm to determine the most direct route - between two specified atoms within the molecular structure. It can return the path as a - list of either the bonds connecting the atoms or the atoms themselves, depending on the - `path_type` parameter. The algorithm initializes by setting the distance to all atoms as - infinite, except for the starting atom, which is set to zero. It then iteratively - selects the atom with the smallest distance that has not been visited, updates the - distances to its neighbors, and marks it as visited. This process continues until the - destination atom is reached or all atoms have been visited. Finally, it constructs the - path from the destination atom back to the starting atom using the recorded previous - hops. - - Parameters - :param atom_1 AtomProperties: - The starting atom from which to find the shortest path. - :param atom_2 AtomProperties: - The destination atom to which to find the shortest path. - :param path_type str: - Specifies the type of elements to return in the path. 'bond' returns the bonds along the path, 'atom' returns the atoms. Default is 'bond'. - - Returns List[Union['BondProperties', 'AtomProperties']: - A list representing the shortest path between `atom_1` and `atom_2`. If `path_type` - is 'bond', the list contains `BondProperties` objects; if 'atom', it contains - `AtomProperties` objects. - - Raises ValueError: - If `path_type` is neither 'bond' nor 'atom'. - """ + # def find_shortest_path(self, atom_1: 'AtomProperties', atom_2: 'AtomProperties') -> List[Union['Bond', 'AtomProperties']]: + # """ + # Finds the shortest path between two atoms in the molecular graph, returning either the + # sequence of bonds or atoms along the path. + + # This method implements a shortest path algorithm to determine the most direct route + # between two specified atoms within the molecular structure. It can return the path as a + # list of either the bonds connecting the atoms or the atoms themselves, depending on the + # `path_type` parameter. The algorithm initializes by setting the distance to all atoms as + # infinite, except for the starting atom, which is set to zero. It then iteratively + # selects the atom with the smallest distance that has not been visited, updates the + # distances to its neighbors, and marks it as visited. This process continues until the + # destination atom is reached or all atoms have been visited. Finally, it constructs the + # path from the destination atom back to the starting atom using the recorded previous + # hops. + + # Parameters + # :param atom_1 AtomProperties: + # The starting atom from which to find the shortest path. + # :param atom_2 AtomProperties: + # The destination atom to which to find the shortest path. + # :param path_type str: + # Specifies the type of elements to return in the path. 'bond' returns the bonds along the path, 'atom' returns the atoms. Default is 'bond'. + + # Returns List[Union['BondProperties', 'AtomProperties']: + # A list representing the shortest path between `atom_1` and `atom_2`. If `path_type` + # is 'bond', the list contains `BondProperties` objects; if 'atom', it contains + # `AtomProperties` objects. + + # Raises ValueError: + # If `path_type` is neither 'bond' nor 'atom'. + # """ + # distances: Dict['AtomProperties', float] = {} + # previous_hop: Dict['AtomProperties', Optional['AtomProperties']] = {} + # unvisited: Set['AtomProperties'] = set() + # for atom in self.graph: + # distances[atom] = float('inf') + # previous_hop[atom] = None + # unvisited.add(atom) + # distances[atom_1] = 0.0 + # while unvisited: + # current_atom: Optional['AtomProperties'] = None + # minimum: float = float('inf') + # for atom in unvisited: + # dist: float = distances[atom] + # if dist < minimum: + # current_atom: 'AtomProperties' = atom + # minimum = dist + # if current_atom is None or current_atom == atom_2: + # break + # unvisited.remove(current_atom) + + # for neighbour in self.graph[current_atom]: + # if neighbour in unvisited: + # alternative_distance: float = distances[current_atom] + 1.0 + # if alternative_distance < distances[neighbour]: + # distances[neighbour] = alternative_distance + # previous_hop[neighbour] = current_atom + + # path_atoms: List['AtomProperties'] = [] + # current_atom: Optional['AtomProperties'] = atom_2 + # if previous_hop[current_atom] or current_atom == atom_1: + # while current_atom: + # path_atoms.insert(0, current_atom) + # current_atom = previous_hop[current_atom] + + # path: List[Union['Bond', 'AtomProperties']] = [] + # for i in range(1, len(path_atoms)): + # atom_1 = path_atoms[i - 1] + # atom_2 = path_atoms[i] + # bond = self.bond_lookup(atom_1, atom_2) + # path.append(bond) + # return path + + def find_shortest_path(self, atom_1: 'AtomProperties', atom_2: 'AtomProperties') -> List['AtomProperties']: distances: Dict['AtomProperties', float] = {} previous_hop: Dict['AtomProperties', Optional['AtomProperties']] = {} unvisited: Set['AtomProperties'] = set() + + # Инициализация расстояний и предыдущих переходов for atom in self.graph: distances[atom] = float('inf') - previous_hop[atom] = None + previous_hop[atom] = None unvisited.add(atom) + distances[atom_1] = 0.0 while unvisited: current_atom: Optional['AtomProperties'] = None minimum: float = float('inf') - # Find the atom with the smallest distance value that has not yet been visited + # Поиск атома с минимальным расстоянием for atom in unvisited: dist: float = distances[atom] - if dist < minimum: - current_atom: 'AtomProperties' = atom + if dist < minimum:## + current_atom = atom minimum = dist - if current_atom is None: - break - if current_atom == atom_2: + # Если нет текущего атома или достигли целевого атома, выходим из цикла + if current_atom is None or current_atom == atom_2: break + unvisited.remove(current_atom) - # If there exists a shorter path between the source atom and the neighbours, update distance + # Обновление расстояний до соседей for neighbour in self.graph[current_atom]: if neighbour in unvisited: alternative_distance: float = distances[current_atom] + 1.0 - if alternative_distance < distances[neighbour]: distances[neighbour] = alternative_distance previous_hop[neighbour] = current_atom - # Construct the path of atoms + # Сбор пути в виде списка атомов path_atoms: List['AtomProperties'] = [] current_atom: Optional['AtomProperties'] = atom_2 - if previous_hop[current_atom] or current_atom == atom_1: - while current_atom: + # Проверка на наличие предыдущих переходов и сбор пути + if previous_hop[current_atom] is not None or current_atom == atom_1: + while current_atom is not None: path_atoms.insert(0, current_atom) current_atom = previous_hop[current_atom] - if path_type == 'bond': - path: List[Union['BondProperties', 'AtomProperties']] = [] - for i in range(1, len(path_atoms)): - atom_1 = path_atoms[i - 1] - atom_2 = path_atoms[i] - bond = self.bond_lookup(atom_1, atom_2) - path.append(bond) - return path - elif path_type == 'atom': - return path_atoms - else: - raise ValueError("Path type must be 'bond' or 'atom'.") + return path_atoms # Возвращаем список атомов вместо связей - - def resolve_secondary_overlaps(self, sorted_scores: List[Tuple[float, 'AtomProperties']]) -> None: + def resolve_secondary_overlaps(self, sorted_scores: \ + List[Tuple[float, 'AtomProperties']]) -> None: """ Resolves secondary overlaps in the molecular structure by adjusting the positions of atoms based on their overlap scores. @@ -2607,23 +2511,18 @@ def resolve_secondary_overlaps(self, sorted_scores: List[Tuple[float, 'AtomPrope if atom.neighbours: continue closest_atom: 'AtomProperties' = self.get_closest_atom(atom) - neighbours = closest_atom.neighbours - if len(neighbours) <= 1: - if not closest_atom.previous_position: - closest_position: float = atom.neighbours[0].position - else: - closest_position: float = closest_atom.previous_position - else: - if not closest_atom.previous_position: - closest_position: float = atom.neighbours[0].position - else: - closest_position: float = closest_atom.position - if not atom.previous_position: - atom_previous_position: float = atom.neighbours[0].position + if len(closest_atom.neighbours) <= 1: + closest_position: float = closest_atom.previous_position \ + if closest_atom.previous_position else atom.neighbours[0].position else: - atom_previous_position: float = atom.previous_position - atom.position.rotate_away_from_vector(closest_position, \ - atom_previous_position, math.radians(20)) + closest_position: float = closest_atom.position \ + if closest_atom.previous_position else atom.neighbours[0].position + + atom_previous_position: float = atom.previous_position \ + if atom.previous_position else atom.neighbours[0].position + + atom.position = atom.position.rotate_away_from_vector(closest_position, \ + atom_previous_position, math.radians(20)) @@ -2659,4 +2558,9 @@ def get_closest_atom(self, atom: 'AtomProperties') -> 'AtomProperties': closest_atom: 'AtomProperties' = atom_2 return closest_atom -__all__ = ['Calculate2d'] \ No newline at end of file + +def calculate2d_coord(order, self) -> List[Vector[float, float]]: + obj = Calculate2d() + return obj._calculate2d_coord(order, self) + +__all__ = ['calculate2d_coord'] \ No newline at end of file diff --git a/chython/algorithms/calculate2d/KKLayout.py b/chython/algorithms/calculate2d/KKLayout.py index a6f8b8e7..9a5d537e 100644 --- a/chython/algorithms/calculate2d/KKLayout.py +++ b/chython/algorithms/calculate2d/KKLayout.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright 2024 Denis Lipatov -# Copyright 2024 Vyacheslav Grigorev -# Copyright 2024 Timur Gimadiev +# Copyright 2024, 2025 Denis Lipatov +# Copyright 2024, 2025 Vyacheslav Grigorev +# Copyright 2024, 2025 Timur Gimadiev # This file is part of chython. # # chython is free software; you can redistribute it and/or modify @@ -18,6 +18,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, see . # + """ Defines the KKLayout class, utilized for arranging molecular structures using the Kamada-Kawai algorithm. @@ -40,8 +41,10 @@ architecture, making it invaluable for scientific and educational purposes. """ from typing import Dict, List, TYPE_CHECKING, Tuple -from .MathHelper import Vector, Polygon +from ...periodictable.base.vector import Vector +from .polygon import Polygon import math + if TYPE_CHECKING: from .Calculate2d import Calculate2d from .Properties import * @@ -51,8 +54,9 @@ class KKLayout: Class for calculating the optimal arrangement of atoms in a molecular structure using the Kamada-Kawai algorithm. """ + def __init__(self, structure: 'Calculate2d', atoms: List['AtomProperties'], \ - center: 'Vector', start_atom: 'AtomProperties', bond_length: float, \ + center: Vector, start_atom: 'AtomProperties', bond_length: float, \ threshold: float=0.1, inner_threshold: float=0.1, max_iteration: int=2000, max_inner_iteration: int=50, max_energy: int=1e9): """ @@ -117,7 +121,7 @@ def __init__(self, structure: 'Calculate2d', atoms: List['AtomProperties'], \ """ self.structure: 'Calculate2d' = structure self.atoms: List['AtomProperties'] = atoms - self.center: 'Vector' = center + self.center: Vector = center self.start_atom: 'AtomProperties' = start_atom self.edge_strength: int = bond_length self.threshold: float = threshold @@ -168,11 +172,11 @@ def initialise_matrices(self) -> None: a: float = 0.0 for atom in self.atoms: if not atom.positioned: - self.x_positions[atom] = self.center.x + math.cos(a) * radius - self.y_positions[atom] = self.center.y + math.sin(a) * radius + self.x_positions[atom] = self.center[0] + math.cos(a) * radius + self.y_positions[atom] = self.center[1] + math.sin(a) * radius else: - self.x_positions[atom] = atom.position.x - self.y_positions[atom] = atom.position.y + self.x_positions[atom] = atom.position[0] + self.y_positions[atom] = atom.position[1] self.positioned[atom] = atom.positioned a += angle for atom_1 in self.atoms: @@ -256,8 +260,8 @@ def get_kk_layout(self) -> None: self.update(max_energy_atom, d_ex, d_ey) delta, d_ex, d_ey = self.energy(max_energy_atom) for atom in self.atoms: - atom.position.x = self.x_positions[atom] - atom.position.y = self.y_positions[atom] + atom.position[0] = self.x_positions[atom] + atom.position[1] = self.y_positions[atom] atom.positioned = True atom.force_positioned = True diff --git a/chython/algorithms/calculate2d/MathHelper.py b/chython/algorithms/calculate2d/MathHelper.py deleted file mode 100644 index 7e95ac03..00000000 --- a/chython/algorithms/calculate2d/MathHelper.py +++ /dev/null @@ -1,709 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2024 Denis Lipatov -# Copyright 2024 Vyacheslav Grigorev -# Copyright 2024 Timur Gimadiev -# This file is part of chython. -# -# chython is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program; if not, see . -# -""" -This module introduces the `Vector` and `Polygon` classes, designed to perform mathematical -calculations relevant to two-dimensional Cartesian coordinate systems and regular polygons. - -The `Vector` class facilitates operations with coordinates, including vector arithmetic -(addition, subtraction, multiplication/division by scalars), normalization, rotation, and -distance calculations. It also supports methods for determining the angle of a vector, its -length, and whether it lies in a certain quadrant. Additionally, it includes functions for -reflecting vectors about lines, finding the closest atom or point, and rotating vectors around -other vectors or points. - -The `Polygon` class focuses on properties and calculations related to regular polygons, such as -finding the circumradius, calculating central angles, determining the apothem, and identifying -the type of polygon based on its number of sides. It also includes static methods for -calculating normals to lines defined by two vectors and for adding, averaging, and mirroring -vectors. - -Together, these classes provide a comprehensive toolkit for performing geometric computations -essential in fields such as computer graphics, physics simulations, and computational chemistry, -where precise manipulation and analysis of spatial relationships are required. -""" -import math -from typing import Union, TYPE_CHECKING, List - -if TYPE_CHECKING: - from .Properties import AtomProperties - - -class Vector: - """ - The `Vector` class facilitates operations with coordinates, including vector arithmetic - (addition, subtraction, multiplication/division by scalars), normalization, rotation, and - distance calculations. It also supports methods for determining the angle of a vector, its - length, and whether it lies in a certain quadrant. Additionally, it includes functions for - reflecting vectors about lines, finding the closest atom or point, and rotating vectors around - other vectors or points. - """ - def __init__(self, x: Union[int, float], y: Union[int, float]) -> None: - """ - Constructor of the vector class - - Parameters: - :param x Union[int, float]: - The coordinate of the vector along the abscissa axis - :param y Union[int, float]: - The coordinate of the vector along the ordinate axis - - Attributes: - x: the coordinate of the vector along the abscissa axis - y: the coordinate of the vector along the ordinate axis - """ - self.x: float = float(x) - self.y: float = float(y) - - - def __repr__(self) -> str: - """ - The method needed for debugging the code - - Returns a string containing the coordinates of the point - """ - return str(self.x) + ', ' + str(self.y) - - - def copy(self) -> 'Vector': - """ - Creates a copy of the current class object - - Returns a copy of the object - """ - return Vector(self.x, self.y) - - - def subtract(self, vector: 'Vector'): - """ - A method for the operation of subtraction between vectors - - Parameters: - :param vector 'Vector': - Another object of the current class - """ - self.x -= vector.x - self.y -= vector.y - - - def rotate(self, angle: float) -> None: - """ - A method that rotates the vector by the appropriate angle from the signature - of the function and updates the coordinates of the current class object - - Parameters: - :param angle float: - The angle by which the vector should be rotated - """ - new_x: float = self.x * math.cos(angle) - self.y * math.sin(angle) - new_y: float = self.x * math.sin(angle) + self.y * math.cos(angle) - - self.x = new_x - self.y = new_y - - - def add(self, vector: 'Vector') -> None: - """ - A class method that adds vectors and updates the coordinates of the current class object - - Parameters: - :param vector 'Vector': - Another object of the current class - """ - self.x += vector.x - self.y += vector.y - - - def invert(self) -> None: - """ - A class method that inverts the current coordinates of objects of the class - """ - self.x = self.x * -1 - self.y = self.y * -1 - - - def divide(self, scalar: float) -> None: - """ - A class method that divides the coordinates of the current class object - vectors for an arbitrary number - - Parameters: - :param scalar float: - Number divider - """ - self.x = self.x / scalar - self.y = self.y / scalar - - - def normalise(self) -> None: - """ - Normalization of coordinates (dividing them by the length of the vector itself) - """ - if self.length() != 0: - self.divide(self.length()) - - - def angle(self) -> float: - """ - A method that calculates the angle of inclination of the current vector - - Returns float the angle of inclination of the vector - """ - return math.atan2(self.y, self.x) - - - def length(self) -> float: - """ - Calculates the length of the current vector - - Returns float - """ - return math.sqrt((self.x**2) + (self.y**2)) - - - def multiply_by_scalar(self, scalar: float) -> None: - """ - Multiplies the coordinates of the current vector by an arbitrary real number - - Parameters: - :param scalar float - """ - self.x = self.x * scalar - self.y = self.y * scalar - - - def rotate_around_vector(self, angle: float, vector: 'Vector') -> None: - """ - Rotates a point (or vector) around a given vector by a specified angle. - - Parameters: - :param angle float: - The angle by which to rotate the point, typically measured in radians. - :param vector 'Vector': - The vector around which the rotation occurs. This vector serves as the reference - point. - """ - self.x -= vector.x - self.y -= vector.y - - x = self.x * math.cos(angle) - self.y * math.sin(angle) - y = self.x * math.sin(angle) + self.y * math.cos(angle) - - self.x = x + vector.x - self.y = y + vector.y - - - def get_closest_atom(self, atom_1: 'AtomProperties', atom_2: 'AtomProperties') -> 'AtomProperties': - """ - This method determines which of the two atoms (represented by the objects atom_1 and atom_2) - is closer to the current object (represented by self). - - Parameters: - :param atom_1: 'AtomProperties': - The first atom to compare. - :param atom_2: 'AtomProperties': - The second atom to compare. - - Returns 'AtomProperties': - The closest atom. - """ - distance_1 = self.get_squared_distance(atom_1.position) - distance_2 = self.get_squared_distance(atom_2.position) - return atom_1 if distance_1 < distance_2 else atom_2 - - - def get_closest_point_index(self, point_1: 'Vector', point_2: 'Vector') -> int: - """ - The method is designed to determine which of the two specified coordinates (point_1: 'Vector', point_2: 'Vector') - closer to the current point. - - Parameters - :param point_1 'Vector': - The first point to be compared with. It can be a tuple, a list, or an object - representing coordinates. - :param point_2 'Vector': - The second point to compare with. Similarly, it can be a tuple, a list, or an - object. - - Returns int: - The index of the nearest point: 0 for point_1 and 1 for point_2. - """ - distance_1 = self.get_squared_distance(point_1) - distance_2 = self.get_squared_distance(point_2) - return 0 if distance_1 < distance_2 else 1 - - - def get_squared_length(self) -> float: - """ - Calculates the length squared - - Returns float: - Vector length squared - """ - return self.x ** 2 + self.y ** 2 - - - def get_squared_distance(self, vector: 'Vector') -> float: - """ - The method is designed to calculate the square of the distance between the current vector - (represented by self) and the specified vector (or point) represented by the vector object. - - Parameters - :param vector: 'Vector': - An object representing a vector or point from which to calculate the distance. - - Returns float: - The square of the distance - """ - return (vector.x - self.x) ** 2 + (vector.y - self.y) ** 2 - - - def get_distance(self, vector: 'Vector') -> float: - """ - The method is designed to calculate the distance between the current vector (represented by self) and - the specified vector (or point) represented by the vector object. - - Parameters - :param vector: 'Vector': - An object representing a vector or point from which to calculate the distance. - - Returns float: - The distance between the coordinates of the current vector and the passed parameter - """ - return math.sqrt(self.get_squared_distance(vector)) - - - def get_rotation_away_from_vector(self, vector: 'Vector', center: 'Vector', angle: float) -> float: - """ - The method is designed to determine how much the angle of rotation (in a positive or negative direction) - from a given vector measures the distance to this vector. - - Parameters - :param vector 'Vector': - The vector to "move away from". It can be a point or a direction, relative to which - the rotation is taking place. - :param center 'Vector': - The center of rotation around which the object (represented by self) rotates. - :param angle float: - The angle at which the rotation occurs. This value can be positive or negative. - - Returns returns the rotation angle that minimizes the distance to the vector, - either in a positive or negative direction. - """ - tmp = self.copy() - - tmp.rotate_around_vector(angle, center) - squared_distance_1 = tmp.get_squared_distance(vector) - - tmp.rotate_around_vector(-2.0 * angle, center) - squared_distance_2 = tmp.get_squared_distance(vector) - return angle if squared_distance_2 < squared_distance_1 else -angle - - - def rotate_away_from_vector(self, vector: 'Vector', center: 'Vector', angle: float) -> None: - """ - The method is designed to rotate the current object (represented by self) around a given - one center in such a way as to minimize the distance to the specified vector. - If rotation in one direction leads to a decrease in the distance, the function corrects - the rotation,to ensure maximum distance from the vector. - - Parameters - :param vector 'Vector': - The vector to "move away from". It can be a point or a direction, relative to which - the rotation is taking place. - :param center 'Vector': - The center of rotation around which the object rotates. - :param angle float: - The angle at which the rotation occurs. This value can be positive or negative. - """ - self.rotate_around_vector(angle, center) - squared_distance_1 = self.get_squared_distance(vector) - self.rotate_around_vector(-2.0 * angle, center) - squared_distance_2 = self.get_squared_distance(vector) - - if squared_distance_2 < squared_distance_1: - self.rotate_around_vector(2.0 * angle, center) - - - def get_clockwise_orientation(self, vector: 'Vector') -> str: - """ - The method is designed to determine the orientation (positive or negative) between - the current object (represented by self) and the specified vector (represented by - the vector object). - - Parameters - :param vector 'Vector': - The vector relative to which the orientation is determined. - - Returns str: - A string indicating whether the orientation is "clockwise", "counterclockwise" - or "neutral". - """ - a: float = self.y * vector.x - b: float = self.x * vector.y - - if a > b: - return 'clockwise' - elif a == b: - return 'neutral' - else: - return 'counterclockwise' - - - def mirror_about_line(self, line_point_1: 'Vector', line_point_2: 'Vector') -> None: - """ - The method is designed to reflect the current object (represented by self) relative to a - given line, defined by two points (line_point_1 and line_point_2). After performing this - function, the coordinates of the object will be changed so that it is on the opposite - side of the line, keeping the same distance to the line. - - Parameters - :param line_point_1: 'Vector': - The first point defining the line. - :param line_point_2: 'Vector': - The second point defining the line. - """ - dx = line_point_2.x - line_point_1.x - dy = line_point_2.y - line_point_1.y - - 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 - - self.x = new_x - self.y = new_y - - - @staticmethod - def get_position_relative_to_line(vector_start: 'Vector', vector_end: 'Vector', vector: 'Vector') -> int: - """ - Determines the position of a vector relative to a line defined by two points. - - Parameters: - :param vector_start 'Vector': - The start point of the line. - :param vector_end 'Vector': - The end point of the line. - :param vector 'Vector': - The vector whose position relative to the line is to be determined. - - Returns int: - 1 if the vector is to the left of the line, -1 if the vector is to the right of the - line, 0 if the vector lies on the line. - """ - 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: - return -1 - else: - return 0 - - - @staticmethod - def get_directionality_triangle(vector_a: 'Vector', vector_b: 'Vector', vector_c: 'Vector') -> str: - """ - Determines the directionality of the triangle formed by three vectors (or points). - - Parameters: - :param vector_a 'Vector': - The first vertex of the triangle. - :param vector_b 'Vector': - The second vertex of the triangle. - :param vector_c 'Vector': - The third vertex of the triangle. - - Returns str: - - 'clockwise' if the triangle is oriented in a clockwise direction. - - 'counterclockwise' if the triangle is oriented in a counterclockwise direction. - - None if the three points are collinear (lie on the same line). - """ - 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' - elif determinant == 0: - return None - else: - return 'counterclockwise' - - - @staticmethod - def mirror_vector_about_line(line_point_1: 'Vector', line_point_2: 'Vector', point: 'Vector')-> 'Vector': - """ - Mirrors a point (or vector) across a line defined by two points. - - Parameters: - :param line_point_1 'Vector': - The first point defining the line. - :param line_point_2 'Vector': - The second point defining the line. - :param point 'Vector': - The point to be mirrored across the line. - - Returns Vector: - A new Vector representing the mirrored point across the line. - """ - dx = line_point_2.x - line_point_1.x - dy = line_point_2.y - line_point_1.y - - 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 - return Vector(x_new, y_new) - - - @staticmethod - def get_line_angle(point_1: 'Vector', point_2: 'Vector') -> float: - """ - Calculates the angle of a line defined by two points with respect to the positive x-axis. - - Parameters: - point_1 'Vector': - The first point defining the line. - point_2 'Vector': - The second point defining the line. - - Returns float: - The angle of the line in radians, in the range [-π, π]. - """ - difference = Vector.subtract_vectors(point_2, point_1) - return difference.angle() - - - @staticmethod - def subtract_vectors(vector_1: 'Vector', vector_2: 'Vector')-> 'Vector': - """ - Subtracts one vector from another. - - Parameters: - vector_1 'Vector': - The vector from which to subtract. - vector_2 'Vector': - The vector to subtract. - - Returns Vector: - A new Vector representing the result of the subtraction (vector_1 - vector_2). - """ - x = vector_1.x - vector_2.x - y = vector_1.y - vector_2.y - return Vector(x, y) - - - @staticmethod - def add_vectors(vector_1: 'Vector', vector_2: 'Vector') -> 'Vector': - """ - Adds two vectors together. - - Parameters: - vector_1 'Vector': - The first vector to add. - vector_2 'Vector': - The second vector to add. - - Returns Vector: - A new Vector representing the result of the addition (vector_1 + vector_2). - """ - x = vector_1.x + vector_2.x - y = vector_1.y + vector_2.y - return Vector(x, y) - - - @staticmethod - def get_midpoint(vector_1: 'Vector', vector_2: 'Vector') -> 'Vector': - """ - Calculates the midpoint between two vectors. - - Parameters: - vector_1 'Vector': - The first vector. - vector_2 'Vector': - The second vector. - - Returns Vector: - A new Vector representing the midpoint between vector_1 and vector_2. - """ - x = (vector_1.x + vector_2.x) / 2 - y = (vector_1.y + vector_2.y) / 2 - return Vector(x, y) - - - @staticmethod - def get_average(vectors: List['Vector']) -> 'Vector': - """ - Calculates the average of a list of vectors. - - Parameters: - :param vectors List[Vector]: - A list of vectors for which the average is to be calculated. - - Returns: - Vector: A new Vector representing the average of the input vectors. - """ - average_x = 0.0 - average_y = 0.0 - for vector in vectors: - average_x += vector.x - average_y += vector.y - return Vector(average_x / len(vectors), average_y / len(vectors)) - - - @staticmethod - def get_normals(vector_1: 'Vector', vector_2: 'Vector') -> List['Vector']: - """ - Calculates the normal vectors to the line defined by two vectors. - - Parameters: - :param vector_1 'Vector': - The first vector defining the line. - :param vector_2 'Vector': - The second vector defining the line. - - Returns List[Vector]: - A list containing two normal vectors to the line defined by vector_1 and vector_2. - """ - delta = Vector.subtract_vectors(vector_2, vector_1) - return [Vector(-delta.y, delta.x), Vector(delta.y, -delta.x)] - - - @staticmethod - def get_angle_between_vectors(vector_1: 'Vector', vector_2: 'Vector', origin: 'Vector') -> float: - """ - Calculates the angle between two vectors relative to a given origin point. - - Parameters: - :param vector_1 'Vector': - The first vector. - :param vector_2 'Vector': - The second vector. - :param origin 'Vector': - The origin point relative to which the angle is calculated. - - Returns: - float: The angle between vector_1 and vector_2 in radians, in the range [0, π]. - """ - v1_x_diff: float = vector_1.x - origin.x - v1_y_diff: float = vector_1.y - origin.y - v2_x_diff: float = vector_2.x - origin.x - v2_y_diff: float = vector_2.y - origin.y - - dot_product: float = v1_x_diff * v2_x_diff + v1_y_diff * v2_y_diff - length_v1: float = math.sqrt(v1_x_diff ** 2 + v1_y_diff ** 2) - length_v2: float = math.sqrt(v2_x_diff ** 2 + v2_y_diff ** 2) - - cos_angle = dot_product / (length_v1 * length_v2) - return math.acos(cos_angle) - - - - - - - - - - - - - -class Polygon: - """ - The `Polygon` class focuses on properties and calculations related to regular polygons, such as - finding the circumradius, calculating central angles, determining the apothem, and identifying - the type of polygon based on its number of sides. It also includes static methods for - calculating normals to lines defined by two vectors and for adding, averaging, and mirroring - vectors. - """ - def __init__(self, edge_number: float) -> None: - """ - Initializes a Polygon with a specified number of edges. - - Parameters: - :param edge_number float: - The number of edges (sides) of the polygon. - """ - self.edge_number: float = edge_number - - @staticmethod - def find_polygon_radius(edge_length: float, edge_number: float) -> float: - """ - Calculates the radius of the circumcircle of a regular polygon. - - Parameters: - :param edge_length float: - The length of one edge of the polygon. - :param edge_number float: - The number of edges (sides) of the polygon. - - Returns float: - The radius of the circumcircle. - """ - return edge_length / (2 * math.sin(math.pi / edge_number)) - - @staticmethod - def get_central_angle(edge_number: float) -> float: - """ - Calculates the central angle of a regular polygon. - - Parameters: - :param edge_number float: - The number of edges (sides) of the polygon. - - Returns float: - The central angle in radians. - """ - return math.radians(float(360) / edge_number) - - @staticmethod - def get_apothem(radius: float, edge_number: float) -> float: - """ - Calculates the apothem of a regular polygon. - - Parameters: - :param radius float: - The radius of the circumcircle of the polygon. - :param edge_number float: - The number of edges (sides) of the polygon. - - Returns float: - The length of the apothem. - """ - return radius * math.cos(math.pi / edge_number) - - @staticmethod - def get_apothem_from_side_length(length: float, edge_number: float) -> float: - """ - Calculates the apothem of a regular polygon given the side length. - - Parameters: - :param length float: - The length of one edge of the polygon. - :param edge_number float: - The number of edges (sides) of the polygon. - - Returns float: - The length of the apothem. - """ - radius: float = Polygon.find_polygon_radius(length, edge_number) - return Polygon.get_apothem(radius, edge_number) \ No newline at end of file diff --git a/chython/algorithms/calculate2d/Properties.py b/chython/algorithms/calculate2d/Properties.py index 9ae9f31f..9b2fe26d 100644 --- a/chython/algorithms/calculate2d/Properties.py +++ b/chython/algorithms/calculate2d/Properties.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright 2024 Denis Lipatov -# Copyright 2024 Vyacheslav Grigorev -# Copyright 2024 Timur Gimadiev +# Copyright 2024, 2025 Denis Lipatov +# Copyright 2024, 2025 Vyacheslav Grigorev +# Copyright 2024, 2025 Timur Gimadiev # This file is part of chython. # # chython is free software; you can redistribute it and/or modify @@ -18,6 +18,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, see . # + """ This module defines classes that extend the properties of rings, atoms, and bonds within the Kaiton structure, focusing on attributes and methods necessary for coordinate calculations. @@ -27,7 +28,7 @@ to facilitate the calculation of molecular geometries by providing detailed attributes and methods specific to rings, atoms, and bonds, thereby enhancing the structure's utility in algorithms that require precise spatial information. These classes include RingProperties, -AtomProperties, BondProperties, and RingOverlap, each tailored to represent different aspects of +AtomProperties, and RingOverlap, each tailored to represent different aspects of molecular structures with attributes and methods that aid in determining spatial relationships and characteristics inherent to chemical compounds. @@ -37,8 +38,6 @@ (e.g., bridged, spiro, fused). - AtomProperties: Encapsulates atomic properties crucial for molecular geometry calculations, such as atomic symbols, positions, and connectivity. -- BondProperties: Details the computational parameters of chemical bonds, including atom - references and bond types. - RingOverlap: Handles overlaps between rings, identifying shared atoms and determining structural characteristics like bridging. @@ -48,15 +47,15 @@ such as ring systems, atomic configurations, and bond characteristics, making them indispensable for cheminformatics and computational chemistry applications. """ - -from typing import List, Optional, Tuple -from .MathHelper import Vector +from typing import List, Optional +from ...periodictable.base.vector import Vector import math class RingProperties: """ A class on computing parameters of rings """ + def __init__(self: 'RingProperties', ring: List['AtomProperties']) -> None: """ Constructor of the class that complements information about rings in the Kaiton @@ -79,9 +78,6 @@ def __init__(self: 'RingProperties', ring: List['AtomProperties']) -> None: - center 'Vector': The center of the ring in coordinates. - subrings List: A boolean value indicating whether the ring contains subrings. - bridged bool: A boolean value indicating whether the ring is a bridge ring. - - spiro bool: A boolean value indicating whether the ring is a spirocycle. - - fused bool: A boolean value indicating whether it is part of a condensed cyclic system. - - subring_of_bridged bool: A boolean value indicating whether the subrings are bridged. - central_angle float: The central angle of the ring. - neighbouring_rings List[int]: A list of identifiers of neighboring rings to the current one. @@ -90,16 +86,12 @@ def __init__(self: 'RingProperties', ring: List['AtomProperties']) -> None: self.members: List['AtomProperties'] = ring self.members_id: List[int] = [atom.id for atom in self.members] self.positioned: bool = False - self.center: 'Vector' = Vector(0, 0) + self.center: Vector = Vector(0, 0) self.subrings: List = [] self.bridged = False - self.spiro: bool = False - self.fused: bool = False - self.subring_of_bridged = False self.central_angle: float = 0.0 - self.neighbouring_rings: List[int] = [] + self.neighbouring_rings: List[int] = [] - # добавляем в свойства атомов то что они находятся в этом кольце for atom in self.members: atom.ring_indexes.append(self.id) atom.rings.append(self) @@ -121,9 +113,12 @@ def __eq__(self, other: 'RingProperties') -> bool: True if the identifiers of the two instances are equal, indicating they represent the same ring. False otherwise. """ - return False if other is None else self.id == other.id - - + if other is None: + return False + else: + return self.id == other.id + + def __hash__(self) -> int: """ Returns the hash value of the current object, which is the unique identifier of the ring. @@ -138,8 +133,8 @@ def __hash__(self) -> int: The unique identifier of the current object of the class, used as the hash value. """ return self.id - - + + def get_angle(self) -> float: """ Calculates the exterior angle of the polygon formed by the ring in radians. @@ -154,8 +149,8 @@ def get_angle(self) -> float: The exterior angle of the polygon formed by the ring in radians. """ return math.pi - self.central_angle - - + + def __repr__(self) -> str: """ Provides a human-readable representation of the RingProperties object, primarily @@ -204,135 +199,24 @@ def copy(self) -> 'RingProperties': for subring in self.subrings: new_ring.subrings.append(subring) new_ring.bridged = self.bridged - new_ring.subring_of_bridged = self.subring_of_bridged - new_ring.spiro = self.spiro - new_ring.fused = self.fused new_ring.central_angle = self.central_angle return new_ring - - - - - - - - - - - - - - - - - - - - - - - - -class BondProperties: - """ - A class about computational parameters of links - """ - def __init__(self: 'BondProperties', atom1: 'AtomProperties', \ - atom2: 'AtomProperties', bond) -> None: - """ - Constructor of the class that complements information about bonds in the Kaiton - structure, creating new properties or converting existing ones into a more - convenient form for use in coordinate calculation algorithms. - - Parameters: - :param atom1 'AtomProperties': - Reference to the object of the class of the first atom forming this bond. - :param atom2 'AtomProperties': - Reference to the object of the class of the second atom forming this bond. - :param bond: - Reference to the original Kaiton bond class. - - Attributes: - - id Tuple['AtomProperties']: Identifier of the current bond, which is a tuple of - atom identifiers between which this bond exists. - - n int: Identifier of the first atom of this bond. - - m int: Identifier of the second atom of this bond. - - atom1 'AtomProperties': Reference to the object of the class of the first atom of - this bond. - - atom2 'AtomProperties': Reference to the object of the class of the second atom of - this bond. - - type str: String that characterizes the type of bond, primary, secondary, or - tertiary. - - # center (bool): Placeholder for future expansion. - # chiral (bool): Placeholder for future expansion. - # chiral_symbol (Optional[str]): Placeholder for future expansion. - - The constructor initializes the bond properties based on the provided atoms and - determines its type (single, double, triple) based on the order of the bond. - - """ - self.id: Tuple['AtomProperties'] = (atom1.id, atom2.id) - self.n: int = atom1.id #atom1 index - self.m: int = atom2.id #atom2 index - - self.atom1: 'AtomProperties' = atom1 - self.atom2: 'AtomProperties' = atom2 - - # self.center: bool = False # рудименты кода - # self.chiral: bool = False # рудименты кода - # self.chiral_symbol: Optional[str] = None # # рудименты кода - - self.type = Optional[None] - if bond.order == 1: - self.type = 'single' - elif bond.order == 2: - self.type = 'double' - elif bond.order == 3: - self.type = 'triple' - - - - - - - - - - - - - - - - - - - - - - - - class AtomProperties: """ A class about computing parameters of atoms """ - def __init__(self: 'AtomProperties', atom_index: int, symbol: str) -> None: + def __init__(self: 'AtomProperties', atom_index: int) -> None: """ Initializes an instance of the AtomProperties class with data about an atom. Parameters: :param atom_index int: The index of the current atom within the molecular structure. - :param symbol str: - Symbol representing the element according to the periodic table. Attributes: - id int: Unique identifier for the atom. - - symbol str: String characterizing the name of the element according to the periodic table. - ring_indexes List[int]: List of identifiers for rings in which the atom is a participant. @@ -356,13 +240,10 @@ def __init__(self: 'AtomProperties', atom_index: int, symbol: str) -> None: - previous_atom Optional['AtomProperties']: Reference to an AtomProperties object representing the preceding atom in the chain. """ - self.id: int = atom_index - self.symbol: str = symbol self.ring_indexes: List[int] = [] self.rings: List['RingProperties'] = [] - # self.original_rings: List['RingProperties'] = [] self.anchored_rings: List['RingProperties'] = [] self.is_bridge_atom: bool = False self.is_bridge: bool = False @@ -370,8 +251,9 @@ def __init__(self: 'AtomProperties', atom_index: int, symbol: str) -> None: self.bridged_ring = None self.positioned: bool = False - self.previous_position: 'Vector' = Vector(0, 0) - self.position: 'Vector' = Vector(0, 0) + self.previous_position: Vector = Vector(0, 0) + self.position: Vector = Vector(0, 0) + self.angle: Optional[float] = None self.force_positioned: bool = False self.connected_to_ring: bool = False @@ -379,7 +261,7 @@ def __init__(self: 'AtomProperties', atom_index: int, symbol: str) -> None: self.neighbours: List['AtomProperties'] = [] self.previous_atom: Optional['AtomProperties'] = None - + def __eq__(self, other: 'AtomProperties') -> bool: """ Compares two AtomProperties instances for equality based on their identifiers. @@ -391,10 +273,13 @@ def __eq__(self, other: 'AtomProperties') -> bool: Returns bool: True if both instances represent atoms with the same identifier, otherwise False. """ - return False if other is None else self.id == other.id - - - def set_position(self, vector: 'Vector') -> None: + if other is None: + return False + else: + return self.id == other.id + + + def set_position(self, vector: Vector) -> None: """ Sets the position of the current atom to the specified vector. @@ -402,9 +287,9 @@ def set_position(self, vector: 'Vector') -> None: :param vector 'Vector': An instance of the Vector class, whose coordinates are assigned as the position of the current atom. """ - self.position: 'Vector' = vector + self.position: Vector = vector + - def __hash__(self) -> int: """ Returns the hash value of the current object, which is the unique identifier of the atom. @@ -416,8 +301,8 @@ def __hash__(self) -> int: The unique identifier of the current object of the class, used as the hash value. """ return self.id - - + + def __repr__(self) -> str: """ Provides a human-readable representation of the AtomProperties object, primarily @@ -431,9 +316,9 @@ def __repr__(self) -> str: A string combining the atomic symbol and the adjusted atomic index. """ return f'{self.symbol}_{self.id - 1}' - - - def get_angle(self, reference_vector: Optional['Vector']=None) -> float: + + + def get_angle(self, reference_vector: Optional['Vector'] = None) -> float: """ Calculates the angle between the current atom and either the previous atom or a specified reference vector. @@ -454,10 +339,9 @@ def get_angle(self, reference_vector: Optional['Vector']=None) -> float: """ vector_1: float = self.position vector_2: float = self.previous_position if not reference_vector else reference_vector - vector = Vector.subtract_vectors(vector_1, vector_2) - return vector.angle() + vector = vector_1 - vector_2 + return Vector.angle(vector) - def copy(self) -> 'AtomProperties': """ Creates a deep copy of the current AtomProperties instance and returns it as a new @@ -471,10 +355,9 @@ def copy(self) -> 'AtomProperties': A new instance of the AtomProperties class with identical properties to the original atom, but as a separate object in memory. """ - new_atom = AtomProperties(self.id, self.symbol) - new_atom.ring_indexes =self.ring_indexes + new_atom = AtomProperties(self.id) + new_atom.ring_indexes = self.ring_indexes new_atom.rings = self.rings - # new_atom.original_rings = self.original_rings new_atom.anchored_rings = self.anchored_rings new_atom.is_bridge_atom = self.is_bridge_atom new_atom.is_bridge = self.is_bridge @@ -488,34 +371,24 @@ def copy(self) -> 'AtomProperties': new_atom.neighbours = self.neighbours new_atom.previous_atom = self.previous_atom return new_atom - def is_terminal(self) -> bool: "Returns boolean whether a given atom is terminal (has no more than one bond)." return len(self.neighbours) <= 1 + def set_previous_position(self, previous_atom: 'AtomProperties') -> None: "Set previous position atom" self.previous_position = previous_atom.position self.previous_atom = previous_atom - - - - - - - - - - - class RingOverlap: """ Initializes an instance of the RingOverlap class, which represents the overlap between two rings. """ + def __init__(self, ring_1: 'RingProperties', ring_2: 'RingProperties') -> None: """ This class is designed to handle situations where two rings share common atoms, @@ -578,14 +451,8 @@ def is_bridge(self) -> bool: exceeds two or if any atom in the overlap belongs to more than two rings, suggesting a complex bridging configuration. """ - return len(self.atoms) > 2 or any(len(atom.rings) > 2 for atom in self.atoms) - # ниже старая версия функции - # if len(self.atoms) > 2: - # return True - # for atom in self.atoms: - # if len(atom.rings) > 2: - # return True - # return False + return len(self.atoms) > 2 or any(len(atom.rings) > 2 + for atom in self.atoms) def involves_ring(self, ring_id: int) -> bool: @@ -604,7 +471,7 @@ def involves_ring(self, ring_id: int) -> bool: indicating that the ring is part of the current overlap. False otherwise. """ return self.ring_id_1 == ring_id or self.ring_id_2 == ring_id - + def update_other(self, ring_id: int, other_ring_id: int) -> None: """ @@ -628,4 +495,4 @@ def update_other(self, ring_id: int, other_ring_id: int) -> None: if self.ring_id_1 == other_ring_id: self.ring_id_2 = ring_id else: - self.ring_id_1 = ring_id \ No newline at end of file + self.ring_id_1 = ring_id diff --git a/chython/algorithms/calculate2d/__init__.py b/chython/algorithms/calculate2d/__init__.py index 193ea427..c575f393 100644 --- a/chython/algorithms/calculate2d/__init__.py +++ b/chython/algorithms/calculate2d/__init__.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- # -# Copyright 2024 Denis Lipatov -# Copyright 2024 Vyacheslav Grigorev -# Copyright 2024 Timur Gimadiev +# Copyright 2019-2025 Ramil Nugmanov +# Copyright 2024, 2025 Denis Lipatov +# Copyright 2024, 2025 Vyacheslav Grigorev +# Copyright 2024, 2025 Timur Gimadiev # This file is part of chython. # # chython is free software; you can redistribute it and/or modify @@ -18,4 +19,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, see . # -from .clean2d import Calculate2DMolecule, Calculate2DReaction \ No newline at end of file +from .molecule import * +from .reaction import * + +__all__ = ['Calculate2DMolecule', 'Calculate2DReaction'] diff --git a/chython/algorithms/calculate2d/clean2d.py b/chython/algorithms/calculate2d/clean2d.py deleted file mode 100644 index 0fd77b72..00000000 --- a/chython/algorithms/calculate2d/clean2d.py +++ /dev/null @@ -1,180 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2024 Denis Lipatov -# Copyright 2024 Vyacheslav Grigorev -# Copyright 2024 Timur Gimadiev -# This file is part of chython. -# -# chython is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program; if not, see . -# -from typing import TYPE_CHECKING, Union, List -from .Calculate2d import Calculate2d -from math import sqrt - -if TYPE_CHECKING: - from ...containers import ReactionContainer, MoleculeContainer - -try: - from importlib.resources import files -except ImportError: # python3.8 - from importlib_resources import files - - -class Calculate2DMolecule: - __slots__ = () - - def clean2d(self: Union['MoleculeContainer', 'Calculate2DMolecule']): - """ - Calculate 2d layout of graph. https://pubs.acs.org/doi/10.1021/acs.jcim.7b00425 JS implementation used. - """ - plane = {} - entry = iter(sorted(self, key=lambda n: len(self._bonds[n]))) - smiles, order = self.__clean2d_prepare(next(entry)) - - obj = Calculate2d() - xy: List[List[float, float]] = obj._calculate2d_coord(order, self) - - - shift_x, shift_y = xy[0] - for n, (x, y) in zip(order, xy): - plane[n] = (x - shift_x, shift_y - y) - - bonds = [] - for n, m, _ in self.bonds(): - xn, yn = plane[n] - xm, ym = plane[m] - bonds.append(sqrt((xm - xn) ** 2 + (ym - yn) ** 2)) - if bonds: - bond_reduce = sum(bonds) / len(bonds) / .825 - else: - bond_reduce = 1. - - atoms = self._atoms - for n, (x, y) in plane.items(): - a = atoms[n] - a._x = x / bond_reduce - a._y = y / bond_reduce - - if self.connected_components_count > 1: - shift_x = 0. - for c in self.connected_components: - shift_x = self._fix_plane_mean(shift_x, component=c) + .9 - self.__dict__.pop('__cached_method__repr_svg_', None) - - def _fix_plane_mean(self: 'MoleculeContainer', shift_x: float, shift_y=0., component=None) -> float: - plane = self._plane - if component is None: - component = plane - - left_atom = min(component, key=lambda x: plane[x][0]) - right_atom = max(component, key=lambda x: plane[x][0]) - - min_x = plane[left_atom][0] - shift_x - if len(self._atoms[left_atom].atomic_symbol) == 2: - min_x -= 0.2 - - max_x = plane[right_atom][0] - min_x - min_y = min(plane[x][1] for x in component) - max_y = max(plane[x][1] for x in component) - mean_y = (max_y + min_y) / 2 - shift_y - for n in component: - x, y = plane[n] - plane[n] = (x - min_x, y - mean_y) - - if -0.18 <= plane[right_atom][1] <= 0.18: - factor = self._hydrogens[right_atom] - if factor == 1: - max_x += 0.15 - elif factor: - max_x += 0.25 - return max_x - - def _fix_plane_min(self: 'MoleculeContainer', shift_x: float, shift_y=0., component=None) -> float: - plane = self._plane - if component is None: - component = plane - - right_atom = max(component, key=lambda x: plane[x][0]) - min_x = min(plane[x][0] for x in component) - shift_x - max_x = plane[right_atom][0] - min_x - min_y = min(plane[x][1] for x in component) - shift_y - - for n in component: - x, y = plane[n] - plane[n] = (x - min_x, y - min_y) - - if shift_y - 0.18 <= plane[right_atom][1] <= shift_y + 0.18: - factor = self._hydrogens[right_atom] - if factor == 1: - max_x += 0.15 - elif factor: - max_x += 0.25 - return max_x - - - def __clean2d_prepare(self: 'MoleculeContainer', entry): - w = {n: i for i, n in enumerate(self._atoms)} - w[entry] = -1 - smiles, order = self._smiles(w.__getitem__, random=True, charges=False, stereo=False, _return_order=True) - return ''.join(smiles).replace('~', '-'), order - -class Calculate2DReaction: - __slots__ = () - - def clean2d(self: 'ReactionContainer'): - for m in self.molecules(): - m.clean2d() - self.fix_positions() - - def fix_positions(self: 'ReactionContainer'): - shift_x = 0 - reactants = self.reactants - amount = len(reactants) - 1 - signs = [] - for m in reactants: - max_x = m._fix_plane_mean(shift_x) - if amount: - max_x += .2 - signs.append(max_x) - amount -= 1 - shift_x = max_x + 1 - arrow_min = shift_x - - if self.reagents: - shift_x += .4 - for m in self.reagents: - max_x = m._fix_plane_min(shift_x, .5) - shift_x = max_x + 1 - shift_x += .4 - if shift_x - arrow_min < 3: - shift_x = arrow_min + 3 - else: - shift_x += 3 - arrow_max = shift_x - 1 - - products = self.products - amount = len(products) - 1 - for m in products: - max_x = m._fix_plane_mean(shift_x) - if amount: - max_x += .2 - signs.append(max_x) - amount -= 1 - shift_x = max_x + 1 - self._arrow = (arrow_min, arrow_max) - self._signs = tuple(signs) - self.flush_cache() - - -__all__ = ['Calculate2DMolecule', 'Calculate2DReaction'] \ No newline at end of file diff --git a/chython/algorithms/calculate2d/molecule.py b/chython/algorithms/calculate2d/molecule.py index cca6a137..cc70968d 100644 --- a/chython/algorithms/calculate2d/molecule.py +++ b/chython/algorithms/calculate2d/molecule.py @@ -2,6 +2,9 @@ # # Copyright 2019-2025 Ramil Nugmanov # Copyright 2019, 2020 Dinar Batyrshin +# Copyright 2024, 2025 Denis Lipatov +# Copyright 2024, 2025 Vyacheslav Grigorev +# Copyright 2024, 2025 Timur Gimadiev # This file is part of chython. # # chython is free software; you can redistribute it and/or modify @@ -17,12 +20,12 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, see . # + from math import sqrt from random import random -from typing import TYPE_CHECKING, Union, Dict -from ...exceptions import ImplementationError +from typing import TYPE_CHECKING, Union, Dict, TypeVar, List from ...periodictable.base.vector import Vector - +from .Calculate2d import calculate2d_coord try: from importlib.resources import files @@ -31,17 +34,11 @@ if TYPE_CHECKING: - from chython import MoleculeContainer - -try: - from py_mini_racer import MiniRacer, JSEvalException - - ctx = MiniRacer() - ctx.eval('const self = this') - ctx.eval(files(__package__).joinpath('clean2d.js').read_text()) -except RuntimeError: - ctx = None - + from ...containers import MoleculeContainer + Element = TypeVar('Element') + Bond = TypeVar('Bond') + Coords = Vector[float, float] + class Calculate2DMolecule: __slots__ = () @@ -52,20 +49,10 @@ def clean2d(self: Union['MoleculeContainer', 'Calculate2DMolecule']): """ Calculate 2d layout of graph. https://pubs.acs.org/doi/10.1021/acs.jcim.7b00425 JS implementation used. """ - if ctx is None: - raise ImportError('py_mini_racer is not installed or broken') plane = {} entry = iter(sorted(self, key=lambda n: len(self._bonds[n]))) - for _ in range(min(5, len(self))): - smiles, order = self.__clean2d_prepare(next(entry)) - try: - xy = ctx.call('$.clean2d', smiles) - except JSEvalException: - continue - break - else: - raise ImplementationError - + smiles, order = self.__clean2d_prepare(next(entry)) + xy: List[Coords] = calculate2d_coord(order, self) shift_x, shift_y = xy[0] for n, (x, y) in zip(order, xy): plane[n] = (x - shift_x, shift_y - y) @@ -144,6 +131,6 @@ def __clean2d_prepare(self: 'MoleculeContainer', entry): w[entry] = -1 smiles, order = self._smiles(w.__getitem__, random=True, charges=False, stereo=False, _return_order=True) return ''.join(smiles).replace('~', '-'), order - - -__all__ = ['Calculate2DMolecule'] + + +__all__ = ['Calculate2DMolecule'] \ No newline at end of file diff --git a/chython/algorithms/calculate2d/polygon.py b/chython/algorithms/calculate2d/polygon.py new file mode 100644 index 00000000..3cb51a59 --- /dev/null +++ b/chython/algorithms/calculate2d/polygon.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2024, 2025 Denis Lipatov +# Copyright 2024, 2025 Vyacheslav Grigorev +# Copyright 2024, 2025 Timur Gimadiev +# This file is part of chython. +# +# chython is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, see . +# + +""" +This module introduces the `Polygon` class, designed to perform mathematical +calculations relevant to two-dimensional Cartesian coordinate systems and regular polygons. + +The `Polygon` class focuses on properties and calculations related to regular polygons, such as +finding the circumradius, calculating central angles, determining the apothem, and identifying +the type of polygon based on its number of sides. It also includes static methods for +calculating normals to lines defined by two points and for adding, averaging, and mirroring points. + +This class provides a comprehensive toolkit for performing geometric computations +essential in fields such as computer graphics, physics simulations, and computational chemistry, +where precise manipulation and analysis of spatial relationships is required. +""" +import math + +class Polygon: + """ + The `Polygon` class focuses on properties and calculations related to regular polygons, such as + finding the circumradius, calculating central angles, determining the apothem, and identifying + the type of polygon based on its number of sides. It also includes static methods for + calculating normals to lines defined by two vectors and for adding, averaging, and mirroring + vectors. + """ + + def __init__(self, edge_number: float) -> None: + """ + Initializes a Polygon with a specified number of edges. + + Parameters: + :param edge_number float: + The number of edges (sides) of the polygon. + """ + self.edge_number: float = edge_number + + + @staticmethod + def find_polygon_radius(edge_length: float, edge_number: float) -> float: + """ + Calculates the radius of the circumcircle of a regular polygon. + + Parameters: + :param edge_length float: + The length of one edge of the polygon. + :param edge_number float: + The number of edges (sides) of the polygon. + + Returns float: + The radius of the circumcircle. + """ + return edge_length / (2 * math.sin(math.pi / edge_number)) + + + @staticmethod + def get_central_angle(edge_number: float) -> float: + """ + Calculates the central angle of a regular polygon. + + Parameters: + :param edge_number float: + The number of edges (sides) of the polygon. + + Returns float: + The central angle in radians. + """ + return math.radians(float(360) / edge_number) + + + @staticmethod + def get_apothem(radius: float, edge_number: float) -> float: + """ + Calculates the apothem of a regular polygon. + + Parameters: + :param radius float: + The radius of the circumcircle of the polygon. + :param edge_number float: + The number of edges (sides) of the polygon. + + Returns float: + The length of the apothem. + """ + return radius * math.cos(math.pi / edge_number) + + + @staticmethod + def get_apothem_from_side_length(length: float, edge_number: float) -> float: + """ + Calculates the apothem of a regular polygon given the side length. + + Parameters: + :param length float: + The length of one edge of the polygon. + :param edge_number float: + The number of edges (sides) of the polygon. + + Returns float: + The length of the apothem. + """ + radius: float = Polygon.find_polygon_radius(length, edge_number) + return Polygon.get_apothem(radius, edge_number) + +__all__ = ['Polygon'] + \ No newline at end of file diff --git a/chython/algorithms/calculate2d/reaction.py b/chython/algorithms/calculate2d/reaction.py index 536643aa..80ea310b 100644 --- a/chython/algorithms/calculate2d/reaction.py +++ b/chython/algorithms/calculate2d/reaction.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- # # Copyright 2019-2025 Ramil Nugmanov +# Copyright 2024, 2025 Denis Lipatov +# Copyright 2024, 2025 Vyacheslav Grigorev +# Copyright 2024, 2025 Timur Gimadiev # This file is part of chython. # # chython is free software; you can redistribute it and/or modify @@ -16,28 +19,27 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, see . # + from typing import TYPE_CHECKING +try: + from importlib.resources import files +except ImportError: # python3.8 + from importlib_resources import files if TYPE_CHECKING: - from chython import ReactionContainer - - + from ...containers import ReactionContainer + + class Calculate2DReaction: __slots__ = () def clean2d(self: 'ReactionContainer'): - """ - Recalculate 2d coordinates - """ for m in self.molecules(): m.clean2d() self.fix_positions() def fix_positions(self: 'ReactionContainer'): - """ - Fix coordinates of molecules in reaction - """ shift_x = 0 reactants = self.reactants amount = len(reactants) - 1 @@ -74,7 +76,7 @@ def fix_positions(self: 'ReactionContainer'): shift_x = max_x + 1 self._arrow = (arrow_min, arrow_max) self._signs = tuple(signs) - self.flush_cache(keep_molecule_cache=True) + self.flush_cache() -__all__ = ['Calculate2DReaction'] +__all__ = ['Calculate2DReaction'] \ No newline at end of file diff --git a/chython/periodictable/base/vector.py b/chython/periodictable/base/vector.py index c23d2773..7acad60f 100644 --- a/chython/periodictable/base/vector.py +++ b/chython/periodictable/base/vector.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # -# Copyright 2024 Denis Lipatov -# Copyright 2024 Vyacheslav Grigorev -# Copyright 2024 Timur Gimadiev +# Copyright 2024, 2025 Denis Lipatov +# Copyright 2024, 2025 Vyacheslav Grigorev +# Copyright 2024, 2025 Timur Gimadiev # Copyright 2024, 2025 Ramil Nugmanov # This file is part of chython. # @@ -19,48 +19,78 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, see . # -from math import cos, sin, hypot, atan2 +""" +This module introduces the `Vector` class, designed to perform mathematical +calculations relevant to two-dimensional Cartesian coordinate systems. -class Vector: - __slots__ = ('x', 'y') +The `Vector` class facilitates operations with coordinates, including vector arithmetic +(addition, subtraction, multiplication/division by scalars), normalization, rotation, and +distance calculations. It also supports methods for determining the angle of a vector, its +length, and whether it lies in a certain quadrant. Additionally, it includes functions for +reflecting vectors about lines, finding the closest atom or point, and rotating vectors around +other vectors or points. - def __init__(self, x: float = 0., y: float = 0.): - self.x = x - self.y = y +This class provides a comprehensive toolkit for performing geometric computations +essential in fields such as computer graphics, physics simulations, and computational chemistry, +where precise manipulation and analysis of spatial relationships are required. +""" +import numpy as np +import math +from typing import List, TYPE_CHECKING - def __repr__(self): - return f'Vector({self.x}, {self.y})' +if TYPE_CHECKING: + from ...algorithms.calculate2d.Properties import * - def __neg__(self): - """ - A class method that inverts the current coordinates of objects of the class - """ - return Vector(-self.x, -self.y) +class Vector(np.ndarray): + """ + The `Vector` class facilitates operations with coordinates, including vector arithmetic + (addition, subtraction, multiplication/division by scalars), normalization, rotation, and + distance calculations. It also supports methods for determining the angle of a vector, its + length, and whether it lies in a certain quadrant. Additionally, it includes functions for + reflecting vectors about lines, finding the closest atom or point, and rotating vectors around + other vectors or points. + """ + __slots__ = () - def __sub__(self, vector: 'Vector'): - """ - A method for the operation of subtraction between vectors - """ - return Vector(self.x - vector.x, self.y - vector.y) - def __add__(self, vector: 'Vector'): + def __new__(cls, *args) -> np.ndarray: """ - A method for the operation of addition between vectors + Constructor of the Vector class. + + Parameters: + :param args: A variable number of arguments representing the coordinates of the vector. + Each argument in args is expected to be a number (Union[int, float]). + The first two arguments correspond to the x and y coordinates, respectively. + + Returns: + Vector: An instance of the Vector class initialized with the provided coordinates. """ - return Vector(self.x + vector.x, self.y + vector.y) + obj = np.asarray(args, dtype=np.float64).view(cls) + return obj + - def __truediv__(self, scalar: float): + @property + def x(self): """ - A class method that divides the coordinates of the vector by a given scalar + Gets the x-coordinate of the vector along the abscissa axis. + :return: The x-coordinate value (type: Union[int, float]). """ - return Vector(self.x / scalar, self.y / scalar) - - def __mul__(self, scalar: float): + return self[0] + + + @property + def y(self): """ - Multiplies the coordinates of the current vector by an arbitrary real number + Gets the y-coordinate of the vector along the ordinate axis. + :return: The y-coordinate value (type: Union[int, float]). """ - return Vector(self.x * scalar, self.y * scalar) + return self[1] + + + def __repr__(self): + return f'Vector({self.x}, {self.y})' + def __float__(self): """ @@ -68,52 +98,433 @@ def __float__(self): Returns float """ - return hypot(self.x, self.y) + return math.hypot(self.x, self.y) + def __iter__(self): yield self.x yield self.y + def __len__(self): return 2 - def __matmul__(self, vector: 'Vector'): - return self.x * vector.y - self.y * vector.x def __or__(self, vector: 'Vector'): """ Calculate distance between two vectors """ - return hypot(vector.x - self.x, vector.y - self.y) + return math.hypot(vector.x - self.x, vector.y - self.y) + + + def rotate(self, angle: float) -> None: + """ + A method that rotates the vector by the appropriate angle from the signature + of the function and updates the coordinates of the current class object + + Parameters: + :param angle float: + The angle by which the vector should be rotated + """ + rotation_matrix = np.array([ + [np.cos(angle), -np.sin(angle)], + [np.sin(angle), np.cos(angle)] + ], dtype=np.float64) + self[:] = rotation_matrix @ self + - def rotate(self, angle: float, vector: 'Vector' = None): + def invert(self) -> None: """ - A method that rotates the vector by the angle in radians + A class method that inverts the current coordinates of objects of the class """ - c = cos(angle) - s = sin(angle) - if vector is None: - return Vector(self.x * c - self.y * s, self.x * s + self.y * c) - xy = self - vector - return vector + Vector(xy.x * c - xy.y * s, xy.x * s + xy.y * c) + self[:] = -self + - def normalise(self): + def normalise(self) -> None: """ Normalization of coordinates (dividing them by the length of the vector itself) """ - if ln := float(self): - return self / ln - return self + if length := np.linalg.norm(self): + self[:] /= length + + + def angle(self) -> float: + """ + A method that calculates the angle of inclination of the current vector + + Returns float the angle of inclination of the vector + """ + return math.atan2(self.y, self.x) + + + def length(self) -> float: + """ + Calculates the length of the current vector + + Returns float + """ + return np.linalg.norm(self) + + + def rotate_around_vector(self, angle: float, vector: 'Vector') -> None: + """ + Rotates a point (or vector) around a given vector by a specified angle. + + Parameters: + :param angle float: + The angle by which to rotate the point, typically measured in radians. + :param vector 'Vector': + The vector around which the rotation occurs. This vector serves as the reference + point. + """ + translated_vector = self - vector + rotation_matrix = np.array([ + [np.cos(angle), -np.sin(angle)], + [np.sin(angle), np.cos(angle)] + ]) + rotated_vector = rotation_matrix @ translated_vector + self[:] = np.add(rotated_vector, vector) + + + def get_closest_atom(self, atom_1: 'AtomProperties', atom_2: 'AtomProperties') -> 'AtomProperties': + """ + This method determines which of the two atoms (represented by the objects atom_1 and atom_2) + is closer to the current object (represented by self). + + Parameters: + :param atom_1: 'AtomProperties': + The first atom to compare. + :param atom_2: 'AtomProperties': + The second atom to compare. + + Returns 'AtomProperties': + The closest atom. + """ + distance_1 = self.get_squared_distance(atom_1.position) + distance_2 = self.get_squared_distance(atom_2.position) + return atom_1 if distance_1 < distance_2 else atom_2 + + + def get_closest_point_index(self, point_1: 'Vector', point_2: 'Vector') -> int: + """ + The method is designed to determine which of the two specified coordinates (point_1: 'Vector', point_2: 'Vector') + closer to the current point. + + Parameters + :param point_1 'Vector': + The first point to be compared with. It can be a tuple, a list, or an object + representing coordinates. + :param point_2 'Vector': + The second point to compare with. Similarly, it can be a tuple, a list, or an + object. + + Returns int: + The index of the nearest point: 0 for point_1 and 1 for point_2. + """ + distance_1 = self.get_squared_distance(point_1) + distance_2 = self.get_squared_distance(point_2) + return 0 if distance_1 < distance_2 else 1 - def angle(self, vector: 'Vector' = None) -> float: + + def get_squared_length(self) -> float: + """ + Calculates the length squared + + Returns float: + Vector length squared + """ + return self.x ** 2 + self.y ** 2 + + + def get_squared_distance(self, vector: 'Vector') -> float: + """ + The method is designed to calculate the square of the distance between the current vector + (represented by self) and the specified vector (or point) represented by the vector object. + + Parameters + :param vector: 'Vector': + An object representing a vector or point from which to calculate the distance. + + Returns float: + The square of the distance + """ + return np.sum((self - vector) ** 2) + + + def get_distance(self, vector: 'Vector') -> float: + """ + The method is designed to calculate the distance between the current vector (represented by self) and + the specified vector (or point) represented by the vector object. + + Parameters + :param vector: 'Vector': + An object representing a vector or point from which to calculate the distance. + + Returns float: + The distance between the coordinates of the current vector and the passed parameter + """ + return math.sqrt(self.get_squared_distance(vector)) + + + def get_rotation_away_from_vector(self, vector: 'Vector', center: 'Vector', angle: float) -> float: + """ + The method is designed to determine how much the angle of rotation (in a positive or negative direction) + from a given vector measures the distance to this vector. + + Parameters + :param vector 'Vector': + The vector to "move away from". It can be a point or a direction, relative to which + the rotation is taking place. + :param center 'Vector': + The center of rotation around which the object (represented by self) rotates. + :param angle float: + The angle at which the rotation occurs. This value can be positive or negative. + + Returns returns the rotation angle that minimizes the distance to the vector, + either in a positive or negative direction. """ - A method calculates the angle of inclination of the current vector - or the vector between given vector and the current vector. + tmp = self.copy() + tmp.rotate_around_vector(angle, center) + squared_distance_1 = tmp.get_squared_distance(vector) + tmp.rotate_around_vector(-2.0 * angle, center) + squared_distance_2 = tmp.get_squared_distance(vector) + return angle if squared_distance_2 < squared_distance_1 else -angle + + + def rotate_away_from_vector(self, vector: 'Vector', center: 'Vector', angle: float) -> None: + """ + The method is designed to rotate the current object (represented by self) around a given + one center in such a way as to minimize the distance to the specified vector. + If rotation in one direction leads to a decrease in the distance, the function corrects + the rotation,to ensure maximum distance from the vector. + + Parameters + :param vector 'Vector': + The vector to "move away from". It can be a point or a direction, relative to which + the rotation is taking place. + :param center 'Vector': + The center of rotation around which the object rotates. + :param angle float: + The angle at which the rotation occurs. This value can be positive or negative. + """ + self.rotate_around_vector(angle, center) + squared_distance_1 = self.get_squared_distance(vector) + self.rotate_around_vector(-2.0 * angle, center) + squared_distance_2 = self.get_squared_distance(vector) + if squared_distance_2 < squared_distance_1: + self.rotate_around_vector(2.0 * angle, center) + + + def get_clockwise_orientation(self, vector: 'Vector') -> str: + """ + The method is designed to determine the orientation (positive or negative) between + the current object (represented by self) and the specified vector (represented by + the vector object). + + Parameters + :param vector 'Vector': + The vector relative to which the orientation is determined. + + Returns str: + A string indicating whether the orientation is "clockwise", "counterclockwise" + or "neutral". + """ + orientation_value = self.y * vector.x - self.x * vector.y + return 'clockwise' if orientation_value > 0 else 'counterclockwise' \ + if orientation_value < 0 else 'neutral' + + + def mirror_about_line(self, line_point_1: 'Vector', line_point_2: 'Vector') -> None: + """ + The method is designed to reflect the current object (represented by self) relative to a + given line, defined by two points (line_point_1 and line_point_2). After performing this + function, the coordinates of the object will be changed so that it is on the opposite + side of the line, keeping the same distance to the line. + + Parameters + :param line_point_1: 'Vector': + The first point defining the line. + :param line_point_2: 'Vector': + The second point defining the line. + """ + dx = line_point_2.x - line_point_1.x + dy = line_point_2.y - line_point_1.y + + a = (dx**2 - dy**2) / (dx**2 + dy**2) + b = 2 * dx * dy / (dx**2 + dy**2) + self[:] = np.array([ + a * (self.x - line_point_1.x) + b * (self.y - line_point_1.y) + line_point_1.x, + b * (self.x - line_point_1.x) - a * (self.y - line_point_1.y) + line_point_1.y + ]) + + + @staticmethod + def get_position_relative_to_line(vector_start: 'Vector', vector_end: 'Vector', vector: 'Vector') -> int: + """ + Determines the position of a vector relative to a line defined by two points. + + Parameters: + :param vector_start 'Vector': + The start point of the line. + :param vector_end 'Vector': + The end point of the line. + :param vector 'Vector': + The vector whose position relative to the line is to be determined. + + Returns int: + 1 if the vector is to the left of the line, -1 if the vector is to the right of the + line, 0 if the vector lies on the line. + """ + determinant = (vector.x - vector_start.x) * (vector_end.y - vector_start.y) - \ + (vector.y - vector_start.y) * (vector_end.x - vector_start.x) + return np.sign(determinant) + + + @staticmethod + def get_directionality_triangle(vector_a: 'Vector', vector_b: 'Vector', vector_c: 'Vector') -> str: + """ + Determines the directionality of the triangle formed by three vectors (or points). + + Parameters: + :param vector_a 'Vector': + The first vertex of the triangle. + :param vector_b 'Vector': + The second vertex of the triangle. + :param vector_c 'Vector': + The third vertex of the triangle. + + Returns str: + - 'clockwise' if the triangle is oriented in a clockwise direction. + - 'counterclockwise' if the triangle is oriented in a counterclockwise direction. + - None if the three points are collinear (lie on the same line). + """ + 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: + return 'clockwise' if determinant < 0 else 'counterclockwise' + + + @staticmethod + def mirror_vector_about_line(line_point_1: 'Vector', line_point_2: 'Vector', point: 'Vector')-> 'Vector': + """ + Mirrors a point (or vector) across a line defined by two points. + + Parameters: + :param line_point_1 'Vector': + The first point defining the line. + :param line_point_2 'Vector': + The second point defining the line. + :param point 'Vector': + The point to be mirrored across the line. + + Returns Vector: + A new Vector representing the mirrored point across the line. + """ + dx = line_point_2.x - line_point_1.x + dy = line_point_2.y - line_point_1.y + 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 + return Vector(x_new, y_new) + + + @staticmethod + def get_line_angle(point_1: 'Vector', point_2: 'Vector') -> float: + """ + Calculates the angle of a line defined by two points with respect to the positive x-axis. + + Parameters: + point_1 'Vector': + The first point defining the line. + point_2 'Vector': + The second point defining the line. + + Returns float: + The angle of the line in radians, in the range [-π, π]. + """ + difference: 'Vector' = point_2 - point_1 + return difference.angle() + + + @staticmethod + def get_midpoint(vector_1: 'Vector', vector_2: 'Vector') -> 'Vector': + """ + Calculates the midpoint between two vectors. + + Parameters: + vector_1 'Vector': + The first vector. + vector_2 'Vector': + The second vector. + + Returns Vector: + A new Vector representing the midpoint between vector_1 and vector_2. + """ + midpoint: Vector = (vector_1 + vector_2) / 2 + return midpoint + + + @staticmethod + def get_average(vectors: List['Vector']) -> 'Vector': + """ + Calculates the average of a list of vectors. + + Parameters: + :param vectors List[Vector]: + A list of vectors for which the average is to be calculated. + + Returns: + Vector: A new Vector representing the average of the input vectors. + """ + vectors_array = np.array(vectors) + average = np.mean(vectors_array, axis=0) + return Vector(*average) + + + @staticmethod + def get_normals(vector_1: 'Vector', vector_2: 'Vector') -> List['Vector']: + """ + Calculates the normal vectors to the line defined by two vectors. + + Parameters: + :param vector_1 'Vector': + The first vector defining the line. + :param vector_2 'Vector': + The second vector defining the line. + + Returns List[Vector]: + A list containing two normal vectors to the line defined by vector_1 and vector_2. + """ + delta: Vector = vector_2 - vector_1 + return [Vector(-delta.y, delta.x), Vector(delta.y, -delta.x)] + + + @staticmethod + def get_angle_between_vectors(vector_1: 'Vector', vector_2: 'Vector', origin: 'Vector') -> float: + """ + Calculates the angle between two vectors relative to a given origin point. + + Parameters: + :param vector_1 'Vector': + The first vector. + :param vector_2 'Vector': + The second vector. + :param origin 'Vector': + The origin point relative to which the angle is calculated. + + Returns: + float: The angle between vector_1 and vector_2 in radians, in the range [0, π]. """ - if vector is None: - return atan2(self.y, self.x) - else: - return atan2(vector.y - self.y, vector.x - self.x) + v1_diff: np.array = np.subtract(vector_1, origin) + v2_diff: np.array = np.subtract(vector_2, origin) + dot_product = np.dot(v1_diff, v2_diff) + length_v1: float = np.linalg.norm(v1_diff) + length_v2: float = np.linalg.norm(v2_diff) -__all__ = ['Vector'] + cos_angle: float = dot_product / (length_v1 * length_v2) + cos_angle: float = np.clip(cos_angle, -1.0, 1.0) + return np.arccos(cos_angle) + +__all__ = ['Vector'] \ No newline at end of file