Beispiel #1
0
class StepwiseCommand:
    brief = appcli.config_attr()
    dirs = appcli.config_attr()
    usage_io = sys.stderr

    def __init__(self):
        self.quiet = False
        self.force_text = False
Beispiel #2
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)
        ]
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
Beispiel #4
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)
class LaserScanner(Main):
    """\
Image a gel using a laser scanner.

Usage:
    laser_scanner <optics>...

<%! from stepwise_mol_bio import hanging_indent %>\
Arguments:
    <optics>
        A laser/filter combination to use.  The most convenient way to specify 
        such a combination is to give the name of a preset.  The following 
        presets are currently available: 

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

        You can define new presets by adding blocks like the following to your 
        stepwise configuration file:

            [molbio.laser.presets.name]
            laser = <int> or <list of ints>
            filter = <str> or <list of strs>

        You can also explicitly provide laser and filter parameters using the 
        following syntax:

            <laser>/<filter>
"""
    __config__ = [
        DocoptConfig,
        PresetConfig,
        StepwiseConfig.setup('molbio.laser'),
    ]

    presets = appcli.param(
        appcli.Key(StepwiseConfig, 'presets'),
        pick=list,
    )
    preset_briefs = appcli.config_attr()
    preset_brief_template = '{laser} nm'

    # This attribute is a list of:
    # - name of preset (str)
    # - laser/filter (str)
    # - {laser: filter} (dict)
    optics = appcli.param(
        appcli.Key(DocoptConfig, '<optics>'),
        default_factory=list,
    )

    def __init__(self, *optics):
        self.optics = list(optics)

    @classmethod
    def from_laser_filter_pair(cls, laser, filter):
        self = cls()
        self.optics.append({
            'laser': laser,
            'filter': filter,
        })
        return self

    def get_protocol(self):
        optics = [self.parse_optics(x) for x in self.optics]
        lasers = [f"{plural(optics):laser/s}:"
                  ] + [f"{x['laser']} nm" for x in optics]
        filters = [f"{plural(optics):filter/s}:"
                   ] + [x['filter'] for x in optics]

        p = stepwise.Protocol()
        p += pl(
            "Image with a laser scanner:",
            table([lasers, filters], align='<>>'),
        )
        return p

    def parse_optics(self, optics):
        if isinstance(optics, dict):
            return optics

        try:
            # Might be good to make it so that the `presets` attribute it a
            # `Presets` instance rather than a list.  This would affect a bunch
            # of protocol, though.
            return Presets(self.presets)[optics]
        except KeyError:
            try:
                laser, filter = optics.split('/')
                return {'laser': laser, 'filter': filter}
            except ValueError as err:
                raise UsageError(
                    f"expected a preset or '<laser>/<filter>', got {optics!r}"
                ) from None
