Esempio n. 1
0
class App(appcli.App):
    __config__ = [
        appcli.DocoptConfig(),
    ]
    layout_toml = appcli.param('<toml>', cast=Path)
    output = appcli.param('--output', default=None)

    @classmethod
    def main(cls):
        self = cls.from_params()
        appcli.load(self)

        if not self.output:
            if os.fork() != 0:
                sys.exit()

        df, extras = self.load()
        fig = self.plot(df, extras)

        if self.output:
            out = self.output.replace('$', self.layout_toml.stem)
            plt.savefig(out)
            plt.close()
        else:
            plt.show()
Esempio n. 2
0
class Assembly(Main):
    __config__ = [
        DocoptConfig,
        MakerConfig,
    ]

    Fragment = Fragment
    target_pmol_per_frag = 0.06
    min_pmol_per_frag = 0.02

    assemblies = appcli.param(Key(DocoptConfig, parse_assemblies_from_docopt),
                              Key(MakerConfig,
                                  parse_assemblies_from_freezerbox),
                              get=bind_assemblies)
    volume_uL = appcli.param(
        Key(DocoptConfig, '--volume', cast=float),
        Key(MakerConfig, 'volume', cast=parse_volume_uL),
        default=5,
    )
    excess_insert = appcli.param(
        Key(DocoptConfig, '--excess-insert', cast=float),
        default=2,
    )

    group_by = {
        'volume_uL': group_by_identity,
        'excess_insert': group_by_identity,
    }
    merge_by = {
        'assemblies': join_lists,
    }

    def __init__(self, assemblies):
        self.assemblies = assemblies

    def get_num_fragments(self):
        return max(len(x) for x in self.assemblies)

    def get_product_conc(self):
        min_pmol = min(
            x.pmol for x in self.reaction.iter_reagents_by_flag('fragment'))
        return Quantity(1e3 * min_pmol / self.volume_uL, 'nM')

    def get_dependencies(self):
        return {frag.name for assembly in self.assemblies for frag in assembly}

    def _add_fragments_to_reaction(self, rxn, order=-1):
        rxn.hold_ratios.volume = self.volume_uL, 'µL'
        rxn.extra_min_volume = '0.5 µL'
        add_fragments_to_reaction(
            rxn,
            self.assemblies,
            target_pmol=self.target_pmol_per_frag,
            min_pmol=self.min_pmol_per_frag,
            excess_insert=self.excess_insert,
            order=order,
        )
        return rxn
Esempio n. 3
0
class Aliquot(Cleanup):
    """\
Make aliquots

Usage:
    aliquot <volume> [<conc>]

Arguments:
    <volume>
        The volume of each individual aliquot.  No unit is implied, so you 
        must specify one.

    <conc>
        The concentration of the aliquots, if this is not made clear in 
        previous steps.  No unit is implied, so you must specify one.
"""
    __config__ = [
        DocoptConfig,
        MakerConfig,
    ]
    volume = appcli.param(
        Key(DocoptConfig, '<volume>'),
        Key(MakerConfig, 'volume'),
    )
    conc = appcli.param(
        Key(DocoptConfig, '<conc>'),
        Key(MakerConfig, 'conc'),
        default=None,
    )

    group_by = {
        'volume': group_by_identity,
        'conc': group_by_identity,
    }

    def __init__(self, volume, conc=None, product_tags=None):
        self.volume = volume
        if conc: self.conc = conc
        if product_tags: self.product_tags = product_tags

    def get_protocol(self):
        Q = Quantity.from_string

        if self.conc:
            aliquot_info = f'{Q(self.volume)}, {Q(self.conc)}'
        else:
            aliquot_info = f'{Q(self.volume)}'

        if self.product_tags and self.show_product_tags:
            product_tags = f" of: {', '.join(self.product_tags)}"
        else:
            product_tags = "."

        return stepwise.Protocol(
            steps=[f"Make {aliquot_info} aliquots{product_tags}"], )

    def get_product_conc(self):
        return Quantity.from_string(self.conc)
Esempio n. 4
0
class Kld(Main):
    """\
Circularize a linear DNA molecule using T4 DNA ligase, e.g. to reform a plasmid 
after inverse PCR.

Usage:
    kld <dna> [-n <int>]

Arguments:
    <dna>
        The name of the DNA molecule to circularize.

Options:
    -n --num-reactions <int>        [default: ${app.num_reactions}]
        The number of reactions to set up.
"""
    __config__ = [
        appcli.DocoptConfig,
    ]

    dna = appcli.param('<dna>')
    num_reactions = appcli.param(
        '--num-reactions',
        cast=lambda x: int(eval(x)),
        default=1,
    )

    def __init__(self, dna):
        self.dna = dna

    def get_reaction(self):
        kld = stepwise.MasterMix.from_text('''\
        Reagent               Stock        Volume  Master Mix
        ================  =========   ===========  ==========
        water                         to 10.00 μL         yes
        T4 ligase buffer        10x       1.00 μL         yes
        T4 PNK              10 U/μL       0.25 μL         yes
        T4 DNA ligase      400 U/μL       0.25 μL         yes
        DpnI                20 U/μL       0.25 μL         yes
        DNA                50 ng/μL       1.50 μL
        ''')

        kld.num_reactions = self.num_reactions
        kld.extra_percent = 15
        kld['DNA'].name = self.dna

        return kld

    def get_protocol(self):
        p = stepwise.Protocol()
        p += pl(
            f"Setup {plural(self.num_reactions):# ligation reaction/s}:",
            self.reaction,
        )
        p += "Incubate at room temperature for 1h."
        return p
Esempio n. 5
0
    class Template(ShareConfigs, Argument):
        __config__ = [ReagentConfig]

        seq = appcli.param(Key(ReagentConfig, 'seq'), )
        length = appcli.param(
            Key(ReagentConfig, 'length'),
            Method(lambda self: len(self.seq)),
        )
        stock_ng_uL = appcli.param(
            Key(DocoptConfig, '--template-stock', cast=float),
            Key(ReagentConfig, 'conc_ng_uL'),
            default=None,
        )
Esempio n. 6
0
    class Primer(ShareConfigs, Argument):
        __config__ = [ReagentConfig]

        seq = appcli.param(
                Key(ReagentConfig, 'seq'),
        )
        melting_temp_C = appcli.param(
                Key(ReagentConfig, 'melting_temp_C'),
        )
        stock_uM = appcli.param(
                Key(DocoptConfig, '--primer-stock', cast=float),
                Key(ReagentConfig, 'conc_uM'),
                Key(PresetConfig, 'primer_stock_uM'),
                Key(StepwiseConfig, 'primer_stock_uM'),
        )
Esempio n. 7
0
    class Template(ShareConfigs, Argument):
        __config__ = [
                ReagentConfig.setup(
                    db_getter=lambda self: self.db,
                ),
        ]

        stock_nM = appcli.param(
                Key(DocoptConfig, '--template-stock', cast=float),
                Key(ReagentConfig, 'conc_nM'),
                Key(PresetConfig, 'template_stock_nM'),
        )
        is_mrna = appcli.param(
                Key(DocoptConfig, '--mrna'),
                Key(ReagentConfig, 'molecule', cast=lambda x: x == 'RNA'),
        )
Esempio n. 8
0
class WashBarendt(appcli.App):
    """\
Remove unligated linker by ultrafiltration.

Usage:
    wash_barendt [-v <µL>]

Options:
    -v --volume <µL>        [default: ${app.volume_uL}]
        The volume to dilute the purified mRNA to.  Note that this must be 
        greater than 15 µL, since that is the dead volume of the spin filter.

    -m --mwco <kDa>         [default: ${app.mwco_kDa}]
        The MWCO of the spin filter.  According to Millipore Sigma, this should 
        be 2-3x smaller than the molecular weight of the ligated product: 
        https://tinyurl.com/4ffxu8zb
"""
    __config__ = [
            appcli.DocoptConfig(),
    ]

    volume_uL = appcli.param(
            '--volume',
            cast=float,
            default=15,
    )
    mwco_kDa = appcli.param(
            '--mwco',
            cast=int,
            default=100,
    )

    def get_protocol(self):
        p = stepwise.Protocol()
        p += pl(
                "Remove unligated linker by ultrafiltration:",
                s := ul(
                    "Bring reaction to 500 µL with 8M urea.",
                    f"Load onto a {self.mwco_kDa} kDa MWCO spin-filter [1].",
                    "Spin 14000g, 15 min.",
                    "Wash with 500 µL 8M urea.",
                    "Wash with 500 µL nuclease-free water.",
                    "Wash with water again.",
                    "Wash with water again, but spin for 30 min.",
                    "Invert the filter into a clean tube and spin 1000g, 2 min to collect ligated product in a volume of ≈15 µL.",
                ),
        )
Esempio n. 9
0
class Fragment:
    __config__ = [ReagentConfig]

    name = tag = appcli.param()
    conc = appcli.param(Key(ReagentConfig), )
    mw = appcli.param(
        Key(ReagentConfig),
        Method(lambda self: mw_from_length(self.length)),
        default=None,
    )
    length = appcli.param(Key(ReagentConfig), )

    def __init__(self, name=None, *, conc=None, length=None, mw=None):
        if name: self.name = name
        if conc: self.conc = conc
        if length: self.length = length
        if mw: self.mw = mw

    def __repr__(self):
        attrs = 'name', 'conc', 'length', 'mw'
        attr_strs = [
            f'{attr}={value!r}' for attr in attrs
            if (value := getattr(self, attr, None)) is not None
        ]
        return f'Fragment({", ".join(attr_strs)})'

    def __eq__(self, other):
        undef = object()
        attrs = 'name', 'conc', 'length'
        return all(
            getattr(self, attr, undef) == getattr(other, attr, undef)
            for attr in attrs)

    def bind(self, app, force=False):
        if not hasattr(self, 'app') or force:
            self.app = app

    def get_db(self):
        return self.app.db

    def get_conc_nM(self):
        return convert_conc_unit(self.conc, self.mw, 'nM').value

    def del_conc_nM(self):
        pass
Esempio n. 10
0
class List(StepwiseCommand):
    """\
List protocols known to stepwise.

Usage:
    stepwise ls [-d] [-p] [<protocol>]

Options:
    -d --dirs
        Show the directories that will be searched for protocols, rather than 
        the protocols themselves.

    -p --paths
        Don't organize paths by directory.
"""
    __config__ = [
        appcli.DocoptConfig,
    ]

    protocol = appcli.param('<protocol>', default=None)
    dirs_only = appcli.param('--dirs', default=False)
    organize_by_dir = appcli.param('--paths', cast=not_, default=True)

    def main(self):
        appcli.load(self)

        library = Library()
        entries = library.find_entries(self.protocol)
        indent = '  ' if self.organize_by_dir else ''

        for collection, entry_group in groupby(entries,
                                               lambda x: x.collection):
            if self.organize_by_dir:
                print(collection.name)
            if self.dirs_only:
                continue

            for entry in entry_group:
                print(indent + entry.name)

            if self.organize_by_dir:
                print()
Esempio n. 11
0
    class Template(ShareConfigs, Argument):
        __config__ = [ReagentConfig]

        seq = appcli.param(Key(ReagentConfig, 'seq'), )
        stock_ng_uL = appcli.param(
            Key(DocoptConfig, '--dna-stock'),
            Key(ReagentConfig, 'conc_ng_uL'),
            Key(StepwiseConfig, 'dna_stock_ng_uL'),
            cast=float,
        )
        is_circular = appcli.param(
            Key(ReagentConfig, 'is_circular'),
            default=True,
        )
        is_genomic = appcli.param(
            Key(DocoptConfig, '--genomic'),
            default=False,
        )
        target_size_bp = appcli.param(
            Key(MakerConfig, 'size', cast=parse_size_bp),
            default=None,
        )
class UvTransilluminator(Main):
    """\
Image a gel using a UV transilluminator.

Usage:
    uv_transilluminator [<wavelength>]
"""
    __config__ = [appcli.DocoptConfig]

    wavelength_nm = appcli.param('<wavelength>', default=300)

    def get_protocol(self):
        p = stepwise.Protocol()
        p += f"Image with a {self.wavelength_nm} nm UV transilluminator."
        return p
Esempio n. 13
0
class Cleanup(Main):

    product_tags = appcli.param(
            Method(lambda self: [x.tag for x in self.products]),
            default_factory=list,
    )

    def __bareinit__(self):
        super().__bareinit__()
        self.show_product_tags = False

    @classmethod
    def make(cls, db, products):
        makers = list(super().make(db, products))
        show_product_tags = (len(makers) != 1)

        for maker in makers:
            maker.show_product_tags = show_product_tags
            yield maker
