"""Core design modules.
For examples for how to use the design module, see the :doc:`Usage Docs <../usage>`
For a list of design parameters available, take a look at the
:ref:`BoulderIO Parameters <api_default_parameters>`
.. code-block:: python
# a new task
design = Design()
# set template sequence
design.settings.template("AGGTTGCGTGTGTATGGTCGTGTAGTGTGT")
# set left primer sequence
design.settings.left_sequence("GTTGCGTGTGT)
# set as a cloning task
design.settings.as_cloning_task()
# run the design task
design.run()
"""
import re
import webbrowser
from typing import Any
from typing import Dict
from typing import List
from typing import Tuple
from typing import Union
from warnings import warn
import primer3
from .interfaces import AllParameters
from .interfaces import ParameterAccessor
from .results import parse_primer3_results
from primer3plus.constants import DOCURL
from primer3plus.exceptions import Primer3PlusException
from primer3plus.exceptions import Primer3PlusRunTimeError
from primer3plus.exceptions import Primer3PlusWarning
from primer3plus.log import logger
from primer3plus.params import BoulderIO
from primer3plus.params import default_boulderio
from primer3plus.utils import anneal as anneal_primer
from primer3plus.utils import depreciated_warning
[docs]class DesignPresets:
"""Interface for setting design parameters. This is typically accessed from
a :class:`Design <primer3plus.Design>` instance's.
:meth:`Design <primer3plus.Design.set>` method. As in:
.. code-block::
design = Design()
design.settings.left_sequence("AGGGAGATAGATA")
design.run()
"""
def __init__(self, design):
"""Initializes a new interface from a.
:class:`~primer3plus.design.Design`.
:param design: The design
"""
self._design = design
def _resolve(self):
"""Process any extra parameters and process BoulderIO so that it is
digestable by primer3."""
if self._design.PRIMER_USE_OVERHANGS.value:
self._resolve_overhangs(self._design.PRIMER_MIN_ANNEAL_CHECK.value)
if self._design.PRIMER_LONG_OK.value:
self._resolve_max_lengths(lim=BoulderIO.PRIMER_MAX_SIZE_HARD_LIM)
self._resolve_product_sizes()
if not self._design.PRIMER_USE_OVERHANGS.value:
if self._design.SEQUENCE_PRIMER_OVERHANG.value:
warn(
Primer3PlusWarning(
"{} is non-empty (value={}) but {} was False. Overhang was"
" ignored".format(
self._design.SEQUENCE_PRIMER_OVERHANG.name,
self._design.SEQUENCE_PRIMER_OVERHANG.value,
self._design.PRIMER_USE_OVERHANGS.name,
)
)
)
if self._design.SEQUENCE_PRIMER_REVCOMP_OVERHANG.value:
warn(
Primer3PlusWarning(
"{} is non-empty (value={}) but {} was False. Overhang was"
" ignored".format(
self._design.SEQUENCE_PRIMER_REVCOMP_OVERHANG.name,
self._design.SEQUENCE_PRIMER_REVCOMP_OVERHANG.value,
self._design.PRIMER_USE_OVERHANGS.name,
)
)
)
return self
def _post_parse(self, pairs, explain) -> None:
"""Modify results from design parameters (e.g. overhangs)"""
left_long_overhang = self._design._SEQUENCE_LONG_OVERHANG.value
right_long_overhang = self._design._SEQUENCE_REVCOMP_LONG_OVERHANG.value
for pair in pairs.values():
for x in ["LEFT", "RIGHT"]:
pair[x].setdefault("OVERHANG", "")
if self._design.PRIMER_USE_OVERHANGS:
pair["LEFT"]["OVERHANG"] = self._design.SEQUENCE_PRIMER_OVERHANG.value
pair["RIGHT"][
"OVERHANG"
] = self._design.SEQUENCE_PRIMER_REVCOMP_OVERHANG.value
if left_long_overhang:
pair["LEFT"]["SEQUENCE"] = (
left_long_overhang + pair["LEFT"]["SEQUENCE"]
)
pair["LEFT"]["OVERHANG"] = pair["LEFT"]["OVERHANG"][
: -len(left_long_overhang)
]
pair["PAIR"]["PRODUCT_SIZE"] += len(left_long_overhang)
loc = pair["LEFT"]["location"]
pair["LEFT"]["location"] = [
loc[0] - len(left_long_overhang),
len(pair["LEFT"]["SEQUENCE"]),
]
if right_long_overhang:
pair["RIGHT"]["SEQUENCE"] = (
right_long_overhang + pair["RIGHT"]["SEQUENCE"]
)
pair["RIGHT"]["OVERHANG"] = pair["RIGHT"]["OVERHANG"][
: -len(right_long_overhang)
]
pair["PAIR"]["PRODUCT_SIZE"] += len(right_long_overhang)
loc = pair["RIGHT"]["location"]
pair["RIGHT"]["location"] = [
loc[0] + len(right_long_overhang),
len(pair["RIGHT"]["SEQUENCE"]),
]
def _interval_from_sequences(
self, template: str, target: str
) -> Union[None, Tuple[int, int]]:
if isinstance(target, str):
matches = self._get_index_of_match(template, target)
if not matches:
print("Target not in template")
return None
if len(matches) > 1:
print("More than one target found")
return None
return matches[0]
@staticmethod
def _get_index_of_match(template: str, sequence: str) -> List[Tuple[int, int]]:
matches = []
for m in re.finditer(sequence, template, re.IGNORECASE):
matches.append((m.start(0), m.end(0)))
return matches
[docs] def update(self, update: Dict[str, Any]):
"""Update an arbitrary parameter."""
self._design.params.update(update)
return self
[docs] def task(self, task: str) -> "DesignPresets":
"""This tag tells primer3 what task to perform.
http://primer3.ut.ee/primer3web_help.htm#PRIMER_TASK
:param task: the task name
:return self
"""
self.update({"PRIMER_TASK": task})
return self
[docs] def as_cloning_task(self) -> "DesignPresets":
"""Set the design as a cloning task.
http://primer3.ut.ee/primer3web_help.htm#PRIMER_TASK
:return: self
"""
return self.task("pick_cloning_primers")
[docs] def as_generic_task(self) -> "DesignPresets":
"""Set the design as a generic task.
http://primer3.ut.ee/primer3web_help.htm#PRIMER_TASK
:return: self
"""
return self.task("generic")
[docs] def template(self, template: str) -> "DesignPresets":
"""Set the template sequence for the design. This sets the
'SEQUENCE_TEMPLATE' parameter.
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_TEMPLATE
:param template: the template sequence
:return: self
"""
self.update({"SEQUENCE_TEMPLATE": template})
return self
# TODO: set_iterations, set_num_return, set_force_return, set_gradient
[docs] def primer_num_return(self, n: int) -> "DesignPresets":
"""Set the number of primers to return for the design task.
http://primer3.ut.ee/primer3web_help.htm#PRIMER_NUM_RETURN
:param n: number of primers to return
:return: self
"""
return self.update({"PRIMER_NUM_RETURN": n})
[docs] def product_size(
self, interval: Union[Tuple[int, int], List[Tuple[int, int]]], opt=None
) -> "DesignPresets":
"""Set the product size. Optionally include the optimal size.
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PRODUCT_SIZE_RANGE
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PRODUCT_OPT_SIZE
:param interval: a tuple of <min>,<max> or a list of such tuples
:param opt: optional product size as an int.
:return: self
"""
if isinstance(interval, tuple):
interval = [interval]
if opt is not None:
return self.update(
{"PRIMER_PRODUCT_SIZE_RANGE": interval, "PRIMER_PRODUCT_OPT_SIZE": opt}
)
return self.update({"PRIMER_PRODUCT_SIZE_RANGE": interval})
[docs] def pair_region_list(
self, region_list: List[Tuple[int, int, int, int]]
) -> "DesignPresets":
"""The list of regions from which to design primers.
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_PRIMER_PAIR_OK_REGION_LIST
:param region_list: list of regions
:return: self
"""
return self.update({"SEQUENCE_PRIMER_PAIR_OK_REGION_LIST": region_list})
[docs] def left_sequence(self, primer: str) -> "DesignPresets":
"""The sequence of a left primer to check and around which to design
right primers and optional internal oligos. Must be a substring of
SEQUENCE_TEMPLATE.
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_PRIMER
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PICK_RIGHT_PRIMER
:param primer: the primer sequence
:return: self
"""
return self.update({"SEQUENCE_PRIMER": primer, "PRIMER_PICK_RIGHT_PRIMER": 1})
[docs] def right_sequence(self, primer: str) -> "DesignPresets":
"""The sequence of a right primer to check and around which to design
left primers and optional internal oligos. Must be a substring of the
reverse strand of SEQUENCE_TEMPLATE.
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_PRIMER_REVCOMP
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PICK_LEFT_PRIMER
:param primer: the primer sequence
:return: self
"""
return self.update(
{"SEQUENCE_PRIMER_REVCOMP": primer, "PRIMER_PICK_LEFT_PRIMER": 1}
)
@staticmethod
def _trim_long(overhang: str, anneal: str, lim: int) -> Tuple[str, str, str]:
"""Fix the overhang and anneal from the hardcoded BoulderIO primer
lim."""
return overhang, anneal[:-lim], anneal[-lim:]
def _get_left_overhang(self, min_primer_anneal: int):
left = self._design.SEQUENCE_PRIMER.value
if left:
fwd, _ = anneal_primer(
self._design.SEQUENCE_TEMPLATE.value, [left], n_bases=min_primer_anneal
)
if len(fwd) == 0:
raise Primer3PlusRunTimeError("No annealing found for left sequence.")
elif len(fwd) > 1:
raise Primer3PlusRunTimeError(
"More than one annealing found for left sequence."
)
overhang = fwd[0]["overhang"]
anneal = fwd[0]["anneal"]
return overhang, anneal
else:
return "", left
def _get_right_overhang(self, min_primer_anneal: int):
right = self._design.SEQUENCE_PRIMER_REVCOMP.value
if right:
_, rev = anneal_primer(
self._design.SEQUENCE_TEMPLATE.value, [right], n_bases=min_primer_anneal
)
if len(rev) == 0:
raise Primer3PlusRunTimeError("No annealing found for right sequence.")
elif len(rev) > 1:
raise Primer3PlusRunTimeError(
"More than one annealing found for right "
"sequence {}.".format(self._design.SEQUENCE_PRIMER_REVCOMP)
)
overhang = rev[0]["overhang"]
anneal = rev[0]["anneal"]
return overhang, anneal
else:
return "", right
[docs] def left_overhang(self, overhang: str) -> "DesignPresets":
"""Sets the left overhang sequence for the primer. This overhang will.
*always* be in the overhang sequence regardless of other parameters.
If using a primer that anneals with an overhang, this value will
be appended to the 5' end of the overhang.
:param overhang: overhang sequence
:return: self
"""
return self.update({"SEQUENCE_PRIMER_OVERHANG": overhang})
[docs] def right_overhang(self, overhang: str) -> "DesignPresets":
"""Sets the right overhang sequence for the primer. This overhang will.
*always* be in the overhang sequence regardless of other parameters.
If using a primer that anneals with an overhang, this value will
be appended to the 5' end of the overhang.
:param overhang: overhang sequence
:return: self
"""
return self.update({"SEQUENCE_PRIMER_REVCOMP_OVERHANG": overhang})
[docs] def use_overhangs(self, b: bool = True) -> "DesignPresets":
"""Set the BoulderIO to process overhangs.
:param b: boolean to set
:return: self
"""
return self.update({"PRIMER_USE_OVERHANGS": b})
[docs] def long_ok(self, b: bool = True) -> "DesignPresets":
"""Set the BoulderIO to process long primers.
:param b: boolean to set
:return: self
"""
return self.update({"PRIMER_LONG_OK": b})
def _resolve_product_sizes(self):
"""If there are long primers being used, the product_size is no longer
valid as the trimmed sequence is no longer represented in the
originally provided product size.
This re-adjusts the product size to correspond the adjusted
parameters.
"""
# adjust product size range
left_long_overhang = self._design._SEQUENCE_LONG_OVERHANG.value
right_long_overhang = self._design._SEQUENCE_REVCOMP_LONG_OVERHANG.value
product_sizes = self._design.PRIMER_PRODUCT_SIZE_RANGE.value
x = len(left_long_overhang) + len(right_long_overhang)
if isinstance(product_sizes[0], tuple):
new_product_sizes = []
for size in product_sizes:
new_product_sizes.append((size[0] - x, size[1] - x))
self._design.PRIMER_PRODUCT_SIZE_RANGE.value = new_product_sizes
else:
size = self._design.PRIMER_PRODUCT_SIZE_RANGE.value
self._design.PRIMER_PRODUCT_SIZE_RANGE.value = [size[0] - x, size[1] - x]
def _resolve_max_lengths(self, lim: int):
"""Fixes the annealing and overhang sequences for annealing sequences
for primers over the :attr:`BoulderIO.
<primer3plus.paramsBoulderIO.PRIMER_MAX_SIZE_HARD_LIM>`.
Should always be run *after* :meth:`_resolve_overhangs`.
"""
left_anneal = self._design.SEQUENCE_PRIMER.value
right_anneal = self._design.SEQUENCE_PRIMER_REVCOMP.value
left_over = self._design.SEQUENCE_PRIMER_OVERHANG.value
right_over = self._design.SEQUENCE_PRIMER_REVCOMP_OVERHANG.value
left_over, left_long_overhang, left_anneal = self._trim_long(
left_over, left_anneal, lim=lim
)
right_over, right_long_overhang, right_anneal = self._trim_long(
right_over, right_anneal, lim=lim
)
self.left_overhang(left_over + left_long_overhang)
self.right_overhang(right_over + right_long_overhang)
# save the sequences that were trimmed
# TODO: will need to re-add these in the results in overhang, product size, and anneal
# TODO: adjust the product_size
# TODO: adjust any other regions
# TODO: re-adjust tm and add any warnings
# save values for long overhangs
self._left_long_overhang(left_long_overhang)
self._right_long_overhang(right_long_overhang)
self.left_sequence(left_anneal)
self.right_sequence(right_anneal)
def _left_long_overhang(self, x):
self.update({"_SEQUENCE_LONG_OVERHANG": x})
def _right_long_overhang(self, x):
self.update({"_SEQUENCE_REVCOMP_LONG_OVERHANG": x})
def _resolve_overhangs(self, min_primer_anneal: int):
"""Sets the annealing and overhang sequences."""
left_over, left_anneal = self._get_left_overhang(min_primer_anneal)
_loverhang = self._design.SEQUENCE_PRIMER_OVERHANG.value
if _loverhang:
left_over = _loverhang + left_over
# raise ValueError(
# "Left overhang already set to '{}'.".format(_loverhang)
# )
right_over, right_anneal = self._get_right_overhang(min_primer_anneal)
_roverhang = self._design.SEQUENCE_PRIMER_REVCOMP_OVERHANG.value
if _roverhang:
right_over = _roverhang + right_over
# raise ValueError(
# "Right overhang already set to '{}'.".format(_roverhang)
# )
self.left_overhang(left_over)
self.right_overhang(right_over)
self.left_sequence(left_anneal)
self.right_sequence(right_anneal)
[docs] def pick_left_only(self) -> "DesignPresets":
"""Design only the left primer.
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PICK_LEFT_PRIMER
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PICK_RIGHT_PRIMER
:return: self
"""
return self.update(
{"PRIMER_PICK_LEFT_PRIMER": 1, "PRIMER_PICK_RIGHT_PRIMER": 0}
)
[docs] def pick_right_only(self) -> "DesignPresets":
"""Design only the right primer.
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PICK_LEFT_PRIMER
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PICK_RIGHT_PRIMER
:return: self
"""
return self.update(
{"PRIMER_PICK_LEFT_PRIMER": 0, "PRIMER_PICK_RIGHT_PRIMER": 1}
)
[docs] def internal_sequence(self, primer: str) -> "DesignPresets":
"""The sequence of an internal oligo to check and around which to
design left and right primers. Must be a substring of
SEQUENCE_TEMPLATE.
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_INTERNAL_OLIGO
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PICK_INTERNAL_OLIGO
:param primer: :type primer: :return: :rtype:
"""
return self.update(
{"SEQUENCE_INTERNAL_OLIGO": primer, "PRIMER_PICK_INTERNAL_OLIGO": 1}
)
[docs] def primers(self, p1: str, p2: str) -> "DesignPresets":
"""Set the left and right primer sequences.
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_PRIMER
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PICK_RIGHT_PRIMER
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_PRIMER_REVCOMP
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PICK_LEFT_PRIMER
:param p1:
:param p2:
:return:
"""
if p1:
self.left_sequence(p1)
if p2:
self.right_sequence(p2)
return self
def primers_with_overhangs(self, p1: str, p2: str) -> "DesignPresets":
if p1:
self.left_sequence_with_overhang(p1)
if p2:
self.right_sequence_with_overhang(p2)
return self
def _parse_interval(
self, interval: Union[str, Tuple[int, int], List[Tuple[int, int]]]
) -> List[Tuple[int, int]]:
if isinstance(interval, str):
interval = self._interval_from_sequences(
self._design.params["SEQUENCE_TEMPLATE"], interval
)
if isinstance(interval, tuple):
interval = [interval]
return interval
[docs] def included(self, interval: Union[str, Tuple[int, int]]) -> "DesignPresets":
"""Specify interval from which primers must be selected. A sub-region
of the given sequence in which to pick primers. For example, often the
first dozen or so bases of a sequence are vector, and should be
excluded from consideration. The value for this parameter has the form.
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_INCLUDED_REGION
<start>,<length> where <start> is the index of the first base to consider, and
<length> is the number of subsequent bases in the primer-picking region.
:param interval: One of the following: the sequence of the target region,
a tuple of the interval of <start>,<length> or a str
:return: self
"""
if isinstance(interval, str):
interval = self._interval_from_sequences(
self._design.params["SEQUENCE_TEMPLATE"], interval
)
if not len(interval) == 2 or (
not isinstance(interval, tuple) and not isinstance(interval, list)
):
raise TypeError(
"Expect an tuple or list of length 2 but found {}".format(interval)
)
interval = list(interval)
return self.update({"SEQUENCE_INCLUDED_REGION": interval})
[docs] def target(
self, interval: Union[str, Tuple[int, int], List[Tuple[int, int]]]
) -> "DesignPresets":
"""Specify the interval that designed primers must flank. If one or
more targets is specified then a legal primer pair must flank at least
one of them. A target might be a simple sequence repeat site (for
example a CA repeat) or a single-base-pair polymorphism, or an exon for
resequencing. The value should be a space-separated list of.
<start>,<length> pairs where <start> is the index of the first base of
a target,and <length> is its length. See also PRIMER_INSIDE_PENALTY,
PRIMER_OUTSIDE_PENALTY. PRIMER_TASK=pick_sequencing_primers. See
PRIMER_TASK for more information.
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_TEMPLATE
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_TARGET
:param interval: One of the following: the sequence of the target region,
a tuple of the interval of <start>,<length>, or a list of
tuples of <start>,<length>
:return self
"""
return self.update({"SEQUENCE_TARGET": self._parse_interval(interval)})
[docs] def excluded(
self, interval: Union[str, Tuple[int, int], List[Tuple[int, int]]]
) -> "DesignPresets":
"""Primers and oligos may not overlap any region specified in this tag.
The associated value must be a space-separated list of <start>,<length>
pairs where <start> is the index of the first base of the excluded
region, and <length> is its length. This tag is useful for tasks such
as excluding regions of low sequence quality or for excluding regions
containing repetitive elements such as ALUs or LINEs.
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_TEMPLATE
http://primer3.ut.ee/primer3web_help.htm#SEQUENCE_EXCLUDED_REGION
:param interval: One of the following: the sequence of the target region,
a tuple of the interval of <start>,<length>, or a list of
tuples of <start>,<length>
:return: self
"""
return self.update({"SEQUENCE_EXCLUDED_REGION": self._parse_interval(interval)})
[docs] def pick_anyway(self, b=1) -> "DesignPresets":
"""If true use primer provided in SEQUENCE_PRIMER,
SEQUENCE_PRIMER_REVCOMP, or SEQUENCE_INTERNAL_OLIGO even if it violates
specific constraints.
http://primer3.ut.ee/primer3web_help.htm#PRIMER_PICK_ANYWAY
:param b: default True
:return self
"""
return self.update({"PRIMER_PICK_ANYWAY": b})
def clip(x, mn, mx):
return max(min(x, mx), mn)
[docs]class DesignBase:
"""Base design."""
DEFAULT_PARAMS = default_boulderio #: default parameters
DEFAULT_GRADIENT = dict(
PRIMER_MAX_SIZE=(1, DEFAULT_PARAMS["PRIMER_MAX_SIZE"], 36),
PRIMER_MIN_SIZE=(-1, 16, DEFAULT_PARAMS["PRIMER_MAX_SIZE"]),
PRIMER_MAX_TM=(1, DEFAULT_PARAMS["PRIMER_MAX_SIZE"], 80),
PRIMER_MIN_TM=(-1, 48, DEFAULT_PARAMS["PRIMER_MIN_TM"]),
PRIMER_MAX_HAIRPIN_TH=(1, DEFAULT_PARAMS["PRIMER_MAX_HAIRPIN_TH"], 60),
) #: the default gradient to use for the :meth:`Design.run_and_optimize` method.
_CHECK_PRIMERS = "check_primers"
_GENERIC = "generic"
_PICK_PRIMER_LIST = "pick_primer_list"
_PICK_SEQUENCING_PRIMERS = "pick_sequencing_primers"
_PICK_CLONING_PRIMERS = "pick_cloning_primers"
_PICK_DISCRIMINATIVE_PRIMERS = "pick_discriminative_primers"
def __init__(
self,
gradient: Dict[
str, Tuple[Union[float, int], Union[float, int], Union[float, int]]
] = None,
params: BoulderIO = None,
quiet_runtime: bool = False,
):
"""Initializes a new design.
:param gradient: the design gradient.
:param quiet_runtime: if True will siliently ignore any runtime errors.
"""
if params is None:
params = self.DEFAULT_PARAMS.copy()
self.params = params
self.logger = logger(self)
self.gradient = gradient
self.quiet_runtime = quiet_runtime
def _raise_run_time_error(self, msg: str) -> Primer3PlusRunTimeError:
"""Raise a Primer3PlusRunTime exception. If parameters are named in the
msg, print off some debugging information at the end of the message.
:param msg: the error msg
:return: the run time exception
"""
parameter_explain = set()
for name, value in self.params._params.items():
if name in msg:
parameter_explain.add("\t" + str(value))
parameter_explain = sorted(parameter_explain)
return Primer3PlusRunTimeError(msg + "\n" + "\n".join(parameter_explain))
def _run(self, params: BoulderIO = None) -> Tuple[List[Dict], List[Dict]]:
"""Design primers. Optionally provide additional parameters.
:param params:
:return: results
"""
if params is None:
params = self.params
try:
res = primer3.bindings.designPrimers(params._sequence(), params._globals())
except OSError as e:
if not self.quiet_runtime:
raise self._raise_run_time_error(str(e)) from e
else:
return {}, {"PRIMER_ERROR": str(e)}
except Primer3PlusRunTimeError as e:
if not self.quiet_runtime:
raise self._raise_run_time_error(str(e)) from e
else:
return {}, {"PRIMER_ERROR": str(e)}
except Primer3PlusException as e:
raise self._raise_run_time_error(str(e)) from e
pairs, explain = parse_primer3_results(res)
self.settings._post_parse(pairs, explain)
return pairs, explain
[docs] def run(self) -> Tuple[List[Dict], List[Dict]]:
"""Design primers. Optionally provide additional parameters.
:param params:
:return: results
"""
return self._run()
[docs] def run_and_optimize(
self,
max_iterations,
params: BoulderIO = None,
gradient: Dict[
str, Tuple[Union[float, int], Union[float, int], Union[float, int]]
] = None,
run_kwargs: dict = None,
) -> Tuple[List[dict], List[dict]]:
"""Design primers and relax constraints. If primer design is
unsuccessful, relax parameters as defined in
primer3plust.Design.DEFAULT_GRADIENT. Repeat for the specified number
of max_iterations.
:param max_iterations: the max number of iterations to perform relaxation
:param params: optional parameters to provide
:param gradient: optional gradient to provide. If not provided,
Design.DEFAULT_GRADIENT will be used. The gradient is a
dictionary off 3 tuples, the step the min and the max.
:return: results
"""
if gradient is None:
gradient = self.gradient or self.DEFAULT_GRADIENT
if params is None:
params = self.params
pairs, explain = self._run(params)
i = 0
while i < max_iterations and len(pairs) == 0:
i += 1
update = self._update_dict(params, gradient=gradient)
if update:
self.logger.info("Updated: {}".format(update))
else:
self.logger.info("Reached end of gradient.")
break
self.params.update(update)
pairs, explain = self._run(params)
return pairs, explain
@staticmethod
def _update_dict(params, gradient):
update = {}
for param_key, gradient_tuple in gradient.items():
delta, mn, mx = gradient_tuple
try:
val = params[param_key] + delta
val = clip(val, mn, mx)
if params[param_key] != val:
update[param_key] = val
except Exception as e:
raise e
return update
[docs] @staticmethod
def open_help():
"""Open the documentation help in a new browser tab."""
webbrowser.open(DOCURL)
[docs] def copy(self):
"""Copy this design and its parameters."""
designer = self.__class__()
designer.params = self.params.copy()
def __copy__(self):
return self.copy()
[docs]class RestoreAfterRun:
"""Class to restore boulderio to its original parameters after a run."""
def __init__(self, boulderio):
self.params = boulderio
def __enter__(self):
for v in self.params._params.values():
v.hold_restore()
def __exit__(self, a, b, c):
for v in self.params._params.values():
v.restore()
[docs]class Design(DesignBase, AllParameters):
def __init__(self):
"""Initialize a new design. Set parameters using.
:attr:`Design.settings`, which
returns an instance of
:class:`DesignPresets <primer3plus.design.DesignPresets>`.
Alternatively, parameters can be accessed more directly using
the name of the parameter descriptor. For a list of parameters available, see
:ref:`BoulderIO Parameters <api_default_parameters>`.
.. code-block::
design = Design()
design.settings.template("AGGCTGTAGTGCTTGTAGCTGGTTGCGTTACTGTG")
design.settings.left_sequence("GTAGTGCTTGTA")
design.SEQUENCE_ID.value = "MY ID"
design.run()
"""
super().__init__()
self._settings = DesignPresets(self)
def set(self, key, value):
self.params.defs[key].value = value
def get(self, key):
return self.params.defs[key]
@property
def settings(self) -> "DesignPresets":
"""Return the :class:`DesignPresets <primer3plus.design.DesignPresets>`
instance for this design."""
return self._settings
@property
def presets(self):
depreciated_warning("'presets' has been renamed to 'settings'")
return self.settings
[docs] def update(self, data: Dict[str, Any]):
"""Update an arbitrary parameter."""
return self.params.update(data)
[docs] def run(self) -> Tuple[List[Dict], List[Dict]]:
"""Design primers. Optionally provide additional parameters.
:param params:
:return: results
"""
with RestoreAfterRun(self.params):
self.settings._resolve()
return super()._run(None)
[docs] def run_and_optimize(
self,
max_iterations,
params: BoulderIO = None,
gradient: Dict[
str, Tuple[Union[float, int], Union[float, int], Union[float, int]]
] = None,
pick_anyway: bool = False,
) -> Tuple[List[dict], List[dict]]:
"""Design primers. If primer design is unsuccessful, relax parameters
as defined in primer3plust.Design.DEFAULT_GRADIENT. Repeat for the
specified number of max_iterations.
:param max_iterations: the max number of iterations to perform relaxation
:param params: optional parameters to provide
:param gradient: optional gradient to provide. If not provided,
Design.DEFAULT_GRADIENT will be used. The gradient is a
dictionary off 3 tuples, the step the min and the max.
:param pick_anyway: if set to True, if the optimization finds no pairs,
pick a pair anyways.
:return: results
"""
with RestoreAfterRun(self.params):
self.settings._resolve()
pairs, explain = super().run_and_optimize(max_iterations)
if pick_anyway and not pairs:
self.settings.pick_anyway(1)
pairs, explain = super().run()
return pairs, explain
[docs]def new(params=None):
"""Start a new design."""
design = Design()
if params:
design.params.update(params)