class SpinCleanup(Cleanup):
    """\
Purify a PCR reaction using a silica spin column.

Usage:
    spin_cleanup [<preset>] [-s <µL>] [-d <buffer>] [-v <µL>]

<%! from stepwise_mol_bio import hanging_indent %>\
Arguments:
    <preset>                        [default: ${app.preset}]
        The default parameters to use.  Typically these correspond to 
        commercial kits:

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

Options:
    -s --sample-volume <µL>
        The volume of the sample, in µL.

    -d --elute-buffer <name>
        The buffer to elute in.

    -v --elute-volume <µL>
        The volume of purified DNA/RNA to elute, in µL.  The default value 
        depends on the preset, but can usually be lowered to get more 
        concentrated product.  A warning will be displayed if the requested 
        volume is lower than the minimum recommended by the kit manufacturer.

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

        ${hanging_indent(app.config_paths, 8)}

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

    molbio.spin_cleanup.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.spin_cleanup.presets.<name>.protocol_name
        How to refer to the whole protocol.  Commonly this is the name of the 
        spin column kit.

    molbio.spin_cleanup.presets.<name>.protocol_link
        A link (typically minified) to the complete protocol, e.g. as published 
        by the manufacturer of the columns.  This is not required, but if 
        specified, will be included in the protocol as a footnote.

    molbio.spin_cleanup.presets.<name>.column_name
        How to refer to the specific spin column used in the protocol.

    molbio.spin_cleanup.presets.<name>.spin_speed_g
        How fast to spin the column in each centrifugation step, in units of 
        g-force.

    molbio.spin_cleanup.presets.<name>.column_capacity_ug
        The maximum binding capacity of the column, in µg.  This information is 
        added to the protocol as a footnote.

    molbio.spin_cleanup.presets.<name>.sample_type
        How to generically refer to the sample in the protocol, e.g. "DNA".

    molbio.spin_cleanup.presets.<name>.sample_volume_uL
        The volume of sample to load on the column, in µL.  Alternatively, this 
        can be a dictionary with keys 'min' and/or 'max' specifying the minimum 
        and maximum allowed sample volumes, respectively.

    molbio.spin_cleanup.presets.<name>.bind_buffer
        The name(s) of the buffer(s) to use to bind the sample to column.  This 
        can be either a string or a list of strings.  Use a list to specify 
        that multiple buffers (e.g. binding buffer and ethanol) should be mixed 
        with the sample before it is loaded on the column.  If this option is a 
        list, the `bind_volume_uL` and `bind_volume_x` options must also be 
        lists of the same length (or left unspecified).
        
    molbio.spin_cleanup.presets.<name>.bind_volume_uL
        How much `bind_buffer` to use, in µL.  This can be either a number or a 
        list of numbers; see `bind_buffer` for more details.  This takes 
        precedence over the `bind_volume_x` setting.  

    molbio.spin_cleanup.presets.<name>.bind_volume_x
        How much `bind_buffer` to use, as a multiple of the sample volume.  
        This can be a number or a list of numbers; see `bind_buffer` for more 
        details.  This is superseded by the `bind_volume_uL` setting.  

    molbio.spin_cleanup.presets.<name>.bind_spin_sec
        How long to centrifuge the column during the bind step.

    molbio.spin_cleanup.presets.<name>.bind_vacuum
        Whether or not to use a vacuum manifold for the bind step.  The default 
        is False.  If True, the `bind_spin_sec` option is ignored.

    molbio.spin_cleanup.presets.<name>.pH_buffer
        The name of the buffer to use when adjusting the pH of the sample.

    molbio.spin_cleanup.presets.<name>.pH_volume_uL
        How much `pH_buffer` to use, in µL.  This takes precedence over the 
        `pH_volume_x` setting.

    molbio.spin_cleanup.presets.<name>.pH_volume_x
        How much `pH_buffer` to use, as a multiple of the sample volume.  
        This is superseded by the `pH_volume_uL` setting.

    molbio.spin_cleanup.presets.<name>.pH_color
        The color the sample/binding buffer should be after reaching the 
        correct pH.

    molbio.spin_cleanup.presets.<name>.wash_buffer
        The name of the buffer to use when washing the column.  This can either 
        be a string or a list of strings.  Use a list to specify that there 
        should be multiple wash steps.  If this option is a list, the 
        `wash_volume_uL`, `wash_spin_sec`, and `wash_vacuum` options must also 
        be lists of the same length (or left unspecified).

    molbio.spin_cleanup.presets.<name>.wash_volume_uL
        The volume of `wash_buffer` to use, in µL.  This can either be a number 
        or a list of numbers; see `wash_buffer` for more details.

    molbio.spin_cleanup.presets.<name>.wash_spin_sec
        How long to centrifuge the column during the wash step.  This can 
        either be a number or a list of numbers; see `wash_buffer` for more 
        details.

    molbio.spin_cleanup.presets.<name>.wash_vacuum
        Whether or not to use a vacuum manifold for the wash step.  This can 
        either be a boolean or a list of booleans; see `wash_buffer` for more 
        details.  The default is False.  If True, the `wash_spin_sec` option is 
        ignored.

    molbio.spin_cleanup.presets.<name>.dry_spin_sec
        How long to centrifuge the column after the wash step(s), e.g. to 
        remove any residual ethanol.  If left unspecified, this step will not 
        be included in the protocol.

    molbio.spin_cleanup.presets.<name>.elute_buffer
        The default value for the `--elute-buffer` flag.

    molbio.spin_cleanup.presets.<name>.elute_volume_uL
        The default value for the `--elute-volume` flag.

    molbio.spin_cleanup.presets.<name>.elute_min_volume_uL
        The minimum recommended volume to elute in.  Smaller volumes can still 
        be specified, but will be accompanied by a warning.

    molbio.spin_cleanup.presets.<name>.elute_wait_sec
        How long to incubate the column with elution buffer before eluting, in 
        seconds.

    molbio.spin_cleanup.presets.<name>.elute_spin_sec
        How long to centrifuge the column when eluting.

Database:
    Spin-column cleanup protocols can appear in the "Cleanups" column of a 
    FreezerBox database:

        spin-cleanup [<preset>] [volume=<µL>] [buffer=<name>]
    
    <preset>
        See `<preset>`.

    volume=<µL>
        See `--elute-volume`.  Must specify a unit.

    buffer=<µL>
        See `--elute-buffer`.
"""
    __config__ = [
            DocoptConfig,
            MakerConfig,
            PresetConfig,
            StepwiseConfig.setup('molbio.spin_cleanup'),
    ]
    preset_briefs = appcli.config_attr()
    config_paths = appcli.config_attr()
    preset_brief_template = '{protocol_name}'

    presets = appcli.param(
            Key(StepwiseConfig, 'presets'),
            pick=list,
    )
    preset = appcli.param(
            Key(DocoptConfig, '<preset>'),
            Key(MakerConfig, 1),
            Key(StepwiseConfig, 'default_preset'),
    )
    protocol_name = appcli.param(
            Key(PresetConfig, 'protocol_name'),
    )
    protocol_link = appcli.param(
            Key(PresetConfig, 'protocol_link'),
            default=None,
    )
    column_name = appcli.param(
            Key(PresetConfig, 'column_name'),
            default='silica spin column',
    )
    spin_speed_g = appcli.param(
            Key(PresetConfig, 'spin_speed_g'),
            default=None,
    )
    column_capacity_ug = appcli.param(
            Key(PresetConfig, 'column_capacity_ug'),
            default=None,
    )
    sample_type = appcli.param(
            Key(PresetConfig, 'sample_type'),
            default='DNA',
    )
    sample_volume_uL = appcli.param(
            Key(DocoptConfig, '--sample-volume', cast=float),
            default=None,
    )
    target_sample_volume_uL = appcli.param(
            Key(PresetConfig, 'sample_volume_uL'),
            default=None,
    )
    bind_buffer = appcli.param(
            Key(PresetConfig, 'bind_buffer'),
    )
    bind_volume_uL = appcli.param(
            Key(PresetConfig, 'bind_volume_uL'),
            default=None
    )
    bind_volume_x = appcli.param(
            Key(PresetConfig, 'bind_volume_x'),
            default=None
    )
    bind_spin_sec = appcli.param(
            Key(PresetConfig, 'bind_spin_sec'),
            default=None
    )
    bind_vacuum = appcli.param(
            Key(PresetConfig, 'bind_vacuum'),
            default=False,
    )
    ph_buffer = appcli.param(
            Key(PresetConfig, 'pH_buffer'),
            default=None,
    )
    ph_volume_uL = appcli.param(
            Key(PresetConfig, 'pH_volume_uL'),
            default=None
    )
    ph_volume_x = appcli.param(
            Key(PresetConfig, 'pH_volume_x'),
            default=None
    )
    ph_color = appcli.param(
            Key(PresetConfig, 'pH_color'),
    )
    wash_buffer = appcli.param(
            Key(PresetConfig, 'wash_buffer'),
    )
    wash_volume_uL = appcli.param(
            Key(PresetConfig, 'wash_volume_uL'),
    )
    wash_spin_sec = appcli.param(
            Key(PresetConfig, 'wash_spin_sec'),
            default=None,
    )
    wash_vacuum = appcli.param(
            Key(PresetConfig, 'wash_vacuum'),
            default=False,
    )
    dry_spin_sec = appcli.param(
            Key(PresetConfig, 'dry_spin_sec'),
            default=None,
    )
    elute_buffer = appcli.param(
            Key(DocoptConfig, '--elute-buffer'),
            Key(MakerConfig, 'buffer'),
            Key(PresetConfig, 'elute_buffer'),
    )
    elute_volume_uL = appcli.param(
            Key(DocoptConfig, '--elute-volume', cast=float),
            Key(MakerConfig, 'volume', cast=parse_volume_uL),
            Key(PresetConfig, 'elute_volume_uL'),
    )
    elute_min_volume_uL = appcli.param(
            Key(PresetConfig, 'elute_min_volume_uL'),
            default=None,
    )
    elute_wait_sec = appcli.param(
            Key(PresetConfig, 'elute_wait_sec'),
            default=None,
    )
    elute_spin_sec = appcli.param(
            Key(PresetConfig, 'elute_spin_sec'),
    )

    group_by = {
            'preset': group_by_identity,
            'elute_buffer': group_by_identity,
            'elute_volume_uL': group_by_identity,
    }

    def __init__(self, preset=None):
        if preset is not None:
            self.preset = preset

    def get_protocol(self):
        p = stepwise.Protocol()
        pl = stepwise.paragraph_list()
        ul = stepwise.unordered_list()

        def break_if_too_long(pl, ul, n=4):
            if len(ul) > n:
                ul = stepwise.unordered_list()
                pl += ul
            return ul

        footnotes = []
        if self.protocol_link:
            footnotes.append(self.protocol_link)
        if self.column_capacity_ug:
            footnotes.append(f"Column capacity: {self.column_capacity_ug} µg")

        if self.product_tags and self.show_product_tags:
            product_tags = oxford_comma(self.product_tags) + ' '
        else:
            product_tags = ''

        p += pl
        pl += f"Purify {product_tags}using {self.protocol_name}{p.add_footnotes(*footnotes)}:"
        pl += ul

        if self.spin_speed_g:
            ul += f"Perform all spin steps at {self.spin_speed_g}g."

        ## Dilute
        if x := self.target_sample_volume_uL:
            v = self.sample_volume_uL

            if not isinstance(x, dict):
                target = f'{x} µL'
                skip = v and v == x
                self.sample_volume_uL = x
            elif 'min' in x and 'max' in x:
                target = f"between {x['min']}–{x['max']} µL"
                skip = v and x['min'] <= v <= x['max']
            elif 'min' in x:
                target = f"at least {x['min']} µL"
                skip = v and x['min'] <= v
            elif 'max' in x:
                target = f"at most {x['max']} µL"
                skip = v and v <= x['max']

            if not skip:
                ul += f"Ensure that the sample is {target}."

        ## Bind
        bind_params = zip_params(
                self.bind_buffer,
                self.bind_volume_x,
                self.bind_volume_uL,
        )
        for bind_buffer, bind_volume_x, bind_volume_uL in bind_params:
            bind_volume = resolve_volume(bind_volume_uL, bind_volume_x, self.sample_volume_uL)
            ul += f"Add {bind_volume} {bind_buffer} to the crude {self.sample_type}."

        if self.ph_buffer:
            ph_volume = resolve_volume(self.ph_volume_uL, self.ph_volume_x, self.sample_volume_uL)
            ul += f"If not {self.ph_color}: Add {ph_volume} {self.ph_buffer}."

        ul += f"Load on a {self.column_name}."
        ul += flush_column(self.bind_spin_sec, self.bind_vacuum)
        ul = break_if_too_long(pl, ul)

        ## Wash
        wash_params = zip_params(
                self.wash_buffer,
                self.wash_volume_uL,
                self.wash_spin_sec,
                self.wash_vacuum,
        )
        for wash_buffer, wash_volume_uL, wash_spin_sec, wash_vacuum in wash_params:
            ul += f"Add {wash_volume_uL} µL {wash_buffer}."
            ul += flush_column(wash_spin_sec, wash_vacuum)

        ## Dry
        if self.dry_spin_sec:
            ul += flush_column(self.dry_spin_sec)

        ul = break_if_too_long(pl, ul)

        ## Elute
        if self.elute_volume_uL < self.elute_min_volume_uL:
            warn(f"Elution volume ({self.elute_volume_uL} µL) is below the recommended minimum ({self.elute_min_volume_uL} µL).")

        ul += f"Add {self.elute_volume_uL} µL {self.elute_buffer}."
        if self.elute_wait_sec:
            ul += f"Wait at least {format_sec(self.elute_wait_sec)}."
        ul += flush_column(self.elute_spin_sec, keep_flowthrough=True)

        return p
