<jupyter_start><jupyter_text>Predicting reaction performance in C–N cross-coupling using machine learning DOI: 10.1126/science.aar5169 <NAME>.; <NAME>.; <NAME>.; <NAME>.; <NAME>. *Science*, **2018**, *360*, 186-190. Import schema and helper functions<jupyter_code>import ord_schema
from datetime import datetime
from ord_schema.proto import reaction_pb2
from ord_schema.units import UnitResolver
from ord_schema import validations
from ord_schema import message_helpers

unit_resolver = UnitResolver()<jupyter_output><empty_output><jupyter_text>Define a single reactionSingle reaction from the SI to be used as a template for the remaining entries.Start by writing a helper function for defining stock solutions.<jupyter_code># TODO(ccoley) Replace use of this helper class with the message_helpers.set_solute_moles
class stock_solution:
    """Helper class for defining stock solutions."""
    
    def __init__(self, reaction, stock_name):
        self.stock = reaction.inputs[stock_name]
        self.concentration = 0.0
        self.moles = 0.0
        self.volume = 0.0

    def add_solute(self, role, name, SMILES=None, is_limiting=False, preparation='NONE',
                   moles=0.0, volume_liters=0.0):
        """Add solute to solution. Keep track of moles of solute and total volume."""
        
        # Solution volume is sum of solute and solvent volumes
        self.moles += float(moles)
        self.volume += float(volume_liters)
        
        # Add solute and ID
        self.solute = self.stock.components.add()
        self.solute.reaction_role = reaction_pb2.ReactionRole.__dict__[role]
        self.solute.identifiers.add(value=name, type='NAME')
        if SMILES != None:
            self.solute.identifiers.add(value=SMILES, type='SMILES')
               
        # Other details
        self.solute.preparations.add().type = reaction_pb2.CompoundPreparation.PreparationType.Value(preparation)
        self.solute.is_limiting = is_limiting
        
    def add_solvent(self, name,  SMILES=None, preparation='NONE', volume_liters=0.0):
        """Add solvent to solution. Keep track of total volume."""
        
        # Solution volume is sum of solute and solvent volumes
        self.volume += float(volume_liters)
        
        # Add solute and ID
        self.solvent = self.stock.components.add()
        self.solvent.reaction_role = reaction_pb2.ReactionRole.SOLVENT
        self.solvent.identifiers.add(value=name, type='NAME')
        if SMILES != None:
            self.solvent.identifiers.add(value=SMILES, type='SMILES')
        
        # Other details
        self.solvent.preparations.add().type = reaction_pb2.CompoundPreparation.PreparationType.Value(preparation)
        
    def mix(self, concentration_molar=0):
        """Mix function resolves moles and volume from availible information (concentration, moles, volume)"""
        
        self.concentration = concentration_molar
        
        # Resolve concentration
        if self.moles > 0 and self.volume > 0:
            self.solute.amount.moles.CopyFrom(unit_resolver.resolve(f'{self.moles*(10**6):16f} umol'))
            self.solvent.amount.volume.CopyFrom(unit_resolver.resolve(f'{self.volume*(10**6):16f} uL'))
        elif self.concentration > 0 and self.volume > 0:
            self.moles = self.concentration * self.volume
            self.solute.amount.moles.CopyFrom(unit_resolver.resolve(f'{self.moles*(10**6):16f} umol'))
            self.solvent.amount.volume.CopyFrom(unit_resolver.resolve(f'{self.volume*(10**6):16f} uL'))<jupyter_output><empty_output><jupyter_text>**Define reaction inputs**:- Catalyst in DMSO (0.05 M)- Electrophile in DMSO (0.50 M)- Nucleophile in DMSO (0.50 M)- Additive in DMSO (0.50 M)- Base in DMSO (0.75 M)- The SI does not indicate an order of addition<jupyter_code># Define Reaction
reaction = reaction_pb2.Reaction()
reaction.identifiers.add(value=r'Buchwald-H<NAME>', type='NAME')

# Catalyst stock solution
catalyst = stock_solution(reaction, r'Pd precatalyst in DMSO')
catalyst.add_solute('CATALYST', r'XPhos', SMILES=r'CC(C)C1=CC(C(C)C)=CC(C(C)C)=C1C2=C(P(C3CCCCC3)C4CCCCC4)C=CC=C2')
catalyst.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=200e-9)
catalyst.mix(concentration_molar=0.05)

