Esempio n. 1
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. 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 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. 4
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. 5
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. 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]

        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,
        )
Esempio n. 8
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'
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. 10
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. 11
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
Esempio n. 12
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. 13
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. 14
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 EthanolPrecipitation(Main):
    """\
Purify and concentrate nucleic acids by ethanol precipitation.

This protocol is primarily based on [Li2020].

Usage:
    ethanol_precipitation [<names>...] [options]

Arguments:
    <names>
        The names of the constructs to precipitate.

Options:
    -p --preset <name>                    [default: ${app.preset}]
        There are four versions of the protocol, each optimized for a different 
        nucleic acid species.  Use this option to specify which version to use.  
        The names are case-insensitive:

        plasmid:
            Optimized with 10 kb circular plasmid.  This protocol is probably 
            also most appropriate for linear molecules of comparable size (e.g. 
            restriction digested plasmids).

        pcr:
            Optimized with 150 bp linear, doubled-stranded DNA.

        primer:
            Optimized with 20 nt single-stranded DNA.

        microrna:
            Optimized with 20 nt single-stranded RNA.

    -s --solvent <name>
        The organic solvent to use for the precipitation.  The names are 
        case-insensitive.

        etoh: Ethanol
            - Gives higher yield than isopropanol for short RNA/DNA, and 
              comparable yield for longer DNA [Li2020].
            - Evaporates more easily after the precipitation.

        iproh: Isopropanol
            - Has been said to work better than ethanol for dilute samples, 
              although this was not tested by [Li2020].
            - Requires less volume, which may be beneficial when working with 
              large volumes.
            - Better at dissolving (and therefore removing) protein and 
              polysaccharide contaminants.
            - Precipitates more salt, resulting in higher salt contamination.

    -a --cation <name>
        The cation to use for the precipitation.  This is automatically 
        determined by the protocol, but you can specify a different choice 
        (e.g. based on what you have on hand).  The names are case-insensitive:

        na: ${app.cations['na']['conc']} ${app.cations['na']['name']}
        mg: ${app.cations['mg']['conc']} ${app.cations['mg']['name']}

        Other cations were tested in [Li2020], but either NaAc or MgCl₂ was the 
        best in every condition.

    -c --carrier <name>
        The carrier, or coprecipitator, to add to the reaction.  This is 
        automatically determined by the protocol, but you can specify a 
        different choice (e.g. based on what you have on hand).  The names are 
        case-insensitive:

        lpa: ${app.carriers['lpa']['name']}
            Not known to interfere with any downstream application.  Not 
            derived from a biological source, so very unlikely to have any 
            nucleic acid contamination.

        glycogen:
            Mostly inert, but may interfere with protein/DNA interactions 
            [Gaillard1990] and reverse transcription (at concentrations 
            >2 mg/mL).  Derived from biological source, so may contain trace 
            contaminating nucleic acids.  You can purchase glycogen crosslinked 
            to a blue dye, which makes the pellet even easier to see.

        trna: ${app.carriers['trna']['name']}
            Interferes with the quantification of the nucleic acid by Nanodrop, 
            which is problematic for many applications.

    -b --buffer <name>                      [default: ${app.buffer}]
        The aqueous buffer to resuspend the precipitated nucleic acid in.

    -v --buffer-volume <µL>
        The volume of resuspension buffer to use, in µL.

    -I --no-incubation
        Exclude the incubation step.

    -W --no-wash
        Exclude the wash step.
            
References:
    Li Y et al.  A systematic investigation of key factors of nucleic acid 
    precipitation toward optimized DNA/RNA isolation.  BioTechniques 68, 
    191–199 (2020).

    Gaillard C, Strauss F.  Ethanol precipitation of DNA with linear 
    polyacrylamide as carrier.  Nucleic Acids Res. 18(2), 378 (1990).

    Sambrook J & Russell DW.  Standard ethanol precipitation of DNA in 
    microcentrifuge tubes.  Cold Spring Harb Protoc (2006).
"""

    __config__ = [
        DocoptConfig,
        PresetConfig,
        StepwiseConfig('molbio.ethanol_precipitation'),
    ]

    presets = {
        'plasmid': {
            'solvent': 'etoh',
            'solvent_volume': {
                'etoh': 3,
                'iproh': 1,
            },
            'cation': {
                'etoh': 'na',
                'iproh': 'na',
            },
            'carrier': {
                'etoh': 'lpa',
                'iproh': 'lpa',
            },
            'incubation_time': None,
            'incubation_temp_C': None,
            'centrifugation_time_min': 60,
            'centrifugation_temp_C': 4,
            'centrifugation_speed': '>7500g',
        },
        'pcr': {
            'solvent': 'etoh',
            'solvent_volume': {
                'etoh': 2,
                'iproh': Fraction(3, 4),
            },
            'cation': {
                'etoh': 'mg',
                'iproh': 'mg',
            },
            'carrier': {
                'etoh': 'glycogen',
                'iproh': 'lpa',
            },
            'incubation_time': 'overnight',
            'incubation_temp_C': -20,
            'centrifugation_time_min': 60,
            'centrifugation_temp_C': 4,
            'centrifugation_speed': '>7500g'
        },
        'primer': {
            'solvent': 'etoh',
            'solvent_volume': {
                'etoh': 4,
                'iproh': 1,
            },
            'cation': {
                'etoh': 'na',
                'iproh': 'mg',
            },
            'carrier': {
                'etoh': 'glycogen',
                'iproh': 'glycogen',
            },
            'incubation_time': 'overnight',
            'incubation_temp_C': 4,
            'centrifugation_time_min': 60,
            'centrifugation_temp_C': 4,
            'centrifugation_speed': '>18000g',
        },
        'microrna': {
            'solvent': 'etoh',
            'solvent_volume': {
                'etoh': 4,
                'iproh': Fraction(3, 4),
            },
            'cation': {
                'etoh': 'mg',
                'iproh': 'na',
            },
            'carrier': {
                'etoh': 'glycogen',
                'iproh': 'lpa',
            },
            'incubation_time': 'overnight',
            'incubation_temp_C': -20,
            'centrifugation_time_min': 60,
            'centrifugation_temp_C': 4,
            'centrifugation_speed': '>21000g',
        },
    }
    solvents = {
        'etoh': {
            'name': '100% ethanol',
        },
        'iproh': {
            'name': 'isopropanol',
        },
    }
    carriers = {
        'trna': {
            'name': "yeast tRNA",
            'conc': "20 ng/µL",
        },
        'glycogen': {
            'name': "glycogen",
            'conc': "50 ng/µL",
        },
        'lpa': {
            'name': "linear polyacrylamide (LPA)",
            'conc': "20 ng/µL",
        },
    }
    cations = {
        'na': {
            'name': "sodium acetate, pH=5.2",
            'conc': "300 mM",
        },
        'mg': {
            'name': "magnesium chloride (MgCl₂)",
            'conc': "10 mM",
        },
    }

    preset = appcli.param(
        Key(DocoptConfig, '--preset'),
        Key(StepwiseConfig, 'preset'),
        ignore=None,
    )
    names = appcli.param(
        Key(DocoptConfig, '<names>'),
        default=None,
    )
    solvent = appcli.param(
        Key(DocoptConfig, '--solvent'),
        Key(PresetConfig, 'solvent'),
    )
    solvent_volume = appcli.param(
        Key(PresetConfig, 'solvent_volume'),
        get=by_solvent,
    )
    buffer = appcli.param(
        Key(DocoptConfig, '--buffer'),
        default='water',
    )
    buffer_volume_uL = appcli.param(
        Key(DocoptConfig, '--buffer-volume'),
        default=None,
    )
    cation = appcli.param(
        Key(DocoptConfig, '--cation'),
        Key(PresetConfig, 'cation'),
        get=by_solvent,
    )
    carrier = appcli.param(
        Key(DocoptConfig, '--carrier'),
        Key(PresetConfig, 'carrier'),
        get=by_solvent,
    )
    incubation = appcli.param(
        Key(DocoptConfig, '--no-incubation', cast=not_),
        default=True,
    )
    incubation_time = appcli.param(Key(PresetConfig, 'incubation_time'), )
    incubation_temp_C = appcli.param(Key(PresetConfig, 'incubation_temp_C'), )
    wash = appcli.param(
        Key(DocoptConfig, '--no-wash', cast=not_),
        default=True,
    )
    centrifugation_time_min = appcli.param(
        Key(PresetConfig, 'centrifugation_time_min'), )
    centrifugation_temp_C = appcli.param(
        Key(PresetConfig, 'centrifugation_temp_C'), )
    centrifugation_speed = appcli.param(
        Key(PresetConfig, 'centrifugation_speed'), )

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

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

        if self.names:
            p += pl(
                f"Purify {','.join(self.names)} by ethanol precipitation [1,2]:",
                s)
        else:
            p += pl("Perform an ethanol precipitation [1,2]:", s)

        s += f"""\
                Add {self.cation_name} to {self.cation_conc}."""
        s += f"""\
                Add {self.carrier_name} to {self.carrier_conc}."""
        s += f"""\
                Add {plural(self.solvent_volume):# volume/s} 
                {self.solvent_name} and mix well."""
        s += f"""\
                If necessary, divide the sample between microfuge tubes
                such that none holds more than 400 µL."""

        if self.incubation and (t := self.incubation_time):
            incubation_time = "overnight" if t == 'overnight' else f"for {t}"
            s += f"""\
                    Incubate at {self.incubation_temp_C}°C {incubation_time} 
                    [3]."""

        s += f"""\
                Centrifuge {self.centrifugation_speed}, 
                {self.centrifugation_time_min} min, 
                {self.centrifugation_temp_C}°C.  Remove the supernatant, 
                but save it in case the precipitation needs to be repeated."""

        if self.wash:
            s += f"""\
                    Add 800 µL recently-prepared 70% ethanol [4]."""
            s += f"""\
                    Centrifuge {self.centrifugation_speed}, 2 min, 
                    {self.centrifugation_temp_C}°C.  Discard supernatant."""
        s += f"""\
                Centrifuge {self.centrifugation_speed}, 30 s, 
                {self.centrifugation_temp_C}°C.  Discard any remaining 
                supernatant.
        """
        s += f"""\
                Leave the tube open at room temperature until ethanol has 
                evaporated [5]."""

        s += f"""\
                Resuspend the pellet in {f'{self.buffer_volume_uL} µL' if 
                self.buffer_volume_uL else 'any volume'} of {self.buffer} 
                [6]."""

        p.footnotes[1] = pre(
            textwrap.dedent("""\
                Li2020: 10.2144/btn-2019-0109
                Sambrook2006: 10.1101/pdb.prot4456"""))
        p.footnotes[2] = """\
                This protocol was optimized for 100 ng/µL nucleic acid.  If 
                your sample is substantially more dilute, it may be necessary 
                to compensate by increasing the incubation time, the 
                centrifugation time, or the centrifugation speed.
        """
        p.footnotes[3] = """\
                DNA can be stored indefinitely in ethanolic solutions at either 
                0°C or −20°C.
        """
        p.footnotes[4] = """\
                Ethanol evaporates more quickly than water, so a solution that 
                was 70% ethanol several months ago may be significantly more 
                aqueous now.  If you are unsure, 100 µL of 70% EtOH should 
                weigh 88.6 mg.
        """
        p.footnotes[5] = """\
                Do not dry pellets of nucleic acid in a lyophilizer, as this 
                causes denaturation of small (<400-nucleotide) fragments of DNA 
                and greatly reduces the recovery of larger fragments of DNA. 

                If necessary, the open tube containing the redissolved DNA can 
                be incubated for 2-3 minutes at 45°C in a heating block to 
                allow any traces of ethanol to evaporate.
        """
        p.footnotes[6] = """\
                Up to 50% of the DNA is smeared on the wall of the tube. To 
                recover all of the DNA, push a bead of fluid backward and 
                forward over the appropriate quadrant of wall with a pipette 
                tip.
        """

        p.prune_footnotes()
        return p