Beispiel #7
0
class Gel(Main):
    """\
Load, run, and stain gels.

Usage:
    gel <preset> <samples> [options]

<%! from stepwise_mol_bio import hanging_indent %>\
Arguments:
    <preset>
        What kind of gel to run.  The following presets are available:

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

    <samples>
        The names of the samples to run, separated by commas.  This can also be 
        a number, which will be taken as the number of samples to run.

Options:
    -p --percent <number>
        The percentage of polyacrylamide/agarose in the gel being run.

    -a --additive <str>
        An extra component to include in the gel itself, e.g. 1x EtBr.

    -b --buffer <str>
        The buffer to run the gel in, e.g. TAE.

    -c --sample-conc <value>
        The concentration of the sample.  This will be used to scale how much 
        sample is mixed with loading buffer, with the goal of mixing the same 
        quantity of material specified in the preset.  In order to use this 
        option, the preset must specify a sample concentration.  The units of 
        that concentration will be used for this concentration.

    -v --sample-volume <µL>
        The volume of sample to mix with loading buffer, in µL.  This does not 
        scale the concentration, and may increase or decrease the amount of 
        sample loaded relative to what's specified in the preset.

    --mix-volume <µL>
        The volume of the sample/loading buffer mix to prepare for each sample.  
        For example, if you want to run two gels, but the preset only makes 
        enough mix for one, use this option to make more.

    --mix-extra <percent>
        How much extra sample/loading buffer mix to make.

    -M --no-mix
        Don't describe how to prepare the sample/loading buffer mix.

    --incubate-temp <°C>
        What temperature to incubate the sample/loading buffer at before 
        loading it onto the gel.  The incubation step will be skipped if 
        neither `--incubate-temp` nor `--incubate-time` are specified (either 
        on the command-line or via the preset).

    --incubate-time <min>
        How long to incubate the sample/loading buffer at the specified 
        temperature before loading it onto the gel.  The incubation step will 
        be skipped if neither `--incubate-temp` nor `--incubate-time` are 
        specified (either on the command-line or via the preset).

    --prerun-volts
        The voltage to pre-run the gel at.  By default, this will be the same 
        as the voltage the gel is run at.

    --prerun-time
        How long to prerun the gel, in minutes.

    -l --load-volume <µL>
        The volume of the sample/loading buffer mix to load onto the gel.

    --run-volts <V>
        The voltage to run the gel at.

    -r --run-time <min>
        How long to run the gel, in minutes.

    -s --stain <command>
        The name (and arguments) of a protocol describing how to stain the gel.  
        For example, this could be 'gelred' or 'coomassie -f'.
        
    -S --no-stain
        Don't describe how to stain/visualize the gel.

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

        ${hanging_indent(app.config_paths, 8)}

    molbio.gel.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.gel.presets.<name>.title:
        How to briefly describe the gel in the protocol.  The default is 
        "electrophoresis".

    molbio.gel.presets.<name>.inherit:
        Copy all settings from another preset.  This can be used to make small 
        tweaks to a gel protocol, e.g. "SDS PAGE at lower-than-usual voltage".

    molbio.gel.presets.<name>.sample_mix:
        A table describing how to prepare the samples for the gel, in the 
        format understood by `stepwise.MasterMix.from_string()`.  The table 
        must contain one reagent named "sample".  This will be replaced with 
        the actual sample name specified on the command line, if possible.  If 
        no table is specified, the sample mix step will be left out of the 
        protocol.

    molbio.gel.presets.<name>.sample_conc:
        The default value for the `--sample-conc` option.

    molbio.gel.presets.<name>.sample_volume_uL:
        The default value for the `--sample-volume` option.

    molbio.gel.presets.<name>.ladder:
        The name of the ladder to use with this gel.

    molbio.gel.presets.<name>.ladder_volume_uL:
        How much ladder to load, in µL.

    molbio.gel.presets.<name>.mix_volume_uL:
        The default value for the `--mix-volume` option.
        
    molbio.gel.presets.<name>.mix_extra_percent:
        The default value for the `--mix-extra` option.

    molbio.gel.presets.<name>.incubate_temp_C:
        The default value for the `--incubate-temp` option.

    molbio.gel.presets.<name>.incubate_time_min:
        The default value for the `--incubate-time` option.

    molbio.gel.presets.<name>.gel_type:
        What kind of gel to use, e.g. "Bis-Tris/MES SDS PAGE", "TBE/urea PAGE", 
        "TAE/agarose", etc.  Don't include the gel percentage here; use the 
        `gel_percent` setting for that.

    molbio.gel.presets.<name>.gel_percent:
        The default value for the `--percent` option.

    molbio.gel.presets.<name>.gel_additive:
        The default value for the `--additive` option.

    molbio.gel.presets.<name>.gel_buffer:
        The default value for the `--buffer` option.
        
    molbio.gel.presets.<name>.load_volume_uL:
        The default value for the `--load-volume` option.

    molbio.gel.presets.<name>.prerun_volts:
        The default value for the `--prerun-volts` option.

    molbio.gel.presets.<name>.prerun_time_min:
        The default value for the `--prerun-time` option.

    molbio.gel.presets.<name>.run_volts:
        The default value for the `--run-volts` option.

    molbio.gel.presets.<name>.run_time_min:
        The default value for the `--run-time` option.

    molbio.gel.presets.<name>.stain:
        The default value for the `--stain` option.  If unspecified, there will 
        be no staining step by default.

    molbio.gel.presets.<name>.protocol_link:
        A hyperlink to an online description of the protocol, e.g. from the gel 
        manufacturer.  This link will be included as a footnote.
"""

    __config__ = [
        DocoptConfig,
        PresetConfig,
        StepwiseConfig.setup('molbio.gel'),
    ]
    preset_briefs = appcli.config_attr()
    config_paths = appcli.config_attr()

    presets = appcli.param(
        Key(StepwiseConfig, 'presets'),
        pick=list,
    )
    preset = appcli.param(Key(DocoptConfig, '<preset>'), )
    title = appcli.param(
        Key(PresetConfig, 'title'),
        default='electrophoresis',
    )
    num_samples = appcli.param(
        Key(DocoptConfig, '<samples>', cast=parse_num_samples),
        ignore=None,
        default=1,
    )
    sample_name = appcli.param(
        Key(DocoptConfig, '<samples>', cast=parse_sample_name),
        default=None,
    )
    sample_mix_str = appcli.param(
        Key(DocoptConfig, '--no-mix', cast=lambda x: None),
        Key(PresetConfig, 'sample_mix'),
        default=None,
    )
    sample_conc = appcli.param(
        Key(DocoptConfig, '--sample-conc'),
        Key(PresetConfig, 'sample_conc'),
        cast=float,
        default=None,
    )
    sample_volume_uL = appcli.param(
        Key(DocoptConfig, '--sample-volume'),
        Key(PresetConfig, 'sample_volume_uL'),
        cast=float,
        default=None,
    )
    ladder_name = appcli.param(
        Key(PresetConfig, 'ladder'),
        default=None,
    )
    ladder_volume_uL = appcli.param(Key(PresetConfig, 'ladder_volume_uL'), )
    mix_volume_uL = appcli.param(
        Key(DocoptConfig, '--mix-volume'),
        Key(PresetConfig, 'mix_volume_uL'),
        cast=float,
        default=None,
    )
    mix_extra_percent = appcli.param(
        Key(DocoptConfig, '--mix-extra'),
        Key(PresetConfig, 'mix_extra_percent'),
        cast=float,
        default=50,
    )
    incubate_temp_C = appcli.param(
        Key(DocoptConfig, '--incubate-temp'),
        Key(PresetConfig, 'incubate_temp_C'),
        cast=float,
    )
    incubate_time_min = appcli.param(
        Key(DocoptConfig, '--incubate-time'),
        Key(PresetConfig, 'incubate_time_min'),
        cast=int,
    )
    gel_type = appcli.param(Key(PresetConfig, 'gel_type'), )
    gel_percent = appcli.param(
        Key(DocoptConfig, '--percent'),
        Key(PresetConfig, 'gel_percent'),
    )
    gel_additive = appcli.param(
        Key(DocoptConfig, '--additive'),
        Key(PresetConfig, 'gel_additive'),
        default=None,
    )
    gel_buffer = appcli.param(
        Key(DocoptConfig, '--buffer'),
        Key(PresetConfig, 'gel_buffer'),
    )
    load_volume_uL = appcli.param(
        Key(DocoptConfig, '--load-volume'),
        Key(PresetConfig, 'load_volume_uL'),
        cast=float,
    )
    prerun_volts = appcli.param(
        Key(DocoptConfig, '--prerun-volts'),
        Key(PresetConfig, 'prerun_volts'),
        Method(lambda self: self.run_volts),
        cast=float,
    )
    prerun_time_min = appcli.param(
        Key(DocoptConfig, '--prerun-time'),
        Key(PresetConfig, 'prerun_time_min'),
        cast=int,
        default=None,
    )
    run_volts = appcli.param(
        Key(DocoptConfig, '--run-volts'),
        Key(PresetConfig, 'run_volts'),
        cast=float,
    )
    run_time_min = appcli.param(
        Key(DocoptConfig, '--run-time'),
        Key(PresetConfig, 'run_time_min'),
        cast=int,
    )
    stain = appcli.param(
        Key(DocoptConfig, '--stain'),
        Key(DocoptConfig, '--no-stain', cast=lambda x: None),
        Key(PresetConfig, 'stain'),
        default=None,
    )
    protocol_link = appcli.param(
        Key(PresetConfig, 'protocol_link'),
        default=None,
    )

    def __init__(self, preset, num_samples=None):
        self.preset = preset
        self.num_samples = num_samples

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

        if self.sample_mix:
            p += self.prep_step

        p += self.run_step

        if self.stain:
            p += stepwise.load(self.stain)

        return p

    def del_protocol(self):
        pass

    def get_prep_step(self):
        def both_or_neither(key1, key2):
            has_key1 = has_key2 = True

            try:
                value1 = getattr(self, key1)
            except AttributeError:
                has_key1 = False

            try:
                value2 = getattr(self, key2)
            except AttributeError:
                has_key2 = False

            if has_key1 and not has_key2:
                raise ConfigError(f"specified {key1!r} but not {key2!r}")
            if has_key2 and not has_key1:
                raise ConfigError(f"specified {key2!r} but not {key1!r}")

            if has_key1 and has_key2:
                return value1, value2
            else:
                return False

        s = pl(
            f"Prepare {plural(self.num_samples):# sample/s} for {self.title}:",
            self.sample_mix,
        )
        if x := both_or_neither('incubate_temp_C', 'incubate_time_min'):
            temp_C, time_min = x
            s += ul(f"Incubate at {temp_C:g}°C for {time_min:g} min.")

        return s