Esempio n. 14
0
class Which(StepwiseCommand):
    """\
Show the full path to the specified protocol.

Usage:
    stepwise which <protocol>
"""
    __config__ = [
            appcli.DocoptConfig,
    ]
    protocol = appcli.param('<protocol>')

    def main(self):
        appcli.load(self)

        library = Library()
        entries = library.find_entries(self.protocol)

        for entry in entries:
            print(entry.path)
Esempio n. 15
0
class Edit(StepwiseCommand):
    """\
Edit the specified protocol using $EDITOR.

Usage:
    stepwise edit <protocol>
"""
    __config__ = [
            appcli.DocoptConfig,
    ]
    protocol = appcli.param('<protocol>')


    def main(self):
        appcli.load(self)

        library = Library()
        entry = library.find_entry(self.protocol)
        cmd = os.environ['EDITOR'], entry.path
        subp.run(cmd)
Esempio n. 16
0
class Anneal(Main):
    """\
Anneal two complementary oligos.

Usage:
    anneal <oligo_1> <oligo_2> [-n <num_rxns>] [-v <µL>] [options]

Arguments:
    <oligo_1> <oligo_2>
        The names of the two oligos to anneal.

Options:
    -n --num-rxns <num_rxns>            [default: ${app.num_reactions}]
        The number of reactions to set up.

    -v --volume <µL>                    [default: ${app.volume_uL}]
        The volume of each annealing reaction in µL.

    -c --oligo-conc <µM>
        The final concentration of each oligo in the reaction, in µM.  This 
        will also be the concentration of the annealed duplex, if the reaction 
        goes to completion.  The default is to use as much oligo as possible.

    -C --oligo-stock <µM[,µM]>          [default: ${app.oligo_stock_uM}]
        The stock concentrations of the oligos, in µM.  You can optionally use 
        a comma to specify different stock concentrations for the two oligos.

    -m --master-mix <reagents>          [default: ${','.join(app.master_mix)}]
        The reagents to include in the master mix.  The following reagents are 
        understood: '1' (the first oligo), '2' (the second oligo), or the name 
        of either oligo specified on the command line. To specify both 
        reagents, separate the two names with a comma.
"""
    __config__ = [
        appcli.DocoptConfig,
    ]
    oligo_1 = appcli.param('<oligo_1>')
    oligo_2 = appcli.param('<oligo_2>')
    num_reactions = appcli.param('--num-rxns', default=1, cast=int)
    volume_uL = appcli.param('--volume', default=4, cast=float)
    oligo_conc_uM = appcli.param('--oligo-conc', default=None, cast=float)
    oligo_stock_uM = appcli.param('--oligo-stock',
                                  default=100,
                                  cast=float_pair)
    master_mix = appcli.param('--master-mix', default={'1'}, cast=comma_set)

    def __init__(self, oligo_1, oligo_2):
        self.oligo_1 = oligo_1
        self.oligo_2 = oligo_2

    def get_reaction(self):
        rxn = stepwise.MasterMix.from_text("""\
            Reagent  Stock     Volume  MM?
            =======  =====  =========  ===
            water           to 4.0 µL  yes
            PBS      10x       0.4 µL  yes
        """)
        rxn.num_reactions = self.num_reactions
        rxn.hold_ratios.volume = self.volume_uL, 'µL'

        rxn['oligo1'].name = self.oligo_1
        rxn['oligo2'].name = self.oligo_2
        rxn['oligo1'].master_mix = bool({'1', self.oligo_1} & self.master_mix)
        rxn['oligo2'].master_mix = bool({'2', self.oligo_2} & self.master_mix)
        rxn['oligo1'].stock_conc = pair(self.oligo_stock_uM, 0), 'µM'
        rxn['oligo2'].stock_conc = pair(self.oligo_stock_uM, 1), 'µM'

        if self.oligo_conc_uM:
            rxn['oligo1'].hold_stock_conc.conc = self.oligo_conc_uM, 'µM'
            rxn['oligo2'].hold_stock_conc.conc = self.oligo_conc_uM, 'µM'

        else:
            V = rxn.get_free_volume_excluding('oligo1', 'oligo2')
            C1 = rxn['oligo1'].stock_conc
            C2 = rxn['oligo2'].stock_conc
            C12 = C1 + C2

            rxn['oligo1'].volume = V * (C2 / C12)
            rxn['oligo2'].volume = V * (C1 / C12)

        return rxn

    def get_protocol(self):
        protocol = stepwise.Protocol()
        n = self.num_reactions

        protocol += f"""\
Setup {plural(n):# annealing reaction/s}:

{self.reaction}
"""
        protocol += f"""\
Perform the {plural(n):annealing reaction/s}:

- Incubate at 95°C for 2 min.
- Cool at room temperature.
"""
        return protocol
Esempio n. 17
0
class InVitroTranslation(Main):
    """\
Express proteins from purified DNA templates.

Usage:
    ivtt <templates>... [-p <name>] [-v <µL>] [-n <rxns>] [-x <percent>] 
        [-c <nM>] [-C <nM>] [-mrIX] [-a <name;conc;vol;mm>]... [-t <time>]
        [-T <°C>]

Arguments:
    <templates>
        The templates to express.  The number of reactions will be inferred 
        from this list.

<%! from stepwise_mol_bio import hanging_indent %>\
Options:
    -p --preset <name>
        What default reaction parameters to use.  The following parameters are 
        currently available:

        ${hanging_indent(app.preset_briefs, 8*' ')}

    -v --volume <µL>
        The volume of the reaction in µL.  By default, the volume specified by 
        the reaction table in the chosen preset will be used.

    -n --num-reactions <int>
        The number of reactions to set up.  By default, this is inferred from
        the number of templates.

    -x --extra-percent <percent>
        How much extra master mix to prepare, as a percentage of the minimum 
        required master mix volume.

    -c --template-conc <nM>
        The desired final concentration of template in the reaction.  

    -C --template-stock <nM>
        The stock concentration of the template DNA or mRNA, in units of nM.  
        If not specified, a concentration will be queried from the PO₄ 
        database.  In this case, all templates must be in the database and must 
        have identical concentrations.

    -m --master-mix
        Include the template in the master mix.

    -r --mrna
        Use mRNA as the template instead of DNA.

    -X --no-template
        Don't include the template in the reaction, e.g. as a negative control.

    -I --no-inhibitor
        Don't include RNase inhibitor in the reaction.

    -a --additive <name;conc;vol;mm>
        Add an additional reagent to the reaction.  See `sw reaction -h` for a 
        complete description of the syntax.  This option can be specified 
        multiple times.

    -t --incubation-time <time>         [default: ${app.incubation_time}]
        The amount of time to incubate the reactions.  No unit is assumed, so 
        be sure to include one.  If '0', the incubation step will be removed 
        from the protocol (e.g. so it can be added back at a later point).

    -T --incubation-temperature <°C>    [default: ${app.incubation_temp_C}]
        The temperature to incubate the reactions at, in °C.
"""
    __config__ = [
            DocoptConfig,
            PresetConfig,
            StepwiseConfig.setup('molbio.ivtt'),
    ]
    preset_briefs = appcli.config_attr()
    preset_brief_template = '{kit}'

    class Template(ShareConfigs, Argument):
        __config__ = [
                ReagentConfig.setup(
                    db_getter=lambda self: self.db,
                ),
        ]

        stock_nM = appcli.param(
                Key(DocoptConfig, '--template-stock', cast=float),
                Key(ReagentConfig, 'conc_nM'),
                Key(PresetConfig, 'template_stock_nM'),
        )
        is_mrna = appcli.param(
                Key(DocoptConfig, '--mrna'),
                Key(ReagentConfig, 'molecule', cast=lambda x: x == 'RNA'),
        )

    presets = appcli.param(
            Key(StepwiseConfig, 'presets'),
            pick=list,
    )
    preset = appcli.param(
            Key(DocoptConfig, '--preset'),
            Key(StepwiseConfig, 'default_preset'),
    )
    base_reaction = appcli.param(
            Key(PresetConfig, 'reaction'),
            cast=stepwise.MasterMix.from_text,
    )
    title = appcli.param(
            Key(PresetConfig, 'title'),
            Key(PresetConfig, 'kit'),
    )
    templates = appcli.param(
            Key(DocoptConfig, parse_templates_from_docopt),
            get=bind_arguments,
    )
    volume_uL = appcli.param(
            Key(DocoptConfig, '--volume', cast=eval),
            default=None,
    )
    default_volume_uL = appcli.param(
            # The difference between `default_volume_uL` and `volume_uL` is 
            # that the default additives are applied to the reaction after the 
            # default volume is set, but before the non-default volume is set.  
            # This allows the volume of the additive to be scaled 
            # proportionally to the volume of the reaction that the additive 
            # was specified for.
            Key(PresetConfig, 'volume_uL'),
            Key(StepwiseConfig, 'default_volume_uL'),
            default=None,
    )
    num_reactions = appcli.param(
            Key(DocoptConfig, '--num-reactions', cast=eval),
            default=None,
            get=lambda self, x: x or len(self.templates),
    )
    extra_percent = appcli.param(
            Key(DocoptConfig, '--extra-percent', cast=float),
            default=10,
    )
    template_conc_nM = appcli.param(
            Key(DocoptConfig, '--template-conc', cast=float),
            Key(PresetConfig, 'template_conc_nM'),
            default=None,
    )
    master_mix = appcli.param(
            Key(DocoptConfig, '--master-mix'),
            default=False,
    )
    use_template = appcli.param(
            Key(DocoptConfig, '--no-template', cast=not_),
            default=True,
    )
    use_rnase_inhibitor = appcli.param(
            Key(DocoptConfig, '--no-inhibitor', cast=not_),
            default=True,
    )
    additives = appcli.param(
            Key(DocoptConfig, '--additive'),
            default_factory=list,
    )
    default_additives = appcli.param(
            Key(PresetConfig, 'additives'),
            default_factory=list,
    )
    setup_instructions = appcli.param(
            Key(PresetConfig, 'setup_instructions'),
            default_factory=list,
    )
    setup_footnote = appcli.param(
            Key(PresetConfig, 'setup_footnote'),
            default=None,
    )
    incubation_time = appcli.param(
            Key(DocoptConfig, '--incubation-time'),
            Key(PresetConfig, 'incubation_time'),
    )
    incubation_temp_C = appcli.param(
            Key(DocoptConfig, '--incubation-temp'),
            Key(PresetConfig, 'incubation_temp_C'),
            cast=float,
    )
    incubation_footnote = appcli.param(
            Key(PresetConfig, 'incubation_footnote'),
            default=None,
    )

    def get_protocol(self):
        p = stepwise.Protocol()
        rxn = self.reaction

        p += pl(
                f"Setup {plural(self.num_reactions):# {self.title} reaction/s}{p.add_footnotes(self.setup_footnote)}:",
                rxn,
                ul(*self.setup_instructions),
        )
        if self.incubation_time != '0':
            p += f"Incubate at {self.incubation_temp_C:g}°C for {self.incubation_time}{p.add_footnotes(self.incubation_footnote)}."

        return p

    def get_reaction(self):

        def add_reagents(additives):
            nonlocal i
            # It would be better if there was a utility in stepwise for parsing 
            # `sw reaction`-style strings.  Maybe `Reagent.from_text()`.
            for i, additive in enumerate(additives, i):
                reagent, stock_conc, volume, master_mix = additive.split(';')
                rxn[reagent].stock_conc = stock_conc
                rxn[reagent].volume = volume
                rxn[reagent].master_mix = {'+': True, '-': False, '': False}[master_mix.strip()]
                rxn[reagent].order = i

        rxn = deepcopy(self.base_reaction)
        rxn.num_reactions = self.num_reactions

        for i, reagent in enumerate(rxn):
            reagent.order = i

        if self.default_volume_uL:
            rxn.hold_ratios.volume = self.default_volume_uL, 'µL'

        add_reagents(self.default_additives)

        if self.volume_uL:
            rxn.hold_ratios.volume = self.volume_uL, 'µL'

        add_reagents(self.additives)

        if self.use_mrna:
            template = 'mRNA'
            require_reagent(rxn, 'mRNA')
            del_reagent_if_present(rxn, 'DNA')
            del_reagents_by_flag(rxn, 'dna')
        else:
            template = 'DNA'
            require_reagent(rxn, 'DNA')
            del_reagent_if_present(rxn, 'mRNA')
            del_reagents_by_flag(rxn, 'mrna')

        rxn[template].name = f"{','.join(self.templates)}"
        rxn[template].master_mix = self.master_mix
        rxn[template].hold_conc.stock_conc = self.template_stock_nM, 'nM'

        if self.template_conc_nM:
            rxn[template].hold_stock_conc.conc = self.template_conc_nM, 'nM'
        elif self.use_template:
            warn("Template concentrations must be empirically optimized.\nThe default value is just a plausible starting point.")

        if not self.use_template:
            del rxn[template]

        if not self.use_rnase_inhibitor:
            del_reagents_by_flag(rxn, 'rnase')

        # Make sure the template is added last.
        rxn[template].order = i+1

        if self.use_template:
            rxn.fix_volumes(template)

        rxn.extra_percent = self.extra_percent

        return rxn

    def get_template_stock_nM(self):
        return min(x.stock_nM for x in self.templates)

    @property
    def use_mrna(self):
        return unanimous(x.is_mrna for x in self.templates)
