Topology¶
A Topology represents molecular connectivity as a graph: atoms are vertices, bonds are edges.
This layer is designed for connectivity queries and derived interactions, angles/dihedrals without manual bookkeeping.
When to use topology You have atoms + bonds and want derived angles/dihedrals., You need graph queries: neighbors, paths, components, rings/substructures., and You want a stable, recomputable view of connectivity after edits..
Key idea
Topology is typically derived from structure, e.g., Atomistic.get_topo. If bonds change, re-derive topology.
Creating Topologies¶
Topology can be created from scratch by adding atoms and bonds, or extracted from existing Atomistic or Frame objects. The graph representation allows efficient querying of connectivity patterns and automatic detection of angles, dihedrals, and impropers.
from molpy.core.topology import Topology
import numpy as np
# Create empty topology
topo = Topology()
# Add atoms (vertices) - can add multiple at once
topo.add_atoms(4) # Creates 4 vertices (indices 0-3)
# Add bonds (edges) - can add single or multiple bonds
topo.add_bond(0, 1) # Single bond
topo.add_bonds([(1, 2), (2, 3)]) # Multiple bonds at once
print(f"Atoms: {topo.n_atoms}") # 4
print(f"Bonds: {topo.n_bonds}") # 3
print(f"Bond list:\n{topo.bonds}") # [[0, 1], [1, 2], [2, 3]]
# Access atoms and bonds as arrays
print(f"Atom indices: {topo.atoms}") # [0, 1, 2, 3]
Atoms: 4 Bonds: 3 Bond list: [[0 1] [1 2] [2 3]] Atom indices: [0 1 2 3]
Graph Algorithms and Connectivity¶
Since Topology inherits from igraph.Graph, you have access to the full igraph API for graph analysis. Common operations include connectivity checks, shortest paths, neighbor queries, and subgraph matching. These algorithms are essential for topology analysis, ring detection, and substructure identification.
# Check if graph is connected (all atoms reachable)
print(f"Is connected? {topo.is_connected()}") # True
# Find shortest path between atoms
path = topo.get_shortest_paths(0, 3)[0]
print(f"Shortest path 0->3: {path}") # [0, 1, 2, 3]
# Get neighbors (directly bonded atoms)
neighbors = topo.neighbors(1)
print(f"Neighbors of atom 1: {list(neighbors)}") # [0, 2]
# Check if two atoms are adjacent (bonded)
print(f"Atoms 0 and 1 are adjacent: {topo.are_adjacent(0, 1)}") # True
print(f"Atoms 0 and 3 are adjacent: {topo.are_adjacent(0, 3)}") # False
# Get degree (number of bonds per atom)
degrees = topo.degree()
print(f"Atom degrees: {degrees}") # [1, 2, 2, 1] for linear chain
Is connected? True Shortest path 0->3: [0, 1, 2, 3] Neighbors of atom 1: [0, 2] Atoms 0 and 1 are adjacent: True Atoms 0 and 3 are adjacent: False Atom degrees: [1, 2, 2, 1]
Extracting Topology from Structures¶
Topology is commonly extracted from Atomistic. The get_topo() method automatically builds the graph from bond connectivity. For Frame, you need to construct the Topology manually from the blocks.
from molpy.core.atomistic import Atomistic
import molpy as mp
# Create a molecule with bonds
mol = Atomistic()
c1 = mol.def_atom(symbol="C", x=0, y=0, z=0)
c2 = mol.def_atom(symbol="C", x=1.5, y=0, z=0)
c3 = mol.def_atom(symbol="C", x=3.0, y=0, z=0)
mol.def_bond(c1, c2)
mol.def_bond(c2, c3)
# Extract topology from Atomistic
mol_topo = mol.get_topo()
print(f"Topology: {mol_topo.n_atoms} atoms, {mol_topo.n_bonds} bonds")
# Extract from Frame (requires bonds block)
frame = mp.Frame()
frame["atoms"] = mp.Block({
"x": [0.0, 1.5, 3.0],
"y": [0.0, 0.0, 0.0],
"z": [0.0, 0.0, 0.0]
})
frame["bonds"] = mp.Block({
"i": [0, 1],
"j": [1, 2]
})
# Manual topology from Frame
i = frame["bonds"]["i"]
j = frame["bonds"]["j"]
frame_topo = Topology()
frame_topo.add_atoms(frame["atoms"].nrows)
frame_topo.add_bonds(zip(i, j))
print(f"Frame topology: {frame_topo.n_atoms} atoms, {frame_topo.n_bonds} bonds")
Topology: 3 atoms, 2 bonds Frame topology: 3 atoms, 2 bonds
Higher-Order Connectivity¶
Topology automatically detects angles, three-atom sequences, dihedrals, four-atom sequences, and impropers through subgraph isomorphism. These patterns are computed on-demand from the bond graph, avoiding explicit storage while enabling efficient querying. The detection uses pattern matching: angles match the pattern A-B-C, two bonds, dihedrals match A-B-C-D, three bonds, and impropers match A-B, A-C, A-D, three bonds from a central atom.
# Automatic detection of angles and dihedrals
print(f"Angles: {topo.n_angles}") # 2 for linear chain: (0-1-2), (1-2-3)
print(f"Dihedrals: {topo.n_dihedrals}") # 1 for linear chain: (0-1-2-3)
# Access angle and dihedral arrays
print(f"Angle triplets:\n{topo.angles}") # [[0, 1, 2], [1, 2, 3]]
print(f"Dihedral quadruplets:\n{topo.dihedrals}") # [[0, 1, 2, 3]]
# Create a branched structure to see more angles
branched = Topology()
branched.add_atoms(5)
branched.add_bonds([(0, 1), (1, 2), (1, 3), (1, 4)]) # Star topology
print(f"\nBranched structure:")
print(f" Atoms: {branched.n_atoms}, Bonds: {branched.n_bonds}")
print(f" Angles: {branched.n_angles}") # 3 angles: (2-1-3), (2-1-4), (3-1-4)
print(f" Angles:\n{branched.angles}")
Angles: 2 Dihedrals: 1 Angle triplets: [[0 1 2] [1 2 3]] Dihedral quadruplets: [[0 1 2 3]] Branched structure: Atoms: 5, Bonds: 4 Angles: 6 Angles: [[0 1 2] [0 1 3] [0 1 4] [2 1 3] [2 1 4] [3 1 4]]
Example: Topology Analysis Workflow¶
This example demonstrates a complete workflow: building topology from a molecular structure, analyzing connectivity patterns, and using graph algorithms for structural analysis.
from molpy.core.topology import Topology
from molpy.core.atomistic import Atomistic
# Build a small molecule (propane-like: C-C-C with hydrogens)
mol = Atomistic()
# Carbon chain
c1 = mol.def_atom(symbol="C", x=0.0, y=0.0, z=0.0)
c2 = mol.def_atom(symbol="C", x=1.5, y=0.0, z=0.0)
c3 = mol.def_atom(symbol="C", x=3.0, y=0, z=0.0)
# Hydrogens
h1 = mol.def_atom(symbol="H", x=-0.5, y=0.5, z=0.0)
h2 = mol.def_atom(symbol="H", x=-0.5, y=-0.5, z=0.0)
h3 = mol.def_atom(symbol="H", x=1.0, y=0.5, z=0.0)
h4 = mol.def_atom(symbol="H", x=1.0, y=-0.5, z=0.0)
h5 = mol.def_atom(symbol="H", x=3.5, y=0.5, z=0.0)
h6 = mol.def_atom(symbol="H", x=3.5, y=-0.5, z=0.0)
# Define bonds
mol.def_bond(c1, c2)
mol.def_bond(c2, c3)
mol.def_bond(c1, h1)
mol.def_bond(c1, h2)
mol.def_bond(c2, h3)
mol.def_bond(c2, h4)
mol.def_bond(c3, h5)
mol.def_bond(c3, h6)
# Extract topology
topo = mol.get_topo()
# Analyze connectivity
print(f"Total atoms: {topo.n_atoms}")
print(f"Total bonds: {topo.n_bonds}")
print(f"Angles: {topo.n_angles}")
print(f"Dihedrals: {topo.n_dihedrals}")
# Find backbone (carbon chain) using graph algorithms
# Get carbon indices (assuming first 3 atoms are carbons)
carbon_indices = [0, 1, 2]
backbone_path = topo.get_shortest_paths(0, 2)[0]
print(f"\nCarbon backbone path: {backbone_path}")
# Analyze each carbon's connectivity
for i in carbon_indices:
neighbors = list(topo.neighbors(i))
degree = topo.degree(i)
print(f"C{i}: degree={degree}, neighbors={neighbors}")
# Find all angles involving carbons
carbon_angles = topo.angles[np.isin(topo.angles[:, 0], carbon_indices) |
np.isin(topo.angles[:, 2], carbon_indices)]
print(f"\nAngles involving carbons:\n{carbon_angles}")
# Check for rings (none in this linear structure)
print(f"\nIs acyclic: {topo.is_dag()}") # Directed acyclic graph check
# print(f"Number of cycles: {len(topo.get_all_cycles())}") # Method not available # Should be 0
Total atoms: 9 Total bonds: 8 Angles: 12 Dihedrals: 12 Carbon backbone path: [0, 1, 2] C0: degree=3, neighbors=[1, 3, 4] C1: degree=4, neighbors=[0, 2, 5, 6] C2: degree=3, neighbors=[1, 7, 8] Angles involving carbons: [[0 1 2] [0 1 5] [0 1 6] [1 0 3] [1 0 4] [1 2 7] [1 2 8] [2 1 5] [2 1 6]] Is acyclic: False
Summary¶
Topology is the connectivity layer: fast queries on bonds-as-graph., Angles/dihedrals are derived from the bond graph, so re-derive after edits., and Graph algorithms, neighbors, paths, components make structural analysis concise..