Source code for tmol.kinematics._metadata
import enum
import math
import attr
import torch
import pandas
from tmol.types import (
ConvertAttrs,
TensorGroup,
Tensor,
)
from tmol.utility import vals_to_name_cat, names_to_val_cat
from tmol.kinematics import NodeType, KinDOF, KinForest
[docs]
class DOFTypes(enum.IntEnum):
"""High-level class of kinematic DOF types."""
jump = 0
bond_angle = enum.auto()
bond_distance = enum.auto()
bond_torsion = enum.auto()
[docs]
@attr.s(frozen=True, slots=True, auto_attribs=True)
class DOFMetadata(TensorGroup, ConvertAttrs):
"""The location, type, and descriptive ids of valid dofs within a KinForest.
Descriptive entries for dofs within a KinForest, this provides a 1-d
structure to select and report a subset of entries within a KinDOF buffer.
DOFMetadata sets are used to indicate mobile vs fixed dofs for KinematicOp
dof to coordinate functions.
DOFMetadata supports isomorphic conversion between a DataFrame and
TensorGroup representation to support symbolic selection. This converts the
IntEnum encoded "dof_type" entry into a string categorical column.
The DOFMetadata data members, just like the KinForest, suffer from the same
confusion about what an index represents because there are two ways to index
the data:
- The "Target Order" (TO) that refers to the index of an atom in the PoseStack
it came from where the coordinate tensor is squashed to (N,3)
- The "KinForest Order" (KFO) that refers to the order that an atom's node appears
in the KinForest; this second ordering puts the index of any child atom after
the index for any parent atom
The DOFMetadata class indexes all available DOFs in the system. There are 9 possible
DOFs per atom (either 3 for BondedAtoms or 9 for JumpAtoms), but in actuality,
there are many fewer valid DOFs. The DOFMetadata class indexes valid DOFs.
For each valid DOF i, there's:
- node_idx[i]: the KFO index of the atom that DOF i belongs to
- dof_idx[i]: the index between 0-8 for DOF i on its atom
- dof_type[i]: the DOF type (either a BondDOFType or a JumpDOFType) for DOF i
- parent_id[i]: the TO index for the parent to node_idx[i] for DOF i
- child_id[i]: the TO index for node_idx[i] for DOF i
The DOFMetadata class is primarily used to index into torch tensors in python,
and therefore all of its dtypes are 64-bit integers.
"""
node_idx: Tensor[torch.long][...]
dof_idx: Tensor[torch.long][...]
dof_type: Tensor[torch.long][...]
parent_id: Tensor[torch.long][...]
child_id: Tensor[torch.long][...]
[docs]
@classmethod
def for_kinforest(cls, kinforest: KinForest):
"""Return all valid dofs within a KinForest."""
# Setup a dof type table the same shape as the kinematic dofs,
# marking all potential movable dofs with the abstract dof type.
# Leaving all non-movable or invalid dofs as nan. Essentially,
# [n-atoms x 9]
dof_types = KinDOF.full(kinforest.shape, math.nan)
parentIdx = kinforest.parent.to(dtype=torch.long)
# count the number of children each KFO node has and then ask is that number > 0
node_has_children = (
torch.zeros_like(kinforest.id).put_(
parentIdx, torch.ones_like(kinforest.parent), accumulate=True
)
> 0
)
bsel = kinforest.doftype == NodeType.bond
dof_types.bond.phi_p[bsel] = DOFTypes.bond_angle
dof_types.bond.theta[bsel] = DOFTypes.bond_angle
dof_types.bond.d[bsel] = DOFTypes.bond_distance
# Only flag the "child phi" if the node has affected children.
dof_types.bond.phi_c[bsel & node_has_children] = DOFTypes.bond_torsion
jsel = kinforest.doftype == NodeType.jump
dof_types.jump.RBx[jsel] = DOFTypes.jump
dof_types.jump.RBy[jsel] = DOFTypes.jump
dof_types.jump.RBz[jsel] = DOFTypes.jump
dof_types.jump.RBdel_alpha[jsel] = DOFTypes.jump
dof_types.jump.RBdel_beta[jsel] = DOFTypes.jump
dof_types.jump.RBdel_gamma[jsel] = DOFTypes.jump
# Get indices of all marked dofs.
node_idx, dof_idx = (~torch.isnan(dof_types.raw)).nonzero(as_tuple=True)
# Unpack into the node/dof index, then expand
return cls(
node_idx=node_idx,
dof_idx=dof_idx,
dof_type=dof_types.raw[node_idx, dof_idx],
child_id=kinforest.id[node_idx],
parent_id=kinforest.id[parentIdx[node_idx]],
)
def to_frame(self) -> pandas.DataFrame:
assert len(self.shape) == 1
columns = attr.asdict(self)
# NOTE vals_to_name_cat and other functions from
# tmol.utility._categorical will silently fail if passed torch.Tensor
columns["dof_type"] = vals_to_name_cat(DOFTypes, columns["dof_type"].numpy())
return pandas.DataFrame(columns)
[docs]
@classmethod
def from_frame(cls, frame):
"""Convert from DataFrame to metadata, discarding any unneeded columns."""
cols = {n: c.values for n, c in dict(frame).items()}
if isinstance(cols["dof_type"], pandas.Categorical):
cols["dof_type"] = names_to_val_cat(
DOFTypes, cols["dof_type"]
).codes.astype(int)
return cls(
node_idx=cols["node_idx"],
dof_idx=cols["dof_idx"],
dof_type=cols["dof_type"],
child_id=cols["child_id"],
parent_id=cols["parent_id"],
)