# Electrophile stock solution
electrophile = stock_solution(reaction, r'Aryl halide in DMSO')
electrophile.add_solute('REACTANT', r'4-trifuloromethyl chlorobenzene', SMILES=r'ClC1=CC=C(C(F)(F)F)C=C1', is_limiting=True)
electrophile.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=200e-9)
electrophile.mix(concentration_molar=0.50)

# Nucleophile stock solution
nucleophile = stock_solution(reaction, r'Amine in DMSO')
nucleophile.add_solute('REACTANT', r'p-toluidine', SMILES=r'NC1=CC=C(C)C=C1')
nucleophile.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=200e-9)
nucleophile.mix(concentration_molar=0.50)

# Additive stock solution
additive = stock_solution(reaction, r'Additive in DMSO')
additive.add_solute('REAGENT', r'5-phenylisoxazole', SMILES=r'o1nccc1c2ccccc2')
additive.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=200e-9)
additive.mix(concentration_molar=0.50)

# Base stock solution 
base = stock_solution(reaction, r'Base in DMSO')
base.add_solute('REAGENT', r'P2Et', SMILES=r'CN(C)P(N(C)C)(N(C)C)=NP(N(C)C)(N(C)C)=NCC')
base.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=200e-9)
base.mix(concentration_molar=0.75)<jupyter_output><empty_output><jupyter_text>Define reaction setup & conditions<jupyter_code># Reactions performed in 1556 well plate
reaction.setup.vessel.CopyFrom(
    reaction_pb2.Vessel(
        type='WELL_PLATE', 
        material=dict(type='PLASTIC', details='polypropylene'),
        volume=unit_resolver.resolve('12.5 uL')
    )
)
reaction.setup.is_automated = True
reaction.setup.environment.type = reaction.setup.environment.GLOVE_BOX
# Heated - not specified how
t_conds = reaction.conditions.temperature
t_conds.setpoint.CopyFrom(reaction_pb2.Temperature(units='CELSIUS', value=60))
# Glove box work
p_conds = reaction.conditions.pressure
p_conds.control.type = p_conds.PressureControl.SEALED
p_conds.atmosphere.type = p_conds.Atmosphere.NITROGEN 
p_conds.atmosphere.details = 'dry nitrogen'
# No safety notes
reaction.notes.safety_notes = ''<jupyter_output><empty_output><jupyter_text>After 16 h, the plate was opened and the Mosquito was used to add internal standard to each well (3 µL of 0.0025 M di-tert-butylbiphenyl solution in DMSO). At that point, aliquots were sampled into 384-well plates and analyzed by UPLC.<jupyter_code># Standard stock solution 
standard = stock_solution(reaction, r'External standard in DMSO')
standard.add_solute('INTERNAL_STANDARD', r'4,4\'-di-tert-butyl-1,1\'-biphenyl', SMILES=r'CC(C)(C)C1=CC=C(C2=CC=C(C(C)(C)C)C=C2)C=C1')
standard.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=3e-6)
standard.mix(concentration_molar=0.0025)
outcome = reaction.outcomes.add()
outcome.reaction_time.CopyFrom(unit_resolver.resolve('16 hrs'))

# Analyses: UPLC
# Note using LCMS because UPLC is not an option
outcome.analyses['UPLC analysis'].type = reaction_pb2.Analysis.LCMS
outcome.analyses['UPLC analysis'].details = ('UPLC using 3 µL of 0.0025 M di-tert-butylbiphenyl solution in DMSO external standard')
outcome.analyses['UPLC analysis'].instrument_manufacturer = 'Waters Acquity'

# Define product identity
prod_2a = outcome.products.add() 
prod_2a.identifiers.add(value=r'FC(C1=CC=C(NC2=CC=C(C)C=C2)C=C1)(F)F', type='SMILES')
prod_2a.is_desired_product = True
prod_2a.reaction_role = reaction_pb2.ReactionRole.PRODUCT

# The UPLC analysis was used to confirm both identity and yield
prod_2a.measurements.add(type='IDENTITY', analysis_key='UPLC analysis')
prod_2a.measurements.add(type='YIELD', analysis_key='UPLC analysis', percentage=dict(value=10.65781182),
                             uses_internal_standard=True)

# Reaction provenance
reaction.provenance.city = r'Kenilworth, NJ'
reaction.provenance.doi = r'10.1126/science.aar5169'
reaction.provenance.publication_url = r'https://science.sciencemag.org/content/360/6385/186'
reaction.provenance.record_created.time.value = datetime.now().strftime("%m/%d/%Y, %H:%M:%S")
reaction.provenance.record_created.person.CopyFrom(reaction_pb2.Person(
    name='<NAME>', organization='Princeton University', email='<EMAIL>'))<jupyter_output><empty_output><jupyter_text>Validate and examine this final prototypical reaction entry<jupyter_code>outcome.products
