class App(appcli.App): __config__ = [ appcli.DocoptConfig(), ] layout_toml = appcli.param('<toml>', cast=Path) output = appcli.param('--output', default=None) @classmethod def main(cls): self = cls.from_params() appcli.load(self) if not self.output: if os.fork() != 0: sys.exit() df, extras = self.load() fig = self.plot(df, extras) if self.output: out = self.output.replace('$', self.layout_toml.stem) plt.savefig(out) plt.close() else: plt.show()
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 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 Kld(Main): """\ Circularize a linear DNA molecule using T4 DNA ligase, e.g. to reform a plasmid after inverse PCR. Usage: kld <dna> [-n <int>] Arguments: <dna> The name of the DNA molecule to circularize. Options: -n --num-reactions <int> [default: ${app.num_reactions}] The number of reactions to set up. """ __config__ = [ appcli.DocoptConfig, ] dna = appcli.param('<dna>') num_reactions = appcli.param( '--num-reactions', cast=lambda x: int(eval(x)), default=1, ) def __init__(self, dna): self.dna = dna def get_reaction(self): kld = stepwise.MasterMix.from_text('''\ Reagent Stock Volume Master Mix ================ ========= =========== ========== water to 10.00 μL yes T4 ligase buffer 10x 1.00 μL yes T4 PNK 10 U/μL 0.25 μL yes T4 DNA ligase 400 U/μL 0.25 μL yes DpnI 20 U/μL 0.25 μL yes DNA 50 ng/μL 1.50 μL ''') kld.num_reactions = self.num_reactions kld.extra_percent = 15 kld['DNA'].name = self.dna return kld def get_protocol(self): p = stepwise.Protocol() p += pl( f"Setup {plural(self.num_reactions):# ligation reaction/s}:", self.reaction, ) p += "Incubate at room temperature for 1h." return p
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 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.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 WashBarendt(appcli.App): """\ Remove unligated linker by ultrafiltration. Usage: wash_barendt [-v <µL>] Options: -v --volume <µL> [default: ${app.volume_uL}] The volume to dilute the purified mRNA to. Note that this must be greater than 15 µL, since that is the dead volume of the spin filter. -m --mwco <kDa> [default: ${app.mwco_kDa}] The MWCO of the spin filter. According to Millipore Sigma, this should be 2-3x smaller than the molecular weight of the ligated product: https://tinyurl.com/4ffxu8zb """ __config__ = [ appcli.DocoptConfig(), ] volume_uL = appcli.param( '--volume', cast=float, default=15, ) mwco_kDa = appcli.param( '--mwco', cast=int, default=100, ) def get_protocol(self): p = stepwise.Protocol() p += pl( "Remove unligated linker by ultrafiltration:", s := ul( "Bring reaction to 500 µL with 8M urea.", f"Load onto a {self.mwco_kDa} kDa MWCO spin-filter [1].", "Spin 14000g, 15 min.", "Wash with 500 µL 8M urea.", "Wash with 500 µL nuclease-free water.", "Wash with water again.", "Wash with water again, but spin for 30 min.", "Invert the filter into a clean tube and spin 1000g, 2 min to collect ligated product in a volume of ≈15 µL.", ), )
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 List(StepwiseCommand): """\ List protocols known to stepwise. Usage: stepwise ls [-d] [-p] [<protocol>] Options: -d --dirs Show the directories that will be searched for protocols, rather than the protocols themselves. -p --paths Don't organize paths by directory. """ __config__ = [ appcli.DocoptConfig, ] protocol = appcli.param('<protocol>', default=None) dirs_only = appcli.param('--dirs', default=False) organize_by_dir = appcli.param('--paths', cast=not_, default=True) def main(self): appcli.load(self) library = Library() entries = library.find_entries(self.protocol) indent = ' ' if self.organize_by_dir else '' for collection, entry_group in groupby(entries, lambda x: x.collection): if self.organize_by_dir: print(collection.name) if self.dirs_only: continue for entry in entry_group: print(indent + entry.name) if self.organize_by_dir: print()
class Template(ShareConfigs, Argument): __config__ = [ReagentConfig] seq = appcli.param(Key(ReagentConfig, 'seq'), ) stock_ng_uL = appcli.param( Key(DocoptConfig, '--dna-stock'), Key(ReagentConfig, 'conc_ng_uL'), Key(StepwiseConfig, 'dna_stock_ng_uL'), cast=float, ) is_circular = appcli.param( Key(ReagentConfig, 'is_circular'), default=True, ) is_genomic = appcli.param( Key(DocoptConfig, '--genomic'), default=False, ) target_size_bp = appcli.param( Key(MakerConfig, 'size', cast=parse_size_bp), default=None, )
class UvTransilluminator(Main): """\ Image a gel using a UV transilluminator. Usage: uv_transilluminator [<wavelength>] """ __config__ = [appcli.DocoptConfig] wavelength_nm = appcli.param('<wavelength>', default=300) def get_protocol(self): p = stepwise.Protocol() p += f"Image with a {self.wavelength_nm} nm UV transilluminator." return p
class Cleanup(Main): product_tags = appcli.param( Method(lambda self: [x.tag for x in self.products]), default_factory=list, ) def __bareinit__(self): super().__bareinit__() self.show_product_tags = False @classmethod def make(cls, db, products): makers = list(super().make(db, products)) show_product_tags = (len(makers) != 1) for maker in makers: maker.show_product_tags = show_product_tags yield maker
class Which(StepwiseCommand): """\ Show the full path to the specified protocol. Usage: stepwise which <protocol> """ __config__ = [ appcli.DocoptConfig, ] protocol = appcli.param('<protocol>') def main(self): appcli.load(self) library = Library() entries = library.find_entries(self.protocol) for entry in entries: print(entry.path)
class Edit(StepwiseCommand): """\ Edit the specified protocol using $EDITOR. Usage: stepwise edit <protocol> """ __config__ = [ appcli.DocoptConfig, ] protocol = appcli.param('<protocol>') def main(self): appcli.load(self) library = Library() entry = library.find_entry(self.protocol) cmd = os.environ['EDITOR'], entry.path subp.run(cmd)
class Anneal(Main): """\ Anneal two complementary oligos. Usage: anneal <oligo_1> <oligo_2> [-n <num_rxns>] [-v <µL>] [options] Arguments: <oligo_1> <oligo_2> The names of the two oligos to anneal. Options: -n --num-rxns <num_rxns> [default: ${app.num_reactions}] The number of reactions to set up. -v --volume <µL> [default: ${app.volume_uL}] The volume of each annealing reaction in µL. -c --oligo-conc <µM> The final concentration of each oligo in the reaction, in µM. This will also be the concentration of the annealed duplex, if the reaction goes to completion. The default is to use as much oligo as possible. -C --oligo-stock <µM[,µM]> [default: ${app.oligo_stock_uM}] The stock concentrations of the oligos, in µM. You can optionally use a comma to specify different stock concentrations for the two oligos. -m --master-mix <reagents> [default: ${','.join(app.master_mix)}] The reagents to include in the master mix. The following reagents are understood: '1' (the first oligo), '2' (the second oligo), or the name of either oligo specified on the command line. To specify both reagents, separate the two names with a comma. """ __config__ = [ appcli.DocoptConfig, ] oligo_1 = appcli.param('<oligo_1>') oligo_2 = appcli.param('<oligo_2>') num_reactions = appcli.param('--num-rxns', default=1, cast=int) volume_uL = appcli.param('--volume', default=4, cast=float) oligo_conc_uM = appcli.param('--oligo-conc', default=None, cast=float) oligo_stock_uM = appcli.param('--oligo-stock', default=100, cast=float_pair) master_mix = appcli.param('--master-mix', default={'1'}, cast=comma_set) def __init__(self, oligo_1, oligo_2): self.oligo_1 = oligo_1 self.oligo_2 = oligo_2 def get_reaction(self): rxn = stepwise.MasterMix.from_text("""\ Reagent Stock Volume MM? ======= ===== ========= === water to 4.0 µL yes PBS 10x 0.4 µL yes """) rxn.num_reactions = self.num_reactions rxn.hold_ratios.volume = self.volume_uL, 'µL' rxn['oligo1'].name = self.oligo_1 rxn['oligo2'].name = self.oligo_2 rxn['oligo1'].master_mix = bool({'1', self.oligo_1} & self.master_mix) rxn['oligo2'].master_mix = bool({'2', self.oligo_2} & self.master_mix) rxn['oligo1'].stock_conc = pair(self.oligo_stock_uM, 0), 'µM' rxn['oligo2'].stock_conc = pair(self.oligo_stock_uM, 1), 'µM' if self.oligo_conc_uM: rxn['oligo1'].hold_stock_conc.conc = self.oligo_conc_uM, 'µM' rxn['oligo2'].hold_stock_conc.conc = self.oligo_conc_uM, 'µM' else: V = rxn.get_free_volume_excluding('oligo1', 'oligo2') C1 = rxn['oligo1'].stock_conc C2 = rxn['oligo2'].stock_conc C12 = C1 + C2 rxn['oligo1'].volume = V * (C2 / C12) rxn['oligo2'].volume = V * (C1 / C12) return rxn def get_protocol(self): protocol = stepwise.Protocol() n = self.num_reactions protocol += f"""\ Setup {plural(n):# annealing reaction/s}: {self.reaction} """ protocol += f"""\ Perform the {plural(n):annealing reaction/s}: - Incubate at 95°C for 2 min. - Cool at room temperature. """ return protocol
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 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 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 AnnealMrnaLinker(appcli.App): """\ Anneal linker-N and mRNA prior to ligation. Usage: anneal [<mrna>] [<linker>] [-n <int>] [-m <reagents>] [-V <µL>] [-r <µL>] [-R <µM>] [-l <ratio>] [-L <µM>] Arguments: <mrna> The name of the mRNA, e.g. f11. Multiple comma-separated names may be given. <linker> The name of the linker, e.g. o93. Multiple comma-separated names may be given. Options: -n --num-reactions <int> The number of reactions to setup up. The default is the number of unique combinations of mRNA and linker. -m --master-mix <reagents> [default: ${','.join(app.master_mix)}] A comma-separated list of reagents to include in the master mix. This flag is only relevant in <n> is more than 1. The following reagents are understood: mrna, link -r --mrna-volume <µL> [default: ${app.mrna_volume_uL}] The volume of mRNA to use in each annealing reaction, in µL. -R --mrna-stock <µM> The stock concentration of the mRNA, in µM. The volume of mRNA will be updated accordingly to keep the amount of material in the reaction constant. The default is read from the FreezerBox database, or 10 µM if the given mRNA is not in the database. Use '--mrna-volume' to change the amount of mRNA in the reaction. -l --linker-ratio <float> [default: ${app.linker_ratio}] The amount of linker to add to the reaction, relative to the amount of mRNA in the reaction. -L --linker-stock <µM> The stock concentration of the linker, in µM. The volume of linker be updated accordingly, to keep the amount of material in the reaction constant. The default is read from the FreezerBox database, or 10 µM if the given linker is not in the database. """ __config__ = [ DocoptConfig(), ] mrnas = appcli.param( '<mrna>', cast=lambda x: x.split(','), default_factory=list, ) linkers = appcli.param( '<linker>', cast=lambda x: x.split(','), default_factory=list, ) num_reactions = appcli.param( '--num-reactions', cast=int, default=0, get=lambda self, x: x or len(self.mrnas) * len(self.linkers), ) mrna_volume_uL = appcli.param( '--mrna-volume', cast=eval, default=1, ) master_mix = appcli.param( '--master-mix', cast=lambda x: set(x.split(',')), default_factory=set, ) linker_ratio = appcli.param( '--linker-ratio', cast=float, default=1, ) mrna_stock_uM = appcli.param( '--mrna-stock', default=None, ) linker_stock_uM = appcli.param( '--mrna-stock', default=None, ) def __bareinit__(self): self.db = freezerbox.load_db() def __init__(self, mrnas, linkers): self.mrnas = list_if_str(mrnas) self.linkers = list_if_str(linkers) def get_protocol(self): p = stepwise.Protocol() rxn = self.reaction n = rxn.num_reactions p += pl( f"Setup {plural(n):# annealing reaction/s} [1]:", rxn, ) p.footnotes[1] = """\ Using 0.6x linker reduces the amount of unligated linker, see expt #1.""" p += pl( f"Perform the {plural(n):annealing reaction/s}:", ul( "Incubate at 95°C for 2 min.", "Cool at room temperature.", ), ) return p def get_reaction(self): rxn = stepwise.MasterMix() rxn.num_reactions = n = self.num_reactions rxn.solvent = None rxn['mRNA'].stock_conc = consensus( get_conc_uM(self.db, x, self.mrna_stock_uM) for x in self.mrnas) rxn['linker'].stock_conc = consensus( get_conc_uM(self.db, x, self.linker_stock_uM) for x in self.linkers) rxn['mRNA'].volume = self.mrna_volume_uL, 'µL' rxn['linker'].volume = ( self.linker_ratio * rxn['mRNA'].volume * (rxn['mRNA'].stock_conc / rxn['linker'].stock_conc)) rxn['mRNA'].master_mix = 'mrna' in self.master_mix rxn['linker'].master_mix = 'link' in self.master_mix if self.mrnas: rxn['mRNA'].name = ','.join(self.mrnas) if self.linkers: rxn['linker'].name = ','.join(self.linkers) rxn['PBS'].volume = rxn.volume / 9 rxn['PBS'].stock_conc = '10x' rxn['PBS'].order = -1 return rxn
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 Note(StepwiseCommand): """\ Insert a footnote into a protocol. This command can be used to elaborate on a previous step in a protocol. The footnote will be numbered automatically, and any subsequent footnotes will be renumbered accordingly. Usage: stepwise note <footnote> [<where>] [-W] Arguments: <footnote> The text of the footnote to add. <where> A regular expression indicating where the footnote reference should be placed. The steps will be searched in reverse order for this pattern, and the reference will be inserted directly after the first matching substring found. You can use a lookahead assertion to match text that will appear after the reference, e.g. '(?=:)' will place a reference before the first colon that is found. By default the footnote will be placed just before the first period (.) or colon (:) in the previous step. Options: -W --no-wrap Do not automatically line-wrap the given message. The default is to wrap the message to fit within the width specified by the `printer.default.content_width` configuration option. """ __config__ = [ appcli.DocoptConfig, ] text = appcli.param('<footnote>') where = appcli.param('<where>', default=None) wrap = appcli.param('--no-wrap', cast=not_, default=True) def main(self): appcli.load(self) io = ProtocolIO.from_stdin() if io.errors: fatal("protocol has errors, not adding footnote.") if not io.protocol: fatal("no protocol specified.") p = io.protocol footnote = self.text if self.wrap else pre(self.text) pattern = re.compile(self.where or '(?=[.:])') try: p.insert_footnotes(footnote, pattern=pattern) except ValueError: fatal(f"pattern {pattern!r} not found in protocol.") p.merge_footnotes() io.to_stdout(self.force_text)
class Printer: __config__ = [ PresetConfig, StepwiseConfig, ] presets = appcli.param( StepwiseConfig, default_factory=dict, pick=list, ) page_height = appcli.param( PresetConfig, default=56, ) page_width = appcli.param( PresetConfig, default=78, ) content_width = appcli.param( PresetConfig, default=53, ) margin_width = appcli.param( PresetConfig, default=10, ) paps_flags = appcli.param( PresetConfig, default='--font "FreeMono 12" --paper letter --left-margin 0 --right-margin 0 --top-margin 12 --bottom-margin 12', ) lpr_flags = appcli.param( PresetConfig, default='-o sides=one-sided', ) def __init__(self, preset=None): self.preset = preset or get_default_printer_name() def truncate_lines(self, lines): def do_truncate(line): line_width = self.page_width - self.margin_width if len(line) > line_width: return line[:line_width - 1] + '…' else: return line return [do_truncate(x) for x in lines] def check_for_long_lines(self, lines): too_long_lines = [] for lineno, line in enumerate(lines, 1): line = line.rstrip() if len(line) > self.content_width: too_long_lines.append(lineno) if len(too_long_lines) == 0: return elif len(too_long_lines) == 1: warning = "line {} is more than {} characters long." else: warning = "lines {} are more than {} characters long." warning = warning.format( format_range(too_long_lines), self.content_width, ) raise PrinterWarning(warning) def make_pages(self, lines): """ Split the given content into pages by trying to best take advantage of natural paragraph breaks. The return value is a list of "pages", where each page is a list of lines (without trailing newlines). """ pages = [] current_page = [] skip_next_line = False def add_page(current_page): if any(line.strip() for line in current_page): pages.append(current_page[:]) current_page[:] = [] for i, line_i in enumerate(lines): if skip_next_line: skip_next_line = False continue # If the line isn't blank, add it to the current page like usual. if line_i.strip(): current_page.append(line_i) # If the line is blank, find the next blank line and see if it fits on # the same page. If it does, add the blank line to the page and don't # do anything special. If it doesn't, make a new page. Also interpret # two consecutive blank lines as a page break. else: for j, line_j in enumerate(lines[i+1:], i+1): if not line_j.strip(): break else: j = len(lines) if len(current_page) + (j-i) > self.page_height or j == i+1: skip_next_line = (j == i+1) add_page(current_page) elif current_page: current_page.append(line_i) add_page(current_page) return pages def add_margin(self, pages): """ Add a margin on the left to leave room for holes to be punched. """ left_margin = ' ' * self.margin_width + '│ ' return [[left_margin + line for line in page] for page in pages] def print_pages(self, pages): """ Print the given pages. """ from subprocess import Popen, PIPE form_feed = ''
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 SmartMmlv(Main): """\ Synthesize first-strand cDNA (up to 11.7 kb) using SMART MMLV reverse transcriptase. Usage: smart_mmlv <templates> <primers> [-n <rxns>] [-m <reagents>] [-v <µL>] [-t <nM>] [-T <nM>] [-p <fold>] [-P <µM>] [-C] [-q <step>] Arguments: <templates> The names of the templates to reverse transcribe, comma-separated. <primers> The names of the primers to use, comma-separated. Options: -n --num-reactions <rxns> The number of reactions to setup. By default, this is the number of templates specified. -m --master-mix <reagents> A comma-separated list of reagents to include in the annealing master mix. Understood reagents are 'rna' and 'primer'. By default, each reaction is assembled independently. -v --volume <µL> [default: ${app.volume_uL}] The volume of the reaction, in µL. -t --template-conc <nM> [default: ${app.template_conc_nM}] The final concentration of the template in the reverse transcription reaction. Note that a higher concentration will be used in the annealing reaction. -T --template-stock <nM> [default: ${app.template_stock_nM}] The stock concentration of the template, in nM. -p --excess-primer <fold> [default: ${app.primer_excess}] The amount of primer to use, relative to the template concentration. -P --primer-stock <µM> [default: ${app.primer_stock_uM}] The stock concentration of the primer, in µM. -r --rna-min-volume <µL> [default: ${app.rna_min_volume_uL}] The smallest volume of RNA to pipet in any step. This is meant to help avoid pipetting errors when trying to be quantitative, e.g. for qPCR. -a --extra-anneal <percent> [default: ${app.extra_anneal_percent}] How much extra annealing reaction to prepare, e.g. to facilitate multichannel pipetting. -C --no-control Exclude the control reaction with no reverse transcriptase. This is a standard control in RT-qPCR workflows. -q --quench <step> [default: ${app.quench}] How to quench the reaction: heat: Incubate at 70°C. edta: Add EDTA. none: Skip the quench step. Ordering: - SMART MMLV reverse transcriptase (Takara 639523) - Advantage UltraPure PCR deoxynucleotide mix (Takara 639125) References: 1. https://tinyurl.com/y4ash7dl """ __config__ = [ DocoptConfig(), ] templates = appcli.param( '<templates>', cast=comma_list, ) primers = appcli.param( '<primers>', cast=comma_list, ) num_reactions = appcli.param( '--num-reactions', cast=int, default=None, get=lambda self, n: n if n else len(self.templates), ) master_mix = appcli.param( '--master-mix', cast=comma_set, default_factory=set, ) volume_uL = appcli.param( '--volume', cast=float, default=20, ) template_conc_nM = appcli.param( '--template-conc', cast=float, default=50, ) template_stock_nM = appcli.param( '--template-stock', cast=float, default=1000, ) primer_excess = appcli.param( '--excess-primer', cast=float, default=50, ) primer_stock_uM = appcli.param( '--primer-conc', cast=float, default=100, ) rna_min_volume_uL = appcli.param( '--rna-min-volume', cast=float, default=2, ) extra_anneal_percent = appcli.param( '--extra-anneal', cast=float, default=20, ) nrt_control = appcli.param( '--no-control', cast=not_, default=True, ) quench = appcli.param( '--quench', default='heat', ) def get_protocol(self): p = stepwise.Protocol() anneal, mmlv = self.reactions if self.nrt_control: mmlv_nrt = deepcopy(mmlv) mmlv_nrt.num_reactions = 1 anneal.num_reactions += 1 del mmlv_nrt['SMART MMLV RT'] p += pl( f"Anneal the {plural(self.primers):RT primer/s} to the {plural(self.templates):RNA template/s} [1]:", anneal, ) p.footnotes[1] = pre("""\ This protocol is based on the official Takara SMART MMLV reverse transcription protocol: https://tinyurl.com/y4ash7dl However, I made three modifications: - I reduced the volume of the annealing step as much as possible, the increase the concentrations of the oligos. - I added buffer to the annealing step, because annealing works much better with some salt to shield the backbone charge. I don't actually know how much salt is in the buffer, but some is better than none. - I included the MMLV in the transcription master mix, because excluding it seemed too inaccurate. The changes to the annealing step also mean that the master mix has a 1x buffer concentration, so I don't need to worry about the enzyme being unhappy. """) p += "Incubate at 70°C for 3 min, then immediately cool on ice." p += pl( "Setup {plural(mmlv.num_reactions):# reverse transcription reaction/s}:", mmlv, ) if self.nrt_control: p += pl( f"Setup a −reverse transcriptase (NRT) control:", mmlv_nrt, ) p += "Incubate at 42°C for 60 min [2]." p.footnotes[2] = "Samples can be incubated for 50-90 min if necessary." if self.quench == 'none': pass elif self.quench == 'heat': p += "Incubate at 70°C for 15 min." elif self.quench == 'edta': p += "Add 4 µL 60 mM EDTA." else: raise ConfigError( f"unexpected value for quench parameter: {self.quench}") return p def get_reactions(self): # Define the annealing reaction: anneal = stepwise.MasterMix() anneal.num_reactions = self.num_reactions anneal.solvent = None anneal.extra_min_volume = 0.5, 'µL' template_fmol = self.volume_uL * self.template_conc_nM anneal['template'].name = ','.join(self.templates) anneal['template'].stock_conc = self.template_stock_nM, 'nM' anneal[ 'template'].volume = template_fmol / self.template_stock_nM, 'µL' anneal['template'].master_mix = 'rna' in self.master_mix anneal['template'].order = 2 anneal['primer'].name = ','.join(self.primers) anneal['primer'].stock_conc = self.primer_stock_uM, 'µM' anneal[ 'primer'].volume = self.primer_excess * template_fmol / self.primer_stock_uM / 1e3, 'µL' anneal['primer'].master_mix = 'primer' in self.master_mix anneal['primer'].order = 3 # If necessary, dilute the annealing reaction such that it will be # necessary to add the given volume to the RT reaction. The purpose of # this is to ensure that we can pipet this volume accurately, since we # often want to be quantitative about how much RNA we have (e.g. qPCR). if anneal.volume < (self.rna_min_volume_uL, 'µL'): anneal.solvent = 'nuclease-free water' anneal.volume = self.rna_min_volume_uL, 'µL' anneal['first-strand buffer'].stock_conc = '5x' anneal['first-strand buffer'].volume = 0, 'µL' anneal['first-strand buffer'].volume = anneal.volume / 4 anneal['first-strand buffer'].master_mix = bool(self.master_mix) anneal['first-strand buffer'].order = 1 # Define the MMLV reaction: mmlv = stepwise.MasterMix("""\ Reagent Stock Volume MM? ======================== ======== ========== === nuclease-free water to 20.0 µL yes first-strand buffer 5x 4.0 µL yes dNTP mix 10 mM 2.0 µL yes DTT 100 mM 2.0 µL yes SMART MMLV RT 200 U/µL 0.5 µL yes annealed template/primer 0.0 µL """) mmlv.num_reactions = self.num_reactions mmlv.hold_ratios.volume = self.volume_uL, 'µL' mmlv['first-strand buffer'].volume -= anneal[ 'first-strand buffer'].volume mmlv['annealed template/primer'].volume = anneal.volume mmlv['annealed template/primer'].stock_conc = \ anneal['template'].stock_conc * ( anneal['template'].volume / anneal.volume) # Scale the volume of the annealing reaction to guarantee that none of # the volume are too small to pipet accurately. min_pipet_volumes = { 'template': (self.rna_min_volume_uL, 'µL'), } pipet_volumes = [ (anneal.master_mix_volume, '0.5 µL'), ] pipet_volumes += [ (anneal[k].volume, min_pipet_volumes.get(k, '0.5 µL')) for k in ['template', 'primer'] if not anneal[k].master_mix ] anneal.hold_ratios.volume *= max( 1 + self.extra_anneal_percent / 100, *(limit / curr for curr, limit in pipet_volumes), ) return anneal, mmlv
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 Ligate(Assembly): __doc__ = f"""\ Assemble restriction-digested DNA fragments using T4 DNA ligase. Usage: ligate <assemblies>... [-c <conc>]... [-l <length>]... [options] Arguments: {ARGUMENT_DOC} Options: {OPTION_DOC} -k --kinase Add T4 polynucleotide kinase (PNK) to the reaction. This is necessary to ligate ends that are not already 5' phosphorylated (e.g. annealed oligos, PCR products). Database: Ligation reactions can appear in the "Synthesis" column of a FreezerBox database: ligate <assembly> [volume=<µL>] [kinase=<bool>] <assembly> See the <assemblies>... command-line argument. volume=<µL> See --volume. You must include a unit. kinase=<bool> See --kinase. Specify "yes" or "no". """ excess_insert = appcli.param( '--excess-insert', cast=float, default=3, ) use_kinase = appcli.param( '--kinase', default=False, ) def get_reaction(self): rxn = stepwise.MasterMix.from_text('''\ Reagent Stock Volume Master Mix ================ ========= =========== ========== water to 20.00 μL yes T4 ligase buffer 10x 2.00 μL yes T4 DNA ligase 400 U/μL 1.00 μL yes T4 PNK 10 U/μL 1.00 μL yes ''') if not self.use_kinase: del rxn['T4 PNK'] return self._add_fragments_to_reaction(rxn) def del_reaction(self): pass def get_protocol(self): p = stepwise.Protocol() rxn = self.reaction p += pl( f"Setup {plural(rxn.num_reactions):# ligation reaction/s}{p.add_footnotes('https://tinyurl.com/y7gxfv5m')}:", rxn, ) p += pl( "Incubate at the following temperatures:", ul( "25°C for 15 min", "65°C for 10 min", ), ) return p def del_protocol(self): pass
class Library: """ Provide access to every protocol available to the user. The protocols are organized into "collections", which are groups of related protocols. Which collections are available to the user at any moment can depend on factors such as the current working directory, the user's configuration settings, which plugins are installed, whether the internet is available, etc. The primary role of the library is to search through every available collection for protocols matching a given "tag". Tags are fuzzy patterns meant to be succinct (i.e. easily typed on the command line) yet capable of specifically identifying any protocol. See `_match_tag()` for a complete description of the tag syntax. Strictly speaking, collections are groups of "entries" rather than protocols. Entries are protocol factories. For the example of a collection representing a directory, each entry in that collection would represent a file in that directory. The actual protocol instances would come from the entries either reading or executing those files. Collections and entries are intended to be highly polymorphic, so that many different means of accessing protocols (e.g. files, plugins, network drives, websites, etc.) can be supported. """ __config__ = [StepwiseConfig] ignore_globs = appcli.param( 'search.ignore', cast=Schema([str]), default=[], ) local_paths = appcli.param( 'search.find', cast=Schema([str]), default=['protocols'], ) global_paths = appcli.param( 'search.path', cast=Schema([str]), default=[], ) _singleton = None def __init__(self): self.collections = [] def add(collection, i=None): is_available = collection.is_available() is_unique = collection.is_unique(self.collections) is_ignored = any( fnmatch(collection.name, x) for x in self.ignore_globs) if is_available and is_unique and not is_ignored: self.collections.insert( len(self.collections) if i is None else i, collection, ) # Add directories found above the current working directory. cwd = Path.cwd().resolve() for parent in (cwd, *cwd.parents): for name in self.local_paths: add(PathCollection(parent / name)) # Add specific directories specified by the user. for dir in self.global_paths: add(PathCollection(dir)) # Add directories specified by plugins. for plugin in load_and_sort_plugins('stepwise.protocols'): try: add(PluginCollection(plugin)) except AttributeError as err: warn( f"no protocol directory specified for '{plugin.module_name}.{plugin.name}' plugin." ) codicil(str(err)) # Add the current working directory. # Do this after everything else, so it'll get bumped if we happen to be # in a directory represented by one of the other collections. But put # it ahead of all the other collections, so that tags are evaluated for # local paths before anything else. add(CwdCollection(), 0) @classmethod def from_singleton(cls): # Use `Library._singleton` instead of `cls._singleton` so we don't end # up with multiple singletons. if Library._singleton is None: Library._singleton = cls() return Library._singleton def find_entries(self, tag): """ Yield the best-scoring entries matching the given tag. """ scored_entries = [ scored_entry for collection in self.collections for scored_entry in collection.find_entries(tag) ] no_score = object() best_score = max((x for x, _ in scored_entries), default=no_score) return [ entry for score, entry in scored_entries if score == best_score ] def find_entry(self, tag): """ Return the single entry matching the given tag. If there are either zero or multiple entries that match the given tag, an exception will be raised. """ entries = self.find_entries(tag) return one( entries, NoProtocolsFound(tag, self.collections), MultipleProtocolsFound(tag, entries), )
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