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_recordscontains atom entities (AtomRec).top.ringscontains ring entities (RingView/RingRecdata model).top.groupscontains group entities (GroupView/GroupRecdata 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.kindis canonical (eid.kind is Kind.Ringworks reliably).- typed resolvers (
resolve_atom,resolve_ring,resolve_group) make kind expectations explicit. EntityResolvergives 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.