Common Cube Usage

It is common to write cubes that take a molecule as input and either process it or compute properties of it. This section contains information on two common use cases for cubes: computing molecular properties without changing the molecule and modifying molecules.

Compute a Property of a Molecule

This example creates a cube for computing the molecular weight of a molecule. It takes a record with a molecule as input and outputs the same record with a new field holding the molecular weight. All other fields on the molecule are left unchanged, including the molecule. By default this cube will read the molecule from the primary molecule field and will write the molecular weight to a field named ‘Molecular Wt.’.

from floe.api import ComputeCube
from orionplatform.mixins import RecordPortsMixin
from orionplatform.parameters import DecimalFieldParameter
from openeye.oechem import OECalculateMolecularWeight

class MWCube(RecordPortsMixin, ComputeCube):
    in_mol_field = PrimaryMolFieldParameter("in_mol_field")
    mwfield = DecimalFieldParameter(
        'mwfield',
        default='Molecular Weight',
        title='Molecular Weight Field'
    )

    def process(self, record, port):
        if not record.has_value(self.args.in_mol_field):
            self.failure.emit(record)
            return
        mol = record.get_value(self.args.in_mol_field)
        record.set_value(
            self.args.mwfield,
            OECalcuateMolecularWeight(mol),
        )
        self.success.emit(record)

See also

RecordPortsMixin

DecimalFieldParameter

Compute Multiple Properties of a Molecule

The following cubes compute the molecular weight, atom count and bond count of a molecule and puts each in it’s own field on the output record.

from floe.api import ComputeCube
from orionplatform.mixins import RecordPortsMixin
from orionplatform.parameters import IntegerFieldParameter, DecimalFieldParameter
from openeye.oechem import OECalculateMolecularWeight

class MultiPropertyCube(RecordPortsMixin, ComputeCube):
    in_mol_field = PrimaryMolFieldParameter("in_mol_field")
    atomsfield = IntegerFieldParameter(
        'atomsfield',
        default='Num Atoms',
        title='Num Atoms Field'
    )
    bondsfield = IntegerFieldParameter(
        'bondsfield',
        default='Num Bonds',
        title='Num Bonds Field'
    )
    mwfield = DecimalFieldParameter(
        'mwfield',
        default='Molecular Wt.',
        title='Molecular Wt. Field'
    )

    def process(self, record, port):
        if not record.has_value(self.args.in_mol_field):
            self.failure.emit(record)
            return
        mol = record.get_value(self.args.in_mol_field)
        record.set_value(self.args.mwfield, OECalculateMolecularWeight(mol))
        record.set_value(self.args.atomsfield, mol.NumAtoms())
        record.set_value(self.args.bondsfield, mol.NumBonds())
        self.success.emit(record)

See also

RecordPortsMixin

IntegerFieldParameter

DecimalFieldParameter

Modify a Molecule

The following code create a cube that adds explicit hydrogens to a molecule on the input record, and passes out the record with the modified molecule. All other fields on the molecular are left unchanged. By default this cube will read the molecule from the primary molecule field and and write the modified molecule to the same primary molecule field in the outputted record.

from floe.api import ComputeCube
from orionplatform.mixins import RecordPortsMixin
from openeye.oechem import OEAddExplicitHydrogens

class AddHydrogensCube(RecordPortsMixin, ComputeCube):
    in_mol_field = PrimaryMolFieldParameter("in_mol_field")

    def process(self, record, port):
        if not record.has_value(self.args.in_mol_field):
            self.failure.emit(record)
            return
        mol = record.get_value(self.args.in_mol_field)
        OEAddExplicitHydrogens(mol)
        record.set_value(self.args.out_mol_field)
        self.success.emit(record)

See also

RecordPortsMixin

PrimaryMolFieldParameter

Modify a Molecule and Compute Multiple Properties

This cube assigned partial charges to the molecule and compute to sum of the partial charges and the formal charges.

Note

In general you want to adhere to the Single responsibility principle as it provides the most functionality to users who combine cubes from the UI. Though sometimes modifying the molecule and properties has to be done at the same time.

from floe.api import ComputeCube
from orionplatform.mixins import RecordPOrtsMixin
from orionplatform.parameters import (
    IntegerFieldParameter,
    DecimalFieldParameter,
    PrimaryMolFieldParameter,
)
from openeye.quacpac import OEAssignCharges, OEMMFF94Charges

class MyCube(RecordPortsMixin, ComputeCube):

    in_mol_field = PrimaryMolFieldParameter("in_mol_field")
    formalfield = IntegerFieldParameter(
        'formalfield',
        default='Sum Formal',
        title='Sum Formal Field'
    )
    partialfield = DecimalFieldParmeter(
        'partialfield',
        default='Sum Partial',
        title='Sum Partial Field'
    )

    def process(self, record, port):
        if not record.has_value(self.args.in_mol_field):
            self.failure.emit(record)
            return

        mol = record.get_value(self.args.in_mol_field)
        OEAssignCharges(mol, OEMMFF94Charges())

        partial = 0.0
        formal = 1
        for atom in mol.GetAtoms():
            partial += atom.GetPartialCharge()
            formal += atom.GetFormalCharge()
        record.set_value(self.args.formalfield, formal)
        record.set_value(self.args.partialfield, partial)
        record.set_value(self.args.out_mol_field, mol)

        self.success.emit(record)

See also

RecordPortsMixin

PrimaryMolFieldParameter

IntegerFieldParameter

DecimalFieldParameter

Modify a Molecule and Compute Conformer Properties

