Skip to content

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:

  1. build topology
  2. call compute_contacts(...)
  3. 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.