Esempio n. 16
0
class LigateMrnaLinker(appcli.App):
    """\
Ligate linker-N to the mRNA.

Usage:
    ligate <mrna_µL> <mrna_µM> [-n <int>] [-v <µL>] [-x <percent>] [-i <time>] [options]

Arguments:
    <mrna_µL>
        The volume of the annealed mRNA, in µL.  The ligation reaction will 
        be 10x this volume, to dilute the salt from the annealing reaction.

    <mrna_µM>
        The concentration of the mRNA, in µM.  The amount of ligase will be 
        scaled relative to the quantity of mRNA to be ligated.

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

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

    -i --incubate <time>            [default: ${app.incubate_time}]
        How long to incubate the reaction at the temperature indicated by the 
        `-t` flag.  Include a unit.

    -t --incubate-temp <temp>       [default: ${app.incubate_temp}]
        What temperature to incubate the reaction at.  Include a unit.

    -m --master-mix <reagents>      [default: ${','.join(app.master_mix)}]
        Include the indicated reagents in the master mix.  The following 
        reagents are understood:

        pnk: T4 PNK
        lig: T4 RNA ligase
        rna: annealed mRNA/linker
        peg: PEG-8000

    -M --no-master-mix
        Exclude all optional reagents from the master mix, i.e. `-m ''`.

    -L --no-ligase
        Remove the ligase from the reaction, e.g. as a negative control.

    -k --kinase
        Add T4 PNK to the reaction, e.g. if using non-phosphorylated primers.

    -p --peg
        Include PEG-8000 in the reaction.  Many T4 RNA ligase protocols 
        recommend this, but in my hands it does not improve yield.  This may be 
        because my substrates are already annealed.

    -Q --no-quench
        Leave out the 65°C incubation to quench the reaction.  This is useful 
        if the reaction will be quenched by a downstream step anyways.

    -I --no-incubate
        Skip the entire incubation step.  This is useful when setting up 
        multiple ligate reaction in a row; only the last needs include the 
        incubation.
"""
    __config__ = [
            DocoptConfig(),
    ]

    num_reactions = appcli.param(
            '--num-reactions',
            cast=int,
            default=1,
    )
    mrna_volume_uL = appcli.param(
            '<mrna_µL>',
            cast=float,
    )
    mrna_conc_uM = appcli.param(
            '<mrna_µM>',
            cast=eval,
    )
    extra_percent = appcli.param(
            '--extra',
            cast=float,
            default=10,
    )
    incubate_time = appcli.param(
            '--incubate',
            default='10 min',
    )
    incubate_temp = appcli.param(
            '--incubate-temp',
            default='25°C',
    )
    master_mix = appcli.param(
            Key(DocoptConfig, '--no-master-mix', cast=lambda x: set()),
            Key(DocoptConfig, '--master-mix', cast=lambda x: set(x.split(','))),
            default_factory=lambda: {'peg','lig'},
    )
    use_ligase = appcli.param(
            '--no-ligase',
            cast=not_,
            default=True,
    )
    use_kinase = appcli.param(
            '--kinase',
            default=False,
    )
    use_peg = appcli.param(
            '--peg',
            default=False,
    )
    quench = appcli.param(
            '--no-quench',
            cast=not_,
            default=True,
    )
    incubate = appcli.param(
            '--no-incubate',
            cast=not_,
            default=True,
    )

    def __init__(self, anneal: AnnealMrnaLinker):
        anneal_rxn = anneal.reaction
        self.mrna_volume_uL = anneal_rxn.volume.value
        self.mrna_conc_uM = anneal_rxn['mRNA'].conc.value

    def get_protocol(self):
        p = stepwise.Protocol()
        rxn = self.reaction
        rxn_name = 'ligation' if self.use_ligase else 'negative control'
        n = rxn.num_reactions

        p += stepwise.pl(
                f"Setup {plural(n):# {rxn_name} reaction/s}:",
                rxn,
        )
        if self.incubate:
            p += pl(
                    f"Incubate the {plural(n):ligation reaction/s} as follows:",
                    s := ul(
                        f"{self.incubate_temp} for {self.incubate_time}."
                    ),
            )
            if self.quench:
                s += "65°C for 10 min."
Esempio n. 17
0
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
Esempio n. 18
0
    def _calc_extend_time_s(self):
        bp = min(bp for x in self.amplicons if (bp := x.length_bp))

        def round(x):
            if not self.round_extend_time: return x
            if x <= 10: return 10
            if x <= 15: return 15
            return 30 * ceil(x / 30)

        if factor := self.extend_time_s_per_kb:
            return round(factor * bp / 1000)

        return round(self.extend_time_func(bp))

    presets = appcli.param(
            Key(StepwiseConfig, 'presets'),
            pick=list,
    )
    preset = appcli.param(
            Key(DocoptConfig, '--preset'),
            Key(MakerConfig, 'preset'),
            Key(StepwiseConfig, 'default_preset'),
    )
    amplicons = appcli.param(
            Key(DocoptConfig, parse_amplicons_from_docopt),
            Key(MakerConfig, parse_amplicons_from_freezerbox),
            get=bind_arguments,
    )
    product_tag = appcli.param(
            Key(DocoptConfig, '--product'),
    )
Esempio n. 19
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
Esempio n. 20
0
    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)
Esempio n. 21
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. 22
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