Beispiel #8
0
class Pcr(Main):
    """\
Amplify a DNA template using polymerase chain reaction (PCR).

Usage:
    pcr <amplicon>... [-a <°C>] [-x <sec> | -l <kb>] [options]
    pcr (-u <product>) [options]

Arguments:
    <amplicon>
        The names of the templates and forward/reverse primers to use for each 
        reaction, separated by commas.  If a `freezerbox` database is present 
        and these names can be found in it, default values for a number of 
        parameters (e.g. annealing temperate, extension time, etc.) will be 
        derived from it.

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

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

    -u --product <name>
        The name of a product to make.  This name must be present in the 
        FreezerBox database.  In this form of the command, all default settings 
        will be taken from the reaction used to synthesize the given product.

    -l --amplicon-length <bp>
        The length of the amplicon in base pairs (bp).  This can be used to 
        calculate an appropriate extension time.

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

    -v --reaction-volume <μL>
        The volume of the PCR reaction.

    -V --template-volume <µL>
        The volume of template to use in the reaction.  This overrides the 
        value specified by the preset.

    -T --template-stock <conc>
        The stock concentration of template to use in the reaction.  This 
        overrides the value specified by the preset, without affecting the 
        volume.  Include a unit, because none is implied.

    --primer-stock <µM>
        The stock concentration of the primers in µM.  Both primers must have 
        the same concentration.

    -y --num-cycles <n>
        The number of denature/anneal/extend cycles to perform, e.g. 35.

    --initial-denature-temp <°C>
        The temperature of the initial denaturation step in °C, e.g. 95.

    --initial-denature-time <sec>
        The duration of the initial denaturation step in seconds, e.g. 30.

    --denature-temp <°C>
        The temperature of the denaturation step in °C, e.g. 95.

    --denature-time <sec>
        The duration of the denaturation step in seconds, e.g. 10.

    -a --anneal-temp <°C>
        The temperature of the annealing step in °C, e.g. 60.  This is 
        determined by the sequence of the primers.
        
    -g --anneal-temp-gradient <range>
        The range of annealing temperatures that should be tried in a gradient 
        PCR reaction.  The range will be centered at the indicated annealing 
        temperature, and the protocol will indicate the corresponding high and 
        low temperatures.

    --anneal-time <sec>
        The duration of the annealing step in seconds, e.g. 20.

    --extend-temp <°C>
        The temperature of the extension step in °C, e.g. 72.
        
    -x --extend-time <sec>
        The duration of the extension step in seconds.  The rule of thumb is
        30 sec/kb, perhaps longer if you're amplifying a whole plasmid.

    --no-round-extend-time 
        When calculating an extension time from a given amplicon length (e.g. 
        `-l`), do *not* round the result to the nearest 30 sec increment.  Note 
        that when an explicit extension time is given (e.g. `-x`), it is never 
        rounded.

    --final-extend-temp <°C>
        The temperature of the final extension step in °C, e.g. 72.

    --final-extend-time <sec>
        The duration of the annealing step in seconds, e.g. 120.

    --hold-temp <°C>
        The temperature in °C to hold the reaction at after it completes.

    --melt-curve-low-temp <°C>
        The temperature in °C at which to begin recording a melt curve,
        e.g. 65.  This is only relevant for qPCR protocols.

    --melt-curve-high-temp <°C>
        The temperature in °C at which to stop recording a melt curve,
        e.g. 95.  This is only relevant for qPCR protocols.

    --melt-curve-temp-step <°C>
        How much to increment the temperature in °C at each step in the melt 
        curve, e.g. 0.5°C.  This is only relevant for qPCR protocols.

    --melt-curve-time-step <sec>
        The duration in seconds of each step in the melt curve, e.g. 5.  This 
        is only relevant for qPCR protocols.

    --two-step
        Specify that the annealing and extension steps should be combined, e.g. 
        if the primer melting temperatures are very close to the extension 
        temperature or if the amplicon is very short.

    --qpcr
        Specify that this is a qPCR protocol, i.e. that fluorescence should be 
        measured after each thermocyler cycle.
"""

    __config__ = [
            DocoptConfig.setup(usage_getter=lambda self: self.format_usage()),
            MakerConfig,
            PresetConfig,
            StepwiseConfig.setup('molbio.pcr'),
    ]
    usage = appcli.config_attr()
    preset_briefs = appcli.config_attr()

    @autoprop
    class Amplicon(ShareConfigs, Argument):
        __config__ = []

        def _calc_seq(self):
            try:
                return find_amplicon(
                        self.template.seq,
                        self.fwd.seq,
                        self.rev.seq,
                        self.template.is_circular,
                )
            except ValueError:
                err = ConfigError(
                        template=self.template,
                        fwd=self.fwd,
                        rev=self.rev,
                )
                err.brief = "{fwd.tag!r} and {rev.tag!r} do not amplify {template.tag!r}"
                err.info += "{template.tag}: {template.seq}"
                err.info += "{fwd.tag}: {fwd.seq}"
                err.info += "{rev.tag}: {rev.seq}"
                raise err from None

        seq = appcli.param(
                Method(_calc_seq),
        )
        length_bp = appcli.param(
                Key(DocoptConfig, '--amplicon-length', cast=float),
                Method(lambda self: len(self.seq)),
                default=None
        )

        @classmethod
        def from_tags(cls, template, fwd, rev, **kwargs):
            return cls(
                    Pcr.Template(template),
                    Pcr.Primer(fwd),
                    Pcr.Primer(rev),
                    **kwargs,
            )

        def __init__(self, template, fwd, rev, **kwargs):
            self.template = template
            self.fwd = fwd
            self.rev = rev
            self._set_known_attrs(kwargs)

        def __repr__(self):
            return f'{self.__class__.__qualname__}({self.template!r}, {self.fwd!r}, {self.rev!r})'

        def get_reagents(self):
            return self.template, *self.primers

        def get_reagent_tags(self):
            return tuple(x.tag for x in self.reagents)

        def get_primers(self):
            return self.fwd, self.rev

        def set_primers(self, primers):
            self.fwd, self.rev = primers

        def on_bind(self, app):
            super().on_bind(app)
            for reagent in self.reagents:
                reagent.bind(app)

    class Template(Argument):
        __config__ = [ReagentConfig]

        seq = appcli.param('seq')
        is_circular = appcli.param('is_circular')

    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'),
        )

    def _calc_anneal_temp_C(self):
        return [
                self.anneal_temp_func(
                    amplicon.fwd.melting_temp_C,
                    amplicon.rev.melting_temp_C,
                )
                for amplicon in self.amplicons
        ]

    def _calc_extend_time_s(self):
        bp = min(bp for x in self.amplicons if (bp := x.length_bp))