Esempio n. 18
0
class GoldenGate(Assembly):
    __doc__ = f"""\
Perform a Golden Gate assembly reaction.

Usage:
    golden_gate <assemblies>... [-c <conc>]... [-l <length>]... [options]

Arguments:
{ARGUMENT_DOC}

Options:
{OPTION_DOC}

    -e --enzymes <type_IIS>         [default: ${{','.join(app.enzymes)}}]
        The name(s) of the Type IIS restriction enzyme(s) to use for the 
        reaction.  To use more than one enzyme, enter comma-separated names.  
        Only NEB enzymes are supported.

Database:
    Golden gate assemblies can appear in the "Synthesis" column of a FreezerBox 
    database:
        
        golden-gate <assembly> [enzymes=<type IIS>] [volume=<µL>]

    <assembly>
        See the <assemblies>... command-line argument.

    volume=<µL>
        See --volume.  You must include a unit.

    enzymes=<type IIS>
        See --enzymes.
"""

    enzymes = appcli.param(
        Key(DocoptConfig, '--enzymes'),
        Key(MakerConfig, 'enzymes'),
        cast=lambda x: x.split(','),
        default=['BsaI-HFv2'],
    )

    group_by = {
        **Assembly.group_by,
        'enzymes': group_by_identity,
    }

    def get_reaction(self):
        rxn = stepwise.MasterMix()
        rxn.volume = '20 µL'

        rxn['T4 ligase buffer'].volume = '2.0 μL'
        rxn['T4 ligase buffer'].stock_conc = '10x'
        rxn['T4 ligase buffer'].master_mix = True
        rxn['T4 ligase buffer'].order = 2

        enz_uL = 0.5 if self.num_fragments <= 10 else 1.0

        rxn['T4 DNA ligase'].volume = enz_uL, 'µL'
        rxn['T4 DNA ligase'].stock_conc = '400 U/μL'
        rxn['T4 DNA ligase'].master_mix = True
        rxn['T4 DNA ligase'].order = 3

        enzyme_db = NebRestrictionEnzymeDatabase()

        for enzyme in self.enzymes:
            stock = enzyme_db[enzyme]['concentration'] / 1000
            rxn[enzyme].volume = enz_uL, 'µL'
            rxn[enzyme].stock_conc = stock, 'U/µL'
            rxn[enzyme].master_mix = True
            rxn[enzyme].order = 4

        return self._add_fragments_to_reaction(rxn)

    def del_reaction(self):
        pass

    def get_protocol(self):
        # Maybe this should be the getter function for the assembly param...

        p = stepwise.Protocol()
        rxn = self.reaction
        n = rxn.num_reactions
        n_frags = self.num_fragments

        f = 'https://tinyurl.com/yaa5mqz5'
        p += pl(
            f"Setup {plural(n):# Golden Gate assembl/y/ies}{p.add_footnotes(f)}:",
            rxn,
        )
        if n_frags <= 2:
            p += pl(
                "Run the following thermocycler protocol:",
                ul("37°C for 5 min", ),
                "Or, to maximize the number of transformants:",
                ul(
                    "37°C for 60 min",
                    "60°C for 5 min",
                ),
            )

        elif n_frags <= 4:
            p += pl(
                "Run the following thermocycler protocol:",
                ul(
                    "37°C for 60 min",
                    "60°C for 5 min",
                ),
            )

        elif n_frags <= 10:
            p += pl(
                "Run the following thermocycler protocol:",
                ul(
                    "Repeat 30 times:",
                    ul(
                        "37°C for 1 min",
                        "16°C for 1 min",
                    ),
                    "60°C for 5 min",
                ),
            )

        else:
            p += pl(
                "Run the following thermocycler protocol:",
                ul(
                    "Repeat 30 times:",
                    ul(
                        "37°C for 5 min",
                        "16°C for 5 min",
                    ),
                    "60°C for 5 min",
                ),
            )

        return p

    def del_protocol(self):
        pass
Esempio n. 19
0
class Ivt(Main):
    """\
Synthesize RNA using an in vitro transcription reaction.

Usage:
    ivt <templates>... [options]

Arguments:
    <templates>
        The names to the DNA templates to transcribe.  If these names can be 
        looked up in a FreezerBox database, some parameters (e.g. template 
        length) will automatically be determined.

<%! from stepwise_mol_bio import hanging_indent %>\
Options:
    -p --preset <name>          [default: ${app.preset}]
        What default reaction parameters to use.  The following parameters are 
        currently available:

        ${hanging_indent(app.preset_briefs, 8)}

    -v --volume <µL>
        The volume of the transcription reaction, in µL.

    -C --template-stock <ng/µL>
        The stock concentration of the template DNA (in ng/µL).  By default, 
        the concentration specified in the preset will be used.  Changing this 
        option will not change the quantity of template added to the reaction 
        (the volume will be scaled proportionally).

    -t --template-mass <ng>
        How much template DNA to use (in ng).  The template volume will be 
        scaled in order to reach the given quantity.  If the template is not 
        concentrated enough to reach the given quantity, the reaction will just 
        contain as much DNA as possible instead.  In order to use this option, 
        the stock concentration of the template must be in units of ng/µL.

    -V --template-volume <µL>
        The volume of DNA to add to the reaction (in µL).  This will be 
        adjusted if necessary to fit within the total volume of the reaction.

    -s --short
        Indicate that all of the templates are shorter than 300 bp.  This 
        allows the reactions to be setup with less material, so that more 
        reactions can be performed with a single kit.

    -i --incubation-time <minutes>
        How long to incubate the transcription reaction, in minutes.

    -T --incubation-temp <°C>
        What temperature the incubate the transcription reaction at, in °C.

    -x --extra <percent>        [default: ${app.extra_percent}]
        How much extra master mix to create.

    -R --toggle-rntp-mix
        Indicate whether you're using an rNTP mix, or whether you need to add 
        each rNTP individually to the reaction.  This option toggles the 
        default value specified in the configuration file.

    -a --toggle-dnase-treatment
        Indicate whether you'd like to include the optional DNase treatment 
        step.  This option toggles the default value specified in the 
        configuration file.

Configuration:
    Default values for this protocol can be specified in any of the following 
    stepwise configuration files:

        ${hanging_indent(app.config_paths, 8)}

    molbio.ivt.default_preset:
        The default value for the `--preset` option.

    molbio.ivt.rntp_mix:
        The default value for the `--toggle-rntp-mix` flag.

    molbio.ivt.dnase_treatment:
        The default value for the `--toggle-dnase-treament` flag.

    molbio.ivt.presets:
        Named groups of default reaction parameters.  Typically each preset 
        corresponds to a particular kit or protocol.  See below for the various 
        settings that can be specified in each preset.

    molbio.ivt.presets.<name>.brief:
        A brief description of the preset.  This is displayed in the usage info 
        for the `--preset` option.

    molbio.ivt.presets.<name>.inherit:
        Copy all settings from another preset.  This can be used to make small 
        tweaks to a protocol, e.g. "HiScribe with a non-standard additive".

    molbio.ivt.presets.<name>.reaction:
        A table detailing all the components of the transcription reaction, in 
        the format understood by `stepwise.MasterMix.from_string()`.  
        Optionally, this setting can be a dictionary with keys "long" and 
        "short", each corresponding to a reaction table.  This allows different 
        reaction parameters to be used for long and short templates.

        The DNA template reagent must be named "template".  The rNTP reagents 
        may be marked with the "rntp" flag.  The `--rntp-mix` flag will replace 
        any reagents so marked with a single reagent named "rNTP mix".

    molbio.ivt.presets.<name>.instructions:
        A list of miscellaneous instructions pertaining to how the reaction 
        should be set up, e.g. how to thaw the reagents, what temperature to 
        handle the reagents at, etc.

    molbio.ivt.presets.<name>.extra_percent:
        How much extra master mix to make, as a percentage of the volume of a 
        single reaction.

    molbio.ivt.presets.<name>.incubation_time_min:
        See `--incubation-time`.  This setting can also be a dictionary with 
        keys "long" and "short", to specify different incubation times for long 
        and short templates.

    molbio.ivt.presets.<name>.incubation_temp_C:
        See `--incubation-temp`.

    molbio.ivt.presets.<name>.length_threshold
        The length of a template in base pairs that separates long from short 
        templates. Template lengths will be queried from the FreezerBox 
        database if possible.

    molbio.ivt.presets.<name>.dnase.default
        The default value for the `--dnase` flag.

    molbio.ivt.presets.<name>.dnase.reaction
        A table detailing all the components of the optional DNase reaction.  
        One component must be named "transcription reaction".

    molbio.ivt.presets.<name>.dnase.incubation_time_min
        The incubation time (in minutes) for the optional DNase reaction.

    molbio.ivt.presets.<name>.dnase.incubation_temp_C
        The incubation temperature (in °C) for the optional DNase reaction.

    molbio.ivt.presets.<name>.dnase.footnotes.reaction
    molbio.ivt.presets.<name>.dnase.footnotes.incubation
    molbio.ivt.presets.<name>.dnase.footnotes.dnase
        Lists of footnotes for the reaction setup, incubation, and DNase 
        treatment steps, respectively.

Database:
    In vitro transcription reactions can appear in the "Synthesis" column of a 
    FreezerBox database.  The associated database entry will automatically be 
    considered ssRNA, e.g. for the purpose of molecular weight calculations:

        ivt template=<tag> [preset=<preset>] [volume=<µL>] [time=<min>]
            [temp=<°C>]

    template=<tag>
        See `<templates>`.  Only one template can be specified.

    preset=<preset>
        See `--preset`.

    volume=<µL>
        See `--volume`.

    time=<min>
        See `--incubation-time`.

    temp=<°C>
        See `--incubation-temp`.

Template Preparation:
    The following information is taken directly from the HiScribe and 
    MEGAscript manuals:

    Plasmid Templates:

        To produce RNA transcript of a defined length, plasmid DNA must be 
        completely linearized with a restriction enzyme downstream of the 
        insert to be transcribed.  Circular plasmid templates will generate 
        long heterogeneous RNA transcripts in higher quantities because of high 
        processivity of T7 RNA polymerase.  Be aware that there has been one 
        report of low level transcription from the inappropriate template 
        strand in plasmids cut with restriction enzymes leaving 3' overhanging 
        ends [Schendorn and Mierindorf, 1985].

        DNA from some miniprep procedures may be contaminated with residual 
        RNase A.  Also, restriction enzymes occasionally introduce RNase or 
        other inhibitors of transcription.  When transcription from a template 
        is suboptimal, it is often helpful to treat the template DNA with 
        proteinase K (100–200 μg/mL) and 0.5% SDS for 30 min at 50°C, follow 
        this with phenol/chloroform extraction (using an equal volume) and 
        ethanol precipitation.

    PCR Templates:

        PCR products containing T7 RNA Polymerase promoter in the correct 
        orientation can be transcribed.  Though PCR mixture can be used 
        directly, better yields will be obtained with purified PCR products.  
        PCR products can be purified according to the protocol for plasmid 
        restriction digests above, or by using commercially available spin 
        columns (we recommend Monarch PCR & DNA Cleanup Kit, NEB #T1030).  PCR 
        products should be examined on an agarose gel to estimate concentration 
        and to confirm amplicon size prior to its use as a template.  Depending 
        on the PCR products, 0.1–0.5 μg of PCR fragments can be used in a 20 μL 
        in vitro transcription reaction.

    Synthetic DNA Oligonucleotides:

        Synthetic DNA oligonucleotides which are either entirely 
        double-stranded or mostly single-stranded with a double-stranded T7 
        promoter sequence can be used for transcription.  In general, the 
        yields are relatively low and also variable depending upon the 
        sequence, purity, and preparation of the synthetic oligonucleotides.
"""
    __config__ = [
        DocoptConfig,
        MakerConfig,
        TemplateConfig,
        PresetConfig,
        StepwiseConfig.setup('molbio.ivt'),
    ]
    preset_briefs = appcli.config_attr()
    config_paths = appcli.config_attr()

    class Template(ShareConfigs, Argument):
        __config__ = [ReagentConfig]

        seq = appcli.param(Key(ReagentConfig, 'seq'), )
        length = appcli.param(
            Key(ReagentConfig, 'length'),
            Method(lambda self: len(self.seq)),
        )
        stock_ng_uL = appcli.param(
            Key(DocoptConfig, '--template-stock', cast=float),
            Key(ReagentConfig, 'conc_ng_uL'),
            default=None,
        )

    def _calc_short(self):
        return all(x.length <= self.template_length_threshold
                   for x in self.templates)

    def _pick_by_short(self, values):
        return pick_by_short(values, self.short)

    presets = appcli.param(
        Key(StepwiseConfig, 'presets'),
        pick=list,
    )
    preset = appcli.param(
        Key(DocoptConfig, '--preset'),
        Key(MakerConfig, 'preset'),
        Key(StepwiseConfig, 'default_preset'),
    )
    reaction_prototype = appcli.param(
        Key(PresetConfig, 'reaction', cast=parse_reaction),
        get=_pick_by_short,
    )
    templates = appcli.param(
        Key(DocoptConfig,
            '<templates>',
            cast=lambda tags: [Ivt.Template(x) for x in tags]),
        Key(MakerConfig, 'template', cast=lambda x: [Ivt.Template(x)]),
        get=bind_arguments,
    )
    template_length_threshold = appcli.param(
        Key(PresetConfig, 'length_threshold'), )
    template_volume_uL = appcli.param(
        Key(DocoptConfig, '--template-volume', cast=float),
        default=None,
    )
    template_mass_ng = appcli.param(
        Key(DocoptConfig, '--template-mass', cast=float),
        default=None,
    )
    short = appcli.param(
        Key(DocoptConfig, '--short'),
        Method(_calc_short),
        default=False,
    )
    volume_uL = appcli.param(
        Key(DocoptConfig, '--volume-uL', cast=float),
        Key(MakerConfig, 'volume', cast=parse_volume_uL),
        Key(PresetConfig, 'volume_uL'),
        default=None,
    )
    rntp_mix = appcli.toggle_param(
        Key(DocoptConfig, '--no-rntp-mix', toggle=True),
        Key(StepwiseConfig, 'rntp_mix'),
        default=True,
    )
    extra_percent = appcli.param(
        Key(DocoptConfig, '--extra-percent'),
        Key(PresetConfig, 'extra_percent'),
        cast=float,
        default=10,
    )
    instructions = appcli.param(
        Key(PresetConfig, 'instructions'),
        default_factory=list,
    )
    incubation_times_min = appcli.param(
        Key(DocoptConfig, '--incubation-time'),
        Key(MakerConfig, 'time', cast=parse_time_m),
        Key(PresetConfig, 'incubation_time_min'),
    )
    incubation_temp_C = appcli.param(
        Key(DocoptConfig, '--incubation-temp'),
        Key(MakerConfig, 'temp', cast=parse_temp_C),
        Key(PresetConfig, 'incubation_temp_C'),
    )
    dnase = appcli.toggle_param(
        Key(DocoptConfig, '--toggle-dnase-treatment', toggle=True),
        Key(PresetConfig, 'dnase.treatment'),
        Key(StepwiseConfig, 'dnase_treatment'),
        default=False,
    )
    dnase_reaction_prototype = appcli.param(
        Key(PresetConfig, 'dnase.reaction', cast=MasterMix), )
    dnase_incubation_time_min = appcli.param(
        Key(PresetConfig, 'dnase.incubation_time_min'), )
    dnase_incubation_temp_C = appcli.param(
        Key(PresetConfig, 'dnase.incubation_temp_C'), )
    footnotes = appcli.param(
        Key(PresetConfig, 'footnotes'),
        default_factory=dict,
    )

    group_by = {
        'preset': group_by_identity,
        'volume_uL': group_by_identity,
        'incubation_times_min': group_by_identity,
        'incubation_temp_C': group_by_identity,
    }
    merge_by = {
        'templates': join_lists,
    }

    def __init__(self, templates):
        self.templates = templates

    def __repr__(self):
        return f'Ivt(templates={self.templates!r})'

    def get_protocol(self):
        p = stepwise.Protocol()

        ## Clean your bench
        p += stepwise.load('rnasezap')

        ## In vitro transcription
        rxn = self.reaction
        n = plural(rxn.num_reactions)
        f = self.footnotes.get('reaction', [])
        p += paragraph_list(
            f"Setup {n:# in vitro transcription reaction/s}{p.add_footnotes(*f)}:",
            rxn,
            unordered_list(*self.instructions),
        )

        f = self.footnotes.get('incubation', [])
        if self.short and affected_by_short(self.incubation_times_min):
            f += [
                f"Reaction time is different than usual because the template is short (<{self.template_length_threshold} bp)."
            ]
        p += f"Incubate at {self.incubation_temp_C}°C for {format_min(pick_by_short(self.incubation_times_min, self.short))}{p.add_footnotes(*f)}."

        ## DNase treatment
        if self.dnase:
            f = self.footnotes.get('dnase', [])
            p += paragraph_list(
                f"Setup {n:# DNase reaction/s}{p.add_footnotes(*f)}:",
                self.dnase_reaction,
            )
            p += f"Incubate at {self.dnase_incubation_temp_C}°C for {format_min(self.dnase_incubation_time_min)}."

        return p

    def get_reaction(self):
        rxn = self.reaction_prototype.copy()
        rxn.num_reactions = len(self.templates)
        rxn.extra_percent = self.extra_percent

        if self.volume_uL:
            rxn.hold_ratios.volume = self.volume_uL, 'µL'

        if self.rntp_mix:
            rntps = []

            for i, reagent in enumerate(rxn):
                reagent.order = i
                if 'rntp' in reagent.flags:
                    rntps.append(reagent)

            if not rntps:
                err = ConfigError("cannot make rNTP mix", preset=self.preset)
                err.blame += "no reagents flagged as 'rntp'"
                err.hints += "you may need to add this information to the [molbio.ivt.{preset}] preset"
                raise err

            rxn['rNTP mix'].volume = sum(x.volume for x in rntps)
            rxn['rNTP mix'].stock_conc = sum(x.stock_conc
                                             for x in rntps) / len(rntps)
            rxn['rNTP mix'].master_mix = all(x.master_mix for x in rntps)
            rxn['rNTP mix'].order = rntps[0].order

            for rntp in rntps:
                del rxn[rntp.name]

        rxn['template'].name = ','.join(x.tag for x in self.templates)

        template_stocks_ng_uL = [
            ng_uL for x in self.templates if (ng_uL := x.stock_ng_uL)
        ]