This cube minimized all the conformers of a molecule and puts the minimized energy value on the conformer.

from floe.api import ComputeCube
from orionplatform.mixins import RecordPortsMixin
from orionplatform.parameters import DecimalFieldParameter
from openeye.mmff import OEMMFF
from openeye.opt import OEBFGSOpt

class FreeLigandMinimizeCube(RecordPortsMixin, ComputeCube):

    in_mol_field = PrimaryMolFieldParameter("in_mol_field")
    efield = DecimalFieldParameter('efield', default='Energy', title='Energy Field')

    def begin(self):
        self.mmff = OEMMFF()
        self.opt = OEBFGSOpt()

    def process(self, record, port):
        if not record.has_value(self.args.in_mol_field):
            self.failure.emit(record)
            return

        mol = record.get_value(self.args.in_mol_field)
        self.mmff.PrepMol(mol)
        xyz = OEDoubleArray(3*mol.GetMaxAtomIdx())
        for conf in mol.GetConfs():
            conf.GetCoords(xyz)
            energy = self.opt(self.mmff, xyz, xyz)
            conf.SetCoords(xyz)
            conf_record = record.get_conf_record(conf)
            conf_record.set_value(self.args.efield, energy)
            record.set_conf_record(conf, conf_record)

        record.set_value(self.args.out_mol_field, mol)
        self.success.emit(record)

See also

RecordPortsMixin

IntegerFieldParameter

DecimalFieldParameter

Updating Records in Dataset Fields

The properties calculated by a cube for an input molecule can be either written as a new dataset with DatasetWriterCube or used to update Fields in the originating dataset.

The following example shows how to subclass BaseDatasetFieldAddOrReplaceCube and set it to update selected Fields in the Orion dataset.

Note

Currently only following Field types can be updated: datarecord.Types.Bool, datarecord.Types.Int, :py:class:datarecord.Types.Float, datarecord.Types.String, datarecord.Types.Chem.Mol

Note

BaseDatasetFieldAddOrReplaceCube currently does not prevent inconsistent metadata between updated record and a Field.

from floe.api import WorkFloe
from openeye.oechem import OECalculateMolecularWeight

from orionplatform.mixins import RecordPortsMixin
from orionplatform.parameters import (
    IntegerFieldParameter,
    DecimalFieldParameter,
    PrimaryMolFieldParameter,
)
from orionplatform.cubes import (
    DatasetReaderCube,
    BaseDatasetFieldAddOrReplaceCube,
    ComputeCube,
)


class MultiPropertyCube(RecordPortsMixin, ComputeCube):

    in_mol_field = PrimaryMolFieldParameter("in_mol_field")

    atomsfield = IntegerFieldParameter(
        "atomsfield", default="Num Atoms", title="Num Atoms Field"
    )
    bondsfield = IntegerFieldParameter(
        "bondsfield", default="Num Bonds", title="Num Bonds Field"
    )
    mwfield = DecimalFieldParameter(
        "mwfield", default="Molecular Wt.", title="Molecular Wt. Field"
    )

    def process(self, record, port):
        if not record.has_value(self.args.in_mol_field):
            self.failure.emit(record)
            return
        mol = record.get_value(self.args.in_mol_field)
        record.set_value(self.args.mwfield, OECalculateMolecularWeight(mol))
        record.set_value(self.args.atomsfield, mol.NumAtoms())
        record.set_value(self.args.bondsfield, mol.NumBonds())
        self.success.emit(record)


# Subclass BaseDatasetFieldAddOrReplaceCube and set which Fields it can update by defining FieldParameters.
# The selection of the Fields that will be updated by the Cube is set by promoting `field_to_update` parameter.
class DatasetFieldUpdaterCube(BaseDatasetFieldAddOrReplaceCube):
    atomsfield = IntegerFieldParameter(
        "atomsfield", default="Num Atoms", title="Num Atoms Field"
    )
    bondsfield = IntegerFieldParameter(
        "bondsfield", default="Num Bonds", title="Num Bonds Field"
    )
    mwfield = DecimalFieldParameter(
        "mwfield", default="Molecular Wt.", title="Molecular Wt. Field"
    )


floe = WorkFloe("Dataset Field Updater")

ifs = DatasetReaderCube("ifs")
calculator = MultiPropertyCube("calculator")
field_updater = DatasetFieldUpdaterCube("field_updater")
floe.add_cubes(ifs, calculator, field_updater)

ifs.modify_parameter(
    ifs.data_in, promoted_name="in", title="Input Dataset of molecules"
)
calculator.modify_parameter(
    calculator.atomsfield,
    promoted=True,
    promoted_name="n_atoms",
    default="Num Atoms Field",
)
calculator.modify_parameter(
    calculator.mwfield,
    promoted=True,
    promoted_name="weight",
    default="Molecular Wt.",
)
field_updater.modify_parameter(
    field_updater.atomsfield,
    promoted=True,
    promoted_name="n_atoms",
    default="Num Atoms Field",
)
field_updater.modify_parameter(
    field_updater.mwfield,
    promoted=True,
    promoted_name="weight",
    default="Molecular Wt.",
)
# Toggle which fields will be updated by setting *default* value of 'fields_to_update' parameter
field_updater.modify_parameter(
    field_updater.fields_to_update,
    promoted=True,
    choices=["n_atoms", "weight", "bondsfield"],
    default=["n_atoms", "weight"],
)

# Connect Ports
ifs.success.connect(calculator.intake)
calculator.success.connect(field_updater.intake)

if __name__ == "__main__":
    floe.run()