validations.validate_message(reaction)
reaction<jupyter_output><empty_output><jupyter_text>Full HTE Data Set<jupyter_code># Get full set of reactions: I preprocessed this to have SMILES for each component.
# Note I am only including the data that was used for modeling - there are some
#     controls and failed reactions in the SI (if we even want them?).

import pandas as pd
import os

if not os.path.isfile('experiment_index.csv'):
    !wget https://github.com/Open-Reaction-Database/ord-schema/raw/main/examples/9_Ahneman_Science_CN_Coupling/experiment_index.csv

index = pd.read_csv('experiment_index.csv')
index
# I happened to have ID tables around so we can give the components names

def match_name(column, list_path):
    """Match names from csv files to SMILES."""

    if not os.path.isfile(list_path):
        !wget https://github.com/Open-Reaction-Database/ord-schema/raw/main/examples/9_Ahneman_Science_CN_Coupling/{list_path}
    
    component_list = pd.read_csv(list_path)
    
    # Get SMILES column
    for col in component_list.columns.values:
        if 'SMILES' in col:
            smi_col = col
            
    # Get name column
    names = index[column].copy()
    for i in range(len(component_list)):
        names = names.replace(component_list[smi_col][i], component_list['name'][i])
    
    return names.values

index['Aryl_halide_name'] = match_name('Aryl_halide_SMILES', 'aryl_halide-list.csv')
index['Additive_name'] = match_name('Additive_SMILES', 'additive-list.csv')
index['Base_name'] = match_name('Base_SMILES', 'base-list.csv')
index['Ligand_name'] = match_name('Ligand_SMILES', 'ligand-list.csv')

index.head()
# Products aren't listed - Use rdkit to get them

from rdkit import Chem
from rdkit.Chem import AllChem

def amination(aryl_halide):
    """Get product based on aryl halide identity."""
    
    replace_with = Chem.MolFromSmiles('NC1=CC=C(C)C=C1')
    pattern = Chem.MolFromSmarts('[Cl,Br,I]')
    molecule = Chem.MolFromSmiles(aryl_halide)
    product = AllChem.ReplaceSubstructs(molecule, pattern, replace_with)
    
    return Chem.MolToSmiles(product[0])
    
index['Product_SMILES'] = [amination(aryl_halide) for aryl_halide in index['Aryl_halide_SMILES'].tolist()]

index.head()
# Reorder the dataframe
index = index[['Ligand_SMILES', 'Ligand_name',
               'Aryl_halide_SMILES', 'Aryl_halide_name',
               'Additive_SMILES', 'Additive_name',
               'Base_SMILES', 'Base_name',
               'Product_SMILES', 'yield']]
# Gonna time execution

import time

class timer:
    """
    Returns wall clock-time
    """
    
    def __init__(self, name):
        
        self.start = time.time()
        self.name = name
        
    def stop(self):
        self.end = time.time()    
        print(self.name + ': ' + str(self.end - self.start) + ' s')<jupyter_output><empty_output><jupyter_text>The only aspects of reaction data that vary are: (1) ligand, (2) electrophile, (3) additive, and (4) base.<jupyter_code>t = timer('3955 Entries')