Esempio n. 20
0
class AnnealMrnaLinker(appcli.App):
    """\
Anneal linker-N and mRNA prior to ligation.

Usage:
    anneal [<mrna>] [<linker>] [-n <int>] [-m <reagents>] [-V <µL>]
            [-r <µL>] [-R <µM>] [-l <ratio>] [-L <µM>]

Arguments:
    <mrna>
        The name of the mRNA, e.g. f11.  Multiple comma-separated names may be 
        given.

    <linker>
        The name of the linker, e.g. o93.  Multiple comma-separated names may 
        be given.

Options:
    -n --num-reactions <int>
        The number of reactions to setup up.  The default is the number of 
        unique combinations of mRNA and linker.

    -m --master-mix <reagents>          [default: ${','.join(app.master_mix)}]
        A comma-separated list of reagents to include in the master mix.  This 
        flag is only relevant in <n> is more than 1.  The following reagents 
        are understood: mrna, link

    -r --mrna-volume <µL>               [default: ${app.mrna_volume_uL}]
        The volume of mRNA to use in each annealing reaction, in µL.

    -R --mrna-stock <µM>
        The stock concentration of the mRNA, in µM.  The volume of mRNA will be 
        updated accordingly to keep the amount of material in the reaction 
        constant.  The default is read from the FreezerBox database, or 10 µM 
        if the given mRNA is not in the database.  Use '--mrna-volume' to 
        change the amount of mRNA in the reaction.

    -l --linker-ratio <float>           [default: ${app.linker_ratio}]
        The amount of linker to add to the reaction, relative to the amount of 
        mRNA in the reaction.

    -L --linker-stock <µM>
        The stock concentration of the linker, in µM.  The volume of linker be 
        updated accordingly, to keep the amount of material in the reaction 
        constant.  The default is read from the FreezerBox database, or 10 µM 
        if the given linker is not in the database.  
"""
    __config__ = [
        DocoptConfig(),
    ]

    mrnas = appcli.param(
        '<mrna>',
        cast=lambda x: x.split(','),
        default_factory=list,
    )
    linkers = appcli.param(
        '<linker>',
        cast=lambda x: x.split(','),
        default_factory=list,
    )
    num_reactions = appcli.param(
        '--num-reactions',
        cast=int,
        default=0,
        get=lambda self, x: x or len(self.mrnas) * len(self.linkers),
    )
    mrna_volume_uL = appcli.param(
        '--mrna-volume',
        cast=eval,
        default=1,
    )
    master_mix = appcli.param(
        '--master-mix',
        cast=lambda x: set(x.split(',')),
        default_factory=set,
    )
    linker_ratio = appcli.param(
        '--linker-ratio',
        cast=float,
        default=1,
    )
    mrna_stock_uM = appcli.param(
        '--mrna-stock',
        default=None,
    )
    linker_stock_uM = appcli.param(
        '--mrna-stock',
        default=None,
    )

    def __bareinit__(self):
        self.db = freezerbox.load_db()

    def __init__(self, mrnas, linkers):
        self.mrnas = list_if_str(mrnas)
        self.linkers = list_if_str(linkers)

    def get_protocol(self):
        p = stepwise.Protocol()
        rxn = self.reaction
        n = rxn.num_reactions

        p += pl(
            f"Setup {plural(n):# annealing reaction/s} [1]:",
            rxn,
        )

        p.footnotes[1] = """\
                Using 0.6x linker reduces the amount of unligated 
                linker, see expt #1."""

        p += pl(
            f"Perform the {plural(n):annealing reaction/s}:",
            ul(
                "Incubate at 95°C for 2 min.",
                "Cool at room temperature.",
            ),
        )

        return p

    def get_reaction(self):
        rxn = stepwise.MasterMix()
        rxn.num_reactions = n = self.num_reactions
        rxn.solvent = None

        rxn['mRNA'].stock_conc = consensus(
            get_conc_uM(self.db, x, self.mrna_stock_uM) for x in self.mrnas)
        rxn['linker'].stock_conc = consensus(
            get_conc_uM(self.db, x, self.linker_stock_uM)
            for x in self.linkers)

        rxn['mRNA'].volume = self.mrna_volume_uL, 'µL'
        rxn['linker'].volume = (
            self.linker_ratio * rxn['mRNA'].volume *
            (rxn['mRNA'].stock_conc / rxn['linker'].stock_conc))

        rxn['mRNA'].master_mix = 'mrna' in self.master_mix
        rxn['linker'].master_mix = 'link' in self.master_mix

        if self.mrnas:
            rxn['mRNA'].name = ','.join(self.mrnas)
        if self.linkers:
            rxn['linker'].name = ','.join(self.linkers)

        rxn['PBS'].volume = rxn.volume / 9
        rxn['PBS'].stock_conc = '10x'
        rxn['PBS'].order = -1

        return rxn
