Contacts¶
Contacts are Lahuta's way to represent non-covalent interactions between interaction sites in a structure.
Those sites are entities (Atom, Ring, Group), not only atoms, so contact output can describe chemistry at the correct resolution.
If you have not read the entities page yet, we recommend that you read it first: Entities.
The Easiest Approach¶
For most users, the easiest path is:
- build topology
- call
compute_contacts(...) - inspect a
ContactSet
from lahuta import LahutaSystem
from lahuta.entities import compute_contacts
system = LahutaSystem("core/data/ubi.cif")
if not system.build_topology():
raise RuntimeError("Topology build failed")
top = system.get_topology()
contacts = compute_contacts(top) # default provider: molstar
print("contacts:", contacts.size())
print("first contact:", contacts[0])
contacts is a ContactSet. Each Contact contains:
- lhs and rhs: entity IDs
- distance_sq: squared distance in A^2
- type: interaction type
Filtering To One Interaction Type¶
If you want only one type (for example hydrogen bonds), pass only=...:
from lahuta import InteractionType
from lahuta.entities import compute_contacts
hbonds = compute_contacts(top, provider="molstar", only=InteractionType.HydrogenBond)
print("hydrogen bonds:", hbonds.size())
You can also pass multiple types as a list.
Understanding Contacts With Entity Resolution¶
A contact stores EntityID pairs. To make them human-readable quickly, use:
- contact.describe(topology)
- contact.to_dict(topology)
c = contacts[0]
print(c.describe(top))
print(c.to_dict(top))
For deeper typed access, use EntityResolver:
from lahuta import EntityResolver
resolver = EntityResolver(top)
left_rec, right_rec = resolver.resolve_contact(contacts[0])
print(type(left_rec), type(right_rec))
This is where contacts connect directly to entities: each contact side resolves to AtomRec, RingRec, or GroupRec.
From Easy To Custom: find_contacts(...)¶
compute_contacts(...) runs a predefined provider.
find_contacts(...) lets you define your own search using entity selectors and an optional tester.
from lahuta import AtomType
from lahuta.entities import atoms, find_contacts
hydrophobic_atoms = atoms(lambda a: a.type.has(AtomType.Hydrophobic))
hydrophobic_contacts = find_contacts(top, hydrophobic_atoms, distance_max=4.5)
print("hydrophobic-like contacts:", hydrophobic_contacts.size())
You can also search across different entity kinds:
from lahuta import AtomType
from lahuta.entities import groups, rings, find_contacts
charged_groups = groups(lambda g: g.a_type.has(AtomType.PositiveCharge) or g.a_type.has(AtomType.NegativeCharge))
aromatic_rings = rings(lambda r: r.aromatic)
group_ring = find_contacts(top, charged_groups, aromatic_rings, distance_max=6.0)
print("group-ring contacts:", group_ring.size())
What Is A Tester?¶
A tester is a function that decides whether a candidate pair should become a contact, and if yes, which interaction type it should get.
Signature:
- tester(i: int, j: int, d2: float) -> InteractionType
Where:
- i, j: entity indices
- d2: squared distance
Return:
- a concrete InteractionType to keep/classify the pair
- InteractionType.NoInteraction to reject it
from lahuta import InteractionType
from lahuta.entities import find_contacts, rings
aromatic_rings = rings(lambda r: r.aromatic)
def pi_like_tester(i: int, j: int, d2: float) -> InteractionType:
if d2 <= 6.0 * 6.0:
return InteractionType.PiStacking
return InteractionType.NoInteraction
pi_like = find_contacts(top, aromatic_rings, tester=pi_like_tester, distance_max=6.0)
print("pi-like contacts:", pi_like.size())
This is the most customizable layer: selectors define candidate entities, and the tester defines the contact rule.