Enumeration

DELi includes support for enumerating DNA-encoded libraries (DELs). All it requires is that your libraries files have the necessary reaction information and that your building block sets have a SMILES column. See the defining DELs docs for more info on how to define these files.

Note

Enumeration does not require any DNA tag information, thus information about the DNA tags and schema are not required.

The enumerator performs the following:

  1. Extracts reaction information from the library files

  2. Builds a reaction workflow (using RDKit)

  3. Runs the reaction for every combination of building blocks for all cycles

How Enumeration Works

DELi treats enumeration as a directed acyclic graph (DAG) of reactions. Each node in the graph is a reaction step, and each edge is a possible next reaction. This allows for complex reaction schemes to be represented, including branching paths and multiple routes through the synthesis process. For example, when adding cycle 2, some building blocks might undergo a esterification reaction, while others might undergo an amide coupling. DELi allows you to specify these different routes by provide two different steps, one for the esterification and one for the amide coupling, and then specifying which building blocks (using building block subsets) go through which step.

There is also support for reaction priority, where you can specify a preferred priority of reactions to try for any given reaction step. Say you want to try and find the primary amine first, then a secondary amine if there is no primary. DELi’s enumeration module can handle this we well. See the reaction docs for more info on how to configure this in a file.

Warning

As you can imagine, enumeration can be extremely memory/compute intensive for large libraries. Careful consideration should be taken about when and how to enumerate a library.

Enumeration Python API

The top-level object handling enumeration is the enumerator object. It is possible to build these objects manually, but the easiest way is to load a Library object from a library JSON file and then call the enumerate method on it or extract the enumerator object using the get_enumerator method. However, below is an example of how to build on yourself.

First you need to create/load the building block sets that will be enumerated.

from deli.dels.building_block_set import BuildingBlockSet
bb_set1 = BuildingBlockSet.load("path/to/bb_set1.csv")
bb_set2 = BuildingBlockSet.load("path/to/bb_set2.csv")
bb_set3 = BuildingBlockSet.load("path/to/bb_set3.csv")

for bb_set in [bb_set1, bb_set2, bb_set3]:
    assert bb_set.has_smiles()  # Ensure SMILES are present in the building block sets

Then you need to create the ReactionTree object that defines the reaction scheme. This is done by first listing out all the reaction steps.

from deli.enumeration.reaction import ReactionStep, Reaction, ReactionTree, BBSetReactant, ProductReactant, StaticReactant
step1 = ReactionStep(
    step_name="Cycle 1",
    step_id="1",
    reaction=Reaction("[C:1](=O)[OH].[NH2:2]>>[C:1](=O)[N:2]")
    reactants= [BBSetReactant(bb_set_reactant_id="bb_set1"), StaticReactant(smiles="[13C]N")],
)
step2 = ReactionStep(
    step_name="Cycle 2",
    step_id="2",
    reaction=Reaction("[C:1](=O)[OH].[NH2:2]>>[C:1](=O)[N:2]")
    reactants= [ProductReactant("1"), BBSetReactant(bb_set_reactant_id="bb_set3")],
)
step3 = ReactionStep(
    step_name="Cycle 3",
    step_id="3",
    reaction=Reaction("[C:1](=O)[OH].[NH2:2]>>[C:1](=O)[N:2]")
    reactants= [ProductReactant("2"), BBSetReactant(bb_set_reactant_id="bb_set3")],
)

reaction_tree = ReactionTree(steps=[step1, step2, step3])

Now we can define the enumerator object itself.

from deli.enumeration.enumerator import DELEnumerator
enumerator = DELEnumerator(
    reaction_tree=reaction_tree,
    building_block_sets=[bb_set1, bb_set2, bb_set3]
)

Enumeration with DELi

DELi provides enumeration functionality through the Library class. Objects of this class can be enumerated using the enumerate method:

from deli.dels.library import Library
library = Library.load("path/to/library.json")
for compound in library.enumerate():
    type(compound)  # An `EnumeratedDELCompound` object
    del_id = compound.compound_id  # Access DEL ID of enumerated compound
    smi = compound.smi  # Access SMILES of enumerated compound
    mol = compound.mol  # Access RDKit mol object

By default, the enumerate method will not fail if a reaction cannot be carried out for a set of blocks. Instead it will return a DELCompound object rather than an EnumeratedDELCompound object. This means the DEL will lack a smi and mol attribute. If you want the method to fail instead, you can just set the fail_on_error argument to True:

for compound in library.enumerate(fail_on_error=True):
    # If a reaction fails, an EnumerationRunError will be raised
    pass

Note that DELi will tell you which library and which set of building blocks caused the reaction to fail if you want to try and figure out the issue. If you just want a list of pure EnumeratedDELCompound``s You can also just drop failed compounds by setting ``drop_failed=True:

for compound in library.enumerate(drop_failed=True):
    # Only `EnumeratedDELCompound` objects will be returned
    assert type(compound) is EnumeratedDELCompound
    smi = compound.smi
    mol = compound.mol

fail_on_error and drop_failed are mutually exclusive, so only one of them can be True or DELi will throw an exception.

You can extract the Enumerator object from the Library object, but doing so detaches it from the info about the library. This means it cannot create DELCompound objects with the proper library information and DEL IDs. Instead it just tells you which building blocks were used and generated product

enumerator = library.get_enumerator()
for result in enumerator.enumerate():
    bbs = result[0] # List of building block used to make this compound
    mol = result[1]  # RDKit mol object of the enumerated compound

Enumerating to a file

More often than not, you will want to save the result of enumeration to a file. While you can just generate all the enumerated compounds in memory and then write them to a file, that can be very memory intensive. Instead you can use the enumerate_to_csv_file method:

library.enumerate_to_csv_file(
    out_path="enumerated_library.csv",
    separator=","
    dropped_failed: bool = False,
    fail_on_error: bool = False,
    use_tqdm: bool = True,
)

This will write the results of enumeration directly to a CSV file on the fly to keep the memory usage low. The CSV file will have the following columns:

  • DEL_ID: the DEL id of the compound

  • SMILES: the SMILES of the compound

  • LIB_ID: the id of the library the compound belongs to

  • for each building block set in the library :
    • BB<cycle#>_ID: the ids of the building blocks used to build the compound

    • BB<cycle#>_SMILES: the smiles of the building block sets used to build the compound

Single Compound Enumeration

You can also enumerate a single compound by providing the building block object or the IDs of the building blocks to use:

bb_cycle1 = library.bb_sets[0].get_bb_by_id("BB_1")
bb_cycle2 = library.bb_sets[1].get_bb_by_id("BB_234")
bb_cycle3 = library.bb_sets[2].get_bb_by_id("BB_624")

compound_a = library.enumerate_by_bbs([bb_cycle1, bb_cycle2, bb_cycle3])
compound_b = library.enumerate_by_bb_ids(["BB_1", "BB_234", "BB_624"])

assert compound_a.smi == compound_b.smi  # will be True

This can be useful if you only want a handful or specific subset of the DEL to be enumerated.

Command-Line Interface

DELi’s CLI includes a command for enumerating DELs from a library file: deli enumerate. All you need to do is provide the path to a library JSON file. See the CLI docs for more info.

Loading Enumerators from Files

You can load a DELEnumerator object directly from a JSON file using the DELEnumerator.load method. The JSON file must contain the reaction tree and building block set definitions. See the defining enumerators docs for more info on how to define these files.