Esempio n. 21
0
class Dilute(appcli.App):
    """\
Calculate dilutions.

Usage:
    dilute <stocks>... (-v <µL> | -w <µL> | -V <µL>) [-c <conc>] [-C <conc>] 
        [-w <Da>] [-d <name>]

Arguments:
    <stocks>
        A list of stock solutions to dilute.  Any number of stocks can be 
        specified, and each argument can take one of two forms:

        Colon-separated fields:
            Specify the name and, if necessary, the concentration and molecular 
            weight for a single stock solution:

                <name>[:<conc>[:<mw>]]

            If either of the optional parameters aren't specified, they will be 
            looked up in the FreezerBox database using the given name.  Note 
            that the molecular weight is only required for certain unit 
            conversions, e.g. ng/µL to nM.  The name can be empty (or not in 
            the database) so long as enough information to calculate the 
            dilutions is supplied on the command-line.

        Path to existing TSV file:
            Read names, concentrations, and molecular weights for any number of 
            stock solutions from the given TSV file.  Information is read from 
            the following columns (any other columns are ignored):

            name:               "Sample Name" or "Sample ID"
            concentration:      "Nucleic Acid(ng/uL)" or "Nucleic Acid"
            molecular weight:   "Molecular Weight" or "MW"

            These column names are intended to match the files exported by 
            NanoDrop ONE and NanoDrop 2000 spectrophotometers, although it's 
            certainly possible to make these files yourself.

Options:
    -c --conc <conc>
        The final concentration achieve after dilution.  If not specified, 
        concentrations will be queried from the FreezerBox database.

    -C --stock-conc <conc>
        The stock concentration to use in the dilution calculations.  This 
        option will override any concentrations specified in the FreezerBox 
        database, but not any specified by the <stocks> argument.

    -v --volume <uL>
        The volume of concentrated stock solution to use in each dilution.  
        This can either be a single value or a comma-separated list of values.  
        If a single value is given, that value will be used for each dilution.  
        If multiple values are given, there must be exactly one value for each 
        stock solution, and the values will be associated with the stock 
        solutions in the order they were given.

    -D --diluent-volume <uL>
        The volume of diluent to use in each dilution.  This can either be a 
        single value or a comma-separated list of values, see the `--volume` 
        option for details.

    -V --total-volume <µL>
        The combined volume of stock and diluent to reach after each dilution.  
        This can either be a single value or a comma-separated list of values, 
        see the `--volume` option for details.

    --mw <Da>
        The molecular weight to use in the dilution calculations, e.g. if 
        converting between ng/µL and nM.  This option will override any 
        molecular weights specified in the FreezerBox database, but not any 
        specified by the <stocks> argument.

    -d --diluent <name>
        The name of the diluent to use.

Database:
    Dilution protocols can appear in the "Cleanup" column of a FreezerBox 
    database:

        dilute [conc=<conc>] [diluent=<name>]

    conc=<conc>
        See --conc.

        This setting is automatically applied as the concentration of the 
        associated database entry, unless superseded by the "Concentration" 
        column or a later cleanup step.  In other words, this concentration may 
        be used by any protocol that queries the FreezerBox database.

        Often, a concentration specified by this setting is regarded as the 
        "desired concentration", with the "actual concentration" being given in 
        the "Concentration" column in the event that the desired concentration 
        cannot be reached.

    diluent=<name>
        See --diluent.

"""
    __config__ = [
        DocoptConfig,
        MakerConfig,
    ]

    stocks = appcli.param(
        Key(DocoptConfig, '<stocks>'),
        Method(lambda self: [Stock(x, None, None) for x in self.products]),
        cast=parse_stocks,
    )
    target_conc = appcli.param(
        Key(DocoptConfig, '--conc'),
        Key(MakerConfig, 'conc'),
        cast=parse_conc,
        default=None,
    )
    stock_conc = appcli.param(
        Key(DocoptConfig, '--stock-conc'),
        cast=parse_conc,
        default=None,
    )
    stock_volume_uL = appcli.param(
        Key(DocoptConfig, '--volume'),
        cast=parse_volume,
        default=None,
    )
    diluent_volume_uL = appcli.param(
        Key(DocoptConfig, '--diluent-volume'),
        cast=parse_volume,
        default=None,
    )
    target_volume_uL = appcli.param(
        Key(DocoptConfig, '--total-volume'),
        cast=parse_volume,
        default=None,
    )
    mw = appcli.param(
        Key(DocoptConfig, '--mw'),
        cast=parse_mw,
        default=None,
    )
    diluent = appcli.param(
        Key(DocoptConfig, '--diluent'),
        Key(MakerConfig, 'diluent'),
        default=None,
    )
    show_stub = appcli.param(default=False, )

    def __bareinit__(self):
        self._db = None

    def __init__(self, stocks):
        self.stocks = stocks

    @classmethod
    def make(cls, db, products):
        def factory():
            app = cls.from_params()
            app.db = db
            app.show_stub = True
            return app

        yield from iter_combo_makers(
            factory,
            map(cls.from_product, products),
            group_by={
                'target_conc': group_by_identity,
                'diluent': group_by_identity,
            },
        )

    @classmethod
    def from_product(cls, product):
        app = cls.from_params()
        app.products = [product]
        app.load(MakerConfig)
        return app

    def get_db(self):
        if self._db is None:
            self._db = freezerbox.load_db()
        return self._db

    def set_db(self, db):
        self._db = db

    def get_concs(self):
        rows = []

        def first_valid(*options, strict=True, error=ValueError):
            for option in options:
                if callable(option):
                    try:
                        return option()
                    except QueryError:
                        continue

                if option is not None:
                    return option

            if strict:
                raise error
            else:
                return None

        for stock in self.stocks:
            rows.append(row := {})
            row['tag'] = tag = stock.tag
            row['target_conc'] = first_valid(
                self.target_conc,
                lambda: self.db[tag].conc,
                error=ValueError(f"{tag}: no target concentration specified"),
            )
            row['stock_conc'] = first_valid(
                stock.conc,
                self.stock_conc,
                lambda: self.db[tag].conc,
                error=ValueError(f"{tag}: no stock concentration specified"),
            )
            row['mw'] = first_valid(
                stock.mw,
                self.mw,
                lambda: self.db[tag].mw,
                strict=False,
            )
            row['stock_conc_converted'] = convert_conc_unit(
                row['stock_conc'],
                row['mw'],
                row['target_conc'].unit,
            )

        return pd.DataFrame(rows)

    def get_dilutions(self):
        self._check_volumes()

        df = self.concs
        k = df['stock_conc_converted'] / df['target_conc']
        dont_calc = (k <= 1)
        k[dont_calc] = pd.NA  # Avoid dividing by zero.

        if uL := self.stock_volume_uL:
            df['stock_uL'] = uL
            df['diluent_uL'] = uL * (k - 1)

        elif uL := self.diluent_volume_uL:
            df['diluent_uL'] = uL
            df['stock_uL'] = uL / (k - 1)
            uL = 'any'
Esempio n. 22
0
class Note(StepwiseCommand):
    """\
Insert a footnote into a protocol.

This command can be used to elaborate on a previous step in a protocol.  The 
footnote will be numbered automatically, and any subsequent footnotes will be 
renumbered accordingly.

Usage:
    stepwise note <footnote> [<where>] [-W]

Arguments:
    <footnote>
         The text of the footnote to add.

    <where>
        A regular expression indicating where the footnote reference should be 
        placed.  The steps will be searched in reverse order for this pattern, 
        and the reference will be inserted directly after the first matching 
        substring found.

        You can use a lookahead assertion to match text that will appear after 
        the reference, e.g. '(?=:)' will place a reference before the first 
        colon that is found.  By default the footnote will be placed just 
        before the first period (.) or colon (:) in the previous step.

Options:
    -W --no-wrap
        Do not automatically line-wrap the given message.  The default is to 
        wrap the message to fit within the width specified by the 
        `printer.default.content_width` configuration option.
"""
    __config__ = [
        appcli.DocoptConfig,
    ]

    text = appcli.param('<footnote>')
    where = appcli.param('<where>', default=None)
    wrap = appcli.param('--no-wrap', cast=not_, default=True)

    def main(self):
        appcli.load(self)

        io = ProtocolIO.from_stdin()
        if io.errors:
            fatal("protocol has errors, not adding footnote.")
        if not io.protocol:
            fatal("no protocol specified.")

        p = io.protocol
        footnote = self.text if self.wrap else pre(self.text)
        pattern = re.compile(self.where or '(?=[.:])')

        try:
            p.insert_footnotes(footnote, pattern=pattern)
        except ValueError:
            fatal(f"pattern {pattern!r} not found in protocol.")

        p.merge_footnotes()

        io.to_stdout(self.force_text)
Esempio n. 23
0
class Printer:
    __config__ = [
            PresetConfig,
            StepwiseConfig,
    ]

    presets = appcli.param(
            StepwiseConfig,
            default_factory=dict,
            pick=list,
    )
    page_height = appcli.param(
            PresetConfig,
            default=56,
    )
    page_width = appcli.param(
            PresetConfig,
            default=78,
    )
    content_width = appcli.param(
            PresetConfig,
            default=53,
    )
    margin_width = appcli.param(
            PresetConfig,
            default=10,
    )
    paps_flags = appcli.param(
            PresetConfig,
            default='--font "FreeMono 12" --paper letter --left-margin 0 --right-margin 0 --top-margin 12 --bottom-margin 12',
    )
    lpr_flags = appcli.param(
            PresetConfig,
            default='-o sides=one-sided',
    )

    def __init__(self, preset=None):
        self.preset = preset or get_default_printer_name()


    def truncate_lines(self, lines):
        def do_truncate(line):
            line_width = self.page_width - self.margin_width
            if len(line) > line_width:
                return line[:line_width - 1] + '…'
            else:
                return line

        return [do_truncate(x) for x in lines]

    def check_for_long_lines(self, lines):
        too_long_lines = []

        for lineno, line in enumerate(lines, 1):
            line = line.rstrip()
            if len(line) > self.content_width:
                too_long_lines.append(lineno)

        if len(too_long_lines) == 0:
            return
        elif len(too_long_lines) == 1:
            warning = "line {} is more than {} characters long."
        else:
            warning = "lines {} are more than {} characters long."

        warning = warning.format(
                format_range(too_long_lines),
                self.content_width,
        )
        raise PrinterWarning(warning)
        
    def make_pages(self, lines):
        """
        Split the given content into pages by trying to best take advantage of 
        natural paragraph breaks.

        The return value is a list of "pages", where each page is a list of lines 
        (without trailing newlines).
        """
        pages = []
        current_page = []
        skip_next_line = False

        def add_page(current_page):
            if any(line.strip() for line in current_page):
                pages.append(current_page[:])
            current_page[:] = []

        for i, line_i in enumerate(lines):

            if skip_next_line:
                skip_next_line = False
                continue

            # If the line isn't blank, add it to the current page like usual.

            if line_i.strip():
                current_page.append(line_i)

            # If the line is blank, find the next blank line and see if it fits on 
            # the same page.  If it does, add the blank line to the page and don't 
            # do anything special.  If it doesn't, make a new page.  Also interpret 
            # two consecutive blank lines as a page break.

            else:
                for j, line_j in enumerate(lines[i+1:], i+1):
                    if not line_j.strip():
                        break
                else:
                    j = len(lines)

                if len(current_page) + (j-i) > self.page_height or j == i+1:
                    skip_next_line = (j == i+1)
                    add_page(current_page)
                elif current_page:
                    current_page.append(line_i)

        add_page(current_page)
        return pages

    def add_margin(self, pages):
        """
        Add a margin on the left to leave room for holes to be punched.
        """
        left_margin = ' ' * self.margin_width + '│ '
        return [[left_margin + line for line in page] for page in pages]

    def print_pages(self, pages):
        """
        Print the given pages.
        """
        from subprocess import Popen, PIPE
        form_feed = ''
Esempio n. 24
0
class Sample:
    __config__ = [
            ReagentConfig.setup(
                db_getter=lambda self: self.app.db,
                tag_getter=lambda self: self.name,
            )
    ]

    def _calc_mass_ug(self):
        try:
            stock_conc_ug_uL = convert_conc_unit(self.stock_conc, self.mw, 'µg/µL')
        except ParseError:
            err = ConfigError(sample=self)
            err.brief = "can't calculate mass in µg for sample: {sample.name!r}"
            err.info += "MW: {sample.mw}"
            err.info += "stock conc: {sample.stock_conc}"
            err.info += "volume: {sample.stock_conc}"
            raise err
        else:
            return Quantity(stock_conc_ug_uL.value * self.volume_uL, 'µg')

    name = appcli.param()
    molecule = appcli.param(
            Method(lambda self: self.app.default_molecule),
            Key(ReagentConfig, 'molecule'),
            default='RNA',
            ignore=None,
    )
    stock_conc = appcli.param(
            Method(lambda self: self.app.default_stock_conc),
            Key(ReagentConfig, 'conc'),
            ignore=None,
    )
    volume_uL = appcli.param(
            Method(lambda self: self.app.default_volume_uL),
            ignore=None,
    )
    mw = appcli.param(
            Key(ReagentConfig, 'mw'),
            default=None,
            ignore=None,
    )
    mass_ug = appcli.param(
            Method(_calc_mass_ug),
            ignore=None,
    )

    @classmethod
    def from_colon_separated_string(cls, value):
        fields = {i: x for i, x in enumerate(value.split(':'))}

        name = fields[0]
        molecule = fields.get(1) or None
        stock_conc = fields.get(2) or None
        volume_uL = fields.get(3) or None

        if stock_conc:
            stock_conc = parse_conc(stock_conc)
        if volume_uL:
            volume_uL = parse_volume_uL(volume_uL)

        return cls(
                name=name,
                molecule=molecule,
                stock_conc=stock_conc,
                volume_uL=volume_uL,
        )

    def __init__(self, name, molecule=None, stock_conc=None, volume_uL=None, mass_ug=None):
        self.app = None
        self.name = name
        self.molecule = molecule
        self.stock_conc = stock_conc
        self.volume_uL = volume_uL
        self.mass_ug = mass_ug

    def __repr__(self):
        attrs = ['name', 'molecule', 'stock_conc', 'volume_uL', 'mass_ug']
        attr_reprs = [
                f'{k}={v!r}'
                for k in attrs
                if (v := getattr(self, k, None))
        ]
