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)
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
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, )
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'), )
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
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'), )
class Template(ShareConfigs, Argument): __config__ = [ReagentConfig] seq = appcli.param(Key(ReagentConfig, 'seq'), ) stock_ng_uL = appcli.param( Key(DocoptConfig, '--dna-stock'), Key(ReagentConfig, 'conc_ng_uL'), Key(StepwiseConfig, 'dna_stock_ng_uL'), cast=float, ) is_circular = appcli.param( Key(ReagentConfig, 'is_circular'), default=True, ) is_genomic = appcli.param( Key(DocoptConfig, '--genomic'), default=False, ) target_size_bp = appcli.param( Key(MakerConfig, 'size', cast=parse_size_bp), default=None, )
class 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)) ]
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
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
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
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
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
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."
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
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'), )
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
class Amplicon(ShareConfigs, Argument): __config__ = [] def _calc_seq(self): try: return find_amplicon( self.template.seq, self.fwd.seq, self.rev.seq, self.template.is_circular, ) except ValueError: err = ConfigError( template=self.template, fwd=self.fwd, rev=self.rev, ) err.brief = "{fwd.tag!r} and {rev.tag!r} do not amplify {template.tag!r}" err.info += "{template.tag}: {template.seq}" err.info += "{fwd.tag}: {fwd.seq}" err.info += "{rev.tag}: {rev.seq}" raise err from None seq = appcli.param( Method(_calc_seq), ) length_bp = appcli.param( Key(DocoptConfig, '--amplicon-length', cast=float), Method(lambda self: len(self.seq)), default=None ) @classmethod def from_tags(cls, template, fwd, rev, **kwargs): return cls( Pcr.Template(template), Pcr.Primer(fwd), Pcr.Primer(rev), **kwargs, ) def __init__(self, template, fwd, rev, **kwargs): self.template = template self.fwd = fwd self.rev = rev self._set_known_attrs(kwargs) def __repr__(self): return f'{self.__class__.__qualname__}({self.template!r}, {self.fwd!r}, {self.rev!r})' def get_reagents(self): return self.template, *self.primers def get_reagent_tags(self): return tuple(x.tag for x in self.reagents) def get_primers(self): return self.fwd, self.rev def set_primers(self, primers): self.fwd, self.rev = primers def on_bind(self, app): super().on_bind(app) for reagent in self.reagents: reagent.bind(app)
class Ivt(Main): """\ Synthesize RNA using an in vitro transcription reaction. Usage: ivt <templates>... [options] Arguments: <templates> The names to the DNA templates to transcribe. If these names can be looked up in a FreezerBox database, some parameters (e.g. template length) will automatically be determined. <%! from stepwise_mol_bio import hanging_indent %>\ Options: -p --preset <name> [default: ${app.preset}] What default reaction parameters to use. The following parameters are currently available: ${hanging_indent(app.preset_briefs, 8)} -v --volume <µL> The volume of the transcription reaction, in µL. -C --template-stock <ng/µL> The stock concentration of the template DNA (in ng/µL). By default, the concentration specified in the preset will be used. Changing this option will not change the quantity of template added to the reaction (the volume will be scaled proportionally). -t --template-mass <ng> How much template DNA to use (in ng). The template volume will be scaled in order to reach the given quantity. If the template is not concentrated enough to reach the given quantity, the reaction will just contain as much DNA as possible instead. In order to use this option, the stock concentration of the template must be in units of ng/µL. -V --template-volume <µL> The volume of DNA to add to the reaction (in µL). This will be adjusted if necessary to fit within the total volume of the reaction. -s --short Indicate that all of the templates are shorter than 300 bp. This allows the reactions to be setup with less material, so that more reactions can be performed with a single kit. -i --incubation-time <minutes> How long to incubate the transcription reaction, in minutes. -T --incubation-temp <°C> What temperature the incubate the transcription reaction at, in °C. -x --extra <percent> [default: ${app.extra_percent}] How much extra master mix to create. -R --toggle-rntp-mix Indicate whether you're using an rNTP mix, or whether you need to add each rNTP individually to the reaction. This option toggles the default value specified in the configuration file. -a --toggle-dnase-treatment Indicate whether you'd like to include the optional DNase treatment step. This option toggles the default value specified in the configuration file. Configuration: Default values for this protocol can be specified in any of the following stepwise configuration files: ${hanging_indent(app.config_paths, 8)} molbio.ivt.default_preset: The default value for the `--preset` option. molbio.ivt.rntp_mix: The default value for the `--toggle-rntp-mix` flag. molbio.ivt.dnase_treatment: The default value for the `--toggle-dnase-treament` flag. molbio.ivt.presets: Named groups of default reaction parameters. Typically each preset corresponds to a particular kit or protocol. See below for the various settings that can be specified in each preset. molbio.ivt.presets.<name>.brief: A brief description of the preset. This is displayed in the usage info for the `--preset` option. molbio.ivt.presets.<name>.inherit: Copy all settings from another preset. This can be used to make small tweaks to a protocol, e.g. "HiScribe with a non-standard additive". molbio.ivt.presets.<name>.reaction: A table detailing all the components of the transcription reaction, in the format understood by `stepwise.MasterMix.from_string()`. Optionally, this setting can be a dictionary with keys "long" and "short", each corresponding to a reaction table. This allows different reaction parameters to be used for long and short templates. The DNA template reagent must be named "template". The rNTP reagents may be marked with the "rntp" flag. The `--rntp-mix` flag will replace any reagents so marked with a single reagent named "rNTP mix". molbio.ivt.presets.<name>.instructions: A list of miscellaneous instructions pertaining to how the reaction should be set up, e.g. how to thaw the reagents, what temperature to handle the reagents at, etc. molbio.ivt.presets.<name>.extra_percent: How much extra master mix to make, as a percentage of the volume of a single reaction. molbio.ivt.presets.<name>.incubation_time_min: See `--incubation-time`. This setting can also be a dictionary with keys "long" and "short", to specify different incubation times for long and short templates. molbio.ivt.presets.<name>.incubation_temp_C: See `--incubation-temp`. molbio.ivt.presets.<name>.length_threshold The length of a template in base pairs that separates long from short templates. Template lengths will be queried from the FreezerBox database if possible. molbio.ivt.presets.<name>.dnase.default The default value for the `--dnase` flag. molbio.ivt.presets.<name>.dnase.reaction A table detailing all the components of the optional DNase reaction. One component must be named "transcription reaction". molbio.ivt.presets.<name>.dnase.incubation_time_min The incubation time (in minutes) for the optional DNase reaction. molbio.ivt.presets.<name>.dnase.incubation_temp_C The incubation temperature (in °C) for the optional DNase reaction. molbio.ivt.presets.<name>.dnase.footnotes.reaction molbio.ivt.presets.<name>.dnase.footnotes.incubation molbio.ivt.presets.<name>.dnase.footnotes.dnase Lists of footnotes for the reaction setup, incubation, and DNase treatment steps, respectively. Database: In vitro transcription reactions can appear in the "Synthesis" column of a FreezerBox database. The associated database entry will automatically be considered ssRNA, e.g. for the purpose of molecular weight calculations: ivt template=<tag> [preset=<preset>] [volume=<µL>] [time=<min>] [temp=<°C>] template=<tag> See `<templates>`. Only one template can be specified. preset=<preset> See `--preset`. volume=<µL> See `--volume`. time=<min> See `--incubation-time`. temp=<°C> See `--incubation-temp`. Template Preparation: The following information is taken directly from the HiScribe and MEGAscript manuals: Plasmid Templates: To produce RNA transcript of a defined length, plasmid DNA must be completely linearized with a restriction enzyme downstream of the insert to be transcribed. Circular plasmid templates will generate long heterogeneous RNA transcripts in higher quantities because of high processivity of T7 RNA polymerase. Be aware that there has been one report of low level transcription from the inappropriate template strand in plasmids cut with restriction enzymes leaving 3' overhanging ends [Schendorn and Mierindorf, 1985]. DNA from some miniprep procedures may be contaminated with residual RNase A. Also, restriction enzymes occasionally introduce RNase or other inhibitors of transcription. When transcription from a template is suboptimal, it is often helpful to treat the template DNA with proteinase K (100–200 μg/mL) and 0.5% SDS for 30 min at 50°C, follow this with phenol/chloroform extraction (using an equal volume) and ethanol precipitation. PCR Templates: PCR products containing T7 RNA Polymerase promoter in the correct orientation can be transcribed. Though PCR mixture can be used directly, better yields will be obtained with purified PCR products. PCR products can be purified according to the protocol for plasmid restriction digests above, or by using commercially available spin columns (we recommend Monarch PCR & DNA Cleanup Kit, NEB #T1030). PCR products should be examined on an agarose gel to estimate concentration and to confirm amplicon size prior to its use as a template. Depending on the PCR products, 0.1–0.5 μg of PCR fragments can be used in a 20 μL in vitro transcription reaction. Synthetic DNA Oligonucleotides: Synthetic DNA oligonucleotides which are either entirely double-stranded or mostly single-stranded with a double-stranded T7 promoter sequence can be used for transcription. In general, the yields are relatively low and also variable depending upon the sequence, purity, and preparation of the synthetic oligonucleotides. """ __config__ = [ DocoptConfig, MakerConfig, TemplateConfig, PresetConfig, StepwiseConfig.setup('molbio.ivt'), ] preset_briefs = appcli.config_attr() config_paths = appcli.config_attr() class Template(ShareConfigs, Argument): __config__ = [ReagentConfig] seq = appcli.param(Key(ReagentConfig, 'seq'), ) length = appcli.param( Key(ReagentConfig, 'length'), Method(lambda self: len(self.seq)), ) stock_ng_uL = appcli.param( Key(DocoptConfig, '--template-stock', cast=float), Key(ReagentConfig, 'conc_ng_uL'), default=None, ) def _calc_short(self): return all(x.length <= self.template_length_threshold for x in self.templates) def _pick_by_short(self, values): return pick_by_short(values, self.short) presets = appcli.param( Key(StepwiseConfig, 'presets'), pick=list, ) preset = appcli.param( Key(DocoptConfig, '--preset'), Key(MakerConfig, 'preset'), Key(StepwiseConfig, 'default_preset'), ) reaction_prototype = appcli.param( Key(PresetConfig, 'reaction', cast=parse_reaction), get=_pick_by_short, ) templates = appcli.param( Key(DocoptConfig, '<templates>', cast=lambda tags: [Ivt.Template(x) for x in tags]), Key(MakerConfig, 'template', cast=lambda x: [Ivt.Template(x)]), get=bind_arguments, ) template_length_threshold = appcli.param( Key(PresetConfig, 'length_threshold'), ) template_volume_uL = appcli.param( Key(DocoptConfig, '--template-volume', cast=float), default=None, ) template_mass_ng = appcli.param( Key(DocoptConfig, '--template-mass', cast=float), default=None, ) short = appcli.param( Key(DocoptConfig, '--short'), Method(_calc_short), default=False, ) volume_uL = appcli.param( Key(DocoptConfig, '--volume-uL', cast=float), Key(MakerConfig, 'volume', cast=parse_volume_uL), Key(PresetConfig, 'volume_uL'), default=None, ) rntp_mix = appcli.toggle_param( Key(DocoptConfig, '--no-rntp-mix', toggle=True), Key(StepwiseConfig, 'rntp_mix'), default=True, ) extra_percent = appcli.param( Key(DocoptConfig, '--extra-percent'), Key(PresetConfig, 'extra_percent'), cast=float, default=10, ) instructions = appcli.param( Key(PresetConfig, 'instructions'), default_factory=list, ) incubation_times_min = appcli.param( Key(DocoptConfig, '--incubation-time'), Key(MakerConfig, 'time', cast=parse_time_m), Key(PresetConfig, 'incubation_time_min'), ) incubation_temp_C = appcli.param( Key(DocoptConfig, '--incubation-temp'), Key(MakerConfig, 'temp', cast=parse_temp_C), Key(PresetConfig, 'incubation_temp_C'), ) dnase = appcli.toggle_param( Key(DocoptConfig, '--toggle-dnase-treatment', toggle=True), Key(PresetConfig, 'dnase.treatment'), Key(StepwiseConfig, 'dnase_treatment'), default=False, ) dnase_reaction_prototype = appcli.param( Key(PresetConfig, 'dnase.reaction', cast=MasterMix), ) dnase_incubation_time_min = appcli.param( Key(PresetConfig, 'dnase.incubation_time_min'), ) dnase_incubation_temp_C = appcli.param( Key(PresetConfig, 'dnase.incubation_temp_C'), ) footnotes = appcli.param( Key(PresetConfig, 'footnotes'), default_factory=dict, ) group_by = { 'preset': group_by_identity, 'volume_uL': group_by_identity, 'incubation_times_min': group_by_identity, 'incubation_temp_C': group_by_identity, } merge_by = { 'templates': join_lists, } def __init__(self, templates): self.templates = templates def __repr__(self): return f'Ivt(templates={self.templates!r})' def get_protocol(self): p = stepwise.Protocol() ## Clean your bench p += stepwise.load('rnasezap') ## In vitro transcription rxn = self.reaction n = plural(rxn.num_reactions) f = self.footnotes.get('reaction', []) p += paragraph_list( f"Setup {n:# in vitro transcription reaction/s}{p.add_footnotes(*f)}:", rxn, unordered_list(*self.instructions), ) f = self.footnotes.get('incubation', []) if self.short and affected_by_short(self.incubation_times_min): f += [ f"Reaction time is different than usual because the template is short (<{self.template_length_threshold} bp)." ] p += f"Incubate at {self.incubation_temp_C}°C for {format_min(pick_by_short(self.incubation_times_min, self.short))}{p.add_footnotes(*f)}." ## DNase treatment if self.dnase: f = self.footnotes.get('dnase', []) p += paragraph_list( f"Setup {n:# DNase reaction/s}{p.add_footnotes(*f)}:", self.dnase_reaction, ) p += f"Incubate at {self.dnase_incubation_temp_C}°C for {format_min(self.dnase_incubation_time_min)}." return p def get_reaction(self): rxn = self.reaction_prototype.copy() rxn.num_reactions = len(self.templates) rxn.extra_percent = self.extra_percent if self.volume_uL: rxn.hold_ratios.volume = self.volume_uL, 'µL' if self.rntp_mix: rntps = [] for i, reagent in enumerate(rxn): reagent.order = i if 'rntp' in reagent.flags: rntps.append(reagent) if not rntps: err = ConfigError("cannot make rNTP mix", preset=self.preset) err.blame += "no reagents flagged as 'rntp'" err.hints += "you may need to add this information to the [molbio.ivt.{preset}] preset" raise err rxn['rNTP mix'].volume = sum(x.volume for x in rntps) rxn['rNTP mix'].stock_conc = sum(x.stock_conc for x in rntps) / len(rntps) rxn['rNTP mix'].master_mix = all(x.master_mix for x in rntps) rxn['rNTP mix'].order = rntps[0].order for rntp in rntps: del rxn[rntp.name] rxn['template'].name = ','.join(x.tag for x in self.templates) template_stocks_ng_uL = [ ng_uL for x in self.templates if (ng_uL := x.stock_ng_uL) ]
class 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