Beispiel #9
0
class Stain(Main):
    """\
Stain a gel.

Usage:
    stain <preset> [-t <min>] [-i <cmd> | -I]

<%! from stepwise_mol_bio import hanging_indent %>\
Arguments:
    <preset>
        The default parameters to use.  The following presets are available:

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

Options:
    -t --time <min>
        How long to incubate the gel in the stain, in minutes.

    -i --imaging-protocol <cmd>

    -I --no-imaging
        Don't include the imaging step in the protocol (e.g. so you can provide
        a customized alternative).
"""
    __config__ = [
            DocoptConfig,
            PresetConfig,
            StepwiseConfig.setup('molbio.stain'),
    ]
    preset_briefs = appcli.config_attr()

    presets = appcli.param(
            Key(StepwiseConfig, 'presets'),
            pick=list,
    )
    preset = appcli.param(
            Key(DocoptConfig, '<preset>'),
    )
    title = appcli.param(
            Key(PresetConfig),
    )
    light_sensitive = appcli.param(
            Key(PresetConfig),
            default=False,
    )
    prep_buffer = appcli.param(
            Key(PresetConfig),
    )
    prep_volume_mL = appcli.param(
            Key(PresetConfig),
            default=30,
    )
    prep_time_min = appcli.param(
            Key(PresetConfig),
            Key(PresetConfig, 'prep_time_hr', cast=lambda x: x*60),
    )
    prep_microwave = appcli.param(
            Key(PresetConfig),
            default=False,
    )
    prep_steps = appcli.param(
            Key(PresetConfig),
    )
    prep_repeats = appcli.param(
            Key(PresetConfig),
            default=1,
    )
    stain_buffer = appcli.param(
            Key(PresetConfig),
    )
    stain_volume_mL = appcli.param(
            Key(PresetConfig),
            Key(StepwiseConfig, 'default_stain_volume_mL'),
            default=30,
    )
    stain_time_min = appcli.param(
            Key(DocoptConfig, '--time'),
            Key(PresetConfig),
            Key(PresetConfig, 'stain_time_hr', cast=lambda x: x*60),
    )
    stain_microwave = appcli.param(
            Key(PresetConfig),
            default=False,
    )
    stain_steps = appcli.param(
            Key(PresetConfig),
    )
    stain_repeats = appcli.param(
            Key(PresetConfig),
            default=1,
    )
    stain_rinse_repeats = appcli.param(
            Key(PresetConfig),
            default=0,
    )
    destain_buffer = appcli.param(
            Key(PresetConfig),
    )
    destain_volume_mL = appcli.param(
            Key(PresetConfig),
            default=30,
    )
    destain_time_min = appcli.param(
            Key(PresetConfig),
            Key(PresetConfig, 'destain_time_hr', cast=lambda x: x*60),
    )
    destain_microwave = appcli.param(
            Key(PresetConfig),
            default=False,
    )
    destain_steps = appcli.param(
            Key(PresetConfig),
    )
    destain_repeats = appcli.param(
            Key(PresetConfig),
            default=1,
    )
    imaging_cmd = appcli.param(
            Key(DocoptConfig, '--imaging-protocol'),
            Key(DocoptConfig, '--no-imaging', cast=not_),
            Key(PresetConfig, 'imaging_protocol'),
            default=None,
    )
    protocol_link = appcli.param(
            Key(PresetConfig),
            default=None,
    )
    footnotes = appcli.param(
            Key(PresetConfig),
            default_factory=list,
    )

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

        footnotes = [x] if (x := self.protocol_link) else []
        footnotes += self.footnotes

        s = pl(f"Stain gel with {self.title}{p.add_footnotes(*footnotes)}:")

        if self.light_sensitive:
            #s += "Keep gel in the dark in all following steps."
            s += ul("Keep the stain protected from light.")
            #s += ul("In all following steps, protect the stain from light.")

        s += self._format_stage('prep')
        s += self._format_stage('stain')
        s += self._format_stage('destain')

        p += s

        if self.imaging_cmd:
            p += stepwise.load(self.imaging_cmd)

        return p

    def _format_stage(self, prefix):

        class SkipStage(Exception):
            pass

        def has(attr):
            return hasattr(self, f'{prefix}_{attr}')

        def get(attr, *args):
            return getattr(self, f'{prefix}_{attr}', *args)


        def have_repeats():
            n = get('repeats')
            return n > 1 if isinstance(n, int) else bool(n)

        def format_repeats():
            n = get('repeats')
            if isinstance(n, int):
                n = f'{n}x'
            return f"Repeat {n}:"

        def format_submerge():
            if not has('buffer'): raise SkipStage
            return f"Submerge gel in ≈{get('volume_mL', 30)} mL {get('buffer')}."

        def format_microwave():
            if get('microwave', False):
                return f"Microwave until almost boiling (≈{format_sec(get('microwave_time_s', 45))})."

        def format_incubate():
            return f"Shake gently for {format_min(get('time_min'))}."

        step_templates = get('steps', ['submerge', 'microwave', 'incubate'])
        step_formatters = {
                'submerge': format_submerge,
                'microwave': format_microwave,
                'incubate': format_incubate,
        }

        out = steps = ul()
        for template in step_templates:
            formatter = step_formatters.get(template, lambda: template)
            try:
                steps += formatter()
            except SkipStage:
                return

        if have_repeats():
            out = ul(pl(format_repeats(), steps, br='\n'))

        if n := get('rinse_repeats', False):
            out += f"Rinse {plural(n)://#x }with water."

        return out