Esempio n. 25
0
class PagePurify(Cleanup):
    """\
Purify nucleic acids by PAGE.

Usage:
    page_purify <samples>... [-p <preset>] [-P <gel>] [-c <conc>] [-v <µL>]
        [-b <bands>] [-R | -D] [-C]

Arguments:
    <samples>
        A description of each crude sample to purify.  Each argument can 
        contain several pieces of information, separated by colons as follows:

            name[:molecule[:conc[:volume]]]

        name:
            The name of the product.  If this is corresponds to a tag in the 
            FreezerBox database, default values for the other parameters will 
            be read from the database.

        molecule:
            What kind of molecule the product is: either "RNA" or "DNA" 
            (case-insensitive).  Use `--rna` or `--dna` if all samples are the 
            same molecule.

        conc:
            The concentration of the product.  This may include a unit.  If no 
            unit is specified, µg/µL is assumed.  Use `--conc` if all samples 
            are the same concentration.

        volume:
            The volume of the product to load on the gel, in µL.  Do not 
            include a unit.  Use `--volume` if all samples have the same 
            volume.
            
<%! from stepwise_mol_bio import hanging_indent %>\
Options:
    -p --preset <name>          [default: ${app.preset}]
        The default parameters to use.  The following presets are available:

        ${hanging_indent(app.preset_briefs, 8*' ')}

    -P --gel-preset <name>
        The default gel electrophoresis parameters to use.  See `sw gel -h` for 
        a list of available presets.

    -c --conc <float>
        The concentration of each sample.  This may include a unit.  If no unit 
        is specified, µg/µL is assumed.  This is superseded by concentrations 
        specified in the <samples> argument, but supersedes concentrations 
        found in the FreezerBox database.

    -v --volume <µL>
        The volume of each sample to load on the gel, in µL.  Do not include a 
        unit.  This is superseded by volumes specified in the <samples> 
        argument.

    -b --bands <names>
        A comma separated list of names identifying the bands to cut out of the 
        gel.  Typically these would be the lengths of the desired products, 
        e.g. "400 nt" or "1.5 kb".

    -R --rna
        Assume that each sample is RNA.  This is superseded by the <samples> 
        argument, but supersedes the FreezerBox database.

    -D --dna
        Assume that each sample is DNA.  This is superseded by the <samples> 
        argument, but supersedes the FreezerBox database.

    -C --no-cleanup
        Don't include the final spin-column purification step in the protocol.

Configuration:
    Default values for this protocol can be specified in any of the following 
    stepwise configuration files:

        ${hanging_indent(app.config_paths, 8)}

    molbio.page_purify.default_preset:
        The default value for the `--preset` option.

    molbio.page_purify.presets:
        Named groups of default reaction parameters.  Typically each preset 
        corresponds to a particular kit or protocol.  See below for the various 
        settings that can be specified in each preset.

    molbio.page_purify.presets.<name>.conc
        The default value for the `--conc` option.  Note that if this option is 
        set, concentrations will never be read from the FreezerBox database.

    molbio.page_purify.presets.<name>.volume_uL
        The default value for the `--volume` option.

    molbio.page_purify.presets.<name>.molecule
        The default value for the `--rna`/`--dna` options.  This should either 
        be "RNA" or "DNA".

    molbio.page_purify.presets.<name>.gel_preset
        The default value for the `--gel-preset` option.

    molbio.page_purify.presets.<name>.bands
        The default value for the `--bands` option.

    molbio.page_purify.presets.<name>.cleanup_preset
        The default presets to use for the spin column cleanup step after 
        recovering the DNA/RNA from the gel.  See `sw spin_cleanup -h` for a 
        list of valid presets.  This option should be a dictionary with the 
        keys 'rna' and 'dna', each specifying the preset to use with samples of 
        the corresponding type.  Alternatively, this option can be 'false' to 
        indicate that the cleanup step should be skipped (see `--no-cleanup`).

Database:
    PAGE purification protocols can appear in the "Cleanups" column of a 
    FreezerBox database:

        page-purify [preset=<name>] [gel=<name>] [conc=<conc>] [volume=<µL>]
    
    preset=<name>
        See `--preset`.

    gel=<name>
        See `--gel-preset`.

    conc=<conc>
        See `--conc`.  Must include a unit.

    volume=<µL>
        See `--volume`.  Must include a unit.
"""
    __config__ = [
            DocoptConfig,
            MakerConfig,
            PresetConfig,
            StepwiseConfig.setup('molbio.page_purify'),
    ]
    Sample = Sample
    preset_briefs = appcli.config_attr()
    config_paths = appcli.config_attr()

    presets = appcli.param(
            Key(StepwiseConfig, 'presets'),
            pick=list,
    )
    preset = appcli.param(
            Key(DocoptConfig, '--preset'),
            Key(MakerConfig, 'preset'),
            Key(StepwiseConfig, 'default_preset'),
    )
    samples = appcli.param(
            Key(DocoptConfig, '<samples>', cast=parse_samples),
            Method(lambda self: [Sample(name=x.tag) for x in self.products]),
    )
    default_stock_conc = appcli.param(
            Key(DocoptConfig, '--conc', cast=parse_conc),
            Key(MakerConfig, 'conc', cast=Quantity.from_string),
            Key(PresetConfig, 'conc', cast=Quantity.from_string),
    )
    default_volume_uL = appcli.param(
            Key(DocoptConfig, '--volume', cast=parse_volume_uL),
            Key(MakerConfig, 'volume', cast=parse_strict_volume_uL),
            Key(PresetConfig, 'volume_uL', cast=parse_volume_uL),
            default=5,
    )
    default_molecule = appcli.param(
            Key(DocoptConfig, '--dna', cast=lambda x: 'DNA'),
            Key(DocoptConfig, '--rna', cast=lambda x: 'RNA'),
            Key(PresetConfig, 'molecule'),
    )
    gel_preset = appcli.param(
            Key(DocoptConfig, '--gel-preset'),
            Key(MakerConfig, 'gel'),
            Key(PresetConfig, 'gel_preset'),
    )
    gel_percent = appcli.param(
            Key(DocoptConfig, '--gel-percent'),
            default=None,
    )
    gel_run_volts = appcli.param(
            Key(DocoptConfig, '--gel-run-volts'),
            default=None,
    )
    gel_run_time_min = appcli.param(
            Key(DocoptConfig, '--gel-run-time-min'),
            default=None,
    )
    desired_bands = appcli.param(
            Key(DocoptConfig, '--bands', cast=comma_list),
            Key(PresetConfig, 'bands'),
            default_factory=list,
    )
    rna_cleanup_preset = appcli.param(
            Key(PresetConfig, 'cleanup_preset.rna'),
    )
    dna_cleanup_preset = appcli.param(
            Key(PresetConfig, 'cleanup_preset.dna'),
    )
    cleanup = appcli.param(
            Key(DocoptConfig, '--no-cleanup', cast=not_),
            Key(PresetConfig, 'cleanup_preset', cast=bool),
    )

    group_by = {
            'preset': group_by_identity,
            'gel_preset': group_by_identity,
    }
    merge_by = {
            'samples': join_lists,
    }

    def __init__(self, samples):
        self.samples = samples

    def get_protocol(self):
        self._bind_samples()

        p = stepwise.Protocol()
        p += self.gel_electrophoresis_steps
        p += self.gel_extraction_steps
        p += self.product_recovery_steps
        return p

    def get_gel_electrophoresis_steps(self):
        p = stepwise.Protocol()
        n = plural(self.samples)
        gel = Gel(self.gel_preset)

        mix = gel.sample_mix
        k = mix.volume / mix['sample'].volume

        for sample in self.samples:
            sample.stock_conc
            sample.sample_volume_per_lane_uL = min(
                    sample.volume_uL,
                    sample.volume_uL / ceil(sample.mass_ug / '20 µg'),
                    mix['sample'].volume.value or inf,
            )
            sample.load_volume_per_lane_uL = k * sample.sample_volume_per_lane_uL
            sample.num_lanes = int(ceil(sample.volume_uL / sample.sample_volume_per_lane_uL))

        conc_vol_groups = group_by_identity(
                self.samples,
                key=lambda x: (x.stock_conc, x.volume_uL),
        )
        for (conc, volume_uL), group in conc_vol_groups:
            prep_gel = Gel(self.gel_preset)
            prep_gel.num_samples = len(group)
            mix = prep_gel.sample_mix

            mix.hold_ratios.volume = k * volume_uL, 'µL'
            mix['sample'].name = ','.join(str(x.name) for x in group)
            mix['sample'].stock_conc = conc

            p += prep_gel.prep_step

        if x := self.gel_percent:
            gel.gel_percent = x
        if x := self.gel_run_volts:
            gel.run_volts = x
