Skip to content

Entities

An entity is an interaction site. Some sites are single atoms, but many biologically important sites are larger chemical features such as aromatic rings or charged functional groups.
If you model everything as atom-only, you lose that higher-level meaning.

Lahuta therefore represents three entity kinds:

  • Kind.Atom: one atom-level site.
  • Kind.Ring: an aromatic or non-aromatic ring feature.
  • Kind.Group: a functional group feature.

This is the core reason entities exist: a single model that can represent atom-based and feature-based interaction sites consistently.

from lahuta import LahutaSystem

system = LahutaSystem("core/data/ubi.cif")
if not system.build_topology():
    raise RuntimeError("Topology build failed")

top = system.get_topology()
print("atom entities:", len(top.atom_records))
print("ring entities:", len(top.rings))
print("group entities:", len(top.groups))

Why Use Entities Instead Of Only Atoms

Entities let us express interaction chemistry at the right level:

  • Ring-based interactions can be represented as ring entities, not reduced to one arbitrary atom pair (or all pairwise interactions).
  • Functional-group interactions can be represented as group entities.
  • Atom-level interactions still work through atom entities.

So Lahuta can represent heterogeneous pairs such as Atom-Ring, Ring-Ring, and Group-Group, not only Atom-Atom.

How Lahuta Defines Entities

Topology is where entities are computed and stored. After topology build:

  • top.atom_records contains atom entities (AtomRec).
  • top.rings contains ring entities (RingView / RingRec data model).
  • top.groups contains group entities (GroupView / GroupRec data model).

Entities are referenced by EntityID, a compact identifier with two parts: - kind: one of Kind.Atom, Kind.Ring, Kind.Group - index: the entity index within that kind

EntityID is hashable, sortable, and stable to pass around in Python.

from lahuta import EntityID, Kind

atom_id = EntityID.make(Kind.Atom, 10)
ring_id = EntityID.make(Kind.Ring, 0)
group_id = EntityID.make(Kind.Group, 2)

print(atom_id, atom_id.kind, atom_id.index)
print(ring_id, ring_id.kind is Kind.Ring)

Constructing Entity IDs From Topology

A common pattern is to derive IDs from topology objects you already have:

from lahuta import EntityID, Kind

atom_ids = [EntityID.make(Kind.Atom, int(a.idx)) for a in top.atom_records]
ring_ids = [EntityID.make(Kind.Ring, i) for i, _ in enumerate(top.rings)]
group_ids = [EntityID.make(Kind.Group, i) for i, _ in enumerate(top.groups)]

print(len(atom_ids), len(ring_ids), len(group_ids))

Type-Safe Resolution

When you need to go from EntityID back to concrete typed records, we provide two resolution styles.

If you already know the kind, use typed topology resolvers:

from lahuta import EntityID, Kind

eid = EntityID.make(Kind.Ring, 0)

if eid.kind is Kind.Atom:
    atom_rec = top.resolve_atom(eid)
elif eid.kind is Kind.Ring:
    ring_rec = top.resolve_ring(eid)
else:
    group_rec = top.resolve_group(eid)

If kinds vary dynamically, use EntityResolver.resolve(...):

from lahuta import EntityResolver

resolver = EntityResolver(top)
rec = resolver.resolve(eid)
print(type(rec), rec)

This is all type safe. Specifically:

  • EntityID.kind is canonical (eid.kind is Kind.Ring works reliably).
  • typed resolvers (resolve_atom, resolve_ring, resolve_group) make kind expectations explicit.
  • EntityResolver gives a unified API when kind is not known upfront.

This page focuses only on entity modeling and resolution. Contact computation and contact interpretation are covered separately in the contacts page.