Defining a Custom BioMol Class#
In this tutorial, you will
Learn how to create a custom class that inherits from the
BioMolclass.Define a custom
Viewfor feature type-hinting.Understand the benefits of using type hints in your code.
The Problem: Lack of Autocompletion#
Before we begin, let’s discuss why type-hinting is important.
Because the BioMol class doesn’t predefine specific features, it’s hard to know what features are available in a BioMol instance.
For example, autocompletion for mol.atoms.positions doesn’t work.
To address this limitation, you will:
Define a custom
Viewfor feature type-hinting.Create a custom class that inherits from
BioMoland uses the definedView.
Let’s start by importing the necessary modules.
import numpy as np
from biomol import BioMol
from biomol.core import FeatureContainer, IndexTable, NodeFeature, View
Preparing container and index table#
As in the previous tutorial, prepare a FeatureContainer and an IndexTable to hold features and establish hierarchical relationships.
atom_positions = NodeFeature(
value=np.array(
[
[0.0, 0.0, 0.0],
[1.4, 0.0, 0.0],
[1.4, 1.4, 0.0],
[0.0, 1.4, 0.0], # ALA-1
[2.8, 0.0, 0.0],
[4.2, 0.0, 0.0],
[4.2, 1.4, 0.0],
[2.8, 1.4, 0.0], # GLY-2
[5.6, 0.0, 0.0],
[7.0, 0.0, 0.0],
[7.0, 1.4, 0.0],
[5.6, 1.4, 0.0], # ALA-3
],
),
)
atom_names = NodeFeature(value=np.array(["N", "CA", "C", "O"] * 3))
residue_ids = NodeFeature(value=np.array([1, 2, 3]))
residue_names = NodeFeature(value=np.array(["ALA", "GLY", "ALA"]))
chain_ids = NodeFeature(value=np.array(["A"]))
chain_entities = NodeFeature(value=np.array(["PROTEIN"]))
atom_container = FeatureContainer(
{"positions": atom_positions, "name": atom_names},
)
residue_container = FeatureContainer(
{"id": residue_ids, "name": residue_names},
)
chain_container = FeatureContainer(
{"id": chain_ids, "entity": chain_entities},
)
index_table = IndexTable.from_parents(
atom_to_res=np.array([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]),
res_to_chain=np.array([0, 0, 0]),
)
Defining a Custom BioMol#
Next, define a custom View and a custom class that inherits from BioMol class.
The View specifies which features are exposed on the FeatureContainer via property methods.
Note
View is used solely for static type-hinting.
It does not affect runtime behavior and therefore cannot guarantee that features actually exist at runtime.
class MyAtomView(View["MyAtomView", "MyResidueView", "MyChainView", "MyBioMol"]):
"""Custom atom-level View."""
@property
def positions(self) -> NodeFeature:
"""XYZ coordinates of atoms."""
@property
def name(self) -> NodeFeature:
"""Atom name."""
class MyResidueView(View["MyAtomView", "MyResidueView", "MyChainView", "MyBioMol"]):
"""Custom residue-level View."""
@property
def id(self) -> NodeFeature:
"""Residue ID."""
@property
def name(self) -> NodeFeature:
"""Residue name."""
class MyChainView(View["MyAtomView", "MyResidueView", "MyChainView", "MyBioMol"]):
"""Custom chain-level View."""
@property
def id(self) -> NodeFeature:
"""Chain ID."""
@property
def entity(self) -> NodeFeature:
"""Chain entity type."""
class MyBioMol(BioMol["MyAtomView", "MyResidueView", "MyChainView"]):
"""Custom BioMol class."""
Now, you can create an instance of your custom BioMol class and access its features with type-hinting support.
mol = MyBioMol(
atom_container=atom_container,
residue_container=residue_container,
chain_container=chain_container,
index_table=index_table,
)
# ruff: noqa: B018
mol.atoms.positions
mol.atoms.name
mol.residues.id
mol.residues.name
mol.chains.id
mol.chains.entity
NodeFeature(value=array(['PROTEIN'], dtype='<U7'))
Type hints remain effective even in complex expressions that involve multiple hierarchical traversals.
mol.atoms.residues.chains.id
mol.atoms.chains.residues.atoms.name
mol.chains.atoms.chains.residues.id
NodeFeature(value=array([1, 2, 3]))