Esempio n. 26
0
class SmartMmlv(Main):
    """\
Synthesize first-strand cDNA (up to 11.7 kb) using SMART MMLV reverse 
transcriptase.

Usage:
    smart_mmlv <templates> <primers> [-n <rxns>] [-m <reagents>] [-v <µL>]
        [-t <nM>] [-T <nM>] [-p <fold>] [-P <µM>] [-C] [-q <step>] 

Arguments:
    <templates>
        The names of the templates to reverse transcribe, comma-separated.

    <primers>
        The names of the primers to use, comma-separated.

Options:
    -n --num-reactions <rxns>
        The number of reactions to setup.  By default, this is the number of 
        templates specified.

    -m --master-mix <reagents>
        A comma-separated list of reagents to include in the annealing master 
        mix.  Understood reagents are 'rna' and 'primer'.  By default, each 
        reaction is assembled independently.

    -v --volume <µL>                    [default: ${app.volume_uL}]
        The volume of the reaction, in µL.

    -t --template-conc <nM>             [default: ${app.template_conc_nM}]
        The final concentration of the template in the reverse transcription 
        reaction.  Note that a higher concentration will be used in the 
        annealing reaction.

    -T --template-stock <nM>            [default: ${app.template_stock_nM}]
        The stock concentration of the template, in nM.

    -p --excess-primer <fold>           [default: ${app.primer_excess}]
        The amount of primer to use, relative to the template concentration.

    -P --primer-stock <µM>              [default: ${app.primer_stock_uM}]
        The stock concentration of the primer, in µM.

    -r --rna-min-volume <µL>            [default: ${app.rna_min_volume_uL}]
        The smallest volume of RNA to pipet in any step.  This is meant to help 
        avoid pipetting errors when trying to be quantitative, e.g. for qPCR.

    -a --extra-anneal <percent>         [default: ${app.extra_anneal_percent}]
        How much extra annealing reaction to prepare, e.g. to facilitate 
        multichannel pipetting.

    -C --no-control
        Exclude the control reaction with no reverse transcriptase.  This is a 
        standard control in RT-qPCR workflows.

    -q --quench <step>                  [default: ${app.quench}]
        How to quench the reaction:

        heat: Incubate at 70°C.
        edta: Add EDTA.
        none: Skip the quench step.

Ordering:
- SMART MMLV reverse transcriptase (Takara 639523)
- Advantage UltraPure PCR deoxynucleotide mix (Takara 639125)

References:
1. https://tinyurl.com/y4ash7dl
"""
    __config__ = [
        DocoptConfig(),
    ]

    templates = appcli.param(
        '<templates>',
        cast=comma_list,
    )
    primers = appcli.param(
        '<primers>',
        cast=comma_list,
    )
    num_reactions = appcli.param(
        '--num-reactions',
        cast=int,
        default=None,
        get=lambda self, n: n if n else len(self.templates),
    )
    master_mix = appcli.param(
        '--master-mix',
        cast=comma_set,
        default_factory=set,
    )
    volume_uL = appcli.param(
        '--volume',
        cast=float,
        default=20,
    )
    template_conc_nM = appcli.param(
        '--template-conc',
        cast=float,
        default=50,
    )
    template_stock_nM = appcli.param(
        '--template-stock',
        cast=float,
        default=1000,
    )
    primer_excess = appcli.param(
        '--excess-primer',
        cast=float,
        default=50,
    )
    primer_stock_uM = appcli.param(
        '--primer-conc',
        cast=float,
        default=100,
    )
    rna_min_volume_uL = appcli.param(
        '--rna-min-volume',
        cast=float,
        default=2,
    )
    extra_anneal_percent = appcli.param(
        '--extra-anneal',
        cast=float,
        default=20,
    )
    nrt_control = appcli.param(
        '--no-control',
        cast=not_,
        default=True,
    )
    quench = appcli.param(
        '--quench',
        default='heat',
    )

    def get_protocol(self):
        p = stepwise.Protocol()
        anneal, mmlv = self.reactions

        if self.nrt_control:
            mmlv_nrt = deepcopy(mmlv)
            mmlv_nrt.num_reactions = 1
            anneal.num_reactions += 1
            del mmlv_nrt['SMART MMLV RT']

        p += pl(
            f"Anneal the {plural(self.primers):RT primer/s} to the {plural(self.templates):RNA template/s} [1]:",
            anneal,
        )

        p.footnotes[1] = pre("""\
This protocol is based on the official Takara 
SMART MMLV reverse transcription protocol:

https://tinyurl.com/y4ash7dl

However, I made three modifications:

- I reduced the volume of the annealing step as 
  much as possible, the increase the 
  concentrations of the oligos.

- I added buffer to the annealing step, because 
  annealing works much better with some salt to 
  shield the backbone charge.  I don't actually 
  know how much salt is in the buffer, but some is 
  better than none.

- I included the MMLV in the transcription master 
  mix, because excluding it seemed too inaccurate.  
  The changes to the annealing step also mean that 
  the master mix has a 1x buffer concentration, so 
  I don't need to worry about the enzyme being 
  unhappy.
""")
        p += "Incubate at 70°C for 3 min, then immediately cool on ice."
        p += pl(
            "Setup {plural(mmlv.num_reactions):# reverse transcription reaction/s}:",
            mmlv,
        )
        if self.nrt_control:
            p += pl(
                f"Setup a −reverse transcriptase (NRT) control:",
                mmlv_nrt,
            )

        p += "Incubate at 42°C for 60 min [2]."
        p.footnotes[2] = "Samples can be incubated for 50-90 min if necessary."

        if self.quench == 'none':
            pass

        elif self.quench == 'heat':
            p += "Incubate at 70°C for 15 min."

        elif self.quench == 'edta':
            p += "Add 4 µL 60 mM EDTA."

        else:
            raise ConfigError(
                f"unexpected value for quench parameter: {self.quench}")

        return p

    def get_reactions(self):

        # Define the annealing reaction:

        anneal = stepwise.MasterMix()
        anneal.num_reactions = self.num_reactions
        anneal.solvent = None
        anneal.extra_min_volume = 0.5, 'µL'

        template_fmol = self.volume_uL * self.template_conc_nM

        anneal['template'].name = ','.join(self.templates)
        anneal['template'].stock_conc = self.template_stock_nM, 'nM'
        anneal[
            'template'].volume = template_fmol / self.template_stock_nM, 'µL'
        anneal['template'].master_mix = 'rna' in self.master_mix
        anneal['template'].order = 2

        anneal['primer'].name = ','.join(self.primers)
        anneal['primer'].stock_conc = self.primer_stock_uM, 'µM'
        anneal[
            'primer'].volume = self.primer_excess * template_fmol / self.primer_stock_uM / 1e3, 'µL'
        anneal['primer'].master_mix = 'primer' in self.master_mix
        anneal['primer'].order = 3

        # If necessary, dilute the annealing reaction such that it will be
        # necessary to add the given volume to the RT reaction.  The purpose of
        # this is to ensure that we can pipet this volume accurately, since we
        # often want to be quantitative about how much RNA we have (e.g. qPCR).

        if anneal.volume < (self.rna_min_volume_uL, 'µL'):
            anneal.solvent = 'nuclease-free water'
            anneal.volume = self.rna_min_volume_uL, 'µL'

        anneal['first-strand buffer'].stock_conc = '5x'
        anneal['first-strand buffer'].volume = 0, 'µL'
        anneal['first-strand buffer'].volume = anneal.volume / 4
        anneal['first-strand buffer'].master_mix = bool(self.master_mix)
        anneal['first-strand buffer'].order = 1

        # Define the MMLV reaction:

        mmlv = stepwise.MasterMix("""\
                Reagent                      Stock      Volume  MM?
                ========================  ========  ==========  ===
                nuclease-free water                 to 20.0 µL  yes
                first-strand buffer             5x      4.0 µL  yes
                dNTP mix                     10 mM      2.0 µL  yes
                DTT                         100 mM      2.0 µL  yes
                SMART MMLV RT             200 U/µL      0.5 µL  yes
                annealed template/primer                0.0 µL
        """)
        mmlv.num_reactions = self.num_reactions
        mmlv.hold_ratios.volume = self.volume_uL, 'µL'
        mmlv['first-strand buffer'].volume -= anneal[
            'first-strand buffer'].volume
        mmlv['annealed template/primer'].volume = anneal.volume
        mmlv['annealed template/primer'].stock_conc = \
                anneal['template'].stock_conc * (
                        anneal['template'].volume / anneal.volume)

        # Scale the volume of the annealing reaction to guarantee that none of
        # the volume are too small to pipet accurately.

        min_pipet_volumes = {
            'template': (self.rna_min_volume_uL, 'µL'),
        }
        pipet_volumes = [
            (anneal.master_mix_volume, '0.5 µL'),
        ]
        pipet_volumes += [
            (anneal[k].volume, min_pipet_volumes.get(k, '0.5 µL'))
            for k in ['template', 'primer'] if not anneal[k].master_mix
        ]
        anneal.hold_ratios.volume *= max(
            1 + self.extra_anneal_percent / 100,
            *(limit / curr for curr, limit in pipet_volumes),
        )

        return anneal, mmlv
Esempio n. 27
0
class Lyophilize(Cleanup):
    """\
Concentrate samples by lyophilization.

Usage:
    lyophilize [-v <µL> | -c <conc>]

Options:
    -v --volume <µL>
        The volume to bring the samples to after lyophilization.  If no unit is 
        specified, µL is assumed.

    -c --conc <conc>
        The concentration to bring the samples to after lyophilization.  
        Include a unit.  It is assumed that the experimenter will measure the 
        concentration of the sample before lyophilization and calculate the 
        correct volume of buffer to add.
"""
    __config__ = [
        DocoptConfig,
        MakerConfig,
    ]
    volume = appcli.param(
        Key(DocoptConfig,
            '--volume',
            cast=lambda x: parse_volume(x, default_unit='µL')),
        Key(MakerConfig, 'volume', cast=parse_volume),
        default=None,
    )
    conc = appcli.param(
        Key(DocoptConfig, '--conc', cast=parse_conc),
        Key(MakerConfig, 'conc', cast=parse_conc),
        default=None,
    )

    group_by = {
        'volume': group_by_identity,
        'conc': group_by_identity,
    }

    def __init__(self, *, volume=None, conc=None):
        if volume: self.volume = volume
        if conc: self.conc = conc

    def get_protocol(self):
        phrases = []
        n = plural(self.product_tags)

        if self.show_product_tags:
            phrases.append(f"Concentrate the following {n:sample/s}")
        elif self.product_tags:
            phrases.append(f"Concentrate the {n:sample/s}")
        else:
            phrases.append("Concentrate the sample(s)")

        if self.volume and self.conc:
            err = UsageError(volume=self.volume, conc=self.conc)
            err.brief = "cannot specify volume and concentration"
            err.info += "volume: {volume}"
            err.info += "conc: {conc}"
            raise err
        elif self.volume:
            phrases.append(f"to {self.volume}")
        elif self.conc:
            phrases.append(f"to {self.conc}")
        else:
            pass

        if self.show_product_tags:
            phrases.append(
                f"by lyophilization: {', '.join(map(str, self.product_tags))}")
        else:
            phrases.append("by lyophilization.")

        return stepwise.Protocol(steps=[' '.join(phrases)], )

    def get_product_conc(self):
        return self.conc

    def get_product_volume(self):
        return self.volume
Esempio n. 28
0
class Ligate(Assembly):
    __doc__ = f"""\
Assemble restriction-digested DNA fragments using T4 DNA ligase.

Usage:
    ligate <assemblies>... [-c <conc>]... [-l <length>]... [options]

Arguments:
{ARGUMENT_DOC}

Options:
{OPTION_DOC}

    -k --kinase
        Add T4 polynucleotide kinase (PNK) to the reaction.  This is necessary 
        to ligate ends that are not already 5' phosphorylated (e.g. annealed 
        oligos, PCR products).

Database:
    Ligation reactions can appear in the "Synthesis" column of a FreezerBox 
    database:
        
        ligate <assembly> [volume=<µL>] [kinase=<bool>]

    <assembly>
        See the <assemblies>... command-line argument.

    volume=<µL>
        See --volume.  You must include a unit.

    kinase=<bool>
        See --kinase.  Specify "yes" or "no".
"""

    excess_insert = appcli.param(
        '--excess-insert',
        cast=float,
        default=3,
    )
    use_kinase = appcli.param(
        '--kinase',
        default=False,
    )

    def get_reaction(self):
        rxn = stepwise.MasterMix.from_text('''\
        Reagent               Stock        Volume  Master Mix
        ================  =========   ===========  ==========
        water                         to 20.00 μL         yes
        T4 ligase buffer        10x       2.00 μL         yes
        T4 DNA ligase      400 U/μL       1.00 μL         yes
        T4 PNK              10 U/μL       1.00 μL         yes
        ''')
        if not self.use_kinase:
            del rxn['T4 PNK']

        return self._add_fragments_to_reaction(rxn)

    def del_reaction(self):
        pass

    def get_protocol(self):
        p = stepwise.Protocol()
        rxn = self.reaction

        p += pl(
            f"Setup {plural(rxn.num_reactions):# ligation reaction/s}{p.add_footnotes('https://tinyurl.com/y7gxfv5m')}:",
            rxn,
        )
        p += pl(
            "Incubate at the following temperatures:",
            ul(
                "25°C for 15 min",
                "65°C for 10 min",
            ),
        )
        return p

    def del_protocol(self):
        pass
Esempio n. 29
0
class Library:
    """
    Provide access to every protocol available to the user.

    The protocols are organized into "collections", which are groups of related 
    protocols.  Which collections are available to the user at any moment can 
    depend on factors such as the current working directory, the user's 
    configuration settings, which plugins are installed, whether the internet 
    is available, etc.

    The primary role of the library is to search through every available 
    collection for protocols matching a given "tag".  Tags are fuzzy patterns 
    meant to be succinct (i.e. easily typed on the command line) yet capable of 
    specifically identifying any protocol.  See `_match_tag()` for a complete 
    description of the tag syntax.

    Strictly speaking, collections are groups of "entries" rather than 
    protocols.  Entries are protocol factories.  For the example of a 
    collection representing a directory, each entry in that collection would 
    represent a file in that directory.  The actual protocol instances would 
    come from the entries either reading or executing those files.

    Collections and entries are intended to be highly polymorphic, so that many 
    different means of accessing protocols (e.g. files, plugins, network 
    drives, websites, etc.) can be supported.
    """
    __config__ = [StepwiseConfig]

    ignore_globs = appcli.param(
        'search.ignore',
        cast=Schema([str]),
        default=[],
    )
    local_paths = appcli.param(
        'search.find',
        cast=Schema([str]),
        default=['protocols'],
    )
    global_paths = appcli.param(
        'search.path',
        cast=Schema([str]),
        default=[],
    )

    _singleton = None

    def __init__(self):
        self.collections = []

        def add(collection, i=None):
            is_available = collection.is_available()
            is_unique = collection.is_unique(self.collections)
            is_ignored = any(
                fnmatch(collection.name, x) for x in self.ignore_globs)

            if is_available and is_unique and not is_ignored:
                self.collections.insert(
                    len(self.collections) if i is None else i,
                    collection,
                )

        # Add directories found above the current working directory.
        cwd = Path.cwd().resolve()
        for parent in (cwd, *cwd.parents):
            for name in self.local_paths:
                add(PathCollection(parent / name))

        # Add specific directories specified by the user.
        for dir in self.global_paths:
            add(PathCollection(dir))

        # Add directories specified by plugins.
        for plugin in load_and_sort_plugins('stepwise.protocols'):
            try:
                add(PluginCollection(plugin))
            except AttributeError as err:
                warn(
                    f"no protocol directory specified for '{plugin.module_name}.{plugin.name}' plugin."
                )
                codicil(str(err))

        # Add the current working directory.

        # Do this after everything else, so it'll get bumped if we happen to be
        # in a directory represented by one of the other collections.  But put
        # it ahead of all the other collections, so that tags are evaluated for
        # local paths before anything else.

        add(CwdCollection(), 0)

    @classmethod
    def from_singleton(cls):
        # Use `Library._singleton` instead of `cls._singleton` so we don't end
        # up with multiple singletons.
        if Library._singleton is None:
            Library._singleton = cls()
        return Library._singleton

    def find_entries(self, tag):
        """
        Yield the best-scoring entries matching the given tag.
        """
        scored_entries = [
            scored_entry for collection in self.collections
            for scored_entry in collection.find_entries(tag)
        ]
        no_score = object()
        best_score = max((x for x, _ in scored_entries), default=no_score)
        return [
            entry for score, entry in scored_entries if score == best_score
        ]

    def find_entry(self, tag):
        """
        Return the single entry matching the given tag.

        If there are either zero or multiple entries that match the given 
        tag, an exception will be raised.
        """
        entries = self.find_entries(tag)
        return one(
            entries,
            NoProtocolsFound(tag, self.collections),
            MultipleProtocolsFound(tag, entries),
        )