reactions = []
for lig_s, lig_n, elec_s, elec_n, add_s, add_n, base_s, base_n, prod, y in index.values:
    
    # Define Reaction
    reaction = reaction_pb2.Reaction()
    reaction.identifiers.add(value=r'Buchwald-Hartwig Amination', type='NAME')

    # Catalyst stock solution
    catalyst = stock_solution(reaction, r'Pd precatalyst in DMSO')
    catalyst.add_solute('CATALYST', lig_n, SMILES=lig_s)
    catalyst.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=200e-9)
    catalyst.mix(concentration_molar=0.05)

    # Electrophile stock solution
    electrophile = stock_solution(reaction, r'Aryl halide in DMSO')
    electrophile.add_solute('REACTANT', elec_n, SMILES=elec_s, is_limiting=True)
    electrophile.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=200e-9)
    electrophile.mix(concentration_molar=0.50)

    # Nucleophile stock solution
    nucleophile = stock_solution(reaction, r'Amine in DMSO')
    nucleophile.add_solute('REACTANT', r'p-toluidine', SMILES=r'NC1=CC=C(C)C=C1')
    nucleophile.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=200e-9)
    nucleophile.mix(concentration_molar=0.50)

    # Additive stock solution
    additive = stock_solution(reaction, r'Additive in DMSO')
    additive.add_solute('REAGENT', add_n, SMILES=add_s)
    additive.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=200e-9)
    additive.mix(concentration_molar=0.50)

    # Base stock solution 
    base = stock_solution(reaction, r'Base in DMSO')
    base.add_solute('REAGENT', base_n, SMILES=base_s)
    base.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=200e-9)
    base.mix(concentration_molar=0.75)

    # Reactions performed in 1556 well plate
    reaction.setup.vessel.CopyFrom(
        reaction_pb2.Vessel(
            type='WELL_PLATE', 
            material=dict(type='PLASTIC'),
            volume=unit_resolver.resolve('12.5 uL')
        )
    )
    reaction.setup.is_automated = True
    reaction.setup.environment.type = reaction_pb2.ReactionSetup.ReactionEnvironment.GLOVE_BOX

    # Heated - not specified how
    t_conds = reaction.conditions.temperature
    t_conds.setpoint.CopyFrom(reaction_pb2.Temperature(units='CELSIUS', value=60))

    # Glove box work
    p_conds = reaction.conditions.pressure
    p_conds.control.type = p_conds.PressureControl.SEALED
    p_conds.atmosphere.type = p_conds.Atmosphere.NITROGEN 
    p_conds.atmosphere.details = 'dry nitrogen'

    # Notes
    reaction.notes.safety_notes = ''

    # TODO(ccoley) Stock solutions can be defined without using this custom function
    # Standard stock solution 
    standard = stock_solution(reaction, r'External standard in DMSO')
    standard.add_solute('INTERNAL_STANDARD', r'4,4\'-di-tert-butyl-1,1\'-biphenyl', SMILES=r'CC(C)(C)C1=CC=C(C2=CC=C(C(C)(C)C)C=C2)C=C1')
    standard.add_solvent(r'DMSO', SMILES=r'O=S(C)C', volume_liters=3e-6)
    standard.mix(concentration_molar=0.0025)

    outcome = reaction.outcomes.add()
    outcome.reaction_time.CopyFrom(unit_resolver.resolve('16 hrs'))
    
    # Analyses: UPLC/MS
    outcome.analyses['UPLC analysis'].type = reaction_pb2.Analysis.LCMS
    outcome.analyses['UPLC analysis'].details = ('UPLC using 3 µL of 0.0025 M di-tert-butylbiphenyl solution in DMSO external standard')
    outcome.analyses['UPLC analysis'].instrument_manufacturer = 'Waters Acquity'

        # Define product identity
    prod_2a = outcome.products.add() 
    prod_2a.identifiers.add(value=r'FC(C1=CC=C(NC2=CC=C(C)C=C2)C=C1)(F)F', type='SMILES')
    prod_2a.is_desired_product = True
    prod_2a.reaction_role = reaction_pb2.ReactionRole.PRODUCT

    # The UPLC analysis was used to confirm both identity and yield
    prod_2a.measurements.add(type='IDENTITY', analysis_key='UPLC analysis')
    prod_2a.measurements.add(type='YIELD', analysis_key='UPLC analysis', percentage=dict(value=y),
                             uses_internal_standard=True)

    # Reaction provenance
    reaction.provenance.city = r'Kenilworth, NJ'
    reaction.provenance.doi = r'10.1126/science.aar5169'
    reaction.provenance.publication_url = r'https://science.sciencemag.org/content/360/6385/186'
    reaction.provenance.record_created.time.value = datetime.now().strftime("%m/%d/%Y, %H:%M:%S")
    reaction.provenance.record_created.person.CopyFrom(reaction_pb2.Person(
        name='<NAME>', organization='Princeton University', email='<EMAIL>')
    )
    
    # Validate
    output = validations.validate_message(reaction)
    for error in output.errors:
        print(error)
    
    # Append
    reactions.append(reaction)

t.stop()
print(f'Generated {len(reactions)} reactions')
# Inspect random reaction from this set
reactions[15]<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12410471721412694, 'fraction_numerical': 0.02123981229933317, 'mean_word_length': 4.409819639278557, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 47, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '16529', 'license': 'Apache-2.0', 'n_tokens_mistral': 6096, 'n_tokens_neox': 5636, 'n_words': 1054, 'path': 'examples/submissions/9_Ahneman_Science_CN_Coupling/example_Ahenman.ipynb', 'repo_name': 'marcosfelt/ord-schema', 'size': '16279'}