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
# Note: oechem must be imported before OpenEye toolkits
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)
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 DecimalFieldParameter, IntegerFieldParameter
# Note: oechem must be imported before OpenEye toolkits
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)
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
# Note: oechem must be imported before OpenEye toolkits
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)
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 openeye.quacpac import OEAssignCharges, OEMMFF94Charges
from floe.api import ComputeCube
from orionplatform.mixins import RecordPOrtsMixin
from orionplatform.parameters import (
DecimalFieldParameter,
IntegerFieldParameter,
PrimaryMolFieldParameter,
)
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)
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 openeye.mmff import OEMMFF
from openeye.opt import OEBFGSOpt
from floe.api import ComputeCube
from orionplatform.mixins import RecordPortsMixin
from orionplatform.parameters import DecimalFieldParameter
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)
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 orionplatform.mixins import RecordPortsMixin
from orionplatform.cubes import (
ComputeCube,
DatasetReaderCube,
BaseDatasetFieldAddOrReplaceCube,
)
from orionplatform.parameters import (
DecimalFieldParameter,
IntegerFieldParameter,
PrimaryMolFieldParameter,
)
# Note: oechem must be imported before OpenEye toolkits
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)
# 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()