Esempio n. 30
0
class RestrictionDigest(Main):
    """\
Perform restriction digests using the protocol recommended by NEB.

Usage:
    digest <templates> <enzymes> [-d <ng>] [-D <ng/µL>] [-v <µL>] [-n <rxns>]
        [-g]
    digest <product> [-e <enzymes>] [options]

Arguments:
    <templates>
        The DNA to digest.  Use commas to specify multiple templates.  The 
        number of reactions will equal the number of templates.

    <enzymes>
        The restriction enzymes to use.  Only NEB enzymes are currently 
        supported.  If you are using an "HF" enzyme, specify that explicitly.  
        For example, "HindIII" and "HindIII-HF" have different protocols.  
        Enzyme names are case-insensitive, and multiple enzymes can be 
        specified using commas.

    <product>
        A product in the FreezerBox database that was synthesized by 
        restriction digest.  If this form of the command is given, the protocol 
        will take all default values (including the template and enzymes) from 
        the reaction used to synthesize the given product.

Options:
    -d --dna <µg>               [default: ${app.dna_ug}]
        The amount of DNA to digest, in µg.

    -D --dna-stock <ng/µL>
        The stock concentration of the DNA template, in ng/µL.

    -v --target-volume <µL>     [default: ${app.target_volume_uL}]
        The ideal volume for the digestion reaction.  Note that the actual 
        reaction volume may be increased to ensure that the volume of enzyme 
        (which is determined by the amount of DNA to digest, see --dna) is less 
        than 10% of the total reaction volume, as recommended by NEB.

    -t --time <min>
        Incubate the digestion reaction for a non-standard amount of time.  You 
        may optionally specify a unit.  If you don't, minutes are assumed.

    -n --num-reactions <int>
        The number of reactions to setup.  By default, this is inferred from 
        the number of templates.

    -g --genomic
        Indicate that genomic DNA is being digested.  This will double the 
        amount of enzyme used, as recommended by NEB.

    -e --enzymes <list>
        The same as <enzymes>, but for when a product is specified.  
    """
    __config__ = [
        DocoptConfig,
        MakerConfig,
        StepwiseConfig.setup('molbio.digest'),
    ]

    class Template(ShareConfigs, Argument):
        __config__ = [ReagentConfig]

        seq = appcli.param(Key(ReagentConfig, 'seq'), )
        stock_ng_uL = appcli.param(
            Key(DocoptConfig, '--dna-stock'),
            Key(ReagentConfig, 'conc_ng_uL'),
            Key(StepwiseConfig, 'dna_stock_ng_uL'),
            cast=float,
        )
        is_circular = appcli.param(
            Key(ReagentConfig, 'is_circular'),
            default=True,
        )
        is_genomic = appcli.param(
            Key(DocoptConfig, '--genomic'),
            default=False,
        )
        target_size_bp = appcli.param(
            Key(MakerConfig, 'size', cast=parse_size_bp),
            default=None,
        )

    templates = appcli.param(
        Key(DocoptConfig, '<templates>', cast=parse_templates_from_csv),
        Key(MakerConfig, 'template', cast=parse_template_from_freezerbox),
        get=bind_arguments,
    )
    enzyme_names = appcli.param(
        Key(DocoptConfig, '<enzymes>', cast=comma_list),
        Key(DocoptConfig, '--enzymes', cast=comma_list),
        Key(MakerConfig, 'enzymes', cast=comma_list),
    )
    product_tag = appcli.param(Key(DocoptConfig, '<product>'), )
    products = appcli.param(
        Method(lambda self: [self.db[self.product_tag].make_intermediate(0)]),
    )
    num_reactions = appcli.param(
        Key(DocoptConfig, '--num-reactions', cast=int_or_expr),
        Method(lambda self: len(self.templates)),
    )
    dna_ug = appcli.param(
        Key(DocoptConfig,
            '--dna',
            cast=partial(parse_mass_ug, default_unit='µg')),
        Key(MakerConfig, 'mass', cast=parse_mass_ug),
        Key(StepwiseConfig),
        cast=float,
        default=1,
    )
    target_volume_uL = appcli.param(
        Key(DocoptConfig,
            '--target-volume',
            cast=partial(parse_volume_uL, default_unit='µL')),
        Key(MakerConfig, 'volume', cast=parse_volume_uL),
        Key(StepwiseConfig),
        default=10,
    )
    target_size_bp = appcli.param(
        Key(MakerConfig, 'size', cast=parse_size_bp),
        default=None,
    )
    time = appcli.param(
        Key(DocoptConfig,
            '--time',
            cast=partial(parse_time, default_unit='min')),
        Key(MakerConfig, 'time', cast=parse_time),
        default=None,
    )

    group_by = {
        'enzyme_names': group_by_identity,
        'dna_ug': group_by_identity,
        'target_volume_uL': group_by_identity,
        'time': group_by_identity,
    }
    merge_by = {
        'templates': join_lists,
    }

    @classmethod
    def from_tags(cls, template_tags, enzyme_names, db=None):
        templates = [cls.Template(x) for x in template_tags]
        return cls(templates, enzyme_names, db)

    @classmethod
    def from_product(cls, product_tag):
        self = cls.from_params()
        self.product_tag = product_tag
        self.load(MakerConfig)
        return self

    def __bareinit__(self):
        self._enzyme_db = None

    def __init__(self, templates, enzyme_names, db=None):
        self.templates = templates
        self.enzyme_names = enzyme_names
        self.enzyme_db = db

    def get_enzymes(self):
        return [self.enzyme_db[x] for x in self.enzyme_names]

    def get_enzyme_db(self):
        if self._enzyme_db is None:
            self._enzyme_db = NebRestrictionEnzymeDatabase()
        return self._enzyme_db

    def set_enzyme_db(self, db):
        self._enzyme_db = db

    def get_reaction(self):
        # Define a prototypical restriction digest reaction.  Stock
        # concentrations for BSA, SAM, and ATP come from the given catalog
        # numbers.

        rxn = stepwise.MasterMix.from_text("""\
        Reagent   Catalog      Stock    Volume  MM?
        ========  =======  =========  ========  ===
        water                         to 50 µL  yes
        DNA                200 ng/µL      5 µL  yes
        buffer                   10x      5 µL  yes
        bsa         B9200   20 mg/mL      0 µL  yes
        sam         B9003      32 mM      0 µL  yes
        atp         P0756      10 mM      0 µL  yes
        """)

        # Plug in the parameters the user requested.

        rxn.num_reactions = self.num_reactions

        rxn['DNA'].name = ','.join(x.tag for x in self.templates)
        rxn['DNA'].hold_conc.stock_conc = min(x.stock_ng_uL
                                              for x in self.templates), 'ng/µL'

        if len(self.templates) > 1:
            rxn['DNA'].order = -1
            rxn['DNA'].master_mix = False

        for enz in self.enzymes:
            key = enz['name']
            stock = enz['concentration'] / 1000
            is_genomic = any(x.is_genomic for x in self.templates)

            # The prototype reaction has 1 µg of DNA.  NEB recommends 10 U/µg
            # (20 U/µg for genomic DNA), so set the initial enzyme volume
            # according to that.  This will be adjusted later on.

            rxn[key].stock_conc = stock, 'U/µL'
            rxn[key].volume = (20 if is_genomic else 10) / stock, 'µL'
            rxn[key].master_mix = True

        rxn['buffer'].name = pick_compatible_buffer(self.enzymes)

        # Supplements

        known_supplements = []

        def add_supplement(key, name, unit, scale=1):
            conc = max(x['supplement'][key] for x in self.enzymes)
            known_supplements.append(key)

            if not conc:
                del rxn[key]
            else:
                rxn[key].hold_stock_conc.conc = conc * scale, unit
                rxn[key].name = name

        add_supplement('bsa', 'rAlbumin', 'mg/mL', 1e-3)
        add_supplement('sam', 'SAM', 'mM', 1e-3)
        add_supplement('atp', 'ATP', 'mM')

        # Make sure there aren't any supplements we should add that we don't
        # know about.

        for enzyme in self.enzymes:
            for supp, conc in enzyme['supplement'].items():
                if conc > 0 and supp not in known_supplements:
                    err = ConfigError(
                        enzyme=enzyme,
                        supp=supp,
                        conc=conc,
                    )
                    err.brief = "{enzyme[name]!r} requires an unknown supplement: {supp!r}"
                    err.hints += "the restriction digest protocol needs updated"
                    err.hints += "please submit a bug report"
                    raise err

        # Update the reaction volume.  This takes some care, because the
        # reaction volume depends on the enzyme volume, which in turn depends
        # on the DNA quantity.

        k = self.dna_ug / 1  # The prototype reaction has 1 µg DNA.
        dna_vol = k * rxn['DNA'].volume
        enz_vols = {
            enz['name']: k * rxn[enz['name']].volume
            for enz in self.enzymes
        }
        enz_vol = sum(enz_vols.values())

        rxn.hold_ratios.volume = max(
            stepwise.Quantity(self.target_volume_uL, 'µL'),
            10 * enz_vol,

            # This is a bit of a hack.  The goal is to keep the water
            # volume non-negative, but it won't necessarily work if there
            # are supplements.
            10 / 9 * (dna_vol + enz_vol),
        )

        rxn['DNA'].volume = dna_vol
        for enz in self.enzymes:
            key = enz['name']
            rxn[key].volume = enz_vols[key]

        return rxn

    def del_reaction(self):
        pass

    def get_protocol(self):
        from itertools import groupby
        from operator import itemgetter

        protocol = stepwise.Protocol()
        rxn = self.reaction
        rxn_type = (self.enzymes[0]['name']
                    if len(self.enzymes) == 1 else 'restriction')

        def incubate(temp_getter,
                     time_getter,
                     time_formatter=lambda x: f'{x} min'):
            incubate_params = [
                (k, max(time_getter(x) for x in group))
                for k, group in groupby(self.enzymes, temp_getter)
            ]
            return [
                f"{temp}°C for {time_formatter(time)}"
                for temp, time in sorted(incubate_params)
            ]

        if self.time:
            digest_steps = incubate(
                itemgetter('incubateTemp'),
                lambda x: self.time,
                lambda x: x,
            )
        else:
            digest_steps = incubate(
                itemgetter('incubateTemp'),
                lambda x: 15 if x['timeSaver'] else 60,
                lambda x: '5–15 min' if x == 15 else '1 hour',
            )

        inactivate_steps = incubate(
            itemgetter('heatInactivationTemp'),
            itemgetter('heatInactivationTime'),
        )

        protocol += pl(
            f"Setup {plural(rxn.num_reactions):# {rxn_type} digestion/s} [1,2]:",
            rxn,
        )
        protocol += pl(
            f"Incubate at the following temperatures [3]:",
            ul(*digest_steps, *inactivate_steps),
        )

        urls = [x['url'] for x in self.enzymes if x.get('url')]
        protocol.footnotes[1] = pl(*urls, br='\n')
        protocol.footnotes[2] = """\
NEB recommends 5–10 units of enzyme per µg DNA 
(10–20 units for genomic DNA).  Enzyme volume 
should not exceed 10% of the total reaction 
volume to prevent star activity due to excess 
glycerol.
"""
        protocol.footnotes[3] = """\
The heat inactivation step is not necessary if 
the DNA will be purified before use.
"""
        return protocol

    def del_protocol(self):
        pass

    def get_dependencies(self):
        return {x.tag for x in self.templates}

    def get_product_seqs(self):
        for template in self.templates:
            with ConfigError.add_info("tag: {tag}", tag=template.tag):
                yield calc_digest_product(
                    seq=template.seq,
                    enzymes=self.enzyme_names,
                    target_size=template.target_size_bp,
                    is_circular=template.is_circular,
                )

    def get_product_conc(self):
        return self.reaction['DNA'].conc

    def get_product_volume(self):
        return self.reaction.volume