pax_global_header 0000666 0000000 0000000 00000000064 15007617443 0014521 g ustar 00root root 0000000 0000000 52 comment=68c09304341c3656f3dfe63eda37757d6a7b3917
DnaChisel-3.2.16/ 0000775 0000000 0000000 00000000000 15007617443 0013444 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/.github/ 0000775 0000000 0000000 00000000000 15007617443 0015004 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/.github/workflows/ 0000775 0000000 0000000 00000000000 15007617443 0017041 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/.github/workflows/build.yml 0000664 0000000 0000000 00000001541 15007617443 0020664 0 ustar 00root root 0000000 0000000 name: build
on: [push, workflow_dispatch]
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade pytest pytest-cov coveralls
sudo apt-get install ncbi-blast+ bowtie
- name: Install
run: |
pip install -e .[reports,tests]
- name: Test with pytest
run: |
python -m pytest --cov dnachisel --cov-report term-missing
- name: Coveralls
uses: coverallsapp/github-action@v2
continue-on-error: true
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
env:
COVERALLS_SERVICE_NAME: github
DnaChisel-3.2.16/.github/workflows/document.yml 0000664 0000000 0000000 00000000725 15007617443 0021406 0 ustar 00root root 0000000 0000000 name: documentation
on:
workflow_dispatch:
release:
types: [created]
jobs:
document:
runs-on: ubuntu-24.04
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
pages: write
id-token: write
steps:
- id: deployment
uses: sphinx-notes/pages@v3
with:
python_version: 3.12
documentation_path: ./docs
requirements_path: ./docs/requirements.txt
DnaChisel-3.2.16/.github/workflows/publish.yml 0000664 0000000 0000000 00000001754 15007617443 0021241 0 ustar 00root root 0000000 0000000 name: Publish to PyPI
on:
release:
types: [created]
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install
run: |
pip install pytest
sudo apt-get install ncbi-blast+ bowtie
pip install -e .[reports,tests]
- name: Test
run: |
python -m pytest
deploy:
runs-on: ubuntu-24.04
needs: [test]
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
cache-dependency-path: '**/pyproject.toml'
- name: Install dependencies
run: |
pip install setuptools wheel build
- name: Build
run: |
python -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@release/v1
DnaChisel-3.2.16/.gitignore 0000664 0000000 0000000 00000003407 15007617443 0015440 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
DnaChisel-3.2.16/LICENSE 0000664 0000000 0000000 00000002122 15007617443 0014446 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2017 Edinburgh Genome Foundry, University of Edinburgh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
DnaChisel-3.2.16/README.rst 0000664 0000000 0000000 00000020335 15007617443 0015136 0 ustar 00root root 0000000 0000000 .. raw:: html
DNA Chisel - a versatile sequence optimizer
===========================================
.. image:: https://github.com/Edinburgh-Genome-Foundry/DnaChisel/actions/workflows/build.yml/badge.svg
:target: https://github.com/Edinburgh-Genome-Foundry/DnaChisel/actions/workflows/build.yml
:alt: GitHub CI build status
.. image:: https://coveralls.io/repos/github/Edinburgh-Genome-Foundry/DnaChisel/badge.svg?branch=master
:target: https://coveralls.io/github/Edinburgh-Genome-Foundry/DnaChisel?branch=master
DNA Chisel (complete documentation `here `_)
is a Python library for optimizing DNA sequences with respect to a set of
constraints and optimization objectives. It can also be used via a command-line
interface, or a `web application `_.
The library comes with over 15 classes of sequence specifications which can be
composed to, for instance, codon-optimize genes, meet the constraints of a
commercial DNA provider, avoid homologies between sequences, tune GC content,
or all of this at once! Users can also define their own specifications using
Python, making the library suitable for a large range of automated sequence
design applications, and complex custom design projects. A specification can be
either a hard constraint, which must be satisfied in the final sequence, or an
optimization objective, whose score must be maximized.
For more information, please see the publication.
Citation
--------
DNA Chisel, a versatile sequence optimizer, *Valentin Zulkower, Susan Rosser.* `Bioinformatics `_ (2020) 36, 16, 4508–4509
Usage
-----
Defining a problem via scripts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The example below will generate a random sequence and optimize it so that:
- It will be rid of BsaI sites (on both strands).
- GC content will be between 30% and 70% on every 50bp window.
- The reading frame at position 500-1400 will be codon-optimized for *E. coli*.
.. code:: python
from dnachisel import *
# DEFINE THE OPTIMIZATION PROBLEM
problem = DnaOptimizationProblem(
sequence=random_dna_sequence(10000),
constraints=[
AvoidPattern("BsaI_site"),
EnforceGCContent(mini=0.3, maxi=0.7, window=50),
EnforceTranslation(location=(500, 1400))
],
objectives=[CodonOptimize(species='e_coli', location=(500, 1400))]
) # Note: always use a codon optimisation specification with EnforceTranslation
# SOLVE THE CONSTRAINTS, OPTIMIZE WITH RESPECT TO THE OBJECTIVE
problem.resolve_constraints()
problem.optimize()
# PRINT SUMMARIES TO CHECK THAT CONSTRAINTS PASS
print(problem.constraints_text_summary())
print(problem.objectives_text_summary())
# GET THE FINAL SEQUENCE (AS STRING OR ANNOTATED BIOPYTHON RECORDS)
final_sequence = problem.sequence # string
final_record = problem.to_record(with_sequence_edits=True)
Defining a problem via Genbank features
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can also define a problem by annotating directly a Genbank as follows:
.. raw:: html
Note that constraints (colored in blue in the illustration) are features of type
``misc_feature`` with a prefix ``@`` followed by the name of the constraints
and its parameters, which are the same as in python scripts. Optimization
objectives (colored in yellow in the illustration) use prefix ``~``. See
`the Genbank API documentation `_
for more details.
Genbank files with specification annotations can be directly fed to the
`web application `_
or processed via the command line interface:
.. code:: bash
# Output the result to "optimized_record.gb"
dnachisel annotated_record.gb optimized_record.gb
Or via a Python script:
.. code:: python
from dnachisel import DnaOptimizationProblem
problem = DnaOptimizationProblem.from_record("my_record.gb")
problem.optimize_with_report(target="report.zip")
By default, only the built-in specifications of DNA Chisel can be used in the
annotations, however it is easy to add your own specifications to the Genbank
parser, and build applications supporting custom specifications on top of
DNA Chisel.
Reports
~~~~~~~
DNA Chisel also implements features for verification and troubleshooting. For
instance by generating optimization reports:
.. code:: python
problem = DnaOptimizationProblem(...)
problem.optimize_with_report(target="report.zip")
Here is an example of summary report:
.. raw:: html
How it works
------------
DNA Chisel hunts down every constraint breach and suboptimal region by
recreating local version of the problem around these regions. Each type of
constraint can be locally *reduced* and solved in its own way, to ensure fast
and reliable resolution.
Below is an animation of the algorithm in action:
.. raw:: html
Installation
------------
DNA Chisel requires Python 3, and can be installed via a pip command:
.. code::
pip install dnachisel # <= minimal install without reports support
pip install 'dnachisel[reports]' # <= full install with all dependencies
The full installation using ``dnachisel[reports]`` downloads heavier libraries
(Matplotlib, PDF reports, sequenticon) for report generation, but is highly
recommended to use DNA Chisel interactively via Python scripts. Also install
`GeneBlocks `_ and its
dependencies if you wish to include a plot of sequence edits in the report.
Optionally, also install Bowtie to be able to use ``AvoidMatches`` (which
removes short homologies with existing genomes). On Ubuntu:
.. code::
sudo apt-get install bowtie
License = MIT
-------------
DNA Chisel is an open-source software originally written at the `Edinburgh Genome Foundry
`_ by `Zulko `_
and `released on Github `_ under the MIT licence (Copyright 2017 Edinburgh Genome Foundry, University of Edinburgh). Everyone is welcome to contribute!
More biology software
---------------------
.. image:: https://raw.githubusercontent.com/Edinburgh-Genome-Foundry/Edinburgh-Genome-Foundry.github.io/master/static/imgs/logos/egf-codon-horizontal.png
:target: https://edinburgh-genome-foundry.github.io/
DNA Chisel is part of the `EGF Codons `_ synthetic biology software suite for DNA design, manufacturing and validation.
Related projects
----------------
(If you would like to see a DNA Chisel-related project advertized here, please open
an issue or propose a PR)
- `Benchling `_ uses DNA Chisel as part of its sequence
optimization pipeline according to `this webinar video `_.
- `dnachisel-dtailor-mode `_ brings
features from `D-tailor `_
to DNA Chisel, in particular for the generation of large collection of sequences
covering the objectives fitness landscape (i.e. with sequences with are good at
some objectives and bad at others, and vice versa).
DnaChisel-3.2.16/changes.md 0000664 0000000 0000000 00000002011 15007617443 0015370 0 ustar 00root root 0000000 0000000 # Changes
## v3.0.0
**API-breaking changes:**
- The strand on which to look for a pattern in AvoidPattern and
EnforcePatternOccurence is not given by the location (strand 1, -1, or strand
0 to look in both strands).
- AvoidNonUniqueSegments renamed in UniquifyKmers
- Complete rewrite of CodonOptimization, method names are now use_best_codon,
**New features**
- Support for different genetic codes in EnforceTranslation.
- Support for non-standard start codons in EnforceTranslation
- New AvoidMatches specification based on Bowtie.
- AvoidChanges and EnforceChanges can now tunable (EnforceChanges(60%),
AvoidChanges(max_edits=2))
- New SequencePattern MotifPssmPattern.
**Misc.**
- Got rid of some tables like Genetic code, now taken from Biopython.
- Complete code restructuring by cutting large files into smaller ones.
## v2.0.0
- ``problem.n_mutations`` became ``problem.mutations_per_iteration`` for clarity
- ``'harmonized'`` option in ``CodonOptimize()`` replaced by ``harmonized_frequencies`` for clarity DnaChisel-3.2.16/dnachisel/ 0000775 0000000 0000000 00000000000 15007617443 0015376 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/DnaOptimizationProblem/ 0000775 0000000 0000000 00000000000 15007617443 0022030 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/DnaOptimizationProblem/CircularDnaOptimizationProblem.py 0000664 0000000 0000000 00000017004 15007617443 0030523 0 ustar 00root root 0000000 0000000 """Attempt at a special subclass of DnaOptimizationProblem for circular
sequences. Needs more docs. See example in the examples folder.
"""
from ..reports.optimization_reports import (
write_optimization_report,
write_no_solution_report,
)
from .DnaOptimizationProblem import DnaOptimizationProblem
from .NoSolutionError import NoSolutionError
from ..Location import Location
class CircularViewProblem(DnaOptimizationProblem):
"""Class representing an optimization problem as the concatenation of 3
times the same sequence and specifications, in order to model the
circularity of the DNA sequence"""
def _replace_sequence(self, new_sequence):
L = len(new_sequence) // 3
def return_the_loony(a, b, c):
"""Return the element of a, b, c, which is unlike the other 2"""
if a == b:
return c
elif a == c:
return b
else:
return a
self.sequence = 3 * "".join(
[
return_the_loony(
new_sequence[i],
new_sequence[i + L],
new_sequence[i + 2 * L],
)
for i in range(L)
]
)
class CircularDnaOptimizationProblem(DnaOptimizationProblem):
"""Class for solving circular DNA optimization problems.
The high-level interface is the same as for DnaOptimizationProblem:
initialization, resolve_constraints(), and optimize() work the same way.
"""
def _circularized_specs(self, specs, central_specs_only=False):
L = len(self.sequence)
new_specs = []
for spec in specs:
loc = spec.location
if (loc.start, loc.end) == (0, L):
new_location = Location(0, 3 * L, strand=loc.strand)
new_specs.append(
spec.copy_with_changes(location=new_location, derived_from=spec)
)
else:
new_location = loc + L
new_specs += [spec.shifted(i) for i in [0, L, 2 * L]]
central_loc = Location(L, 2 * L)
if central_specs_only:
new_specs = [
spec
for spec in new_specs
if spec.location.overlap_region(central_loc) is not None
]
return new_specs
def _circularized_view(
self,
with_objectives=False,
with_constraints=False,
central_specs_only=False,
):
return CircularViewProblem(
sequence=3 * self.sequence,
constraints=(
self._circularized_specs(
self.constraints, central_specs_only=central_specs_only
)
if with_constraints
else []
),
objectives=(
self._circularized_specs(
self.objectives, central_specs_only=central_specs_only
)
if with_objectives
else []
),
logger=self.logger,
)
def _recentered_evaluations(self, evals):
L = len(self.sequence)
central_loc = Location(L, 2 * L)
for e in list(evals.evaluations):
e.specification = e.specification.derived_from
if e.locations is not None:
e.locations = [
(loc - L)
for loc in e.locations
if loc.overlap_region(central_loc) is not None
]
e.message = e.default_message
return evals
def constraints_evaluations(self, autopass=True):
"""Return the evaluation of constraints.
The "autopass_constraints" enables to just assume that constraints
enforced by the mutation space are verified.
"""
circularized = self._circularized_view(
with_constraints=True, central_specs_only=False
)
evals = circularized.constraints_evaluations(autopass=autopass)
return self._recentered_evaluations(evals)
def all_constraints_pass(self, autopass=True):
"""Return whether the current problem sequence passes all constraints."""
evals = self.constraints_evaluations(autopass=autopass)
return evals.all_evaluations_pass()
def objectives_evaluations(self):
circularized = self._circularized_view(
with_objectives=True, central_specs_only=False
)
evals = circularized.objectives_evaluations()
return self._recentered_evaluations(evals)
def resolve_constraints(self, final_check=True):
problem = self._circularized_view(with_constraints=True)
L = len(self.sequence)
central_loc = Location(L, 2 * L)
for c in problem.constraints:
if c.location.overlap_region(central_loc) is not None:
problem.resolve_constraint(c)
self.sequence = problem.sequence[L : 2 * L]
if final_check:
self.perform_final_constraints_check()
def optimize(self):
problem = self._circularized_view(with_constraints=True, with_objectives=True)
problem.optimize()
L = len(self.sequence)
self.sequence = problem.sequence[L : 2 * L]
def optimize_with_report(
self,
target,
project_name="My project",
file_path=None,
file_content=None,
):
"""Resolve constraints, optimize objectives, write a multi-file report.
WARNING: in case of optimization failure, the report generated will
show a "pseudo-circular" sequence formed by concatenating the sequence
with itself three times.
TODO: fix the warning above, at some point?
The report's content may vary depending on the optimization's success.
Parameters
----------
target
Either a path to a folder that will contain the report, or a path to
a zip archive, or "@memory" to return raw data of a zip archive
containing the report.
project_name
Project name to write on PDF reports
Returns
-------
(success, message, zip_data)
Triplet where success is True/False, message is a one-line string
summary indication whether some clash was found, or some solution, or
maybe no solution was found because the random searches were too
short
"""
self.logger(message="Solving constraints")
try:
self.resolve_constraints()
except NoSolutionError as error:
problem = self._circularized_view(
with_constraints=True, with_objectives=True
)
self.logger(message="No solution found: making report")
data = write_no_solution_report(
target,
problem,
error,
file_path=file_path,
file_content=file_content,
)
start, end, s = error.location.to_tuple()
message = "No solution found in zone [%d, %d]: %s." % (
start,
end,
str(error),
)
return False, message, data
self.logger(message="Now optimizing the sequence")
self.optimize()
self.logger(message="Success! Generating report.")
data = write_optimization_report(
target,
self,
project_name=project_name,
file_path=file_path,
file_content=file_content,
)
return True, "Optimization successful.", data
DnaChisel-3.2.16/dnachisel/DnaOptimizationProblem/DnaOptimizationProblem.py 0000664 0000000 0000000 00000022145 15007617443 0027040 0 ustar 00root root 0000000 0000000 """Define the DnaOptimizationProblem class.
DnaOptimizationProblem is where the whole problem is defined: sequence,
constraints, objectives.
"""
from Bio.SeqRecord import SeqRecord
from proglog import default_bar_logger
from ..Specification.SpecificationSet import SpecificationSet
from ..biotools import sequences_differences_array
from ..MutationSpace import MutationSpace
from ..reports.optimization_reports import (
write_optimization_report,
write_no_solution_report,
)
from .NoSolutionError import NoSolutionError
from . import mixins
class DnaOptimizationProblem(
mixins.ConstraintsSolverMixin,
mixins.ObjectivesMaximizerMixin,
mixins.RecordRepresentationMixin,
):
"""Problem specifications: sequence, constraints, optimization objectives.
The original constraints, objectives, and original sequence of the problem
are stored in the DNA problem. This class also has methods to display
reports on the constraints and objectives, as well as solving the
constraints and objectives.
Examples
--------
>>> from dnachisel import *
>>> problem = DnaOptimizationProblem(
>>> sequence = "ATGCGTGTGTGC...",
>>> constraints = [constraint1, constraint2, ...],
>>> objectives = [objective1, objective2, ...]
>>> )
>>> problem.resolve_constraints()
>>> problem.optimize()
>>> print(problem.constraints_text_summary())
>>> print(problem.objectives_text_summary())
Parameters
----------
sequence
A string of ATGC characters (they must be upper case!), e.g. "ATTGTGTA"
constraints
A list of objects of type ``Specification``.
objectives
A list of objects of type ``Specification`` specifying what must be
optimized in the problem. Note that each objective has a float ``boost``
parameter. The larger the boost, the more the objective is taken into
account during the optimization.
logger
Either None for no logger, 'bar' for a tqdm progress bar logger, or
any ProgLog progress bar logger.
mutations_space
A MutationSpace indicating the possible mutations. In most case the
mutation space will be left to None and computed at problem
initialization (which can be slightly compute-intensive), however some
core DNA Chisel methods will create optimization problems with a provided
mutation_space to save computing time.
Attributes
----------
randomization_threshold
The algorithm will use an exhaustive search when the size of the mutation
space (=the number of possible variants) is above this threshold, and
a (guided) random search when it is above.
max_random_iters
When using a random search, stop after this many iterations
mutations_per_iteration
When using a random search, produce this many sequence mutations each
iteration.
optimization_stagnation_tolerance
When using a random search, stop if the score hasn't improved in
the last "this many" iterations
local_extensions
Try local resolution several times if it fails, increasing the mutable
zone by [N1, N2...] nucleotides on each side, until resolution works.
(by default, an extension of 0bp is tried, then 5bp.
Notes
-----
The dictionary ``self.possible_mutations`` is of the form
``{location1 : list1, location2: list2...}``
where ``location`` is either a single index (e.g. 10) indicating the
position of a nucleotide to be muted, or a couple ``(start, end)``
indicating a whole segment whose sub-sequence should be replaced.
The ``list`` s are lists of possible sequences to replace each location,
e.g. for the mutation of a whole codon ``(3,6): ["ATT", "ACT", "AGT"]``.
"""
randomization_threshold = 10000
max_random_iters = 1000
mutations_per_iteration = 2
optimization_stagnation_tolerance = 100
local_extensions = (0, 5)
def __init__(
self,
sequence,
constraints=None,
objectives=None,
logger="bar",
mutation_space=None,
):
"""Initialize"""
if isinstance(sequence, SeqRecord):
self.record = sequence
self.sequence = str(sequence.seq).upper()
else:
self.record = None
self.sequence = sequence.upper()
self.constraints = [] if constraints is None else list(constraints)
self.objectives = [] if objectives is None else list(objectives)
self.logger = default_bar_logger(
logger,
bars=("objective", "constraint", "location"),
ignored_bars=("mutation",),
min_time_interval=0.2,
)
self.mutation_space = mutation_space
self.initialize()
def initialize(self):
"""Precompute specification sets, evaluations, and mutation space."""
# Find the specifications (objectives, constraints) which are actually
# SpecificationSets, and unpack these to complete the lists of
# objectives and constraints.
for specs in (self.constraints, self.objectives):
specsets = [spec for spec in specs if isinstance(spec, SpecificationSet)]
specs_in_sets = [
spec for specset in specsets for spec in specset.specifications.values()
]
for specset in specsets:
specs.remove(specset)
specs.extend(specs_in_sets)
# INITIALIZE THE CONSTRAINTS AND OBJECTIVES
self.constraints = [
constraint.initialized_on_problem(self, role="constraint")
for constraint in self.constraints
]
self.objectives = [
objective.initialized_on_problem(self, role="objective")
for objective in self.objectives
]
# INITIALIZE THE "BEFORE" CLASS ATTRIBUTES, USED IN REPORTS
self.sequence_before = self.sequence
self._constraints_before = None
self._objectives_before = None
# INITIALIZE THE MUTATION SPACE
if self.mutation_space is None:
self.mutation_space = MutationSpace.from_optimization_problem(self)
# If the original sequence is outside of the allowed mutations
# space, replace the sequence by a sequence which complies with
# the mutation space.
self.sequence = self.mutation_space.constrain_sequence(self.sequence)
def _replace_sequence(self, new_sequence):
"""Replace the current sequence of the problem.
This method is subclassed in CircularDnaOptimization problem where
is is more complex (changing the sequence in one location changes
it in more locations).
"""
self.sequence = new_sequence
def sequence_edits_as_array(self):
"""Return an array [False, False, True...] where True indicates an edit
(i.e. a change at this position between the original problem sequence
and the current one)."""
return sequences_differences_array(self.sequence, self.sequence_before)
def number_of_edits(self):
"""Return the number of nucleotide differences between the original
and current sequence."""
return self.sequence_edits_as_array().sum()
def optimize_with_report(
self,
target,
project_name="My project",
file_path=None,
file_content=None,
):
"""Resolve constraints, optimize objectives, write a multi-file report.
The report's content may vary depending on the optimization's success.
Parameters
----------
target
Either a path to a folder that will contain the report, or a path to
a zip archive, or "@memory" to return raw data of a zip archive
containing the report.
project_name
Project name to write on PDF reports
Returns
-------
(success, message, zip_data)
Triplet where success is True/False, message is a one-line string
summary indication whether some clash was found, or some solution, or
maybe no solution was found because the random searches were too short
"""
self.logger(message="Solving constraints")
try:
self.resolve_constraints()
except NoSolutionError as error:
self.logger(message="No solution found: making report")
data = write_no_solution_report(
target,
self,
error,
file_path=file_path,
file_content=file_content,
)
start, end, s = error.location.to_tuple()
message = "No solution found in zone [%d, %d]: %s." % (
start,
end,
str(error),
)
return False, message, data
self.logger(message="Now optimizing the sequence")
self.optimize()
self.logger(message="Success! Generating report.")
data = write_optimization_report(
target,
self,
project_name=project_name,
file_path=file_path,
file_content=file_content,
)
return True, "Optimization successful.", data
DnaChisel-3.2.16/dnachisel/DnaOptimizationProblem/NoSolutionError.py 0000664 0000000 0000000 00000000764 15007617443 0025534 0 ustar 00root root 0000000 0000000 class NoSolutionError(Exception):
"""Exception returned when a DnaOptimizationProblem aborts.
This means that the constraints are found to be unsatisfiable.
"""
def __init__(self, message, problem, constraint=None, location=None):
"""Initialize."""
Exception.__init__(self, message)
self.message = message
self.problem = problem
self.constraint = constraint
self.location = location
def __str__(self):
return self.message
DnaChisel-3.2.16/dnachisel/DnaOptimizationProblem/__init__.py 0000664 0000000 0000000 00000000435 15007617443 0024143 0 ustar 00root root 0000000 0000000 from .NoSolutionError import NoSolutionError
from .DnaOptimizationProblem import DnaOptimizationProblem
from .CircularDnaOptimizationProblem import CircularDnaOptimizationProblem
__all__ = [
"NoSolutionError",
"DnaOptimizationProblem",
"CircularDnaOptimizationProblem",
]
DnaChisel-3.2.16/dnachisel/DnaOptimizationProblem/mixins/ 0000775 0000000 0000000 00000000000 15007617443 0023337 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/DnaOptimizationProblem/mixins/ConstraintsSolverMixin.py 0000664 0000000 0000000 00000036427 15007617443 0030434 0 ustar 00root root 0000000 0000000 from ...Location import Location
from ...Specification.SpecEvaluation import (
ProblemConstraintsEvaluations,
)
from ..NoSolutionError import NoSolutionError
class ConstraintsSolverMixin:
@property
def constraints_before(self):
""""""
if self._constraints_before is None:
sequence = self.sequence
self.sequence = self.sequence_before
self._constraints_before = self.constraints_evaluations()
self.sequence = sequence
return self._constraints_before
def constraints_evaluations(self, autopass=True):
"""Return a list of the evaluations of each constraint of the problem.
The "autopass" enables to just assume that constraints
enforced by the mutation space are verified.
"""
return ProblemConstraintsEvaluations.from_problem(
self, autopass_constraints=autopass
)
def all_constraints_pass(self, autopass=True):
"""Return whether the current problem sequence passes all constraints."""
if len(self.constraints) == 0:
return True
return all(
c.evaluate(self).passes
for c in self.constraints
if (not autopass) or (not c.enforced_by_nucleotide_restrictions)
)
def constraints_text_summary(self, failed_only=False, autopass=True):
evals = self.constraints_evaluations(autopass=autopass)
if failed_only:
evals = evals.filter("failing")
return evals.to_text()
def get_focus_constraint(self):
focus_constraints = [c for c in self.constraints if c.is_focus]
if len(focus_constraints) == 1:
focus = focus_constraints[0]
other_constraints = [c for c in self.constraints if not c.is_focus]
return focus, other_constraints
return None, None
def resolve_constraints_by_exhaustive_search(self):
"""Solve all constraints by exploring the whole search space.
This method iterates over ``self.iter_mutations_space()`` (space of
all sequences that could be reached through successive mutations) and
stops when it finds a sequence which meets all the constraints of the
problem.
"""
focus_constraint, other_constraints = self.get_focus_constraint()
sequence_before = self.sequence
all_variants = self.mutation_space.all_variants(self.sequence)
space_size = int(self.mutation_space.space_size)
self.logger(mutation__total=space_size)
for variant in self.logger.iter_bar(mutation=all_variants):
self.sequence = variant
if focus_constraint is not None:
if focus_constraint.evaluate(self).passes:
if all(c.evaluate(self).passes for c in other_constraints):
self.logger(mutation__index=space_size)
return
elif self.all_constraints_pass():
self.logger(mutation__index=space_size)
return
self.sequence = sequence_before
raise NoSolutionError(
"Exhaustive search failed to satisfy all constraints.",
problem=self,
)
def resolve_constraints_by_random_mutations(self):
"""Solve all constraints by successive sets of random mutations.
This method modifies the problem sequence by applying a number
``mutations_per_iteration`` of random mutations. The constraints are
then evaluated on the new sequence. If all constraints pass, the new
sequence becomes the problem's new sequence.
If not all constraints pass, the sum of all scores from failing
constraints is considered. If this score is superior to the score of
the previous sequence, the new sequence becomes the problem's new
sequence.
This operation is repeated `max_iter` times at most, after which
a ``NoSolutionError`` is thrown if no solution was found.
"""
focus_constraint, other_constraints = self.get_focus_constraint()
if focus_constraint is not None:
self.resolve_single_constraint_by_random_mutations(
focus_constraint, other_constraints
)
return
evaluations = self.constraints_evaluations()
score = sum([e.score for e in evaluations if not e.passes])
iters = range(self.max_random_iters)
for i in self.logger.iter_bar(mutation=iters):
if all(e.passes for e in evaluations):
self.logger(mutation__index=iters)
return
previous_sequence = self.sequence
self.sequence = self.mutation_space.apply_random_mutations(
n_mutations=self.mutations_per_iteration,
sequence=self.sequence,
)
evaluations = self.constraints_evaluations()
new_score = sum([e.score for e in evaluations if not e.passes])
if new_score > score:
score = new_score
else:
self.sequence = previous_sequence
raise NoSolutionError(
"Random search did not find a solution in the given number of "
"attempts. Try to increase the number of attempts with:\n\n"
"problem.max_random_iters = 5000 # or even 10000, 20000, etc.\n\n"
"If the problem persists, you may be in presence of a complex or "
"unsolvable problem.",
problem=self,
)
def resolve_single_constraint_by_random_mutations(
self, constraint, other_constraints
):
evaluation = constraint.evaluation
score = evaluation.score
iters = range(self.max_random_iters)
for i in self.logger.iter_bar(mutation=iters):
previous_sequence = self.sequence
self.sequence = self.mutation_space.apply_random_mutations(
n_mutations=self.mutations_per_iteration,
sequence=self.sequence,
)
new_evaluation = constraint.evaluate(self)
if new_evaluation.score > score:
if all(c.evaluate(self).passes for c in other_constraints):
score = new_evaluation.score
if new_evaluation.passes:
self.logger(mutation__index=iters)
return
else:
self.sequence = previous_sequence
else:
self.sequence = previous_sequence
raise NoSolutionError(
"Random search did not find a solution in the given number of "
"attempts. Try to increase the number of attempts with:\n\n"
"problem.max_random_iters = 5000 # or even 10000, 20000, etc.\n\n"
"If the problem persists, you may be in presence of a complex or "
"unsolvable problem.",
problem=self,
)
def resolve_constraints_locally(self):
"""Perform a local search, either stochastic or exhaustive."""
if self.mutation_space.space_size < self.randomization_threshold:
self.resolve_constraints_by_exhaustive_search()
else:
self.resolve_constraints_by_random_mutations()
def resolve_constraint(self, constraint):
"""Resolve a constraint through successive localizations."""
# EVALUATE THE CONSTRAINT, FIND BREACHING LOCATIONS
evaluation = constraint.evaluate(self)
if evaluation.passes:
return
locations = sorted(evaluation.locations)
iterator = self.logger.iter_bar(
location=locations, bar_message=lambda loc: str(loc)
)
# FOR EACH LOCATION, CREATE A LOCAL PROBLEM AND RESOLVE LOCALLY.
for i, location in enumerate(iterator):
# SEVERAL "EXTENSIONS" OF THE LOCAL ZONE WILL BE TESTED IN TURN
# IN CASE THE LOCAL SEQUENCE IS FROZEN DUE TO NUCLEOTIDE INTER-
# DEPENDENCIES (CODONS, ETC.)
for extension in self.local_extensions:
new_location = location.extended(extension)
mutation_space = self.mutation_space.localized(new_location)
if mutation_space.space_size == 0:
# If the sequence is frozen at this location, either
# "continue" (go straight to the next, larger extension)
# or if we are already in the largest extension, return
# an error with data that will be used by the report
# generator.
if extension != self.local_extensions[-1]:
continue
else:
error = NoSolutionError(
location=new_location,
problem=self,
message="Constraint breach in region that cannot "
"be mutated.",
)
error.location = new_location
error.constraint = constraint
error.message = "While solving %s in %s:\n\n%s" % (
constraint,
new_location,
str(error),
)
self.logger(
location__index=len(locations),
location__message="Cold exit",
)
raise error
new_location = Location(*mutation_space.choices_span)
# This blocks solves the problem of overlapping breaches,
# which can make the local optimization impossible.
# If the next constraint breach overlaps with the current
# location, localize the constraint with a with_righthand=False
# flag, which will be used by the constraints ".localized"
# method to only consider the right-hand side.
if (i < (len(locations) - 1)) and (
locations[i + 1].overlap_region(new_location)
):
this_local_constraint = constraint.localized(
new_location, with_righthand=False, problem=self
)
else:
this_local_constraint = constraint.localized(
new_location, problem=self
)
evaluation = this_local_constraint.evaluate(self)
# MAYBE THE LOCAL BREACH WAS ALREADY RESOLVED AS A SIDE EFFECT
# OF SOLVING PREVIOUS BREACHES. IN THAT CASE, PASS.
if evaluation.passes:
continue
# ELSE, CREATE A NEW LOCAL PROBLEM WITH LOCALIZED CONSTRAINTS
this_local_constraint.is_focus = True
this_local_constraint.evaluation = evaluation
localized_constraints = [
cst.localized(new_location, problem=self)
for cst in self.constraints
if cst != constraint and not cst.enforced_by_nucleotide_restrictions
]
passing_localized_constraints = [
cst
for cst in localized_constraints
if cst is not None and cst.evaluate(self).passes
]
local_problem = self.__class__(
sequence=self.sequence,
constraints=(
[this_local_constraint] + passing_localized_constraints
),
mutation_space=mutation_space,
)
local_problem.randomization_threshold = self.randomization_threshold
local_problem.max_random_iters = self.max_random_iters
local_problem.mutations_per_iteration = self.mutations_per_iteration
# STORE THE LOCAL PROBLEM IN THE LOGGER.
# This is useful for troubleshooting.
self.logger.store(
problem=self,
local_problem=local_problem,
location=location,
)
# RESOLVE THE LOCAL PROBLEM. RETURN AN ERROR IF IT FAILS.
try:
if hasattr(constraint, "resolution_heuristic"):
constraint.resolution_heuristic(local_problem)
else:
local_problem.resolve_constraints_locally()
self._replace_sequence(local_problem.sequence)
break
except NoSolutionError as error:
if extension == self.local_extensions[-1]:
error.location = new_location
error.constraint = constraint
error.message = "While solving %s in %s:\n\n%s" % (
constraint,
new_location,
str(error),
)
self.logger(
location__index=len(locations),
location__message="Cold exit",
)
raise error
else:
continue
def resolve_constraints(self, final_check=True, cst_filter=None):
"""Solve a particular constraint using local, targeted searches.
Parameters
----------
constraint
The ``Specification`` object for which the sequence should be solved
final_check
If True, a final check of that all constraints pass will be run at
the end of the process, when constraints have been resolved one by
one, to check that the solving of one constraint didn't undo the
solving of another.
cst_filter
An optional filter to only resolve a subset function (constraint => True/False)
"""
constraints = [
c
for c in self.constraints
if not c.enforced_by_nucleotide_restrictions
and ((cst_filter is None) or cst_filter(c))
]
if len(constraints) == 0:
return
constraints = sorted(constraints, key=lambda c: -c.priority)
for constraint in self.logger.iter_bar(
constraint=constraints, bar_message=lambda c: str(c)
):
try:
self.resolve_constraint(constraint=constraint)
except NoSolutionError as error:
self.logger(constraint__index=len(constraints))
raise error
if final_check:
self.perform_final_constraints_check()
def perform_final_constraints_check(self):
for cst in self.constraints:
if not cst.evaluate(self).passes:
raise NoSolutionError(
"The solving of all constraints failed to solve"
" all constraints, as some appear unsolved at the end"
" of the optimization. This is an unintended behavior,"
" likely due to a complex problem. Try running the"
" solver on the same sequence again, or report the"
" error to the maintainers:\n\n"
+ self.constraints_text_summary(failed_only=True, autopass=False),
problem=self,
)
DnaChisel-3.2.16/dnachisel/DnaOptimizationProblem/mixins/ObjectivesMaximizerMixin.py 0000664 0000000 0000000 00000020266 15007617443 0030707 0 ustar 00root root 0000000 0000000 from ...Location import Location
from ...Specification.SpecEvaluation import ProblemObjectivesEvaluations
from ..NoSolutionError import NoSolutionError
class ObjectivesMaximizerMixin:
@property
def objectives_before(self):
if self._objectives_before is None:
sequence = self.sequence
self.sequence = self.sequence_before
self._objectives_before = self.objectives_evaluations()
self.sequence = sequence
return self._objectives_before
def objectives_evaluations(self):
"""Return a list of the evaluation of each objective of the problem"""
return ProblemObjectivesEvaluations.from_problem(self)
def objective_scores_sum(self):
return self.objectives_evaluations().scores_sum()
def objectives_text_summary(self):
return self.objectives_evaluations().to_text()
def optimize_by_exhaustive_search(self):
""" """
if not self.all_constraints_pass():
summary = self.constraints_text_summary(failed_only=True)
raise NoSolutionError(
summary + "Optimization can only be done when all constraints are "
"verified.",
self,
)
if all([obj.best_possible_score is not None for obj in self.objectives]):
best_possible_score = sum(
[obj.best_possible_score for obj in self.objectives]
)
else:
best_possible_score = None
current_best_score = self.objective_scores_sum()
current_best_sequence = self.sequence
all_variants = self.mutation_space.all_variants(self.sequence)
space_size = int(self.mutation_space.space_size)
self.logger(mutation__total=space_size)
for variant in self.logger.iter_bar(mutation=all_variants):
self.sequence = variant
if self.all_constraints_pass():
score = self.objective_scores_sum()
if score > current_best_score:
current_best_score = score
current_best_sequence = self.sequence
if (best_possible_score is not None) and (
current_best_score >= best_possible_score
):
self.logger(mutation__index=space_size)
break
self.sequence = current_best_sequence
def optimize_by_random_mutations(self):
""" """
if not self.all_constraints_pass():
summary = self.constraints_text_summary()
raise ValueError(
summary + "Optimization can only be done when all"
" constraints are verified"
)
score = self.objective_scores_sum()
if all([obj.best_possible_score is not None for obj in self.objectives]):
best_possible_score = sum(
[obj.best_possible_score * obj.boost for obj in self.objectives]
)
else:
best_possible_score = None
iters = self.max_random_iters
stagnating_iterations = 0
for iteration in self.logger.iter_bar(mutation=range(iters)):
if (best_possible_score is not None) and (score >= best_possible_score):
self.logger(mutation__index=iters)
break
if (self.optimization_stagnation_tolerance is not None) and (
stagnating_iterations > self.optimization_stagnation_tolerance
):
break
previous_sequence = self.sequence
self.sequence = self.mutation_space.apply_random_mutations(
n_mutations=self.mutations_per_iteration,
sequence=self.sequence,
)
if self.all_constraints_pass():
new_score = self.objective_scores_sum()
if new_score > score:
score = new_score
stagnating_iterations = 0
else:
self.sequence = previous_sequence
else:
self.sequence = previous_sequence
stagnating_iterations += 1
def optimize_objective(self, objective):
"""Optimize the total objective score, focusing on a single objective.
This method will attempt to increase the global objective score by
focusing on a single objective. First the locations of under-optimal
subsequences for this objective are identified, then these locations
are optimized one after the other, left to right.
For each location, a local problem is created and the optimization uses
either a custom optimization algorithm, an exhaustive search, or a
random search, to optimize the local problem
"""
# EVALUATE OBJECTIVE. RETURN IF THERE IS NOTHING TO BE DONE.
evaluation = objective.evaluate(self)
locations = evaluation.locations
if (objective.best_possible_score is not None) and (
evaluation.score == objective.best_possible_score
):
return
# FOR EACH LOCATION, CREATE AND OPTIMIZE A LOCAL PROBLEM.
for location in self.logger.iter_bar(
location=locations, bar_message=lambda l: str(l)
):
# Localize the mutation space by freezing any nucleotide outside of
# it
mutation_space = self.mutation_space.localized(location)
if mutation_space.space_size == 0:
continue
# Update the location so it matches the span of the mutation_space
# the resulting location will be equal or smaller to the original
# location.
location = Location(*mutation_space.choices_span)
localized_constraints = [
cst.localized(location, problem=self) for cst in self.constraints
]
localized_constraints = [
cst for cst in localized_constraints if cst is not None
]
localized_objectives = [
obj.localized(location, problem=self)
for obj in self.objectives
if obj.boost != 0
]
localized_objectives = [
obj for obj in localized_objectives if obj is not None
]
local_problem = self.__class__(
sequence=self.sequence,
constraints=localized_constraints,
mutation_space=mutation_space,
objectives=localized_objectives,
)
self.logger.store(
problem=self, local_problem=local_problem, location=location
)
local_problem.randomization_threshold = self.randomization_threshold
local_problem.max_random_iters = self.max_random_iters
local_problem.optimization_stagnation_tolerance = (
self.optimization_stagnation_tolerance
)
local_problem.mutations_per_iteration = self.mutations_per_iteration
# OPTIMIZE THE LOCAL PROBLEM
if hasattr(objective, "optimization_heuristic"):
# Some specifications implement their own optimization method.
objective.optimization_heuristic(local_problem)
else:
# Run an exhaustive or random search depending on the size
# of the mutation space.
space_size = local_problem.mutation_space.space_size
exhaustive_search = space_size < self.randomization_threshold
if exhaustive_search:
local_problem.optimize_by_exhaustive_search()
else:
local_problem.optimize_by_random_mutations()
# UPDATE THE PROBLEM's SEQUENCE
self.sequence = local_problem.sequence
def optimize(self):
"""Maximize the total score by optimizing each objective in turn."""
objectives = [
obj
for obj in self.objectives
if not obj.optimize_passively and obj.boost != 0
]
if len(objectives) == 0:
return
for objective in self.logger.iter_bar(
objective=objectives, bar_message=lambda o: str(o)
):
self.optimize_objective(objective=objective)
DnaChisel-3.2.16/dnachisel/DnaOptimizationProblem/mixins/RecordRepresentationMixin.py 0000664 0000000 0000000 00000015733 15007617443 0031070 0 ustar 00root root 0000000 0000000 from ...biotools import (
load_record,
write_record,
sequence_to_biopython_record,
find_specification_label_in_feature,
sequences_differences_segments,
)
from ...Specification.Specification import Specification
from ...Location import Location
class RecordRepresentationMixin:
"""Mixin for DnaOptimizationProblem gathering methods for converting
DnaOptimizationProblems from/to Biopython records and Genbank records."""
@classmethod
def from_record(
cls,
record,
specifications_dict="default",
logger="bar",
extra_constraints=(),
extra_objectives=(),
):
"""Create a DnaOptimizationProblem by parsing a record's annotations.
Examples
--------
>>> problem = DnaOptimizationProblem.from_record("my_record.gb")
>>> problem = DnaOptimizationProblem.from_record(some_biopython_record)
Parameters
----------
record
Either a biopython record or path to a genbank/snapgene file.
specifications_dict
Provide a custom dict with user-defined specifications instead of the
default dict, which contains the built-in specifications.
logger
Logger of the DnaOptimizationProblem
extra_constraints, extra_objectives
List of Specifications to be added to the problem, in addition to
the specifications parsed from the genbank.
"""
# unfortunately the local import below is the most elegant found so
# far. builtin_specifications cannot be imported at the top of this
# file as some built-in specifications use DnaOptimizationProblem
# internally to resolve constructs (see EnforcePatternOccurences)
if isinstance(record, str):
record = load_record(record)
parameters = dict(
sequence=record,
constraints=[] + list(extra_constraints), # shallow copy
objectives=[] + list(extra_objectives), # shallow copy
logger=logger,
)
for feature in record.features:
if feature.type != "misc_feature":
continue
label = find_specification_label_in_feature(feature)
if label is None:
continue
specs = Specification.list_from_biopython_feature(
feature, specifications_dict=specifications_dict
)
for role, specification in specs:
parameters[role + "s"].append(specification)
return cls(**parameters)
def to_record(
self,
filepath=None,
features_type="misc_feature",
with_original_features=True,
with_original_spec_features=False,
with_constraints=True,
with_objectives=True,
with_sequence_edits=False,
colors_dict=None,
use_short_labels=True,
record_id=None,
):
"""Return/write record representing the final sequence and problem.
the many options enable to also annotate specifications, sequence
edits, etc.
Parameters
----------
filepath
Path to a target genbank file where the record will be written. If
none is provided, a Biopython record is returned instead
features_type
Genbank standard type to give to all extra genbank annotation created
by this method (to indicate constraints, objectives, edits, etc.)
with_original_features
If True, the features from the original record provided at problem
creation will be included in the record (if a simple sequence, and
not a record, was originally provided, then there is no such
features)
with_original_spec_features
If False, any feature from the original record provided at problem
creation that defines a DNAChisel Specification be stripped off the
record returned by this method (to make space for the annotations)
created by this method
with_constraints
If True, annotations representing the constraints will be added to
the record
with_objectives
If True, annotations representing the objectives will be added to the
record
with_sequence_edits
If True, annotations representing each nucleotide change will be
added to the record.
colors_dict
A dict indicating the feature color for constraints and objectives.
The default is {"constraint": "#355c87", "objective": "#f9cd60"}.
use_short_labels
If True, the annotations representing constraints and objectives will
use shorter labels to indicate the type of specification.
Notes
-----
If the original problem was created from a Genbank, it is a good idea
to set with_original_spec_features=True and with_constraints=False,
with_objectives=False.
"""
record = sequence_to_biopython_record(self.sequence)
if record_id is not None:
record.id = record_id
record.features = []
if with_constraints:
record.features += [
cst.to_biopython_feature(
role="constraint",
feature_type=features_type,
colors_dict=colors_dict,
use_short_label=use_short_labels,
)
for cst in self.constraints
if cst.__dict__.get("location", False)
]
if with_objectives:
record.features += [
obj.to_biopython_feature(
role="objective",
feature_type=features_type,
colors_dict=colors_dict,
use_short_label=use_short_labels,
)
for obj in self.objectives
]
if with_original_features and (self.record is not None):
record.features += [
f
for f in self.record.features
if with_original_spec_features
or not find_specification_label_in_feature(f)
]
if with_sequence_edits:
record.features += self.sequence_edits_as_features()
if filepath is not None:
write_record(record=record, target=filepath, file_format="genbank")
else:
return record
def sequence_edits_as_features(self, feature_type="misc_feature"):
"""Return a list of Biopython Record Features indicating each of the
edits."""
segments = sequences_differences_segments(self.sequence, self.sequence_before)
return [
Location(start, end).to_biopython_feature(
label="%s=>%s"
% (self.sequence_before[start:end], self.sequence[start:end]),
is_edit="true",
ApEinfo_fwdcolor="#ff0000",
color="#ff0000",
)
for start, end in segments
]
DnaChisel-3.2.16/dnachisel/DnaOptimizationProblem/mixins/__init__.py 0000664 0000000 0000000 00000000451 15007617443 0025450 0 ustar 00root root 0000000 0000000 from .ConstraintsSolverMixin import ConstraintsSolverMixin
from .ObjectivesMaximizerMixin import ObjectivesMaximizerMixin
from .RecordRepresentationMixin import RecordRepresentationMixin
__all__ = [
"ConstraintsSolverMixin",
"ObjectivesMaximizerMixin",
"RecordRepresentationMixin",
]
DnaChisel-3.2.16/dnachisel/Location.py 0000664 0000000 0000000 00000014653 15007617443 0017531 0 ustar 00root root 0000000 0000000 """Implements the Location class.
The class has useful methods for finding overlaps between locations, extract
a subsequence from a sequence, etc.
"""
from functools import total_ordering
from Bio.SeqFeature import FeatureLocation, SeqFeature
from .biotools import reverse_complement
from Bio.SeqFeature import SeqFeature, FeatureLocation
@total_ordering
class Location:
"""Represent a segment of a sequence, with a start, end, and strand.
Warning: we use Python's splicing notation, so Location(5, 10) represents
sequence[5, 6, 7, 8, 9] which corresponds to nucleotides number
6, 7, 8, 9, 10.
The data structure is similar to a Biopython's FeatureLocation, but with
different methods for different purposes.
Parameters
----------
start
Lowest position index of the segment.
end
Highest position index of the segment.
strand
Either 1 or -1 for sense or anti-sense orientation.
"""
__slots__ = ["strand", "start", "end"]
def __init__(self, start, end, strand=0):
"""Initialize."""
self.start = start
self.end = end
self.strand = strand
def overlap_region(self, other_location):
"""Return the overlap span between two locations (None if None)."""
if other_location.start < self.start:
left_location, right_location = other_location, self
else:
left_location, right_location = self, other_location
if right_location.start >= left_location.end:
return None
start = right_location.start
end = min(left_location.end, right_location.end)
return Location(start, end, self.strand)
def extended(
self,
extension_length,
lower_limit=0,
upper_limit=None,
left=True,
right=True,
):
"""Extend the location of a few basepairs on each side."""
if left:
lower = max(lower_limit, self.start - extension_length)
else:
lower = self.start
if right:
upper = self.end + extension_length
if upper_limit is not None:
upper = min(upper_limit, upper)
else:
upper = self.end
return Location(lower, upper, self.strand)
def extract_sequence(self, sequence):
"""Return the subsequence read at the given location."""
result = sequence[self.start : self.end]
if self.strand == -1:
return reverse_complement(result)
else:
return result
def to_tuple(self):
"""Return (start, end, strand)."""
return (self.start, self.end, self.strand)
@property
def indices(self):
result = list(range(self.start, self.end))
return result if (self.strand != -1) else result[::-1]
def __eq__(self, other):
"""Equal to."""
return self.to_tuple() == other.to_tuple()
def __lt__(self, other):
"""Lower than."""
return self.to_tuple() < other.to_tuple()
def __add__(self, number):
"""Return the location shifted by the number."""
return Location(self.start + number, self.end + number, self.strand)
def __sub__(self, number):
"""Return the location shifted by the number."""
return Location(self.start - number, self.end - number, self.strand)
def __repr__(self):
"""Represent."""
result = "%d-%d" % (self.start, self.end)
if self.strand is not None:
result += {1: "(+)", -1: "(-)", 0: ""}[self.strand]
return result
def __len__(self):
"""Size of the location."""
return self.end - self.start
def __hash__(self):
return hash(self.to_tuple())
@staticmethod
def merge_overlapping_locations(locations):
"""Return a list of locations obtained by merging all overlapping."""
if len(locations) == 0:
return locations
locations = sorted(locations)
new_locations = [locations[0]]
for loc in locations[1:]:
if new_locations[-1].overlap_region(loc) is not None:
new_locations[-1].end = max(new_locations[-1].end, loc.end)
else:
new_locations.append(loc)
return new_locations
@staticmethod
def from_biopython_location(location):
"""Return a DnaChisel Location from a Biopython location."""
start, end, strand = [
None if e is None else int(e)
for e in [location.start, location.end, location.strand]
]
return Location(start, end, strand)
@staticmethod
def from_tuple(some_tuple, default_strand=0):
if len(some_tuple) == 2:
start, end = some_tuple
strand = default_strand
else:
start, end, strand = some_tuple
return Location(start, end, strand)
@staticmethod
def from_data(location_data):
"""Return a location, from varied data formats.
This method is used in particular in every built-in specification to
quickly standardize the input location.
``location_data`` can be a tuple (start, end) or (start, end, strand),
or a Biopython FeatureLocation, or a Location instance. In any case,
a new Location object will be returned.
"""
if location_data is None:
return None
if isinstance(location_data, (tuple, list)):
return Location.from_tuple(location_data)
if isinstance(location_data, FeatureLocation):
feature_location = Location.from_biopython_location(location_data)
if feature_location.strand is None:
feature_location.strand = 0
return feature_location
if isinstance(location_data, Location):
return Location(
location_data.start, location_data.end, location_data.strand
)
def to_biopython_location(self):
"""Return a Biopython FeatureLocation equivalent to the location."""
start, end, strand = [
None if e is None else int(e) for e in [self.start, self.end, self.strand]
]
return FeatureLocation(start, end, strand)
def to_biopython_feature(self, feature_type="misc_feature", **qualifiers):
"""Return a Biopython SeqFeature with same location and custom
qualifiers."""
return SeqFeature(
self.to_biopython_location(),
type=feature_type,
qualifiers=qualifiers,
)
DnaChisel-3.2.16/dnachisel/MutationSpace/ 0000775 0000000 0000000 00000000000 15007617443 0020152 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/MutationSpace/MutationChoice.py 0000664 0000000 0000000 00000011775 15007617443 0023452 0 ustar 00root root 0000000 0000000 from ..biotools import windows_overlap
import itertools
import numpy as np
class MutationChoice:
"""Represent a segment of a sequence with several possible variants.
Parameters
----------
segment
A pair (start, end) indicating the range of nucleotides concerned. We
are applying Python range, so
variants
A set of sequence variants, at the given position
Examples
--------
>>> choice = MutationChoice((70, 73), {})
"""
__slots__ = ["segment", "start", "end", "variants", "is_any_nucleotide"]
def __init__(self, segment, variants, is_any_nucleotide=False):
if isinstance(segment, int):
segment = (segment, segment + 1)
self.segment = segment
self.start, self.end = segment
self.variants = variants
self.is_any_nucleotide = is_any_nucleotide
# self.possible_subsequences = set(m.subsequence for m in mutations)
def random_variant(self, sequence):
"""Return one of the variants, randomly."""
subsequence = sequence[self.start : self.end]
variants = [v for v in self.variants if v != subsequence]
# the sorting of variants seems essential to ensure reproducibility
# between sessions.
# it does not slow down the global algorithm (or less than 3%)
variants = sorted(variants)
return variants[np.random.randint(len(variants))]
def merge_with(self, others):
"""Merge this mutation choice with others to form a single choice.
This method returns the only choices on the full interval which are compatible
with at least one choice in each of the MutationChoices.
Examples:
---------
Consider the following:
>>> ((2, 5), {'ATT', 'ATA'})
To be merged with:
>>> [
>>> ((0, 3), {'GTA', 'GCT', 'GTT'}),
>>> ((3, 4), {'A', 'T', 'G', 'C'}),
>>> ((4, 7), {'ATG', 'ACC', 'CTG'})
>>> ]
The result will be:
>>> (0, 7), {'GTATACC', 'GTATATG'}
"""
others = sorted(others, key=lambda o: o.start)
others_start = others[0].start
final_segment = others_start, others[-1].end
final_variants = set()
for candidate in self.variants:
slots = []
for other in others:
istart, iend = windows_overlap(other.segment, self.segment)
slot = []
for variant in other.variants:
subseq = variant[istart - other.start : iend - other.start]
subcandidate = candidate[istart - self.start : iend - self.start]
if subseq == subcandidate:
slot.append(variant)
slots.append(slot)
for subseqs in itertools.product(*slots):
seq = "".join(subseqs)
matching_seq = seq[self.start - others_start : self.end - others_start]
if matching_seq == candidate:
final_variants.add(seq)
return MutationChoice(segment=final_segment, variants=final_variants)
def extract_varying_region(self):
"""Return MutationChoices for the central varying region and 2 flanks.
For instance:
>>> choice = MutationChoice((5, 12), [
>>> 'ATGCGTG',
>>> 'AAAAATG',
>>> 'AAATGTG',
>>> 'ATGAATG',
>>> ])
>>> choice.extract_varying_region()
Result :
>>> [
>>> MutChoice(5-6 A),
>>> MutChoice(6-10 TGCG-AATG-TGAA-AAAA),
>>> MutChoice(10-12 TG)
>>> ]
"""
if len(self.variants) <= 1:
return [self]
variants = list(self.variants)
reference = variants[0]
start = -1
end = len(reference)
for i in range(len(reference)):
for variant in variants[1:]:
if variant[i] != reference[i]:
if start == -1:
start = i
end = i + 1
break
result = []
if start > 0:
result.append(
MutationChoice(
(self.start, self.start + start), set([reference[:start]])
)
)
result.append(
MutationChoice(
(self.start + start, self.start + end),
set([v[start:end] for v in variants]),
)
)
if end < len(reference):
result.append(
MutationChoice(
(self.start + end, self.end),
set([v[end:] for v in variants]),
)
)
return result
def __repr__(self):
"""Represent."""
subsequences = "-".join(self.variants)
return "MutChoice(%d-%d %s)" % (self.start, self.end, subsequences)
def __str__(self):
"""Represent."""
subsequences = "-".join(self.variants)
return "MutChoice(%d-%d %s)" % (self.start, self.end, subsequences)
DnaChisel-3.2.16/dnachisel/MutationSpace/MutationSpace.py 0000664 0000000 0000000 00000024732 15007617443 0023310 0 ustar 00root root 0000000 0000000 """Define MutationSpace"""
import itertools
import numpy as np
from .MutationChoice import MutationChoice
class MutationSpace:
"""Class for mutation space (set of sequence segments and their variants).
Parameters
----------
choices_index
A list L such that L[i] gives the MutationChoice governing the mutations
allowed at position i (and possibly around i).
Examples
--------
>>> # BEWARE: below, similar mutation choices are actually the SAME OBJECT
>>> space = MutationSpace([
MutationChoice((0, 2), {'AT', 'TG'}),
MutationChoice((0, 2), {'AT', 'TG'}),
MutationChoice((2, 5), {'TTC', 'TTA', 'TTT'}), # same
MutationChoice((2, 5), {'TTC', 'TTA', 'TTT'}), #
MutationChoice((2, 5), {'TTC', 'TTA', 'TTT'}),
])
"""
def __init__(self, choices_index, left_padding=0):
"""
choices_index = [MutationChoice(0-2), MutationChoice(0-2),
MutationChoice(3-5), MutationChoice(3-5),
MutationChoice(3-5), ... ]
"""
self.choices_index = left_padding * [None] + choices_index
self.choices_list = []
self.unsolvable_segments = []
self.determined_segments = []
self.multichoices = []
for c in choices_index:
if c is None:
continue
if len(self.choices_list) == 0 or (c != self.choices_list[-1]):
self.choices_list.append(c)
nvariants = len(c.variants)
if nvariants == 0:
self.unsolvable_segments.append(c.segment)
elif nvariants == 1:
self.determined_segments.append((c.segment, list(c.variants)[0]))
else:
self.multichoices.append(c)
@property
def choices_span(self):
"""Return (start, end), segment where multiple choices are possible."""
if self.multichoices == []:
return None
return self.multichoices[0].start, self.multichoices[-1].end
def constrain_sequence(self, sequence):
"""Return a version of the sequence compatible with the mutation space.
All nucleotides of the sequence that are incompatible with the
mutation space are replaced by nucleotides compatible with the space.
"""
new_sequence = bytearray(sequence.encode())
for choice in self.choices_list:
variants = list(choice.variants)
if len(choice.variants) == 0:
raise ValueError(
"Cannot constrain a sequence when some "
"positions are unsolvable, in location "
"(%d-%d)" % (choice.start, choice.end)
)
elif len(variants) == 1:
variant = variants[0]
new_sequence[choice.start : choice.end] = variant.encode()
elif sequence[choice.start : choice.end] not in variants:
variant = sorted(variants)[np.random.randint(0, len(variants))]
new_sequence[choice.start : choice.end] = variant.encode()
return new_sequence.decode()
def localized(self, location):
"""Return a new version with only mutations overlapping the location."""
if hasattr(location, "start"):
start, end = location.start, location.end
else:
start, end = location
return MutationSpace(self.choices_index[start:end], left_padding=start)
@property
def space_size(self):
"""Return the number of possible mutations."""
if len(self.multichoices) == 0:
return 0
choices = [len(choice.variants) for choice in self.multichoices]
# np.prod(choices) can create overflows and warnings, so instead we use
# the mechanism below with log/exp and a min.
return np.exp(min(100, np.log(choices).sum()))
def pick_random_mutations(self, n_mutations, sequence):
"""Draw N random mutations."""
n_mutations = min(len(self.multichoices), n_mutations)
if n_mutations == 1:
index = np.random.randint(len(self.multichoices))
choice = self.multichoices[index]
return [(choice.segment, choice.random_variant(sequence=sequence))]
return [
(choice_.segment, choice_.random_variant(sequence=sequence))
for choice_ in [
self.multichoices[i]
for i in np.random.choice(
len(self.multichoices), n_mutations, replace=False
)
]
]
def apply_random_mutations(self, n_mutations, sequence):
"""Return a sequence with n random mutations applied."""
new_sequence = bytearray(sequence.encode())
for segment, seq in self.pick_random_mutations(n_mutations, sequence):
start, end = segment
new_sequence[start:end] = seq.encode()
return new_sequence.decode()
def all_variants(self, sequence):
"""Iterate through all sequence variants in this mutation space."""
new_sequence = bytearray(sequence.encode())
choice_start, choice_end = self.choices_span
encoded_segment = sequence[choice_start:choice_end].encode()
def sort_variants_by_distance_to_current(choice):
"""This function iterates through the variants of a given choice
using not the alphabetical (which would bias AC over GT) but rather
a kind of 'least-change' order, which biases towards solutions
close to the current sequence.
Impact on overall algorithm speed is < 0.5%."""
current = sequence[choice.segment[0] : choice.segment[1]]
alphasort = {v: i for i, v in enumerate(sorted(choice.variants))}
def sort_key(v):
return (abs(alphasort[v] - alphasort[current]), v)
return sorted(choice.variants, key=sort_key)
variants_slots = [
[
(choice_.segment, v.encode())
for v in sort_variants_by_distance_to_current(choice_)
]
for choice_ in self.multichoices
]
for variants in itertools.product(*variants_slots):
new_sequence[choice_start:choice_end] = encoded_segment
for (start, end), variant in variants:
new_sequence[start:end] = variant
yield new_sequence.decode()
@staticmethod
def from_optimization_problem(problem, new_constraints=None):
"""Create a mutation space from a DNA optimization problem.
This can be used to initialize mutation spaces for new problems.
"""
sequence = problem.sequence
if new_constraints is None:
variants = {"A": "ATGC", "T": "TACG", "G": "GCAT", "C": "CGTA"}
choices_index = [
MutationChoice((i, i + 1), variants=variants[c], is_any_nucleotide=True)
for i, c in enumerate(sequence)
]
constraints = problem.constraints
else:
choices_index = [c for c in problem.mutation_space.choices_index]
constraints = new_constraints
mutation_choices = sorted(
[
MutationChoice(segment=choice[0], variants=set(choice[1]))
for cst in constraints
for choice in cst.restrict_nucleotides(sequence)
],
key=lambda choice: (choice.end - choice.start, choice.start),
)
for choice in mutation_choices:
underlying_choices = choices_index[choice.start : choice.end]
if underlying_choices == []:
new_choice = choice
elif all(c.is_any_nucleotide for c in underlying_choices):
new_choice = choice
else:
new_choice = choice.merge_with(set(underlying_choices))
for choice in new_choice.extract_varying_region():
if choice.end > len(choices_index):
choices_index += (choice.end - len(choices_index)) * [None]
for i in range(choice.start, choice.end):
choices_index[i] = choice
# for i in range(new_choice.start, new_choice.end):
# choices_index[i] = new_choice
return MutationSpace(choices_index)
def string_representation(self, separator="|"):
"""Generates a string of the mutation space.
Examples
--------
>>> print (mutation_space.string_representation())
>>> A|G|ATG|CA|A|CA|T|GTC|AC|T|A|C|T|T|T
>>> T|C| | |G| |C| | |C|T|A|C|A|C
>>> G|A| | | | | | | |G|G|G|G|G|G
"""
depth = max([len(c.variants) for c in self.choices_list])
sequences = ["" for i in range(depth)]
for choice in self.choices_list:
variants = list(choice.variants)
start, end = choice.segment
length = end - start
if len(variants) < depth:
variants += (depth - len(variants)) * [length * " "]
for i, variant in enumerate(variants):
if len(sequences[i]):
sequences[i] += separator
sequences[i] += variant
return "\n".join(sequences)
def plot(self, ax, color="red", y_offset=-2):
for choice in self.choices_list:
start, end = choice.segment
N = len(choice.variants)
delta = 0.2
for x, sign in [(start, +1), (end, -1)]:
_x = x - 0.5 + sign * delta
_y = y_offset + 0.5
ax.plot(
[_x, _x],
[_y, _y - N],
linewidth=0.5,
color=color,
)
for y, variant in enumerate(choice.variants):
for x, nucleotide in enumerate(variant):
ax.plot(
[start - 0.5 + delta, end - 0.5 - delta],
[y_offset - y - 0.5, y_offset - y - 0.5],
color=color,
linewidth=0.5,
)
ax.text(
x + start,
-y + y_offset,
nucleotide,
horizontalalignment="center",
verticalalignment="center",
color=color,
)
max_variants = max([len(c.variants) for c in self.choices_list])
ax.set_ylim(bottom=min(-max_variants - 2, ax.get_ylim()[0]))
DnaChisel-3.2.16/dnachisel/MutationSpace/__init__.py 0000664 0000000 0000000 00000000203 15007617443 0022256 0 ustar 00root root 0000000 0000000 from .MutationSpace import MutationSpace
from .MutationChoice import MutationChoice
__all__ = ["MutationSpace", "MutationChoice"]
DnaChisel-3.2.16/dnachisel/README.md 0000664 0000000 0000000 00000012162 15007617443 0016657 0 ustar 00root root 0000000 0000000 # Code explanation
This document walks you trough the DNA Chisel code. Please request changes if anything is unclear.
## Core classes
Here we explain the relationship between the main classes in DNA Chisel. The next section will explain how the code is organized.
- **DnaOptimizationProblem** is the class to define and solve an optimization problems. Its methods implement all the solver logics. Notable attributes:
- Constraints (list of _Specifications_ that must be verified in the final sequence)
- Objectives (list of _Specifications_ that must be as close to verified as possible in the final sequence)
- A _MutationSpace_ storing the changes that can be made to the sequence
- **CircularDnaOptimizationProblem** is a variant of _DnaOptimizationProblem_ whose optimization algorithm assumes that the sequence is circular.
- **Specification** is a class to represent sequence design rules that can be used either as constraints or objectives. Its methods define what the specification looks at in a sequence, how it behaves on local problems, etc. The evaluation of a _DnaOptimizationProblem_ by a *Specification* produces a *SpecEvaluation*. Notable attributes of *Specifications*:
- A _Location_ indicating where it applies in a sequence (absence of location implies that it applies to the whole sequence).
- A _SequencePattern_ if the Specification is about inserting or removing a pattern.
- **SpecificationSet** (same file) is a special Specification class which actually groups together several _Specifications_. See [AllowPrimers](https://github.com/Edinburgh-Genome-Foundry/DnaChisel/blob/master/dnachisel/builtin_specifications/AllowPrimer.py) for an example.
- **Location** is a class representing the location of a segment of a sequence. It is used primarily to indicate where a _Specification_ applies in the sequence, but also in _SpecEvaluation_ to indicate constraint breaches, in _SequencePattern_ to indicate the location of a pattern, etc.
Class methods describe how _Locations_ are created, extended, merged, intersected, imported from Genbank locations, etc.
- **SpecEvaluation** is a class describing the result of the evaluation of a *DnaOptimizationProblem* by a *Specification*. Contains a score, a message, a list of *Locations* of sub-optimal regions. Several evaluations can be grouped using classes *ProblemConstraintsEvaluations* and *ProblemObjectivesEvaluations*, which implement methods for printing or exporting as Genbank a set of evaluations.
- **SequencePattern** is a class for representing pattern. Methods define how to parse a pattern (such as "BsmBI_site") and find the pattern in a sequence.
- **MutationSpace** is a class to represent the possible mutations at different locations of the sequence for a given problem. It is initialized at the creation of the problem by the problem's constraints. A MutationSpace is basically a list of *MutationChoices* applying at various locations of the sequence. *MutationSpace* methods describe how to extract variants from the mutation space (this is used by DnaOptimizationProblem's solver to explore new sequence variants). A *MutationChoice* comprises a location and a set of sequence choices for this location, and the methods define how to extract an random choice, how to merge *MutationChoices* together, etc.
## Code organization
- **biotools/** contains many methods and data tables related to biology and sequence manipulation, either used in the core DNA Chisel classes, or very helpful when writing DNA Chisel scripts (see this folder's README for more).
- **builtin specifications/** contains all the built-in _Specification_ subclasses, one per file. While most files define a directly usable *Specification* subclass (CodonOptimize, EnforceGCContent, etc.), some files encode generic subclasses meant to be in turn subclassed (*CodonSpecification*, *TerminalSpecification*)
- **DnaOptimizationProblem/** contains the code for the *DnaOptimizationProblem* and *CircularDnaOptimizationProblem* classes. As *DnaOptimizationProblem* implements the solver and is very big, the methods in this class have been regrouped into "mixins" in other files.
- **MutationSpace/** contains the implementation of the *MutationSpace* and *MutationChoice* classes.
- **reports/** contains methods to generate plots, PDF reports, etc. from a _DnaOptimizationProblem_ (before and after its optimization). It also contains assets (logo, stylesheet, template) for the PDF report.
- **Specification/** contains the implementation of the *Specification* class (some methods for this class are regrouped in file *FeatureRepresentationMixin.py* to make smaller files). The *SpecEvaluation/* subfolder contains the implementations of *SpecEvaluation*, *SpecEvaluations*, *ProblemConstraintEvaluations*, *ProblemObjectiveEvaluations*.
- **utils/** contains generic methods built on top of DNA Chisel that can be useful in other libraries/applications. For instance `random_compatible_dna_sequence()` which returns a random sequence verifying the given constraints.
- **Location_py** implements the *Location* class which is used everywhere to define sequence segments.
- **SequencePattern_py** implements the *SequencePattern* class.
DnaChisel-3.2.16/dnachisel/SequencePattern/ 0000775 0000000 0000000 00000000000 15007617443 0020504 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/SequencePattern/DnaNotationPattern.py 0000664 0000000 0000000 00000003310 15007617443 0024627 0 ustar 00root root 0000000 0000000 import itertools
from ..biotools import (
reverse_complement,
NUCLEOTIDE_TO_REGEXPR,
IUPAC_NOTATION,
)
from .SequencePattern import SequencePattern
class DnaNotationPattern(SequencePattern):
"""Class for patterns in plain DNA notation: ATTGCCA, GCNNKTA, etc.
If the sequence is not palindromic, the pattern will be looked for in
both strands of sequences.
"""
def __init__(self, sequence, name=None):
"""Initialize"""
SequencePattern.__init__(
self,
size=len(sequence),
expression=self.dna_sequence_to_regexpr(sequence),
name=name,
is_palyndromic=reverse_complement(sequence) == sequence,
)
self.sequence = sequence
@staticmethod
def dna_sequence_to_regexpr(sequence):
"""Return a regular expression to find the pattern in a sequence."""
regexpr = "".join([NUCLEOTIDE_TO_REGEXPR[n] for n in sequence])
return regexpr
def all_variants(self):
"""Return all ATGC sequence variants of a sequence"""
return [
"".join(nucleotides)
for nucleotides in itertools.product(
*[IUPAC_NOTATION[n] for n in self.sequence]
)
]
def __repr__(self):
"""Represent the pattern as PatternType(name)"""
return self.sequence + ("" if self.name is None else " (%s)" % self.name)
def __str__(self):
"""Represent the pattern as PatternType(name)"""
return self.sequence + ("" if self.name is None else " (%s)" % self.name)
@staticmethod
def from_string(string):
if set(string) <= set(NUCLEOTIDE_TO_REGEXPR.keys()):
return DnaNotationPattern(string)
DnaChisel-3.2.16/dnachisel/SequencePattern/EnzymeSitePattern.py 0000664 0000000 0000000 00000001646 15007617443 0024517 0 ustar 00root root 0000000 0000000 import re
from Bio.Restriction.Restriction_Dictionary import rest_dict
from .DnaNotationPattern import DnaNotationPattern
class EnzymeSitePattern(DnaNotationPattern):
"""Class to represent Enzyme site patterns
Examples
--------
>>> enzyme_pattern = EnzymeSitePattern("BsaI")
>>> constraint = AvoidPattern(enzyme_pattern)
"""
def __init__(self, enzyme_name):
self.enzyme_site = rest_dict[enzyme_name]["site"]
DnaNotationPattern.__init__(self, self.enzyme_site, name=enzyme_name)
@staticmethod
def from_string(string):
"""Convert BsmBI_site to EnzymeSitePattern(BsmBI)"""
match = re.match(r"(\S+)_site", string)
if match is not None:
enzyme_name = match.groups()[0]
if enzyme_name in rest_dict:
return EnzymeSitePattern(enzyme_name)
def __str__(self):
return "%s(%s)" % (self.name, self.enzyme_site)
DnaChisel-3.2.16/dnachisel/SequencePattern/HomopolymerPattern.py 0000664 0000000 0000000 00000001474 15007617443 0024734 0 ustar 00root root 0000000 0000000 import re
from .DnaNotationPattern import DnaNotationPattern
class HomopolymerPattern(DnaNotationPattern):
"""Homopolymer of the form AAAAAAA, TTTTT, etc.
Shorthand string version: "7xA", "9xC", etc.
Examples
--------
>>> pattern = HomopolymerPattern("A", 6)
>>> constraint = AvoidPattern(pattern)
"""
def __init__(self, nucleotide, number):
self.nucleotide = nucleotide
self.number = number
DnaNotationPattern.__init__(self, number * nucleotide)
@staticmethod
def from_string(string):
match = re.match(r"(\d+)x(\S)$", string)
if match is not None:
number, nucleotide = match.groups()
return HomopolymerPattern(nucleotide, int(number))
def __str__(self):
return "%sx%s" % (self.number, self.nucleotide)
DnaChisel-3.2.16/dnachisel/SequencePattern/MotifPssmPattern.py 0000664 0000000 0000000 00000017317 15007617443 0024346 0 ustar 00root root 0000000 0000000 from Bio.Seq import Seq
from Bio import motifs
from .SequencePattern import SequencePattern
import numpy as np
class MotifPssmPattern(SequencePattern):
"""Motif pattern represented by a Position-Specific Scoring Matrix (PSSM).
This class is better instantiated by creating a pattern from a set of
sequence strings with ``MotifPssmPattern.from_sequences()``, or reading
pattern(s) for a file with ``MotifPssmPattern.from_file()``.
Parameters
----------
pssm
A `Bio.motifs.Motif` object.
threshold
locations of the sequence with a PSSM score above this value will be
considered matches. For convenience, a relative_threshold can be
given instead.
relative_threshold
Value between 0 and 1 from which the threshold will be auto-computed.
0 means "match everything", 1 means "only match the one (or several)
sequence(s) with the absolute highest possible score".
"""
def __init__(self, pssm, threshold=None, relative_threshold=None):
if not isinstance(pssm, motifs.Motif):
raise ValueError(
f"Expected PSSM type of `Bio.motifs.Motif`, but {type(pssm)} was passed"
)
self.name = pssm.name
self.pssm = pssm.pssm
if relative_threshold is not None:
mini, maxi = self.pssm.min, self.pssm.max
threshold = mini + relative_threshold * (maxi - mini)
self.threshold = threshold
self.relative_threshold = relative_threshold
self.size = pssm.length
self.pssm_matrix = np.array([self.pssm[n] for n in "ATGC"])
self.is_palyndromic = False
@classmethod
def apply_pseudocounts(cls, motif, pseudocounts):
"""Add pseudocounts to the motif's pssm matrix.
Add nothing if pseudocounts is None, auto-compute pseudocounts if
pseudocounts="jaspar", else just attribute the pseudocounts as-is
to the motif.
"""
if pseudocounts is not None:
if pseudocounts == "jaspar":
pseudocounts = motifs.jaspar.calculate_pseudocounts(motif)
motif.pseudocounts = pseudocounts
def find_matches_in_string(self, sequence):
"""Find all positions where the PSSM score is above threshold."""
# NOTE: Before, I made my PSSM searches with Biopython. It was looong!
# Now I use Numpy and np.choice(), and I never looked back
# sequence = Seq(sequence, alphabet=alphabet)
# search = self.pssm.search(
# sequence, threshold=self.threshold, both=False
# )
indices = find_pssm_matches_with_numpy(
pssm_matrix=self.pssm_matrix, sequence=sequence, threshold=self.threshold
)
return [(i, i + self.size, 1) for i in indices]
@classmethod
def from_sequences(
cls,
sequences,
name="unnamed",
pseudocounts="jaspar",
threshold=None,
relative_threshold=None,
):
"""Return a PSSM pattern computed from same-length sequences.
Parameters
----------
sequences
A list of same-length sequences.
name
Name to give to the pattern (will appear in reports etc.).
pseudocounts
Either a dict {"A": 0.01, "T": ...} or "jaspar" for automatic
pseudocounts from the Biopython.motifs.jaspar module (recommended),
or None for no pseudocounts at all (not recommended!).
threshold
locations of the sequence with a PSSM score above this value will be
considered matches. For convenience, a relative_threshold can be
given instead.
relative_threshold
Value between 0 and 1 from which the threshold will be auto-computed.
0 means "match everything", 1 means "only match the one (or several)
sequence(s) with the absolute highest possible score".
"""
sequences = [Seq(s) for s in sequences]
motif = motifs.create(sequences)
cls.apply_pseudocounts(motif, pseudocounts)
motif.name = name
pssm = motif
return MotifPssmPattern(
pssm=pssm, threshold=threshold, relative_threshold=relative_threshold
)
@classmethod
def list_from_file(
cls,
motifs_file,
file_format,
threshold=None,
pseudocounts="jaspar",
relative_threshold=None,
):
"""Return a list of PSSM patterns from a file in JASPAR, MEME, etc.
Parameters
----------
motifs_file
Path to a motifs file, or file handle.
file_format
File format. one of "jaspar", "meme", "TRANSFAC".
pseudocounts
Either a dict {"A": 0.01, "T": ...} or "jaspar" for automatic
pseudocounts from the Biopython.motifs.jaspar module (recommended),
or None for no pseudocounts at all (not recommended!).
threshold
locations of the sequence with a PSSM score above this value will be
considered matches. For convenience, a relative_threshold can be
given instead.
relative_threshold
Value between 0 and 1 from which the threshold will be auto-computed.
0 means "match everything", 1 means "only match the one (or several)
sequence(s) with the absolute highest possible score".
"""
if isinstance(motifs_file, str):
with open(motifs_file, "r") as f:
motifs_list = motifs.parse(f, file_format)
else:
motifs_list = motifs.parse(motifs_file, file_format)
if pseudocounts is not None:
for motif in motifs_list:
cls.apply_pseudocounts(motif, pseudocounts)
return [
MotifPssmPattern(
pssm, threshold=threshold, relative_threshold=relative_threshold
)
for pssm in motifs_list
]
def __str__(self):
if self.relative_threshold is not None:
threshold = "%d%%" % (100 * self.relative_threshold)
else:
threshold = "%.2f" % self.threshold
return "%s-PSSM(%s+)" % (self.name, threshold)
def __repr__(self):
if self.relative_threshold is not None:
threshold = "%d%%" % (100 * self.relative_threshold)
else:
threshold = "%.2f" % self.threshold
return "%s-PSSM(%s+)" % (self.name, threshold)
def find_pssm_matches_with_numpy(pssm_matrix, sequence, threshold):
"""Return every index in the +1 strand wit a PSSM score above threshold.
My numpy-based implementation is 10 times faster than Biopython for some
reason. Weird. Can someone else check?
Parameters:
-----------
pssm
A matrix whose rows give the frequency motif of ATGC (in this order).
sequence
A string representing a DNA sequence.
threshold
Every index with a score above this threshold will be returned.
"""
nucleotide_to_index = dict(zip("ATGC", range(4)))
len_pattern = len(pssm_matrix[0])
# If sequence is small, use normal python to avoid numpy overhead
if len(sequence) < 60:
nucl_indices = [nucleotide_to_index[n] for n in sequence]
return [
i
for i in range(len(sequence) - len_pattern)
if np.choose(nucl_indices[i : len_pattern + i], pssm_matrix).sum()
>= threshold
]
# If sequence is large, use Numpy for speed. tested experimentally
nucl_indices = np.array([nucleotide_to_index[n] for n in sequence], dtype="uint8")
len_pattern = len(pssm_matrix[0])
scores = np.array(
[
np.choose(nucl_indices[k : len_pattern + k], pssm_matrix).sum()
for k in range(len(sequence) - len_pattern)
]
)
return np.nonzero(scores >= threshold)[0]
DnaChisel-3.2.16/dnachisel/SequencePattern/RepeatedKmerPattern.py 0000664 0000000 0000000 00000002132 15007617443 0024762 0 ustar 00root root 0000000 0000000 import re
from .SequencePattern import SequencePattern
class RepeatedKmerPattern(SequencePattern):
"""Direct repeats like ATT-ATT, ATGC-ATGC-ATGC, etc.
Shorthand string version: "3x4mer", "5x2mer", etc.
Examples
--------
>>> RepeatedKmerPattern(3, 2) # dimers repeated 3 times
"""
def __init__(self, n_repeats, kmer_size):
self.n_repeats = n_repeats
self.kmer_size = kmer_size
SequencePattern.__init__(
self,
size=kmer_size * n_repeats,
expression=r"([ATGC]{%d})\1{%d}" % (kmer_size, n_repeats - 1),
name="%d-repeats %d-mers" % (n_repeats, kmer_size),
is_palyndromic=True, # a repeat on a strand is also on the other
lookahead="loop",
)
@staticmethod
def from_string(string):
match = re.match(r"(\d+)x(\d+)mer$", string)
if match is not None:
n_repeats, kmer_size = match.groups()
return RepeatedKmerPattern(int(n_repeats), int(kmer_size))
def __str__(self):
return "%sx%smer" % (self.n_repeats, self.kmer_size)
DnaChisel-3.2.16/dnachisel/SequencePattern/SequencePattern.py 0000664 0000000 0000000 00000013320 15007617443 0024163 0 ustar 00root root 0000000 0000000 """Implements the SequencePattern, DnaNotationPattern classes.
These classes are responsible for looking for a pattern in a sequence
(including overlapping patterns !), separating patterns with fixed size
and patterns with maximal size (from problem localization purposes).
The module also implements functions to specify common DNA patterns:
homopolymers, repeats, enzymatic restriction sites.
"""
import re
from ..biotools import reverse_complement
from ..Location import Location
class SequencePattern:
"""Pattern that will be looked for in a DNA sequence.
Use this class for matching regular expression patterns, and
DnaNotationPattern for matching explicit sequences or sequences using Ns
etc.
Examples
--------
>>> expression = "A[ATGC]{3,}"
>>> pattern = SequencePattern(expression)
>>> constraint = AvoidPattern(pattern)
Parameters
----------
expression
Any string or regular expression (regex) for matching ATGC nucleotides.
Note that multi-nucleotide symbols such as "N" (for A-T-G-C), or "K"
are not supported by this class, see DnaNotationPattern instead.
size
Size of the pattern, in number of characters. The ``size`` is used to
determine the size of windows when performing local optimization and
constraint solving. It can be important to provide the size when the
``pattern`` string provided represents a complex regular expression whose
maximal matching size cannot be easily evaluated.
For example, if a regex is used to actively remove sites, then a size
should be provided to inform DNA Chisel during optimization.
name
Name of the pattern (will be displayed e.g. when the pattern is printed).
"""
registered_string_pattern_classes = []
def __init__(
self,
expression,
size=None,
name=None,
lookahead="loop",
is_palyndromic=False,
):
# if size is None:
# self.shrink_when_localized = False
# # size = len(expression)
self.expression = expression
self.lookahead = lookahead
if lookahead == "re":
expression = "(?=(%s))" % expression
if "(" not in expression:
expression = "(%s)" % expression
self.lookahead_expression = expression
self.compiled_expression = re.compile(self.lookahead_expression)
self.size = size
self.name = name
self.is_palyndromic = is_palyndromic
def find_matches(self, sequence, location=None, forced_strand=None):
"""Return the locations where the sequence matches the expression.
Parameters
----------
sequence
A string of "ATGC..."
location
Location indicating a segment to which to restrict
the search. Only patterns entirely included in the segment will be
returned
Returns
-------
matches
A list of the locations of matches, of the form
``[(start1, end1), (start2, end2),...]``.
"""
# THE FUNCTION HAS BEEN CALLED WITH A LOCATION AND A FORCED STRAND
if forced_strand is not None:
subsequence = sequence[location.start : location.end]
if forced_strand == 1:
return [
(loc + location.start) for loc in self.find_matches(subsequence)
]
if forced_strand == -1:
subsequence = reverse_complement(subsequence)
return [
Location(
location.end - loc.end,
location.end - loc.start,
strand=-1,
)
for loc in self.find_matches(subsequence)
]
# THE FUNCTION HAS BEEN CALLED WITH A LOCATION ONLY
if location is not None:
strand = location.strand
if strand == 1:
return self.find_matches(sequence, location, 1)
if strand == -1:
if self.is_palyndromic:
return self.find_matches(sequence, location, 1)
else:
return self.find_matches(sequence, location, -1)
if strand == 0:
matches = self.find_matches(sequence, location, 1)
if not self.is_palyndromic:
matches += self.find_matches(sequence, location, -1)
return matches
# THE FUNCTION HAS BEEN CALLED WITH NO LOCATION/STRAND: WHOLE SEQUENCE
matches = self.find_matches_in_string(sequence)
return [Location(start, end, strand) for start, end, strand in matches]
def find_matches_in_string(self, sequence):
if self.lookahead == "loop":
matches = []
position = 0
while True:
result = re.search(self.compiled_expression, sequence)
if result is None:
return matches
start, end = result.start(), result.end()
matches.append((start + position, end + position, 1))
sequence = sequence[start + 1 :]
position += start + 1
else:
return [
(match.start(), match.start() + len(match.groups()[0]), 1)
for match in re.finditer(self.compiled_expression, sequence)
]
def __str__(self):
return self.expression + ("" if self.name is None else " (%s)" % self.name)
@classmethod
def from_string(cls, string):
for myclass in cls.registered_string_pattern_classes:
pattern = myclass.from_string(string)
if pattern is not None:
return pattern
return SequencePattern(string)
DnaChisel-3.2.16/dnachisel/SequencePattern/__init__.py 0000664 0000000 0000000 00000001147 15007617443 0022620 0 ustar 00root root 0000000 0000000 from .SequencePattern import SequencePattern
from .DnaNotationPattern import DnaNotationPattern
from .EnzymeSitePattern import EnzymeSitePattern
from .HomopolymerPattern import HomopolymerPattern
from .RepeatedKmerPattern import RepeatedKmerPattern
from .MotifPssmPattern import MotifPssmPattern
SequencePattern.registered_string_pattern_classes = [
HomopolymerPattern,
RepeatedKmerPattern,
EnzymeSitePattern,
DnaNotationPattern,
]
__all__ = [
"SequencePattern",
"DnaNotationPattern",
"EnzymeSitePattern",
"HomopolymerPattern",
"RepeatedKmerPattern",
"MotifPssmPattern",
]
DnaChisel-3.2.16/dnachisel/Specification/ 0000775 0000000 0000000 00000000000 15007617443 0020156 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/Specification/FeatureRepresentationMixin.py 0000664 0000000 0000000 00000014051 15007617443 0026054 0 ustar 00root root 0000000 0000000 import re
from Bio.SeqFeature import SeqFeature
from ..Location import Location
from ..biotools import find_specification_label_in_feature
class FeatureRepresentationMixin:
"""Mixin for class Specification. Defines methods for converting
Specifications from and to Biopython records."""
DEFAULT_SPECIFICATIONS_DICT = None
def to_biopython_feature(
self,
feature_type="misc_feature",
role="constraint",
colors_dict=None,
use_short_label=True,
**qualifiers
):
"""Return a Biopython feature representing the specification.
The feature label is a string representation of the specification,
and its location indicates the specification's scope.
This method is primarily meant to display specifications in Genbank.
They may result in "viable", DnaChisel-compatible annotations that can
be imported back into DNA Chisel, but this is not the intended goal.
"""
if colors_dict is None:
colors_dict = {"constraint": "#355c87", "objective": "#f9cd60"}
qualifiers["role"] = role
if "label" not in qualifiers:
qualifiers["label"] = self.label(
role=role,
with_location=False,
assignment_symbol=":",
use_short_form=use_short_label,
)
if "color" not in qualifiers:
qualifiers["color"] = colors_dict[role]
qualifiers.update(
dict(
ApEinfo_fwdcolor=qualifiers["color"],
ApEinfo_revcolor=qualifiers["color"],
)
)
return SeqFeature(
self.location.to_biopython_location(),
type=feature_type,
qualifiers=qualifiers,
)
@classmethod
def _format_string_value(cls, value):
"""Converts stringed integers and floats back to numerical. Also
converts "'bla'" => "bla". If the value is a list, apply to all
elements."""
if isinstance(value, (list, tuple)):
return [cls.format_value(v) for v in value]
match = re.match(r"'(.*)'", value)
if match is not None:
return match.groups()[0]
else:
try:
return int(value)
except ValueError:
try:
return float(value)
except Exception:
return value
@classmethod
def from_label(cls, label, location, specifications_dict="default"):
location = Location.from_data(location)
if specifications_dict == "default":
specifications_dict = cls.DEFAULT_SPECIFICATIONS_DICT
label = label.strip()
if not label.endswith(")"):
# Standardizes the expression: @cds => @cds()
label += "()"
# The regular expression below detects spec definitions:
# ~Avoidpattern(ARGS) => ~, AvoidPattern, ARGS
pattern = r"([@~])(\S+)(\(.*\))"
match = re.match(pattern, label)
if match is None:
raise ValueError(
"Failed to recognize a specification definition in label "
"%s at location %s" % (label, location)
)
role, specification, parameters = match.groups()
if specification not in specifications_dict:
raise TypeError("Unknown specification %s" % specification)
specification_class = specifications_dict[specification]
role = {"@": "constraint", "~": "objective"}[role]
# PARSE THE ARGUMENTS AND KEYWORD ARGUMENTS
args, kwargs = [], {}
for arg in parameters[1:-1].split(", "):
if arg == "":
continue
if ":" in arg:
key, value = arg.split(":")
if "|" in value:
value = value.split("|")
kwargs[key] = cls._format_string_value(value)
elif "=" in arg:
key, value = arg.split("=")
if "|" in value:
value = value.split("|")
kwargs[key] = cls._format_string_value(value)
else:
args.append(cls._format_string_value(arg))
kwargs["location"] = location
# ATTEMPT TO CREATE A SPECIFICATION WITH THE GIVEN TYPE AND ARGS
try:
specification_instance = specification_class(*args, **kwargs)
except TypeError as err:
message = err.args[0]
faulty_parameter = message.split("'")[1]
raise TypeError(
"Unknown parameter %s for specification %s "
"at location %s" % (faulty_parameter, specification, kwargs["location"])
)
return role, specification_instance
@classmethod
def list_from_label(cls, label, location, specifications_dict="default"):
"""Return a list of all specs defined in the string label.
The labels are normal spec labels, like @no(BsmBI_site), separated
by the "&" symbol.
"""
return [
cls.from_label(
label=sublabel,
location=location,
specifications_dict=specifications_dict,
)
for sublabel in label.split("&")
]
@classmethod
def list_from_biopython_feature(cls, feature, specifications_dict="default"):
"""Parse a Biopython feature to create an annotation.
The specifications_dict enables to map specification names to the
actual implemented class.
"""
# PARSE THE SPECIFICATION, IDENTIFY THE TYPE AND ARGUMENTS
label = find_specification_label_in_feature(feature)
if label is None:
location = Location.from_biopython_location(feature.location)
raise ValueError(
"Feature at %s cannot be converted into a specification, as "
"no label could be recognized." % location
)
return cls.list_from_label(
label=label,
specifications_dict=specifications_dict,
location=feature.location,
)
DnaChisel-3.2.16/dnachisel/Specification/SpecEvaluation/ 0000775 0000000 0000000 00000000000 15007617443 0023100 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/Specification/SpecEvaluation/ProblemConstraintsEvaluations.py 0000664 0000000 0000000 00000003217 15007617443 0031520 0 ustar 00root root 0000000 0000000 from .SpecEvaluation import SpecEvaluation
from .SpecEvaluations import SpecEvaluations
class ProblemConstraintsEvaluations(SpecEvaluations):
"""Special multi-evaluation class for all constraints of a same problem.
See submethod ``.from_problem``
"""
specifications_role = "constraint"
@staticmethod
def from_problem(problem, autopass_constraints=True):
"""Create an instance by evaluating all constraints in the problem.
The ``problem`` is a DnaChisel DnaOptimizationProblem.
"""
def evaluate(constraint):
if autopass_constraints and constraint.enforced_by_nucleotide_restrictions:
return SpecEvaluation(
constraint,
problem,
score=1,
locations=[],
message="Enforced by nucleotides restrictions",
)
else:
return constraint.evaluate(problem)
return ProblemConstraintsEvaluations(
[evaluate(constraint) for constraint in problem.constraints],
problem=problem,
)
def success_failure_color(self, evaluation):
"""Return color #60f979 if evaluation.passes else #f96c60."""
return "#60f979" if evaluation.passes else "#f96c60"
def text_summary_message(self):
"""Return a global SUCCESS or FAILURE message for all evaluations."""
failed = [e for e in self.evaluations if not e.passes]
if failed == []:
return "SUCCESS - all constraints evaluations pass"
else:
return "FAILURE: %d constraints evaluations failed" % len(failed)
DnaChisel-3.2.16/dnachisel/Specification/SpecEvaluation/ProblemObjectivesEvaluations.py 0000664 0000000 0000000 00000002122 15007617443 0031300 0 ustar 00root root 0000000 0000000 from .SpecEvaluations import SpecEvaluations
class ProblemObjectivesEvaluations(SpecEvaluations):
"""Special multi-evaluation class for all objectives of a same problem.
See submethod ``.from_problem``
"""
color_lightness = 0.8
color_shift = 0.14
specifications_role = "objective"
@staticmethod
def from_problem(problem):
"""Create an instance by evaluating all objectives in the problem.
The ``problem`` is a DnaChisel DnaOptimizationProblem.
"""
return ProblemObjectivesEvaluations(
[specification.evaluate(problem) for specification in problem.objectives],
problem=problem,
)
def success_failure_color(self, evaluation):
"""Return color #cbf960 if evaluation is optimal else #f9a260."""
return "#cbf960" if evaluation.is_optimal else "#f9a260"
def text_summary_message(self):
"""Return a TOTAL SCORE message."""
if len(self.evaluations) == 0:
return "No specifications"
return "TOTAL OBJECTIVES SCORE: " + self.scores_sum(as_text=True)
DnaChisel-3.2.16/dnachisel/Specification/SpecEvaluation/SpecEvaluation.py 0000664 0000000 0000000 00000012510 15007617443 0026373 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""Implements the SpecEvaluation class
SpecEvaluation is a class describing the result of an the evaluation of a
*DnaOptimizationProblem* by a *Specification*. It contains a score, a message,
a list of *Locations* of sub-optimal regions.
Several evaluations can be grouped using classes
*ProblemConstraintsEvaluations* and *ProblemObjectivesEvaluations*, which
implement methods for printing or exporting as Genbank a set of evaluations.
"""
import textwrap
from Bio.SeqFeature import SeqFeature
from ...biotools import score_to_formatted_string
from ...Location import Location
class SpecEvaluation:
"""Store relevant infos about the evaluation of an objective on a problem.
Examples
--------
>>> evaluation_result = ConstraintEvaluation(
>>> specification=specification,
>>> problem = problem,
>>> score= evaluation_score, # float
>>> locations=[Locations list...],
>>> message = "Score: 42 (found 42 sites)"
>>> )
Parameters
----------
objective
The Objective that was evaluated.
problem
The problem that the objective was evaluated on.
score
The score associated to the evaluation.
locations
A list of couples (start, end) indicating the locations on which the
the optimization shoul be localized to improve the objective.
message
A message that will be returned by ``str(evaluation)``. It will notably
be displayed by ``problem.print_objectives_summaries``.
"""
def __init__(
self,
specification,
problem,
score,
locations=None,
message=None,
data=None,
):
"""Initialize."""
self.specification = specification
self.problem = problem
self.score = score
self.passes = score >= 0
self.is_optimal = score == specification.best_possible_score
self.locations = locations
self.message = self.default_message if message is None else message
self.data = {} if data is None else data
@property
def default_message(self):
"""Return the default message for console/reports."""
return "Score: %s. Locations: %s" % (
score_to_formatted_string(self.score),
self.locations,
)
@property
def score_to_formatted_string(self):
return score_to_formatted_string(self.score)
def to_text(self, role="constraint", wrapped=True, max_message_length=500):
"""Return a string representation of the evaluation.
Example output for a constraint:
>>> FAIL � UniquifyAllKmers[10-1000](k:9)
>>> │ Score: -2. Locations: [232-241, 233-242]
Parameters
----------
role
either 'objective' or 'constraint'
wrapped
Whether to wrap-and-indent the result
Returns
-------
message
A long string detailing the evaluations's status and regions of
breach or suboptimality.
"""
message = self.message
if len(message) > max_message_length:
half = int(max_message_length / 2)
message = message[:half] + " ... " + message[-half:]
if wrapped:
indents = 6 if (role == "constraint") else 11
indent = indents * " " + "│ "
message = "\n".join(
textwrap.wrap(
message,
width=80,
initial_indent=indent,
subsequent_indent=indent,
)
)
if role == "objective":
return "{optimal}{score} � {spec} \n{message}".format(
self=self,
optimal="✔" if self.is_optimal else " ",
spec=self.specification.label(with_location=True),
score=score_to_formatted_string(self.score),
message=message,
)
else:
return "{passes} � {spec}\n{message}".format(
self=self,
passes="✔PASS" if self.passes else " FAIL",
spec=self.specification.label(with_location=True),
message=message,
)
def locations_to_biopython_features(
self,
feature_type="misc_feature",
color="red",
label_prefix="",
merge_overlapping=False,
):
"""Return a list of locations (of breach/suboptimality) as annotations.
Parameters
----------
feature_type
Genbank type of the annotations
color
Color property attached to the annotations
label_prefix
The locations will be labelled of the form
"prefix NameOfSpecification()"
merge_overlapping
If true, then overlapping locations (0-5, 2-9) will be merged into a
single one (0-9).
"""
locations = self.locations
if merge_overlapping:
locations = Location.merge_overlapping_locations(locations)
return [
SeqFeature(
location.to_biopython_location(),
type=feature_type,
qualifiers=dict(
label=label_prefix + " " + str(self.specification),
color=color,
),
)
for location in locations
]
DnaChisel-3.2.16/dnachisel/Specification/SpecEvaluation/SpecEvaluations.py 0000664 0000000 0000000 00000014621 15007617443 0026563 0 ustar 00root root 0000000 0000000 from ...biotools import score_to_formatted_string
from ...Location import Location
from ...reports import colors_cycle
class SpecEvaluations:
"""Base class for handling lists of SpecEvaluations.
See ProblemObjectivesEvaluations and ProblemConstraintsEvaluations for
the useful subclasses.
Parameters
----------
evaluations
list of SpecEvaluations
problem
(optional) problem on which the evaluations were carried.
"""
color_lightness = 1.0
color_shift = 0
def __init__(self, evaluations, problem=None):
"""Initialize."""
self.evaluations = evaluations
self.problem = problem
def __iter__(self):
"""Iterate over evaluations."""
return self.evaluations.__iter__()
def __len__(self):
"""Return the number of evaluations."""
return len(self.evaluations)
def all_evaluations_pass(self):
"""Return whether all evaluations pass."""
return all([ev.passes for ev in self.evaluations])
def scores_sum(self, as_text=False):
"""Return the sum of all evaluations scores, factoring their boost.
Scores are multiplied by their respective boost factor.
"""
result = sum([ev.specification.boost * ev.score for ev in self.evaluations])
if as_text:
result = score_to_formatted_string(result)
return result
def filter(self, eval_filter):
"""Create a new instance with a subset of the evaluations.
``eval_filter`` is either a function ``(evaluation) => True/False``
or one of "passing", "failing", "optimal", "suboptimal", to obtain the
corresponding constraints.
"""
if isinstance(eval_filter, str):
eval_filter = {
"passing": lambda e: e.passes,
"failing": lambda e: not e.passes,
"optimal": lambda e: e.is_optimal,
"suboptimal": lambda e: not e.is_optimal,
}[eval_filter]
return self.__class__(
evaluations=[ev for ev in self.evaluations if eval_filter(ev)],
problem=self.problem,
)
def all_locations(self):
return [
location
for evaluation in self.evaluations_with_locations()
for location in evaluation.locations
]
def to_text(self):
"""Return a long representation of the evaluations list."""
return (
"\n".join(
["===> %s" % self.text_summary_message()]
+ [e.to_text(role=self.specifications_role) for e in self.evaluations]
)
+ "\n\n"
)
def evaluations_with_locations(self):
"""Return the list of all evaluations whose location is not None."""
return [ev for ev in self.evaluations if ev.locations is not None]
def success_and_failures_as_features(self, feature_type="misc_feature"):
"""Return all evaluations as Biopython features.
With color property depending on whether the evaluation is passing,
failing, optimal, or suboptimal, the color being determined by
``self.success_failure_color``.
"""
return [
evaluation.specification.to_biopython_feature(
feature_type=feature_type,
color=self.success_failure_color(evaluation),
passes="true" if evaluation.passes else "false",
is_optimal="true" if evaluation.is_optimal else "false",
)
for evaluation in self.evaluations
if evaluation.specification.__dict__.get("location", False)
]
def locations_as_features(
self,
features_type="misc_feature",
with_specifications=True,
label_prefix="From",
colors="cycle",
merge_overlapping=False,
locations_filter=None,
):
"""Return all locations from all evaluations as biopython features.
Parameters
----------
features_type
The Genbank feature type
with_specifications
If True, features are added to the list to indicate the scope of the
different specifications. If false, only the specification breaches
are returned.
label_prefix
Each breach may be labeled "prefix NameOfSpec(props)", for instance,
"From AvoidPattern(100-200)", to indicate where the breach belongs.
colors
Either a list of colors (one for each specification), e.g.
['red', '#7aab71', ...] or "cycle" for cycling through predefined
colors. The colors are applied to all breaches.
locations_filter
A function (location => True/False) deciding whether
"""
if colors == "cycle":
cycle = colors_cycle(
lightness_factor=self.color_lightness,
color_shift=self.color_shift,
)
colors = [next(cycle) for ev in self.evaluations]
features = [
location.to_biopython_feature(
feature_type="misc_feature",
label=" ".join(
[
label_prefix,
ev.specification.label(
use_short_form=True, with_location=False
),
]
),
color=color,
ApEinfo_fwdcolor=color,
ApEinfo_revcolor=color,
)
for (ev, color) in zip(self.evaluations_with_locations(), colors)
for location in (
Location.merge_overlapping_locations(ev.locations)
if merge_overlapping
else ev.locations
)
if (locations_filter is None) or locations_filter(location)
]
if with_specifications:
features += [
ev.specification.to_biopython_feature(
feature_type="misc_feature",
label=ev.specification.label(
use_short_form=True, with_location=False
),
role=self.specifications_role,
color=color,
ApEinfo_fwdcolor=color,
ApEinfo_revcolor=color,
)
for ev, color in zip(self.evaluations, colors)
if ev.specification.__dict__.get("location", False)
]
return features
DnaChisel-3.2.16/dnachisel/Specification/SpecEvaluation/__init__.py 0000664 0000000 0000000 00000000555 15007617443 0025216 0 ustar 00root root 0000000 0000000 from .SpecEvaluation import SpecEvaluation
from .SpecEvaluations import SpecEvaluations
from .ProblemConstraintsEvaluations import ProblemConstraintsEvaluations
from .ProblemObjectivesEvaluations import ProblemObjectivesEvaluations
__all__ = [
"SpecEvaluation",
"SpecEvaluations",
"ProblemConstraintsEvaluations",
"ProblemObjectivesEvaluations",
]
DnaChisel-3.2.16/dnachisel/Specification/Specification.py 0000664 0000000 0000000 00000021421 15007617443 0023310 0 ustar 00root root 0000000 0000000 """Base class for specification.
Notable features implemented here:
- Many empty methods that features subclass will overwrite
- Feature import/export from/to Genbank features.
"""
import copy
from ..Location import Location
from .FeatureRepresentationMixin import FeatureRepresentationMixin
class Specification(FeatureRepresentationMixin):
"""General class to define specifications to optimize.
Note that all specifications have a ``boost`` attribute that is a
multiplicator that will be used when computing the global specification
score of a problem with ``problem.all_objectives_score()``.
New types of specifications are defined by subclassing ``Specification`` and
providing a custom ``evaluate`` and ``localized`` methods.
Parameters
-----------
evaluate
function (sequence) => SpecEvaluation
boost
Relative importance of the Specification's score in a multi-specification
problem.
Attributes
----------
best_possible_score
Best score that the specification can achieve. Used by the optimization
algorithm to understand when no more optimization is required.
optimize_passively (boolean)
Indicates that there should not be a pass of the optimization algorithm
to optimize this objective. Instead, this objective is simply taken into
account when optimizing other objectives.
enforced_by_nucleotide_restrictions (boolean)
When the spec is used as a constraints, this indicates that the
constraint will initially restrict the mutation space in a way that
ensures that the constraint will always be valid. The constraint does
not need to be evaluated again, which speeds up the resolution algorithm.
priority
Value used to sort the specifications and solve/optimize them in order,
with highest priority first.
shorthand_name
Shorter name for the specification class that will be recognized when
parsing annotations from genbanks.
"""
best_possible_score = None
optimize_passively = False
enforced_by_nucleotide_restrictions = False
priority = 0
shorthand_name = None
is_focus = False
def __init__(self, evaluate=None, boost=1.0):
"""Initialize."""
self.boost = boost
if evaluate is not None:
self.evaluate = evaluate
def localized(self, location, problem=None):
"""Return a modified version of the specification for the case where
sequence modifications are only performed inside the provided location.
For instance if an specification concerns local GC content, and we are
only making local mutations to destroy a restriction site, then we only
need to check the local GC content around the restriction site after
each mutation (and not compute it for the whole sequence), so
``EnforceGCContent.localized(location)`` will return an specification
that only looks for GC content around the provided location.
If an specification concerns a DNA segment that is completely disjoint
from the provided location, this must return None.
Must return an object of class ``Constraint``.
"""
return self
def copy_with_changes(self, **kwargs):
"""Return a copy of the Specification with modified properties.
For instance ``new_spec = spec.copy_with_changes(boost=10)``.
"""
new_specification = copy.copy(self)
new_specification.__dict__.update(kwargs)
return new_specification
def shifted(self, shift):
"""Shift the location of the specification.
Some specification classes may have a special method to do side effects
when shifting the location.
Location shifting is used in particular when solving circular DNA
optimization problems.
"""
new_location = None if self.location is None else self.location + shift
return self.copy_with_changes(location=new_location, derived_from=self)
def initialized_on_problem(self, problem, role="constraint"):
"""Complete specification initialization when the sequence gets known.
Some specifications like to know what their role is and on which
sequence they are employed before they complete some values.
"""
return self
def label(
self,
role=None,
with_location=True,
assignment_symbol=":",
use_short_form=False,
use_breach_form=False,
):
"""Return a string label for this specification.
Parameters
----------
role
Either 'constraint' or 'objective' (for prefixing the label with @
or ~), or None.
with_location
If true, the location will appear in the label.
assignment_symbol
Indicates whether to use ":" or "=" or anything else when indicating
parameters values.
use_short_form
If True, the label will use self.short_label(), so for instance
AvoidPattern(BsmBI_site) will become "no BsmBI". How this is handled
is dependent on the specification.
"""
prefix = {"constraint": "@", "objective": "~", None: ""}[role]
if use_short_form:
label = self.short_label()
if with_location:
label += ", %s" % self.location
return prefix + label
if use_breach_form:
label = self.breach_label()
if with_location:
label += ", %s" % self.location
return label
if with_location and hasattr(self, "location") and self.location:
location = "[%s]" % self.location
else:
location = ""
params = self.label_parameters()
if params == []:
params = ""
else:
params = "(%s)" % ", ".join(
[
(
assignment_symbol.join(map(str, p))
if isinstance(p, tuple)
else str(p)
)
for p in params
]
)
return "".join([prefix, self.__class__.__name__, location, params])
def short_label(self):
"""Shorter, less precise label to be used in tables, reports, etc.
This is meant for specifications such as EnforceGCContent(0.4, 0.6)
to be represented as '40-60% GC' in reports tables etc..
"""
return self.__class__.__name__
def breach_label(self):
"""Shorter, less precise label to be used in tables, reports, etc.
This is meant for specifications such as EnforceGCContent(0.4, 0.6)
to be represented as '40-60% GC' in reports tables etc..
"""
return "'%s' breach" % (self.short_label())
def label_parameters(self):
"""In subclasses, returns a list of the creation parameters.
For instance [('pattern', 'ATT'), ('occurences', 2)]
"""
return []
def __str__(self):
"""By default, represent the Specification using its label()"""
return self.label()
def __repr__(self):
"""By default, represent the Specification using its label()"""
return self.label()
def restrict_nucleotides(self, sequence, location=None):
"""Restrict the mutation space to speed up optimization.
This method only kicks in when this specification is used as a
constraint. By default it does nothing, but subclasses such as
EnforceTranslation, AvoidChanges, EnforceSequence, etc. have custom
methods.
In the code, this method is run during the initialize() step of
DNAOptimizationProblem, when the MutationSpace is created for each
constraint
"""
return []
def as_passive_objective(self):
"""Return a copy with optimize_passively set to true.
"Optimize passively" means that when the specification is used as an
objective, the solver will not do a specific pass to optimize this
specification, however this specification's score will be taken into
account in the global score when optimizing other objectives, and
may therefore influence the final sequence.
"""
return self.copy_with_changes(optimize_passively=True)
def _copy_with_full_span_if_no_location(self, problem):
"""Return either self, or a copy with location "everywhere".
And by "everywhere" we mean Location(0, L) where L is the problem's
sequence length.
Most Specifications use this method in their "initialized_on_problem()"
custom method.
"""
if self.location is None:
location = Location(0, len(problem.sequence))
return self.copy_with_changes(location=location)
else:
return self
DnaChisel-3.2.16/dnachisel/Specification/SpecificationSet.py 0000664 0000000 0000000 00000001067 15007617443 0023770 0 ustar 00root root 0000000 0000000 class SpecificationSet:
"""Generic class for writing Specs which are actually made of more specs.
Behaves as a Specification when it comes to instantiation, reading it
from annotated records, etc. but the initialization actually creates a
dictionary of standard Specifications in the DNAOptimizationProblem
"""
def register_specifications(self, specifications):
for name, spec in specifications.items():
spec.parent_specification = self
spec.name_in_parent = name
self.specifications = specifications
DnaChisel-3.2.16/dnachisel/Specification/__init__.py 0000664 0000000 0000000 00000000203 15007617443 0022262 0 ustar 00root root 0000000 0000000 from .Specification import Specification
from .SpecificationSet import SpecificationSet
from .SpecEvaluation import SpecEvaluation
DnaChisel-3.2.16/dnachisel/__init__.py 0000664 0000000 0000000 00000006021 15007617443 0017506 0 ustar 00root root 0000000 0000000 from .DnaOptimizationProblem import (
DnaOptimizationProblem,
CircularDnaOptimizationProblem,
NoSolutionError,
)
from .Location import Location
from .Specification import Specification, SpecificationSet, SpecEvaluation
from .builtin_specifications import (
AllowPrimer,
AvoidBlastMatches,
AvoidChanges,
AvoidHairpins,
AvoidMatches,
AvoidRareCodons,
UniquifyAllKmers,
AvoidHeterodimerization,
AvoidPattern,
AvoidStopCodons,
CodonOptimize,
MaximizeCAI,
HarmonizeRCA,
MatchTargetCodonUsage,
EnforceChanges,
EnforceChoice,
EnforceGCContent,
EnforceMeltingTemperature,
EnforcePatternOccurence,
EnforceRegionsCompatibility,
EnforceSequence,
EnforceTerminalGCContent,
EnforceTranslation,
SequenceLengthBounds,
DEFAULT_SPECIFICATIONS_DICT,
)
from .SequencePattern import (
SequencePattern,
DnaNotationPattern,
HomopolymerPattern,
RepeatedKmerPattern,
EnzymeSitePattern,
MotifPssmPattern,
)
from .biotools import (
blast_sequence,
complement,
list_common_enzymes,
load_record,
write_record,
random_dna_sequence,
random_protein_sequence,
reverse_complement,
reverse_translate,
sequences_differences,
translate,
change_biopython_record_sequence,
annotate_differences,
annotate_pattern_occurrences,
annotate_record,
sequence_to_biopython_record,
sequences_differences_segments,
)
from .utils import random_compatible_dna_sequence
from .version import __version__
Specification.DEFAULT_SPECIFICATIONS_DICT = DEFAULT_SPECIFICATIONS_DICT
__all__ = [
"DnaOptimizationProblem",
"NoSolutionError",
"CircularDnaOptimizationProblem",
"Location",
"AllowPrimer",
"AvoidBlastMatches",
"AvoidChanges",
"AvoidHairpins",
"AvoidMatches",
"AvoidRareCodons",
"UniquifyAllKmers",
"AvoidPattern",
"AvoidStopCodons",
"CodonOptimize",
"MaximizeCAI",
"HarmonizeRCA",
"MatchTargetCodonUsage",
"EnforceChoice",
"EnforceChanges",
"EnforceGCContent",
"EnforceMeltingTemperature",
"EnforcePatternOccurence",
"EnforceRegionsCompatibility",
"EnforceSequence",
"EnforceTerminalGCContent",
"EnforceTranslation",
"SequenceLengthBounds",
"Specification",
"SpecificationSet",
"SpecEvaluation",
"SequencePattern",
"DnaNotationPattern",
"HomopolymerPattern",
"RepeatedKmerPattern",
"EnzymeSitePattern",
"MotifPssmPattern",
"blast_sequence",
"complement",
"list_common_enzymes",
"load_record",
"write_record",
"random_dna_sequence",
"random_protein_sequence",
"reverse_complement",
"reverse_translate",
"sequences_differences",
"translate",
"change_biopython_record_sequence",
"annotate_differences",
"annotate_pattern_occurrences",
"annotate_record",
"sequence_to_biopython_record",
"sequences_differences_segments",
"random_compatible_dna_sequence",
"__version__",
"DEFAULT_SPECIFICATIONS_DICT",
]
DnaChisel-3.2.16/dnachisel/biotools/ 0000775 0000000 0000000 00000000000 15007617443 0017230 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/biotools/README.md 0000664 0000000 0000000 00000003114 15007617443 0020506 0 ustar 00root root 0000000 0000000 # Biotools
This module contains many methods related to biology and sequence manipulation,
either used in the core DNA Chisel classes, or very helpful when writing
DNA Chisel scripts.
- **biotables.py** provides tables (=dictionaries) of biological data, such as genetic code, IUPAC nucleotide definitions, etc.
- **blast_sequence.py** contains a practical BLAST method (using NCBI+). It is used in AvoidBlastMatches but could be used anywhere else.
- **bowtie.py** contains methods for using Bowtie in AvoidMatches.
- **enzymes_operations.py** is for enzyme-related methods. Currently only "list_common_enzymes", which can be practical.
- **formatting_operations.py** contains methods to format strings and numericals, used throughout the library.
- **gc_content.py** contains a method implementing (windowed) GC content and is notably used by EnforceGCContent.
- **genbank_operations.py** contains many Genbank and Biopython record related methods, used intensively in the library and examples.
- **indices_operations.py** contains methods to group or ungroup segments and sets of indices, which are used to handle breach *Locations* in the core code.
- **random_sequences.py** contains methods for generating random sequences.
- **sequences_differences.py** contains methods for comparing sequences and pointing at locations that differ.
- **sequences_operations.py** contains methods for manipulating "ATGC" strings representing sequences (methods include reverse_complement, reverse_translate, etc.)
- **data/** contains data files used by the code in this folder (genetic code, IUPAC definitions, etc.)
DnaChisel-3.2.16/dnachisel/biotools/__init__.py 0000664 0000000 0000000 00000004244 15007617443 0021345 0 ustar 00root root 0000000 0000000 """Biologically-related useful methods."""
from .blast_sequence import blast_sequence
from .bowtie import find_all_bowtie_matches
from .genbank_operations import (
change_biopython_record_sequence,
find_specification_label_in_feature,
load_record,
sequence_to_biopython_record,
annotate_record,
annotate_differences,
annotate_pattern_occurrences,
write_record,
)
from .sequences_operations import (
complement,
reverse_complement,
reverse_translate,
dna_pattern_to_regexpr,
translate,
)
from .enzymes_operations import list_common_enzymes
from .gc_content import gc_content
from .indices_operations import (
group_nearby_indices,
group_nearby_segments,
subdivide_window,
windows_overlap,
)
from .sequences_differences import (
sequences_differences,
sequences_differences_array,
sequences_differences_segments,
)
from .formatting_operations import (
dict_to_pretty_string,
round_all_numbers_in_dict,
score_to_formatted_string,
)
from .random_sequences import random_dna_sequence, random_protein_sequence
from .biotables import (
IUPAC_NOTATION,
NUCLEOTIDE_TO_REGEXPR,
OTHER_BASES,
CODON_TABLE_NAMES,
get_backtranslation_table,
)
__all__ = [
"blast_sequence",
"find_all_bowtie_matches",
"change_biopython_record_sequence",
"find_specification_label_in_feature",
"load_record",
"write_record",
"sequence_to_biopython_record",
"annotate_record",
"annotate_differences",
"annotate_pattern_occurrences",
"complement",
"reverse_complement",
"reverse_translate",
"dna_pattern_to_regexpr",
"translate",
"list_common_enzymes",
"gc_content",
"get_backtranslation_table",
"group_nearby_indices",
"group_nearby_segments",
"subdivide_window",
"windows_overlap",
"sequences_differences",
"sequences_differences_array",
"sequences_differences_segments",
"dict_to_pretty_string",
"round_all_numbers_in_dict",
"score_to_formatted_string",
"random_dna_sequence",
"random_protein_sequence",
"IUPAC_NOTATION",
"NUCLEOTIDE_TO_REGEXPR",
"OTHER_BASES",
"CODON_TABLE_NAMES",
]
DnaChisel-3.2.16/dnachisel/biotools/biotables.py 0000664 0000000 0000000 00000004710 15007617443 0021550 0 ustar 00root root 0000000 0000000 """This module provides useful biological tables as Python dictionaries."""
from collections import defaultdict
import os
from Bio.Data import CodonTable
def reverse_table(table):
"""Return a dictionary {v1: [k1a, k1b,...]} where k1a, k1b are all
the keys of table such that table[k1]=v1.
"""
new_table = defaultdict(lambda: [])
for k, v in sorted(table.items()):
new_table[v].append(k)
return dict(new_table)
def dict_from_csv(filepath, sep=";"):
"""Read a CSV and store entries in a dict."""
with open(filepath, "r") as f:
return {
line.split(sep)[0]: line.split(sep)[1].strip("\n")
for line in f.readlines()
if line not in ("", "\n")
}
data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data")
# TABLES DEFINITIONS START HERE
# COMPLEMENTS = {"A": "T", "T": "A", "C": "G", "G": "C"}
with open(os.path.join(data_dir, "complements.csv"), "r") as f:
COMPLEMENTS = dict([line.split(",") for line in f.read().splitlines()])
OTHER_BASES = {
"A": ["T", "G", "C"],
"T": ["A", "G", "C"],
"G": ["T", "A", "C"],
"C": ["T", "G", "A"],
}
CODON_TABLE_NAMES = list(CodonTable.unambiguous_dna_by_name)
NUCLEOTIDE_TO_REGEXPR = dict_from_csv(
os.path.join(data_dir, "nucleotide_to_regexpr.csv")
)
iupac_file = os.path.join(data_dir, "iupac_notation.csv")
IUPAC_NOTATION = {k: set(v) for k, v in dict_from_csv(iupac_file).items()}
def flatten(l):
return [item for sublist in l for item in sublist]
def get_backtranslation_table(table_name="Standard"):
table = CodonTable.unambiguous_dna_by_name[table_name]
back_translation_table = {}
for codon, amino_acid in table.forward_table.items():
if amino_acid not in back_translation_table:
back_translation_table[amino_acid] = []
back_translation_table[amino_acid].append(codon)
back_translation_table["*"] = table.stop_codons
back_translation_table["START"] = table.start_codons
back_translation_table["X"] = list(
set(flatten(back_translation_table.values())) - set(back_translation_table["*"])
)
back_translation_table["B"] = (
back_translation_table["N"] + back_translation_table["D"]
)
back_translation_table["J"] = (
back_translation_table["L"] + back_translation_table["I"]
)
back_translation_table["Z"] = (
back_translation_table["E"] + back_translation_table["Q"]
)
return back_translation_table
DnaChisel-3.2.16/dnachisel/biotools/blast_sequence.py 0000664 0000000 0000000 00000005626 15007617443 0022610 0 ustar 00root root 0000000 0000000 from Bio.Blast import NCBIXML
import os
import tempfile
import subprocess
import time
def blast_sequence(
sequence,
blast_db=None,
subject_sequences=None,
subject=None,
word_size=4,
perc_identity=80,
num_alignments=1000,
ungapped=False,
num_threads=3,
culling_limit=None,
e_value=None,
task=None,
dust="no",
):
"""Return a Biopython BLAST record of the given sequence BLASTed
against the provided database.
Parameters
----------
sequence
An ATGC sequence
Examples
--------
>>> blast_record = blast_sequence("ATTGTGCGTGTGTGCGT", "blastdb/ecoli")
>>> for alignment in blast_record.alignments:
>>> for hit in alignment.hsps:
>>> print (hit.identities)
"""
xml_file, xml_name = tempfile.mkstemp(".xml")
fasta_file, fasta_name = tempfile.mkstemp(".fa")
with open(fasta_name, "w+") as f:
f.write(">seq\n" + sequence)
close_subject = False
remove_subject = False
if subject is not None:
close_subject = True
if subject_sequences is not None:
close_subject = True
remove_subject = True
subject_file, subject = tempfile.mkstemp(".fa")
if isinstance(subject_sequences[0], str):
subject_sequences = [
("%06d" % i, seq) for i, seq in enumerate(subject_sequences)
]
fasta_content = "\n".join(
[">%s\n%s" % name_sequence for name_sequence in subject_sequences]
)
with open(subject, "w+") as f:
f.write(fasta_content)
def parameter_if_not_none(label, param):
return [label, str(param)] if param else []
command = [
"blastn",
"-out",
xml_name,
"-outfmt",
"5",
"-max_target_seqs",
str(num_alignments),
"-query",
fasta_name,
"-word_size",
str(word_size),
"-num_threads",
str(num_threads),
"-perc_identity",
str(perc_identity),
]
command += (
(["-db", blast_db] if subject is None else ["-subject", subject])
+ parameter_if_not_none("-dust", dust)
+ parameter_if_not_none("-evalue", e_value)
+ parameter_if_not_none("-culling_limit", culling_limit)
+ parameter_if_not_none("-task", task)
)
if ungapped:
command += ["-ungapped"]
process = subprocess.run(command, stderr=subprocess.PIPE, close_fds=True)
if process.returncode:
raise OSError("BLAST failed: %s" % process.stderr)
with open(xml_name, "r") as f:
result = list(NCBIXML.parse(f))
os.fdopen(xml_file, "w").close()
os.fdopen(fasta_file, "w").close()
os.remove(xml_name)
os.remove(fasta_name)
if close_subject:
open(subject, "w").close()
if remove_subject:
os.remove(subject)
if len(result) == 1:
return result[0]
else:
return result
DnaChisel-3.2.16/dnachisel/biotools/bowtie.py 0000664 0000000 0000000 00000004725 15007617443 0021103 0 ustar 00root root 0000000 0000000 import subprocess
import tempfile
import os
def run_process(name, parameters):
process = subprocess.run(parameters, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if process.returncode:
error = process.stderr.decode()
raise OSError("%s failed:\n\n%s" % (name, error))
return process.stdout
def create_bowtie_index_from_sequences(sequences, path):
fasta_path = os.path.join(path, "sequences.fa")
bowtie_path = os.path.join(path, "bowtie")
with open(fasta_path, "w") as f:
f.write(
"\n".join(
[">%d\n%s" % (i, sequence) for i, sequence in enumerate(sequences)]
)
)
run_process(
"build-bowtie", ["bowtie-build", "-f", fasta_path, bowtie_path, "--quiet"]
)
return bowtie_path
def find_all_bowtie_matches(
sequence, bowtie_index_path, match_length, max_mismatches=0
):
"""Return (short) matches between a sequence and a Bowtie index.
The result is of the form [(start, end), n_mismatches)] where (start, end)
indicates the position of the match in the sequence, and n_mismatches is
the number of mismatches with the closest homology in the index.
"""
# CREATE THE PARAMETERS
parameters = ["bowtie"]
parameters += ["--best", "-k", "1"] # only return the best alignments
parameters += ["-v", str(max_mismatches)] # only allow that N mismatches
parameters += [bowtie_index_path]
parameters += ["--quiet", "--suppress", "2,3,4,5,6,7"] # small output
k = match_length
kmers = [sequence[i : i + k] for i in range(len(sequence) - k + 1)]
if k * len(kmers) < 10000:
# Input the sequences directly
tmp_fasta_path = None
parameters += ["-c", ",".join(kmers)]
else:
# Write sequences to a file if too many.
tmp_fasta_path = tempfile.mktemp(".fa")
with open(tmp_fasta_path, "w") as f:
entries = [">%d\n%s" % (i, s) for i, s in enumerate(kmers)]
f.write("\n\n".join(entries))
parameters += ["-f", tmp_fasta_path]
# RUN THE PROCESS
try:
output = run_process("BOWTIE", parameters)
except Exception as err:
raise err
finally:
if tmp_fasta_path is not None:
os.remove(tmp_fasta_path)
output_records = [
line.split("\t") for line in output.decode().split("\n") if len(line)
]
return [
((int(index), int(index) + k), edits.count(":"))
for index, edits in output_records
]
DnaChisel-3.2.16/dnachisel/biotools/data/ 0000775 0000000 0000000 00000000000 15007617443 0020141 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/biotools/data/README.md 0000664 0000000 0000000 00000000726 15007617443 0021425 0 ustar 00root root 0000000 0000000 # Bio data files
- **complements.csv**: Gives the reverse complements of nucleotides, including degenerate IUPAC notation nucleotides (A=>T, C=>G, M=>K, R=>Y...)
- **iupac_notation.csv**: Gives the ATGC nucleotides associated with each letter of the IUPAC notation (A=>A, C=>C, B=>CGT, D=>AGT...)
- **nucleotide_to_regexpr.csv**: Gives for each letter of the IUPAC alphabet all the letters of the IUPAC alphabet which could be a match (A=>A, W=>[ATW], H=>[ACHMTWY]...)
DnaChisel-3.2.16/dnachisel/biotools/data/complements.csv 0000664 0000000 0000000 00000000104 15007617443 0023177 0 ustar 00root root 0000000 0000000 A,T
C,G
G,C
T,A
U,A
W,W
S,S
M,K
K,M
R,Y
Y,R
B,V
D,H
H,D
V,B
N,N
Z,Z
DnaChisel-3.2.16/dnachisel/biotools/data/iupac_notation.csv 0000664 0000000 0000000 00000000121 15007617443 0023664 0 ustar 00root root 0000000 0000000 A;A
C;C
G;G
T;T
U;U
W;AT
S;CG
M;AC
K;GT
R;AG
Y;CT
B;CGT
D;AGT
H;ACT
V;ACG
N;ACGT
DnaChisel-3.2.16/dnachisel/biotools/data/nucleotide_to_regexpr.csv 0000664 0000000 0000000 00000000204 15007617443 0025243 0 ustar 00root root 0000000 0000000 A;A
B;[BCGKSTY]
C;C
D;[ADGKRTW]
G;G
H;[ACHMTWY]
K;[GKT]
M;[ACM]
N;[ABCDGHKMNRSTVWY]
R;[AGR]
S;[CGS]
T;T
V;[ACGMRSV]
W;[ATW]
Y;[CTY]
DnaChisel-3.2.16/dnachisel/biotools/enzymes_operations.py 0000664 0000000 0000000 00000002613 15007617443 0023541 0 ustar 00root root 0000000 0000000 """Methods for enzymes operations."""
from .sequences_operations import all_iupac_variants
from Bio import Restriction
def list_common_enzymes(
site_length=(6,), opt_temp=(37,), min_suppliers=1, site_unlike=()
):
"""Return a list of enzyme names with the given constraints.
Parameters
----------
site_length
List of accepted site lengths (6, 4, ...)
opt_temp
List of accepted optimal temperatures for the enzyme
min_suppliers
Minimal number registered suppliers in the Biopython data. A minimum
of 3 known suppliers returns the most common enzymes.
site_unlike
List of (ambiguous or unambiguous) DNA sequences that should NOT be
recognized by the selected enzymes.
"""
site_unlike = set(
[
variant
for enzyme in site_unlike
for variant in all_iupac_variants(Restriction.__dict__[enzyme].site)
]
)
def is_valid(enzyme_name):
enzyme = Restriction.__dict__[enzyme_name]
return (
len(enzyme.site) in site_length
and enzyme.opt_temp in opt_temp
and len(enzyme.supplier_list()) >= min_suppliers
and len(set(all_iupac_variants(enzyme.site)).intersection(site_unlike)) == 0
)
return [
enzyme_name
for enzyme_name in Restriction.AllEnzymes.elements()
if is_valid(enzyme_name)
]
DnaChisel-3.2.16/dnachisel/biotools/formatting_operations.py 0000664 0000000 0000000 00000002544 15007617443 0024224 0 ustar 00root root 0000000 0000000 """Text and number formatting operations"""
from copy import deepcopy
import json
import numpy as np
def round_all_numbers_in_dict(d, rounding_digits=2, outplace=True):
"""Return a new version of dict d with all floats rounded to N digits."""
if outplace:
d = deepcopy(d)
for k, v in d.items():
if isinstance(v, float):
d[k] = np.round(v, rounding_digits)
if isinstance(v, dict):
round_all_numbers_in_dict(v, rounding_digits, outplace=False)
return d
def dict_to_pretty_string(d, rounding_digits=2, indent=2):
"""Return a nicely JSON-like formatted string to print a dict."""
d = round_all_numbers_in_dict(d, rounding_digits)
formatted_text = json.dumps(d, indent=indent)
for char in '{}",':
formatted_text = formatted_text.replace(char, "")
return formatted_text
def score_to_formatted_string(score, characters=9):
"""Transform a number (score) into a best-format string.
The format will be either int (2234), float (10.234) or engineering
(1.20E5), whichever is shorter. The score is then padded with left
whitespaces to obtained the desired number of ``characters``."""
raw = str(int(score) if (int(score) == score) else score)
as_float = "%.02f" % score
as_eng = "%.02E." % score
return min([raw, as_float, as_eng], key=len).rjust(characters)
DnaChisel-3.2.16/dnachisel/biotools/gc_content.py 0000664 0000000 0000000 00000002273 15007617443 0021731 0 ustar 00root root 0000000 0000000 import numpy as np
def gc_content(sequence, window_size=None):
"""Compute global or local GC content.
Parameters
----------
sequence
An ATGC DNA sequence (upper case!)
window_size
If provided, the local GC content for the different sliding windows of
this size is returned, else the global GC content is returned.
Returns
--------
A number between 0 and 1 indication the proportion
of GC content. If window_size is provided, returns
a list of len(sequence)-window_size values indicating
the local GC contents (sliding-window method). The i-th value
indicates the GC content in the window [i, i+window_size]
"""
# The code is a little cryptic as it uses numpy array operations
# but the speed gain is 300x compared with pure-python string operations
arr = np.frombuffer((sequence + "").encode(), dtype="uint8")
arr_GCs = (arr == 71) | (arr == 67) # 67=C, 71=G
if window_size is None:
return 1.0 * arr_GCs.sum() / len(sequence)
else:
cs = np.cumsum(arr_GCs)
a = cs[window_size - 1 :]
b = np.hstack([[0], cs[:-window_size]])
return 1.0 * (a - b) / window_size
DnaChisel-3.2.16/dnachisel/biotools/genbank_operations.py 0000664 0000000 0000000 00000016042 15007617443 0023455 0 ustar 00root root 0000000 0000000 """Generic methods for reading/modifying Genbank/Biopython records"""
from copy import deepcopy
import numpy as np
from Bio.Seq import Seq
from Bio.SeqFeature import SeqFeature, FeatureLocation
from Bio.SeqRecord import SeqRecord
try:
# Biopython <1.78
from Bio.Alphabet import DNAAlphabet
has_dna_alphabet = True
except ImportError:
# Biopython >=1.78
has_dna_alphabet = False
from Bio import SeqIO
try:
from snapgene_reader import snapgene_file_to_seqrecord
except ImportError:
def snapgene_file_to_seqrecord(*a, **k):
"""Please install the snapgene_reader library to use this function."""
raise ImportError(
"Please install snapgene_reader to import Snapgene .dna files"
)
def load_record(filepath, linear=True, name="unnamed", file_format="auto"):
"""Load a FASTA/Genbank/Snapgene record.
Note that reading Snapgene records requires the library snapgene_reader
installed.
"""
if file_format != "auto":
record = SeqIO.read(filepath, file_format)
elif filepath.lower().endswith(("gb", "gbk")):
record = SeqIO.read(filepath, "genbank")
elif filepath.lower().endswith(("fa", "fasta")):
record = SeqIO.read(filepath, "fasta")
elif filepath.lower().endswith(".dna"):
record = snapgene_file_to_seqrecord(filepath)
else:
raise ValueError("Unknown format for file: %s" % filepath)
record.linear = linear
if name != "unnamed":
record.id = name
record.name = name.replace(" ", "_")[:20]
return record
def annotate_record(
seqrecord, location="full", feature_type="misc_feature", margin=0, **qualifiers
):
"""Add a feature to a Biopython SeqRecord.
Parameters
----------
seqrecord
The biopython seqrecord to be annotated.
location
Either (start, end) or (start, end, strand). (strand defaults to +1).
feature_type
The type associated with the feature.
margin
Number of extra bases added on each side of the given location.
qualifiers
Dictionary that will be the Biopython feature's `qualifiers` attribute.
"""
if location == "full":
location = (margin, len(seqrecord) - margin)
strand = location[2] if len(location) == 3 else 1
seqrecord.features.append(
SeqFeature(
FeatureLocation(location[0], location[1], strand),
qualifiers=qualifiers,
type=feature_type,
)
)
def annotate_differences(record, reference, feature_type="misc_feature", prefix="#"):
"""Annotate differences between two records in a new record.
Returns a version of SeqRecord ``record`` where differences with the
references are annotated as new features.
Parameters
----------
record
The SeqRecord to be compared to the reference.
reference
The reference SeqRecord. Must be the same size as ``reference``.
feature_type
The type of the features added to mark differences.
prefix
Each new feature will be labeled "po" where p is the prefix and o the
original sequence at the feature's location. For instance "#A" or "#TT".
"""
seq1 = str(record.seq)
seq2 = str(reference.seq)
indices_diff = (
np.frombuffer(seq1.encode(), dtype="uint8")
- np.frombuffer(seq2.encode(), dtype="uint8")
).nonzero()[0]
indices_diff = [int(e) for e in indices_diff]
locations = [[indices_diff[0], indices_diff[0]]]
for ind in indices_diff[1:]:
if ind - locations[-1][-1] == 1:
locations[-1][-1] = ind
else:
locations.append([ind, ind])
new_record = deepcopy(record)
for start, end in locations:
annotate_record(
new_record,
location=(start, end + 1),
feature_type=feature_type,
label=prefix + seq2[start : end + 1],
)
return new_record
def annotate_pattern_occurrences(
record, pattern, feature_type="misc_feature", prefix="!"
):
"""Return a new record annotated w. all occurences of pattern in sequence.
Parameters
-----------
record
A Biopython record.
pattern
A DNA Chisel SequencePattern object. See SequencePattern documentation.
feature_type
Type of the annotations in the returned record.
"""
new_record = deepcopy(record)
label = prefix + str(pattern)
for location in pattern.find_matches(str(record.seq)):
annotate_record(
new_record,
location=(location.start, location.end),
feature_type=feature_type,
label=label,
)
return new_record
def change_biopython_record_sequence(record, new_seq):
"""Return a version of the record with the sequence set to new_seq."""
new_record = deepcopy(record)
if has_dna_alphabet:
seq = Seq(new_seq, alphabet=DNAAlphabet())
else:
seq = Seq(new_seq)
new_record.seq = seq
return new_record
def sequence_to_biopython_record(
sequence, id="", name="", features=()
):
"""Return a SeqRecord of the sequence, ready to be Genbanked."""
if has_dna_alphabet:
seq = Seq(sequence, alphabet=DNAAlphabet())
else:
seq = Seq(sequence)
return SeqRecord(
seq=seq,
id=id,
name=name,
features=list(features),
annotations={"molecule_type": "DNA"},
)
def find_specification_label_in_feature(feature):
"""Analyse a Biopython feature to find a DnaChisel Specification in it.
The specification should start with either "@" or "~", in the feature's
field "label" or "note".
"""
for labelfield in ["label", "note"]:
if labelfield not in feature.qualifiers:
continue
potential_label = feature.qualifiers.get(labelfield, "_")
if isinstance(potential_label, list):
potential_label = potential_label[0]
if (potential_label != "") and (potential_label[0] in "@~"):
return potential_label
return None
def write_record(
record,
target,
file_format="genbank",
remove_locationless_features=True,
max_name_length=20,
):
"""Write a record as genbank, fasta, etc. via Biopython, with fixes.
Parameters
----------
record
A biopython record.
target
Path to a file or filelike object.
file_format
Format, either Genbank or fasta.
remove_locationless_features
If True, will remove all features whose location is None, to avoid a
Biopython bug
max_name_length
The record's name will be truncated if longer than this (also here to
avoid a Biopython bug).
"""
record = deepcopy(record)
if remove_locationless_features:
record.features = [f for f in record.features if f.location is not None]
record.name = record.name[:max_name_length]
if has_dna_alphabet:
if str(record.seq.alphabet.__class__.__name__) != "DNAAlphabet":
record.seq.alphabet = DNAAlphabet()
if hasattr(target, "open"):
target = target.open("w")
SeqIO.write(record, target, file_format)
DnaChisel-3.2.16/dnachisel/biotools/indices_operations.py 0000664 0000000 0000000 00000006024 15007617443 0023465 0 ustar 00root root 0000000 0000000 """Generic methods for grouping locations and sets of indices"""
def windows_overlap(window1, window2):
"""Return the overlap span between two windows.
Parameters
----------
window1, window2
Each window is a couple of the form (start, end) indicating the range of
a segment of integers.
Returns
-------
None
In case the two windows do not overlap.
[start, end]
The coordinates of the overlap segment if there is one.
"""
start1, end1 = window1
start2, end2 = window2
if start2 < start1:
return windows_overlap(window2, window1)
if start1 <= start2 <= end1:
return [start2, min(end1, end2)]
else:
return None
def subdivide_window(window, max_span):
"""Subdivide a window (start, end) into windows of size < max_span
(start, i_1), (i_1, i_2), ... (i_n, end)"""
start, end = window
inds = list(range(start, end, max_span)) + [end]
return list(zip(inds, inds[1:]))
def group_nearby_indices(indices, max_gap=None, max_group_spread=None):
"""Return a list of groups of the different indices.
Indices are considered from smaller to larger and placed into groups
Parameters
----------
max_gap
Maximal allowed difference between two consecutive numbers of a group
max_group_spread
Maximal allowed difference between the smallest and largest elements
of a group.
"""
if len(indices) == 0:
return []
indices = sorted(indices)
current_group = [indices[0]]
groups = [current_group]
for ind in indices[1:]:
gap_small_enough = (max_gap is None) or (ind - current_group[-1] < max_gap)
spread_small_enough = (max_group_spread is None) or (
ind - current_group[0] < max_group_spread
)
if gap_small_enough and spread_small_enough:
current_group.append(ind)
else:
current_group = [ind]
groups.append(current_group)
return groups
def group_nearby_segments(segments, max_start_gap=None, max_start_spread=None):
"""Return a list of groups of the different indices.
Indices are considered from smaller to larger and placed into groups
Parameters
----------
max_gap
Maximal allowed difference between two consecutive numbers of a group
max_group_spread
Maximal allowed difference between the smallest and largest elements
of a group.
"""
if len(segments) == 0:
return []
segments = sorted(segments)
current_group = [segments[0]]
groups = [current_group]
for seg in segments[1:]:
gap_small_enough = (max_start_gap is None) or (
seg[0] - current_group[-1][0] < max_start_gap
)
spread_small_enough = (max_start_spread is None) or (
seg[0] - current_group[0][0] < max_start_spread
)
if gap_small_enough and spread_small_enough:
current_group.append(seg)
else:
current_group = [seg]
groups.append(current_group)
return groups
DnaChisel-3.2.16/dnachisel/biotools/random_sequences.py 0000664 0000000 0000000 00000003754 15007617443 0023146 0 ustar 00root root 0000000 0000000 """Methods to generate random sequences.
Also see utils.random_compatible_sequence() for random sequences verifying
user-provided constraints.
"""
import numpy as np
def random_dna_sequence(length, gc_share=None, probas=None, seed=None):
"""Return a random DNA sequence ("ATGGCGT...") with the specified length.
Parameters
----------
length
Length of the DNA sequence.
proba
Frequencies for the different nucleotides, for instance
``probas={"A":0.2, "T":0.3, "G":0.3, "C":0.2}``.
If not specified, all nucleotides are equiprobable (p=0.25).
seed
The seed to feed to the random number generator. When a seed is provided
the random results depend deterministically on the seed, thus enabling
reproducibility
"""
if seed is not None:
np.random.seed(seed)
if gc_share is not None:
g_or_c = gc_share / 2.0
not_g_or_c = (1 - gc_share) / 2.0
probas = {"G": g_or_c, "C": g_or_c, "A": not_g_or_c, "T": not_g_or_c}
if probas is None:
sequence = np.random.choice(list("ATCG"), length)
else:
bases, probas = zip(*probas.items())
sequence = np.random.choice(bases, length, p=probas)
return "".join(sequence)
def random_protein_sequence(length, seed=None):
"""Return a random protein sequence "MNQTW...YL*" of the specified length.
Parameters
----------
length
Length of the protein sequence (in number of amino-acids). Note that the
sequence will always start with ``"M"`` and end with a stop codon ``"*"``
with (length-2) random amino-acids in the middle
seed
The seed to feed to the random number generator. When a seed is provided
the random results depend deterministically on the seed, thus enabling
reproducibility
"""
if seed is not None:
np.random.seed(seed)
aa_list = list("ACEDGFIHKLNQPSRTWVY")
aa_choices = np.random.choice(aa_list, length - 2)
return "M" + "".join(aa_choices) + "*"
DnaChisel-3.2.16/dnachisel/biotools/sequences_differences.py 0000664 0000000 0000000 00000002400 15007617443 0024126 0 ustar 00root root 0000000 0000000 """Methods for finding indices where two sequences differ."""
import numpy as np
def sequences_differences_array(seq1, seq2):
"""Return an array [0, 0, 1, 0, ...] with 1s for sequence differences.
seq1, seq2 should both be ATGC strings.
"""
if len(seq1) != len(seq2):
raise ValueError(
"Only use on same-size sequences (%d, %d)" % (len(seq1), len(seq2))
)
arr1 = np.frombuffer(seq1.encode(), dtype="uint8")
arr2 = np.frombuffer(seq2.encode(), dtype="uint8")
return arr1 != arr2
def sequences_differences(seq1, seq2):
"""Return the number of nucleotides that differ in the two sequences.
seq1, seq2 should be strings of DNA sequences e.g. "ATGCTGTGC"
"""
return sequences_differences_array(seq1, seq2).sum()
def sequences_differences_segments(seq1, seq2):
"""Return the list of segments on which sequence seq1 differs from seq2.
The list is of the form [(start1, end1), (start2, end2), etc.]
Parameters
----------
seq1, seq2
ATGC sequences to be compared
"""
arr = 1 * sequences_differences_array(seq1, seq2)
diffs = np.diff([0] + list(arr) + [0]).nonzero()[0]
half = int(len(diffs) / 2)
return [(diffs[2 * i], diffs[2 * i + 1]) for i in range(half)]
DnaChisel-3.2.16/dnachisel/biotools/sequences_operations.py 0000664 0000000 0000000 00000007342 15007617443 0024046 0 ustar 00root root 0000000 0000000 import itertools
from Bio.Seq import Seq
from Bio.Data import CodonTable
import numpy as np
from .biotables import (
NUCLEOTIDE_TO_REGEXPR,
COMPLEMENTS,
IUPAC_NOTATION,
get_backtranslation_table,
)
def complement(dna_sequence):
"""Return the complement of the DNA sequence.
For instance ``complement("ATGCCG")`` returns ``"TACGGC"``.
Uses Biopython for speed.
"""
if len(dna_sequence) <= 30:
return "".join([COMPLEMENTS[nuc] for nuc in dna_sequence])
# This alternative has overhead but is really fast on long sequences
return str(Seq(dna_sequence).complement())
def reverse_complement(sequence):
"""Return the reverse-complement of the DNA sequence.
For instance ``reverse_complement("ATGCCG")`` returns ``"CGGCAT"``.
Uses Biopython for speed.
"""
return complement(sequence)[::-1]
def reverse_translate(protein_sequence, randomize_codons=False, table="Standard"):
"""Return a DNA sequence which translates to the provided protein sequence.
Parameters
----------
protein_sequence
A sequence string of aminoacids, e.g. "MVKK..."
table
Genetic code table to use (e.g. 'Standard', 'Bacterial', etc.).
See dnachisel.biotools.CODON_TABLE_NAMES for a list of available genetic
code tables.
randomize_codons
If False, the first valid codon found is used for each, which can create
biases (GC content, etc.), if True, each amino acid gets replaced by a
randomly selected codon for this amino acid.
"""
backtranslation_table = get_backtranslation_table(table_name=table)
if randomize_codons:
random_numbers = np.random.randint(0, 1000, len(protein_sequence))
random_indices = [
random_number % len(backtranslation_table[aa])
for aa, random_number in zip(protein_sequence, random_numbers)
]
return "".join(
[
backtranslation_table[aa][random_indice]
for aa, random_indice in zip(protein_sequence, random_indices)
]
)
return "".join([backtranslation_table[aa][0] for aa in protein_sequence])
def translate(dna_sequence, table="Standard", assume_start_codon=False):
"""Translate the DNA sequence into an amino-acids sequence "MLKYQT...".
If ``translation_table`` is the name or number of a NCBI genetic table,
Biopython will be used. See here for options:
http://biopython.org/DIST/docs/tutorial/Tutorial.html#htoc25
``translation_table`` can also be a dictionary of the form
``{"ATT": "M", "CTC": "X", etc.}`` for more exotic translation tables
If assume_start_codon is True and the first codon is a start codon in the
given genetic table, then the first amino acid will be M (methionine).
"""
if isinstance(table, dict):
return "".join(
[table[dna_sequence[i : i + 3]] for i in range(0, len(dna_sequence), 3)]
)
else:
table = CodonTable.unambiguous_dna_by_name[table]
if assume_start_codon and (dna_sequence[:3] in table.start_codons):
return "M" + str(Seq(dna_sequence[3:]).translate(table=table))
return str(Seq(dna_sequence).translate(table=table))
def dna_pattern_to_regexpr(dna_pattern):
"""Return a regular expression pattern for the provided DNA pattern
For instance ``dna_pattern_to_regexpr('ATTNN')`` returns
``"ATT[A|T|G|C][A|T|G|C]"``.
"""
return "".join([NUCLEOTIDE_TO_REGEXPR[n] for n in dna_pattern])
def all_iupac_variants(iupac_sequence):
"""Return all unambiguous possible versions of the given sequence."""
return [
"".join(nucleotides)
for nucleotides in itertools.product(
*[IUPAC_NOTATION[n] for n in iupac_sequence]
)
]
DnaChisel-3.2.16/dnachisel/builtin_specifications/ 0000775 0000000 0000000 00000000000 15007617443 0022127 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/builtin_specifications/AllowPrimer.py 0000664 0000000 0000000 00000006160 15007617443 0024741 0 ustar 00root root 0000000 0000000 """Ensure that the location is primer-friendly.
AllowPrimer is a good example SpecificationSet that will create specifications
of type RepeatedKmerPattern, UniquifyAllKmers, AvoidPattern,
EnforceMeltingTemperature, AvoidHeterodimerization.
"""
from ..Specification.SpecificationSet import SpecificationSet
from ..Location import Location
from ..SequencePattern import RepeatedKmerPattern
from .UniquifyAllKmers import UniquifyAllKmers
from .AvoidPattern import AvoidPattern
from .EnforceMeltingTemperature import EnforceMeltingTemperature
from .AvoidHeterodimerization import AvoidHeterodimerization
class AllowPrimer(SpecificationSet):
"""Enforce various specifications for enabling primers at the location.
This is useful for making sure that you will be able to conduct a PCR or
sanger sequencing with a primer annealing at a particular location of
the sequence.
Shorthand for annotations: "primer".
Parameters
----------
location
The exact location where the primer will anneal, i.e. the subsequence
under this location will be the sequence
tmin, tmax
Minimum and maximum acceptable melting temperatures in Celcius, for
instance 55 and 70. When the used as an optimization objective the
"target" will be (tmin + tmax)/2.
max_homology_length
Maximal length of any homology between the subsequence at that location
and anywhere else in the whole sequence.
avoid_heterodim_with
List of ATGC strings representing external (primer) sequences with which
the optimised location should have no annealing.
max_heterodim_tm
Max melting temperature of the heterodimerization between this subsegment
and the other sequences in "avoid_heterodim_with"
avoided_repeats
Properties of the repeated patterns avoided. List of pairs (K, N)
meaning "avoid K-mers repeated N times in a row".
"""
shorthand_name = "primer"
def __init__(
self,
location=None,
tmin=50,
tmax=70,
max_homology_length=6,
avoid_heterodim_with=None,
max_heterodim_tm=5,
avoided_repeats=((2, 5), (3, 4), (4, 3)),
boost=1.0,
):
location = Location.from_data(location)
specs = {
"unique_sequence": UniquifyAllKmers(
k=max_homology_length, location=location, boost=boost
),
"melting_temperature": EnforceMeltingTemperature(
mini=tmin, maxi=tmax, location=location, boost=boost
),
**{
"repeats_%d_%d"
% (k, n): AvoidPattern(
RepeatedKmerPattern(k, n), location=location, boost=boost
)
for (k, n) in avoided_repeats
},
}
if avoid_heterodim_with is not None:
specs["avoid_heterodimerization"] = AvoidHeterodimerization(
other_primers_sequences=avoid_heterodim_with,
tmax=max_heterodim_tm,
location=location,
boost=boost,
)
self.register_specifications(specs)
self.boost = boost
DnaChisel-3.2.16/dnachisel/builtin_specifications/AvoidBlastMatches.py 0000664 0000000 0000000 00000011110 15007617443 0026030 0 ustar 00root root 0000000 0000000 """Implementation of AvoidBlastMatches."""
from ..Specification import Specification, SpecEvaluation
# from .VoidSpecification import VoidSpecification
from ..biotools import blast_sequence
from ..Location import Location
class AvoidBlastMatches(Specification):
"""Enforce that the sequence has no BLAST matches with a given database.
WARNING: try using AvoidMatches instead, it is much better!!
Uses NCBI Blast+. Only local BLAST is supported/tested as for now
Parameters
----------
blast_db
Path to a local BLAST database. These databases can be obtained with
NCBI's `makeblastdb`. Omit the extension, e.g. `ecoli_db/ecoli_db`.
word_size
Word size used by the BLAST algorithm
perc_identity
Minimal percentage of identity for BLAST matches. 100 means that only
perfect matches are considered.
num_alignments
Number alignments
num_threads
Number of threads/CPU cores to use for the BLAST algorithm.
min_align_length
Minimal length that an alignment should have to be considered.
"""
priority = -2
best_possible_score = 0
blasts_paths = {}
def __init__(
self,
blast_db=None,
sequences=None,
word_size=4,
perc_identity=100,
num_alignments=100000,
num_threads=3,
min_align_length=20,
ungapped=True,
e_value=1e80,
culling_limit=1,
location=None,
boost=1.0,
):
"""Initialize."""
self.blast_db = blast_db
self.sequences = sequences
self.word_size = word_size
self.perc_identity = perc_identity
self.num_alignments = num_alignments
self.num_threads = num_threads
self.min_align_length = min_align_length
self.location = Location.from_data(location)
self.e_value = e_value
self.ungapped = ungapped
self.culling_limit = culling_limit
self.boost = boost
def initialized_on_problem(self, problem, role=None):
return self._copy_with_full_span_if_no_location(problem)
def evaluate(self, problem):
"""Score as (-total number of blast identities in matches)."""
location = self.location
if location is None:
location = Location(0, len(problem.sequence))
sequence = location.extract_sequence(problem.sequence)
blast_record = blast_sequence(
sequence,
blast_db=self.blast_db,
subject_sequences=self.sequences,
word_size=self.word_size,
perc_identity=self.perc_identity,
num_alignments=self.num_alignments,
num_threads=self.num_threads,
ungapped=self.ungapped,
e_value=self.e_value,
culling_limit=self.culling_limit,
task="megablast",
)
if isinstance(blast_record, list):
alignments = [
alignment for rec in blast_record for alignment in rec.alignments
]
else:
alignments = blast_record.alignments
query_hits = [
(
min(hit.query_start, hit.query_end) + location.start - 1,
max(hit.query_start, hit.query_end) + location.start,
1 - 2 * (hit.query_start > hit.query_end),
hit.identities,
)
for alignment in alignments
for hit in alignment.hsps
]
locations = sorted(
[
(start, end, ids)
for (start, end, strand, ids) in query_hits
if (end - start) >= self.min_align_length
]
)
score = -sum([ids for start, end, ids in locations])
locations = [Location(start, end) for start, end, ids in locations]
if locations == []:
return SpecEvaluation(
self, problem, score=1, message="Passed: no BLAST match found"
)
return SpecEvaluation(
self,
problem,
score=score,
locations=locations,
message="Failed - %s matches at %s" % (len(locations), locations),
)
def localized(self, location, problem=None, with_righthand=True):
"""Localize the evaluation."""
new_location = self.location.overlap_region(location)
if new_location is None:
return None
new_location = location.extended(
self.min_align_length - 1, right=with_righthand
)
return self.copy_with_changes(location=new_location)
def feature_label_parameters(self):
return [self.blast_db]
DnaChisel-3.2.16/dnachisel/builtin_specifications/AvoidChanges.py 0000664 0000000 0000000 00000014504 15007617443 0025040 0 ustar 00root root 0000000 0000000 """Implementation of AvoidChanges."""
import numpy as np
from ..Specification import Specification, SpecEvaluation
# from .VoidSpecification import VoidSpecification
from ..biotools import (
sequences_differences_array,
group_nearby_indices,
)
from ..Location import Location
class AvoidChanges(Specification):
"""Specify that some locations of the sequence should not be changed.
Shorthand for annotations: "change".
Parameters
----------
location
Location object indicating the position of the segment that must be
left unchanged. Alternatively,
indices can be provided. If neither is provided, the assumed location
is the whole sequence.
indices
List of indices that must be left unchanged.
target_sequence
At the moment, this is rather an internal variable. Do not use unless
you're not afraid of side effects.
"""
localization_interval_length = 6 # used when optimizing the minimize_diffs
best_possible_score = 0
enforced_by_nucleotide_restrictions = True
shorthand_name = "keep"
priority = -1000
def __init__(
self,
max_edits=0,
max_edits_percent=None,
location=None,
indices=None,
target_sequence=None,
boost=1.0,
):
"""Initialize."""
if location is None and (indices is not None):
location = (min(indices), max(indices) + 1)
self.location = Location.from_data(location)
if (self.location is not None) and self.location.strand == -1:
self.location.strand = 1
self.indices = np.array(indices) if (indices is not None) else None
self.target_sequence = target_sequence
self.max_edits = max_edits
self.max_edits_percent = max_edits_percent
self.boost = boost
def extract_subsequence(self, sequence):
"""Extract a subsequence from the location or indices.
Used to initialize the function when the sequence is provided.
"""
if (self.location is None) and (self.indices is None):
return sequence
elif self.indices is not None:
return "".join(np.array(list(sequence))[self.indices])
else: # self.location is not None:
return self.location.extract_sequence(sequence)
def initialized_on_problem(self, problem, role=None):
"""Find out what sequence it is that we are supposed to conserve."""
result = self._copy_with_full_span_if_no_location(problem)
L = len(result.location if result.indices is None else result.indices)
if result.max_edits_percent is not None:
result.max_edits = np.floor(result.max_edits_percent * L / 100.0)
result.enforced_by_nucleotide_restrictions = result.max_edits == 0
# Initialize the "target_sequence" in two cases:
# - Always at the very beginning
# - When the new sequence is bigger than the previous one
# (used in CircularDnaOptimizationProblem)
if result.target_sequence is None or (
len(result.target_sequence) < len(self.location)
):
result = result.copy_with_changes()
result.target_sequence = self.extract_subsequence(problem.sequence)
return result
def evaluate(self, problem):
"""Return a score equal to -number_of modifications.
Locations are "binned" modifications regions. Each bin has a length
in nucleotides equal to ``localization_interval_length`.`
"""
target = self.target_sequence
sequence = self.extract_subsequence(problem.sequence)
differing_indices = np.nonzero(sequences_differences_array(sequence, target))[0]
if self.indices is not None:
differing_indices = self.indices[differing_indices]
elif self.location is not None:
if self.location.strand == -1:
differing_indices = self.location.end - differing_indices
else:
differing_indices = differing_indices + self.location.start
intervals = [
(r[0], r[-1] + 1)
for r in group_nearby_indices(
differing_indices,
max_group_spread=self.localization_interval_length,
)
]
locations = [Location(start, end, 1) for start, end in intervals]
score = self.max_edits - len(differing_indices)
return SpecEvaluation(self, problem, score=score, locations=locations)
def localized(self, location, problem=None, with_righthand=False):
"""Localize the spec to the overlap of its location and the new."""
if self.max_edits != 0:
return self
start, end = location.start, location.end
if self.indices is not None:
pos = ((start <= self.indices) & (self.indices < end)).nonzero()[0]
new_indices = self.indices[pos]
new_target = "".join(np.array(list(self.target_sequence))[pos])
return self.copy_with_changes(
indices=new_indices, target_sequence=new_target
)
else:
new_location = self.location.overlap_region(location)
if new_location is None:
return None
else:
new_constraint = self.copy_with_changes(location=new_location)
relative_location = new_location + (-self.location.start)
new_constraint.target_sequence = relative_location.extract_sequence(
self.target_sequence
)
return new_constraint
def restrict_nucleotides(self, sequence, location=None):
"""When localizing, forbid any nucleotide but the one already there."""
if self.max_edits or self.max_edits_percent:
return []
if location is not None:
start = max(location.start, self.location.start)
end = min(location.end, self.location.end)
else:
start, end = self.location.start, self.location.end
if self.indices is not None:
return [
((i, i + 1), set([sequence[i : i + 1]]))
for i in self.indices
if start <= i < end
]
else:
return [((start, end), set([sequence[start:end]]))]
def short_label(self):
return "keep"
def breach_label(self):
return "edits"
DnaChisel-3.2.16/dnachisel/builtin_specifications/AvoidHairpins.py 0000664 0000000 0000000 00000006346 15007617443 0025252 0 ustar 00root root 0000000 0000000 """Implementation of AvoidHairpins."""
from ..Specification import Specification, SpecEvaluation
from ..biotools import reverse_complement, group_nearby_segments
from ..Location import Location
class AvoidHairpins(Specification):
"""Avoid Hairpin patterns as defined by the IDT guidelines.
A hairpin is defined by a sequence segment which has a reverse complement
"nearby" in a given window.
Parameters
----------
stem_size
Size of the stem of a hairpin, i.e. the length of the sequence which
should have a reverse complement nearby to be considered a hairpin.
hairpin_window
The window in which the stem's reverse complement should be searched for.
boost
Multiplicative factor, importance of this objective in a multi-objective
optimization.
"""
best_possible_score = 0
def __init__(self, stem_size=20, hairpin_window=200, location=None, boost=1.0):
"""Initialize."""
self.stem_size = stem_size
self.hairpin_window = hairpin_window
self.location = Location.from_data(location)
self.boost = boost
def initialized_on_problem(self, problem, role=None):
return self._copy_with_full_span_if_no_location(problem)
def evaluate(self, problem):
"""Return the score (-number_of_hairpins) and hairpins locations."""
sequence = self.location.extract_sequence(problem.sequence)
reverse = reverse_complement(sequence)
locations = []
for i in range(len(sequence) - self.stem_size):
word = sequence[i : i + self.stem_size]
rest = reverse[-(i + self.hairpin_window) : -(i + self.stem_size)]
if word in rest:
index = rest.index(word)
locations.append((i, i + self.hairpin_window - index - 1))
score = -len(locations)
locations = group_nearby_segments(locations, max_start_spread=10)
locations = sorted([Location(l[0][0], l[-1][1]) for l in locations])
return SpecEvaluation(self, problem, score, locations=locations)
def localized(self, location, problem=None, with_righthand=True):
"""Localize the spec, make sure no neighbouring hairpin is created."""
new_location = self.location.overlap_region(location)
if new_location is None:
return None
# VoidSpecification(parent_specification=self)
else:
new_location.start = max(
self.location.start, new_location.start - self.hairpin_window
)
if with_righthand:
new_location.end = min(
self.location.end, new_location.end + self.hairpin_window
)
return self.copy_with_changes(location=new_location)
def label_parameters(self):
return [
("stem_size", str(self.stem_size)),
("hairpin_window", str(self.hairpin_window)),
]
def short_label(self):
stem = self.stem_size
inside = self.hairpin_window - 2 * self.stem_size
return "No %d-%d-%dbp hairpin" % (stem, inside, stem)
def breach_label(self):
stem = self.stem_size
inside = self.hairpin_window - 2 * self.stem_size
return "%d-%d-%dbp hairpin" % (stem, inside, stem)
DnaChisel-3.2.16/dnachisel/builtin_specifications/AvoidHeterodimerization.py 0000664 0000000 0000000 00000005062 15007617443 0027334 0 ustar 00root root 0000000 0000000 try:
import primer3
PRIMER3_AVAILABLE = True
except (ImportError, OSError):
primer3 = None
from ..Specification import Specification, SpecEvaluation
class AvoidHeterodimerization(Specification):
"""Avoid that the (sub)sequence anneals with other primers.
Parameters
----------
other_primers_sequences
List of ATGC strings representing the sequences of the other primers
with which annealing will be avoided.
tmax
Maximal melting temperature of the interaction between the (sub)sequence
and the other_primers_sequences.
location
Location of the subsequence to be optimized.
boost
Multiplicator for this specification's score when used in a
multi-objective optimization.
"""
def __init__(self, other_primers_sequences, tmax=5, location=None, boost=1.0):
self.other_primers_sequences = other_primers_sequences
self.tmax = tmax
self.location = location
self.boost = boost
def initialized_on_problem(self, problem, role=None):
return self._copy_with_full_span_if_no_location(problem)
def localize(self, location, problem=None):
if self.location.overlap_region(location) is None:
return None
else:
return self
def evaluate(self, problem):
if not PRIMER3_AVAILABLE:
raise ImportError(
"Using avoid_heterodimerization requires primer3"
" installed (pip install primer3-py)"
)
if len(self.other_primers_sequences) == 0:
return SpecEvaluation(
specification=self,
problem=problem,
score=0,
locations=[self.location],
message="No existing primer",
)
sequence = self.location.extract_sequence(problem.sequence)
melting_temps = [
primer3.calcHeterodimer(sequence, other_seq).tm
for other_seq in self.other_primers_sequences
]
largest_tm = max(melting_temps)
# hackish penalty to guide optimization:
penalty = 0.001 * sum(melting_temps) / len(melting_temps)
score = self.tmax - largest_tm - penalty
return SpecEvaluation(
specification=self,
problem=problem,
score=score,
locations=[self.location],
message="Largest Tm = %.1f " % largest_tm,
)
def label_parameters(self):
return [
("primers", len(self.other_primers_sequences)),
("tmax", self.tmax),
]
DnaChisel-3.2.16/dnachisel/builtin_specifications/AvoidMatches.py 0000664 0000000 0000000 00000011336 15007617443 0025054 0 ustar 00root root 0000000 0000000 """Implementation of AvoidMatches."""
from ..Specification import Specification, SpecEvaluation
from ..biotools.bowtie import (
find_all_bowtie_matches,
create_bowtie_index_from_sequences,
)
from ..Location import Location
import tempfile
import shutil
class AvoidMatches(Specification):
"""Enforce that the sequence has no matches longer than N in a given index.
This specification can be used to ensure that a sequence has no matches
with a given organism, or a set of sequences, which can be useful to
create orthogonal sequences or primer-friendly regions.
This specification uses Bowtie in the background and requires Bowtie
installed on your machine (it can be as simple as ``apt install bowtie``
on Ubuntu).
It allows you to specify the ``match_length`` such that no subsegment of
size match_length or more has any homology in the given bowtie index (which
can be built from genomes using e.g. the genome_collector library). An
homology can mean either perfect similarity, or up to 3 mismatches.
Examples
--------
Here is how you automatically get a Bowtie index link with Genome Collector
(it will build the index if it is not yet on your machine), and use the
result to create an ``AvoidMatches`` specification:
>>> from genome_collector import GenomeCollection
>>> collection = GenomeCollection()
>>> e_coli = collection.get_taxid_bowtie_index_path(511145, version="1")
>>> spec = AvoidMatches(bowtie_index=e_coli, match_length=15, mismatches=1)
Parameters
----------
bowtie_index
Path to a local bowtie2 index.
match_length
Length of the matches to avoid
mismatches
Number of single-nucleotide mismatches allowed inside each match. Only
0-3 mismatches is supported (this is what Bowtie supports.)
location
Location of the sequence on which the specification applies
"""
priority = -2
best_possible_score = 0
blasts_paths = {}
shorthand_name = "no_match"
bowtie_indexes_paths = None
def __init__(
self,
match_length,
bowtie_index=None,
sequences=None,
mismatches=0,
location=None,
boost=1,
):
"""Initialize."""
self.bowtie_index = bowtie_index
self.match_length = match_length
self.mismatches = mismatches
self.sequences = sequences
self.location = Location.from_data(location)
self.boost = boost
self._tmp_data_dir = None
if self.sequences is not None:
self._tmp_data_dir = tempfile.mkdtemp(prefix="chisel_AvoidMatch")
self.bowtie_index = create_bowtie_index_from_sequences(
sequences=sequences, path=self._tmp_data_dir
)
self.is_localized = False
def initialized_on_problem(self, problem, role=None):
return self._copy_with_full_span_if_no_location(problem)
def evaluate(self, problem):
"""Find matches and count them negatively"""
location = self.location
if location is None:
location = Location(0, len(problem.sequence))
sequence = location.extract_sequence(problem.sequence)
bowtie_index = self.bowtie_index
if self.bowtie_indexes_paths is not None:
if self.bowtie_index in self.bowtie_indexes_dict:
bowtie_index = self.bowtie_indexes_paths[self.bowtie_index]
# matches will be of the form [ ((start, end), n_mismatches), (...)]
matches = find_all_bowtie_matches(
sequence=sequence,
bowtie_index_path=bowtie_index,
match_length=self.match_length,
max_mismatches=self.mismatches,
)
# the score is designed to be -1 per exact match, and improve when
# there are mismatches more mismatches,
score = -sum([1.0 / (1 + mis) for _, mis in matches])
locations = [Location(start, end) for (start, end), _ in matches]
return SpecEvaluation(
self,
problem,
score=score,
locations=locations,
message="Failed - %s matches at %s" % (len(locations), locations),
)
def localized(self, location, problem=None, with_righthand=True):
"""Localize the evaluation."""
new_location = self.location.overlap_region(location)
if new_location is None:
return None
new_location = location.extended(self.match_length - 1, right=with_righthand)
return self.copy_with_changes(location=new_location, is_localized=True)
def feature_label_parameters(self):
return [self.bowtie_index]
def remove_temp_directory(self):
if self._tmp_data_dir is not None:
shutil.rmtree(self._tmp_data_dir)
DnaChisel-3.2.16/dnachisel/builtin_specifications/AvoidPattern.py 0000664 0000000 0000000 00000010130 15007617443 0025074 0 ustar 00root root 0000000 0000000 """Implement AvoidPattern"""
from ..SequencePattern import SequencePattern
from ..Location import Location
from ..Specification.Specification import Specification
from ..Specification.SpecEvaluation import SpecEvaluation
class AvoidPattern(Specification):
"""Enforce that the given pattern is absent in the sequence.
Shorthand for annotations: "no".
Parameters
----------
pattern
A SequencePattern or DnaNotationPattern. If a ``str`` is given, it will
be converted. Note that providing ``size`` may be necessary for certain
patterns. See SequencePattern documentation for more details.
location
Location of the DNA segment on which to enforce the pattern e.g.
``Location(10, 45, 1)``. For patterns which are not palindromic,
the strand matters! Use +1 for eliminating the pattern on the +1 strand
only, -1 for eliminating the pattern on the -1 strand, and 0 for
eliminating the pattern on both strands. Default ``None`` enforces on
the whole sequence.
strand
Alternative way to set the strand, meant to be used in two cases only:
(1) in a Genbank annotation by setting ``strand=both`` to indicate that
the pattern should be avoided on both strands (otherwise, only the
feature's strand will be considered).
(2) if you want to create a specification without preset location, but
with a set strand: ``AvoidPattern('BsmBI_site', strand=1)``.
The default 'from_location' uses the strand specified in ``location``,
or if that is ``None``, it sets both strands.
"""
best_possible_score = 0
priority = 1
shorthand_name = "no" # will appear as, for instance, @no(BsmBI_site)
def __init__(self, pattern=None, location=None, strand="from_location", boost=1.0):
"""Initialize."""
if isinstance(pattern, str):
pattern = SequencePattern.from_string(pattern)
self.pattern = pattern
self.location = Location.from_data(location)
if strand == "from_location":
if self.location is None:
self.strand = 0
else:
self.strand = self.location.strand
elif strand == "both":
self.strand = 0
elif strand in [-1, 0, 1]:
self.strand = strand
else:
raise ValueError("unknown strand: %s" % strand)
self.boost = boost
def evaluate(self, problem):
"""Return score=-number_of_occurences. And patterns locations."""
locations = self.pattern.find_matches(problem.sequence, self.location)
score = -len(locations)
if score == 0:
message = "Passed. Pattern not found !"
else:
message = "Failed. Pattern found at positions %s" % locations
return SpecEvaluation(
self, problem, score, locations=locations, message=message
)
def short_label(self):
if self.pattern.name is not None:
return "No %s" % self.pattern.name
else:
return "No %s" % self.pattern
def breach_label(self):
if self.pattern.name is not None:
return str(self.pattern.name)
else:
return str(self.pattern)
def initialized_on_problem(self, problem, role="constraint"):
copy_of_constraint = self._copy_with_full_span_if_no_location(problem)
copy_of_constraint.location.strand = self.strand
return copy_of_constraint
def localized(self, location, problem=None, with_righthand=True):
"""Localize the pattern to the given location. Taking into account the
specification's own location, and the size of the pattern."""
if self.location.overlap_region(location) is None:
return None
if self.pattern.size is None:
return self
extended_location = location.extended(
self.pattern.size - 1, right=with_righthand
)
new_location = self.location.overlap_region(extended_location)
return self.copy_with_changes(location=new_location)
def label_parameters(self):
return [("pattern", str(self.pattern))]
DnaChisel-3.2.16/dnachisel/builtin_specifications/AvoidStopCodons.py 0000664 0000000 0000000 00000003732 15007617443 0025564 0 ustar 00root root 0000000 0000000 from ..biotools import translate
from ..Location import Location
from ..Specification import SpecEvaluation
from .CodonSpecification import CodonSpecification
from Bio.Data import CodonTable
class AvoidStopCodons(CodonSpecification):
"""Do not introduce any new stop codon in that frame.
This can be used for research purposes, to avoid breaking a reading frame
when editing it with quasi-synonymous mutations.
"""
def __init__(self, genetic_table="Standard", location=None, boost=1.0):
self.genetic_table = genetic_table
self.boost = boost
self.location = Location.from_data(location)
def initialized_on_problem(self, problem, role):
"""Get translation from the sequence if it is not already set."""
return self._copy_with_full_span_if_no_location(problem)
def localized_on_window(self, new_location, start_codon, end_codon):
return self.copy_with_changes(location=new_location)
def evaluate(self, problem):
location = (
self.location
if self.location is not None
else Location(0, len(problem.sequence))
)
subsequence = location.extract_sequence(problem.sequence)
translation = translate(subsequence, table=self.genetic_table)
errors_locations = [
self.codon_index_to_location(index)
for index in range(len(translation))
if translation[index] == "*"
]
return SpecEvaluation(
self,
problem,
score=-len(errors_locations),
locations=errors_locations,
message=(
"All OK."
if len(errors_locations) == 0
else "Stop codons found at indices %s" % errors_locations
),
)
def __str__(self):
"""Represent."""
return "AvoidStopCodons(%s)" % self.location
def __str__(self):
"""Represent."""
return "AvoidStopCodons(%s)" % self.location
DnaChisel-3.2.16/dnachisel/builtin_specifications/CodonSpecification.py 0000664 0000000 0000000 00000004402 15007617443 0026244 0 ustar 00root root 0000000 0000000 """Implements core specification VoidSpecification."""
from ..Specification import Specification
# from .VoidSpecification import VoidSpecification
from ..Location import Location
class CodonSpecification(Specification):
"""Special class for dealing with codon.
In particular, this class implements a specific localized method
"""
def codon_index_to_location(self, index):
if self.location.strand >= 0:
return Location(
start=self.location.start + 3 * index,
end=self.location.start + 3 * (index + 1),
strand=1,
)
else:
return Location(
start=self.location.end - 3 * (index + 1),
end=self.location.end - 3 * index,
strand=-1,
)
def localized(self, location, problem=None, with_righthand=True):
"""Generic localization method for codon specifications.
Calls the class' ``.localized_on_window`` method at the end.
"""
if self.location is not None:
overlap = self.location.overlap_region(location)
if overlap is None:
return None
else:
# return self
o_start, o_end = overlap.start, overlap.end
w_start, w_end = self.location.start, self.location.end
if self.location.strand != -1:
start_codon = int((o_start - w_start) / 3)
end_codon = int((o_end - w_start - 1) / 3) + 1
new_location = Location(
start=w_start + 3 * start_codon,
end=min(w_end, w_start + 3 * (end_codon)),
strand=self.location.strand,
)
else:
start_codon = int((w_end - o_end) / 3)
end_codon = int((w_end - o_start - 1) / 3) + 1
new_location = Location(
start=max(w_start, w_end - 3 * (end_codon)),
end=w_end - 3 * start_codon,
strand=self.location.strand,
)
return self.localized_on_window(new_location, start_codon, end_codon)
else:
return self
DnaChisel-3.2.16/dnachisel/builtin_specifications/EnforceChanges.py 0000664 0000000 0000000 00000027226 15007617443 0025364 0 ustar 00root root 0000000 0000000 """Implementation of EnforceChanges."""
import numpy as np
from ..Specification import Specification, SpecEvaluation
from ..biotools import (
sequences_differences_array,
group_nearby_indices,
OTHER_BASES,
)
from ..Location import Location
other_bases_sets = {base: other_bases for (base, other_bases) in OTHER_BASES.items()}
class EnforceChanges(Specification):
"""Specify that some locations of the sequence should be changed/different.
The input for this specification is a bit complex as it is different for
objectives and constraints:
- **For constraints**, use EnforceChanges() to force all nucleotides to
be different from the reference (by default, the reference is the
starting sequence). Use EnforceChanges(minimum=3) to enforce at least
3 nucleotide changes, or EnforceChanges(minimum_percent=70) to enforce
at least 70% change. Note that having a minimum makes computations much
slower.
- **For objectives**, use EnforceChanges() to maximize the number of
nucleotide changes, or use EnforceChanges(amount=10) to aim at a
10-nucleotides change (no more, no less), or use
EnforceChanges(minimum_amount=70) to aim at 70% change, no more no less.
As a genbank annotation:
- ``@change``, ``@change(minimum=40%)``, ``@change(minimum=3)`` will
enforce respectively 100% different nucleotides, 40%+ different, and 3+
nucleotides different.
- ``~change``, ``~change(40%)``, ``~change(3)`` to aim at, respectively,
as close as possible to 100% change, 40% changes, or a target of 3
nucleotides changes.
Parameters
----------
location
Location object indicating the position of the segment that must be
different from the original sequence. Alternatively,
indices can be provided. If neither is provided, the assumed location
is the whole sequence.
amount
Number of differences desired in the final sequence when the spec. is
used as an objective. An amount_percent can be provided instead.
amount_percent
Same as ``amount``, except the desired number of differences is decided
at initialization, as a percentage of the number of nucleotides in the
region covered.
minimum
Minimal number of differences enforced in the final sequence when the
spec. is used as an constraint. A minimum_percent can be provided
instead.
minimum_percent
Same as ``minimum``, except the desired number of differences is decided
at initialization, as a percentage of the number of nucleotides in the
region covered.
indices
List of indices that must be different from the original sequence.
reference
At the moment, this is rather an internal variable. Do not use unless
you're not afraid of side effects.
"""
localization_interval_length = 7 # used when optimizing
best_possible_score = 0
shorthand_name = "change"
def __init__(
self,
amount=None,
amount_percent=None,
minimum=None,
minimum_percent=None,
location=None,
indices=None,
reference=None,
boost=1.0,
):
"""Initialize."""
# raise NotImplementedError("This class is not yet implemented")
# if location is None and (indices is not None):
# location = (min(indices), max(indices) + 1)
self.location = Location.from_data(location)
if (self.location is not None) and self.location.strand == -1:
self.location.strand = 1
self.indices = np.array(indices) if (indices is not None) else None
self.reference = reference
# self.passive_objective = passive_objective
self.amount = amount
self.amount_percent = amount_percent
self.minimum = minimum
self.minimum_percent = minimum_percent
if isinstance(amount, str) and amount.endswith("%"):
self.amount = None
self.amount_percent = float(amount[:-1])
if isinstance(minimum, str) and minimum.endswith("%"):
self.minimum = None
self.minimum_percent = float(minimum[:-1])
self.boost = boost
def extract_subsequence(self, sequence):
"""Extract a subsequence from the location or indices.
Used to initialize the function when the sequence is provided.
"""
if (self.location is None) and (self.indices is None):
return sequence
elif self.indices is not None:
return "".join(np.array(list(sequence))[self.indices])
else: # self.location is not None:
return self.location.extract_sequence(sequence)
def initialized_on_problem(self, problem, role=None):
"""Find out what sequence it is that we are supposed to conserve."""
result = self._copy_with_full_span_if_no_location(problem)
L = len(result.location if result.indices is None else result.indices)
# FIRST DEAL WITH THE AMOUNTS OF CHANGE
both_amounts_none = (self.amount is None) and (self.amount_percent is None)
both_minimums_none = (self.minimum is None) and (self.minimum_percent is None)
# Only if minimum_percent=100 is the constraint nucleotide-enforced
# So this variable is updated below if necessary
self.enforced_by_nucleotide_restrictions = False
if role == "constraint":
if not both_amounts_none:
raise ValueError(
"Use minimum(_percent) parameter when using EnforceChanges"
" as a constraint"
)
if both_minimums_none:
result.minimum_percent = 100
if result.minimum_percent is not None:
if result.minimum_percent == 100:
self.enforced_by_nucleotide_restrictions = 100
result.minimum = np.ceil(result.minimum_percent * L / 100.0)
elif role == "objective":
if not both_minimums_none:
raise ValueError(
"Use amount(_percent) parameter when using EnforceChanges "
" as an objective"
)
if both_amounts_none:
result.amount_percent = 100
if result.amount_percent is not None:
result.amount = result.amount_percent * L / 100.0
# THEN DEAL WITH THE REFERENCE
if (self.reference is None) and (self.indices is not None):
result = result.copy_with_changes()
result.reference = self.extract_subsequence(problem.sequence)
elif self.reference is None or (
(self.indices is None) and (len(self.reference) < L)
):
result = result.copy_with_changes()
result.reference = self.extract_subsequence(problem.sequence)
return result
def evaluate(self, problem):
"""Return a score equal to -number_of_equalities.
Locations are "binned" equality regions. Each bin has a length
in nucleotides equal to ``localization_interval_length`.`
"""
# FIND THE INDICES WHERE THE SEQUENCE IS UNCHANGED
# Note: at this stage any minimum_percent or amount_percent have been
# transformed into absolute self.minimum and self.amount.
target = self.reference
sequence = self.extract_subsequence(problem.sequence)
equalities = np.nonzero(1 - sequences_differences_array(sequence, target))[0]
if self.indices is not None:
equalities = self.indices[equalities]
elif self.location is not None:
if self.location.strand == -1:
equalities = self.location.end - equalities
else:
equalities = equalities + self.location.start
def indices_to_intervals(indices):
intervals = group_nearby_indices(
indices, max_group_spread=self.localization_interval_length
)
return [(interval[0], interval[-1] + 1) for interval in intervals]
if self.indices is not None:
n_indices = len(self.indices)
else:
n_indices = len(self.location)
n_differences = n_indices - len(equalities)
if self.minimum is not None:
score = n_differences - self.minimum
intervals = indices_to_intervals(equalities)
else:
score = -abs(n_differences - self.amount)
if n_differences <= self.amount:
intervals = indices_to_intervals(equalities)
else:
differences = [i for i in self.location.indices if i not in equalities]
intervals = indices_to_intervals(differences)
locations = (
[self.location]
if (self.minimum is not None)
else [Location(start, end, 1) for start, end in intervals]
)
return SpecEvaluation(self, problem, score=score, locations=locations)
def localized(self, location, problem=None, with_righthand=True):
"""Localize the spec to the overlap of its location and the new."""
# If minimum_percent=100: everything is covered by mutation restriction
# If minimum_percent<100: no smart strategy return self.
# If amount_percent<100: no smart strategy return self.
# If amount_percent==100: easy to localize, it is always locally 100%
if self.amount_percent != 100:
return self
# Now the only case where localization makes sense: amount_percent=100
start, end = location.start, location.end
if self.indices is not None:
pos = ((start <= self.indices) & (self.indices < end)).nonzero()[0]
new_indices = self.indices[pos]
new_reference = "".join(np.array(list(self.reference))[pos])
return self.copy_with_changes(
indices=new_indices,
reference=new_reference,
minimum=None if self.minimum is None else len(new_indices),
amount=None if self.amount is None else len(new_indices),
)
else:
new_location = self.location.overlap_region(location)
if new_location is None:
return None
else:
new_constraint = self.copy_with_changes(
location=new_location,
minimum=None if self.minimum is None else len(location),
amount=None if self.amount is None else len(location),
)
relative_location = new_location + (-self.location.start)
new_constraint.reference = relative_location.extract_sequence(
self.reference
)
return new_constraint
def restrict_nucleotides(self, sequence, location=None):
"""When localizing, forbid any nucleotide but the one already there."""
# IF IT IS NOT AN ALL-DIFFERENT CONSTRAINT, DO NOTHING
if self.minimum_percent != 100:
return []
# ELSE, IMPOSE THAT EACH NUCL. IS ONE OF THE 3 NON-INITIAL ONES
if location is not None:
start = max(location.start, self.location.start)
end = min(location.end, self.location.end)
else:
start, end = self.location.start, self.location.end
if self.indices is not None:
return [
((i, i + 1), other_bases_sets[sequence[i : i + 1]])
for i in self.indices
if start <= i < end
]
else:
return [
((i, i + 1), other_bases_sets[sequence[i : i + 1]])
for i in range(start, end)
]
def short_label(self):
return "change"
def breach_label(self):
return "unchanged"
DnaChisel-3.2.16/dnachisel/builtin_specifications/EnforceChoice.py 0000664 0000000 0000000 00000006271 15007617443 0025203 0 ustar 00root root 0000000 0000000 """Implement EnforceSequence (DO NOT USE YET: Work in progress, stabilizing)"""
# TODO: factorize with self.sequence ?
from ..SequencePattern import SequencePattern
from ..Location import Location
from ..biotools import reverse_complement
from ..Specification import Specification, SpecEvaluation
class EnforceChoice(Specification):
"""The sequence at the given location must be one of several alternatives.
Shorthand for annotations: "choice".
Parameters
----------
choices
List of same-length ATGC sequences, e.g. ["ATT", "ATC", "ATA"].
location
Location of the DNA segment on which to enforce the pattern e.g.
``Location(10, 45, 1)`` or simply ``(10, 45, 1)``. the location length
must match that of the sequence in the list of ``choices``
"""
localization_interval_length = 6 # used when optimizing
best_possible_score = 0
enforced_by_nucleotide_restrictions = True
shorthand_name = "choice"
def __init__(self, choices=None, location=None, boost=1.0):
"""Initialize."""
if isinstance(choices, str) and "|" in choices:
choices = choices.split("|")
choices = [
SequencePattern.from_string(c) if isinstance(c, str) else c for c in choices
]
# PRECOMPUTE ALL VARIANTS
choices = [variant for choice in choices for variant in choice.all_variants()]
self.choices = choices
self.location = Location.from_data(location)
self.boost = boost
def initialized_on_problem(self, problem, role="constraint"):
"""Find out what sequence it is that we are supposed to conserve."""
result = self._copy_with_full_span_if_no_location(problem)
if not all([len(c) == len(result.location) for c in result.choices]):
raise ValueError(
"All sequence choices should have the same length as the "
"region on which the spec is applied."
)
return result
def evaluate(self, problem):
"""Return a score equal to -number_of modifications.
Locations are "binned" modifications regions. Each bin has a length
in nucleotides equal to ``localization_interval_length`.`
"""
sequence = self.location.extract_sequence(problem.sequence)
score = 0 if (sequence in self.choices) else -1
locations = [] if (score == 0) else [self.location]
return SpecEvaluation(self, problem, score=score, locations=locations)
def localized(self, location, problem=None, with_righthand=True):
"""Localize the spec to the overlap of its location and the new."""
return self
def restrict_nucleotides(self, sequence, location=None):
"""As a constraint, put the choices in the mutation space."""
if self.location.strand != -1:
choices = set(self.choices)
else:
choices = set([reverse_complement(c) for c in self.choices])
return [((self.location.start, self.location.end), choices)]
def __repr__(self):
"""Represent."""
return "EnforceChoice(%s)" % str(self.location)
def __str__(self):
"""Represent."""
return "EnforceChoice(%s)" % str(self.location)
DnaChisel-3.2.16/dnachisel/builtin_specifications/EnforceGCContent.py 0000664 0000000 0000000 00000015450 15007617443 0025634 0 ustar 00root root 0000000 0000000 """Implement EnforceGCContent."""
import numpy as np
import re
from ..biotools import gc_content, group_nearby_segments
from ..Location import Location
from ..Specification import Specification, SpecEvaluation
class EnforceGCContent(Specification):
"""Specification on the local or global proportion of G/C nucleotides.
Shorthand for annotations: "gc".
Examples
--------
>>> # Enforce global GC content between 40 and 70 percent.
>>> Specification = GCContentSpecification(0.4, 0.7)
>>> # Enforce 30-80 percent local GC content over 50-nucleotides windows
>>> Specification = GCContentSpecification(0.3, 0.8, window=50)
Parameters
----------
mini
Minimal proportion of G-C (e.g. ``0.35``)
maxi
Maximal proportion of G-C (e.g. ``0.75``)
target
Target proportion of GC (e.g. ``0.4``), which can be used instead of
``mini`` and ``maxi`` when using the specification as an optimization
objective.
window
Length of the sliding window, in nucleotides, for local GC content.
If not provided, the global GC content of the whole sequence is
considered
location
Location objet indicating that the Specification only applies to a
subsegment of the sequence. Make sure it is bigger than ``window``
if both parameters are provided
"""
best_possible_score = 0
locations_span = 50 # The resolution will use locations size
shorthand_name = "gc"
def __init__(
self,
mini=0,
maxi=1.0,
target=None,
window=None,
location=None,
boost=1.0,
):
"""Initialize."""
if isinstance(mini, str):
mini, maxi, target, _window = self.string_to_parameters(mini)
if _window is not None:
window = _window
if target is not None:
mini = maxi = target
self.target = target
self.mini = mini
self.maxi = maxi
self.window = window
location = Location.from_data(location)
if location is not None and (location.strand == -1):
location = Location(location.start, location.end, 1)
self.location = location
self.boost = boost
@staticmethod
def string_to_parameters(pattern):
target = None
gc_pattern = r"(\d+)(|-(\d+))%($|/(\d+)bp)"
match = re.match(gc_pattern, pattern)
mini, _, maxi, _, window = match.groups()
if maxi is None:
target = mini
mini = None
mini, maxi, target = [
None if e is None else int(e) / 100.0 for e in (mini, maxi, target)
]
window = None if window is None else int(window)
return mini, maxi, target, window
def initialized_on_problem(self, problem, role=None):
return self._copy_with_full_span_if_no_location(problem)
def evaluate(self, problem):
"""Return the sum of breaches extent for all windowed breaches."""
wstart, wend = self.location.start, self.location.end
sequence = self.location.extract_sequence(problem.sequence)
gc = gc_content(sequence, window_size=self.window)
breaches = np.atleast_1d(
np.maximum(0, self.mini - gc) + np.maximum(0, gc - self.maxi)
)
score = -breaches.sum()
breaches_starts = wstart + (breaches > 0).nonzero()[0]
if len(breaches_starts) == 0:
breaches_locations = []
elif len(breaches_starts) == 1:
if self.window is not None:
start = breaches_starts[0]
breaches_locations = [[start, start + self.window]]
else:
breaches_locations = [[wstart, wend]]
else:
segments = [(bs, bs + self.window) for bs in breaches_starts]
groups = group_nearby_segments(
segments, max_start_spread=max(1, self.locations_span)
)
breaches_locations = [(group[0][0], group[-1][-1]) for group in groups]
if breaches_locations == []:
message = "Passed !"
else:
breaches_locations = [Location(*loc) for loc in breaches_locations]
message = "Out of bound on segments " + ", ".join(
[str(l) for l in breaches_locations]
)
return SpecEvaluation(
self, problem, score, locations=breaches_locations, message=message
)
def localized(self, location, problem=None, with_righthand=True):
"""Localize the GC content evaluation.
For a location, the GC content evaluation will be restricted
to [start - window, end + window]
"""
# if self.location is not None:
if self.window is None:
# No specific location policy at the moment for non-windowed GC
# content. The same specification is returned.
# NOTE: this could be refined by computing
# how much the local bounds should be in order for the global
# sequence not get out of bound. This would be faster but much
# more complicated.
return self
new_location = self.location.overlap_region(location)
if new_location is None:
return None
else:
extension = 0 if self.window is None else self.window - 1
extended_location = location.extended(extension, right=with_righthand)
new_location = self.location.overlap_region(extended_location)
return self.copy_with_changes(location=new_location)
def label_parameters(self):
show_mini = self.mini is not None and (self.target is None)
show_maxi = self.maxi is not None and (self.target is None)
show_target = not (show_mini or show_maxi)
show_window = self.window is not None
return (
show_mini * [("mini", "%.2f" % self.mini)]
+ show_maxi * [("maxi", "%.2f" % self.maxi)]
+ show_target * [("target", ("%.2f" % self.target) if self.target else "")]
+ show_window * [("window", ("%d" % self.window) if self.window else "")]
)
def short_label(self):
if self.target is not None:
result = "%d%% GC" % (np.round(100 * self.target))
else:
result = "%d-%d%% GC" % (
np.round(100 * self.mini),
np.round(100 * self.maxi),
)
if self.window is not None:
result += "/%dbp" % self.window
return result
def breach_label(self):
if self.target is not None:
result = "GC != %d%% " % (np.round(100 * self.target))
else:
result = "GC outside %d-%d%%" % (
np.round(100 * self.mini),
np.round(100 * self.maxi),
)
if self.window is not None:
result += "/%dbp" % self.window
return result
DnaChisel-3.2.16/dnachisel/builtin_specifications/EnforceMeltingTemperature.py 0000664 0000000 0000000 00000004762 15007617443 0027631 0 ustar 00root root 0000000 0000000 try:
import primer3
PRIMER3_AVAILABLE = True
except ImportError:
primer3 = None
PRIMER3_AVAILABLE = False
from Bio.SeqUtils import MeltingTemp as bio_mt
from ..Location import Location
from ..Specification import Specification, SpecEvaluation
class EnforceMeltingTemperature(Specification):
"""Ensure that the subsequence's Tm is in a certain segment/target.
Shorthand for annotations: "tm".
Parameters
----------
mini, maxi
Minimum and maximum acceptable melting temperatures in Celcius, for
instance 55 and 70. A "target" can be provided instead when using this
specification as an optimization objective.
target
Target melting temperature. Will be overridden by (mini+maxi)/2 if these
are provided. The "target" parameter is only practical when the spec is
used as an optimization objective.
location
Location of the subsequence whose melting temperature is considered.
Can be None if the whole sequence is to be considered.
boost
Multiplicator for this specification's score when used in a
multi-objective optimization.
"""
shorthand_name = "tm"
def __init__(self, mini=None, maxi=None, target=None, location=None, boost=1.0):
"""Initialize."""
if isinstance(mini, str) and mini.endswith("C"):
# PROCESS CASES "45-55%" and "45%"
split = mini[:-1].split("-")
if len(split) == 2:
mini, maxi = float(split[0]), float(split[1])
else:
target = float(split[0])
if target is not None:
mini = maxi = target
else:
target = 0.5 * (mini + maxi)
self.mini = mini
self.maxi = maxi
self.target = target
self.location = Location.from_data(location)
self.boost = boost
def initialized_on_problem(self, problem, role=None):
return self._copy_with_full_span_if_no_location(problem)
def evaluate(self, problem):
"""Return the sum of breaches extent for all windowed breaches."""
sequence = self.location.extract_sequence(problem.sequence)
predictor = primer3.calcTm if PRIMER3_AVAILABLE else bio_mt.Tm_NN
tm = predictor(sequence)
score = 0.5 * (self.maxi - self.mini) - abs(tm - self.target)
return SpecEvaluation(
specification=self,
problem=problem,
score=score,
locations=[self.location],
message="Tm = %.1f " % tm,
)
DnaChisel-3.2.16/dnachisel/builtin_specifications/EnforcePatternOccurence.py 0000664 0000000 0000000 00000017613 15007617443 0027257 0 ustar 00root root 0000000 0000000 """Implement EnforcePatternOccurence"""
from ..MutationSpace import MutationSpace
from ..SequencePattern import SequencePattern, DnaNotationPattern
from ..Location import Location
from ..biotools import reverse_complement
from ..DnaOptimizationProblem.DnaOptimizationProblem import DnaOptimizationProblem
from ..DnaOptimizationProblem.NoSolutionError import NoSolutionError
from ..Specification import Specification, SpecEvaluation
from .EnforceSequence import EnforceSequence
class EnforcePatternOccurence(Specification):
"""Enforce a number of occurrences of the given pattern in the sequence.
Shorthand for annotations: "insert" (although this specification can be
used to both insert new occurences of a pattern, or destroy supernumerary
patterns)
Parameters
----------
pattern
A SequencePattern or DnaNotationPattern or a string such as "AATTG",
"BsmBI_site", etc. See SequencePattern documentation for more details.
occurences
Desired number of occurrences of the pattern.
location
Location of the DNA segment on which to enforce the pattern e.g.
``Location(10, 45, 1)``. Default ``None`` means the whole sequence.
center
If True, new inserted patterns will prioritize locations at the center
of the specification's location. Else the insertion will happen at
the beginning of the location.
strand
Alternative way to set the strand, meant to be used in two cases only:
(1) in a Genbank annotation by setting ``strand='both'`` to indicate that
the pattern could be on both strands (otherwise, only the
feature's strand will be considered).
(2) if you want to create a specification without preset location, but
with a set strand: ``EnforcePatternOccurence('BsmBI_site', strand=1)``.
The default 'from_location' uses the strand specified in ``location``, or
if that is ``None``, it sets both strands.
"""
best_possible_score = 0
priority = -1
shorthand_name = "insert"
def __init__(
self,
pattern=None,
occurences=1,
location=None,
strand="from_location",
center=True,
boost=1.0,
):
"""Initialize."""
if isinstance(pattern, str):
pattern = SequencePattern.from_string(pattern)
self.pattern = pattern
self.location = Location.from_data(location)
if strand == "from_location":
if self.location is None:
self.strand = 0
else:
self.strand = self.location.strand
elif strand == "both":
self.strand = 0
elif strand in [-1, 0, 1]:
self.strand = strand
else:
raise ValueError("unknown strand: %s" % strand)
self.occurences = occurences
self.center = center
self.boost = boost
def initialized_on_problem(self, problem, role=None):
copy_of_constraint = self._copy_with_full_span_if_no_location(problem)
copy_of_constraint.location.strand = self.strand
return copy_of_constraint
def evaluate(self, problem):
"""Score the difference between expected and observed n_occurences."""
matches = self.pattern.find_matches(
problem.sequence,
self.location,
)
score = -abs(len(matches) - self.occurences)
if score == 0:
message = "Passed. Pattern found at positions %s" % matches
else:
if self.occurences == 0:
message = "Failed. Pattern not found."
else:
message = (
"Failed. Pattern found %d times instead of %d"
" wanted, at locations %s"
) % (len(matches), self.occurences, matches)
return SpecEvaluation(
self,
problem,
score,
message=message,
locations=[self.location],
data=dict(matches=matches),
)
def localized(self, location, problem=None):
"""Localize the evaluation."""
new_location = self.location.overlap_region(location)
if new_location is None:
return None
# VoidSpecification(parent_specification=self)
else:
return self
def insert_pattern_in_problem(self, problem, reverse=False):
"""Insert the pattern in the problem's sequence by successive tries.
This heuristic is attempted to get the number of occurrences in the
pattern from 0 to some number.
"""
sequence_to_insert = self.pattern.sequence
if reverse:
sequence_to_insert = reverse_complement(sequence_to_insert)
L = self.pattern.size
starts = range(self.location.start, self.location.end - L)
if self.center:
center = 0.5 * (self.location.start + self.location.end)
starts = sorted(starts, key=lambda s: abs(s - center))
for start in starts:
new_location = Location(start, start + L, self.location.strand)
new_constraint = EnforceSequence(
sequence=sequence_to_insert, location=new_location
)
new_space = MutationSpace.from_optimization_problem(
problem, new_constraints=[new_constraint]
)
if len(new_space.unsolvable_segments) > 0:
continue
new_sequence = new_space.constrain_sequence(problem.sequence)
new_constraints = problem.constraints + [new_constraint]
new_problem = DnaOptimizationProblem(
sequence=new_sequence,
constraints=new_constraints,
mutation_space=new_space,
logger=None,
)
if self.evaluate(new_problem).passes:
try:
new_problem.resolve_constraints()
problem.sequence = new_problem.sequence
return
except NoSolutionError:
pass
if (not reverse) and (not self.pattern.is_palyndromic):
self.insert_pattern_in_problem(problem, reverse=True)
return
raise NoSolutionError(
problem=problem,
location=self.location,
message="Insertion of pattern %s in %s failed"
% (self.pattern.sequence, self.location),
)
def resolution_heuristic(self, problem):
"""Resolve using custom insertion if possible."""
if isinstance(self.pattern, DnaNotationPattern):
evaluation = self.evaluate(problem)
if evaluation.passes:
return
n_matches = len(evaluation.data["matches"])
if n_matches < self.occurences:
other_constraints = [c for c in problem.constraints if c is not self]
new_problem = problem
for i in range(self.occurences - n_matches):
new_occurence_cst = self.copy_with_changes(
occurences=n_matches + i + 1
)
new_problem = DnaOptimizationProblem(
sequence=new_problem.sequence,
constraints=other_constraints + [new_occurence_cst],
mutation_space=problem.mutation_space,
)
new_occurence_cst.insert_pattern_in_problem(new_problem)
problem.sequence = new_problem.sequence
return
problem.resolve_constraints_locally() # default resolution method
def label_parameters(self):
# result = [('enzyme', self.enzyme) if (self.enzyme is not None)
# else (self.pattern.sequence
# if hasattr(self.pattern, 'sequence')
# else str(self.pattern))]
result = [str(self.pattern)]
if self.occurences != 1:
result += ["occurence", str(self.occurences)]
return result
DnaChisel-3.2.16/dnachisel/builtin_specifications/EnforceRegionsCompatibility.py 0000664 0000000 0000000 00000004534 15007617443 0030151 0 ustar 00root root 0000000 0000000 from collections import Counter
import itertools
from ..Location import Location
from ..Specification import Specification, SpecEvaluation
class EnforceRegionsCompatibility(Specification):
"""Ensure that different subregions satisfy compatibility constraints."""
max_possible_score = 0
def __init__(
self, locations, compatibility_condition, condition_label="", boost=1.0
):
self.locations = [Location.from_tuple(location) for location in locations]
self.compatibility_condition = compatibility_condition
self.condition_label = condition_label
self.boost = boost
def evaluate(self, problem):
incompatible_locations_pairs = []
for r1, r2 in itertools.combinations(self.locations, 2):
if not self.compatibility_condition(r1, r2, problem):
incompatible_locations_pairs.append((r1, r2))
all_locations_with_incompatibility = [
region
for incompatibles_pair in incompatible_locations_pairs
for region in incompatibles_pair
]
counter = Counter(all_locations_with_incompatibility)
all_locations_with_incompatibility = sorted(
list(set(all_locations_with_incompatibility)), key=counter.get
)
score = -len(incompatible_locations_pairs)
if score == 0:
message = "All compatible !"
else:
message = "Found the following incompatibilities: %s" % (
incompatible_locations_pairs
)
return SpecEvaluation(
self,
problem,
score=score,
locations=all_locations_with_incompatibility,
message=message,
)
def localized(self, location, problem=None):
if any(location.overlap_region(rl) for rl in self.locations):
return self
else:
return None # (parent=self)
def __repr__(self):
return "CompatRegions(%s%s...)" % (self.condition_label, self.locations[0])
def __str__(self):
return "CompatRegions(%s%s...)" % (self.condition_label, self.locations[0])
def label_parameters(self):
params = [("locations", ", ".join([str(l) for l in self.locations]))]
if self.condition_label not in [None, ""]:
params.append(("condition", self.condition_label))
return params
DnaChisel-3.2.16/dnachisel/builtin_specifications/EnforceSequence.py 0000664 0000000 0000000 00000011023 15007617443 0025550 0 ustar 00root root 0000000 0000000 """Implement EnforceSequence (DO NOT USE YET: Work in progress, stabilizing)"""
# TODO: factorize with self.sequence ?
import numpy as np
from ..Specification import Specification, SpecEvaluation
from ..Location import Location
from ..biotools import group_nearby_indices, reverse_complement, IUPAC_NOTATION
class EnforceSequence(Specification):
"""Enforces a (possibly degenerate) sequence at some location.
Shorthand for annotations: "sequence".
Parameters
----------
sequence
An ATGC string representing the wanted sequence, possibly degenerated,
for instance ATTCGCGTYTTKWNAA
location
Location of the DNA segment on which to enforce the pattern e.g.
``Location(10, 45, 1)`` or simply ``(10, 45, 1)``
"""
localization_interval_length = 6 # used when optimizing
best_possible_score = 0
enforced_by_nucleotide_restrictions = True
shorthand_name = "sequence"
def __init__(self, sequence=None, location=None, boost=1.0):
"""Initialize."""
self.sequence = sequence
self.location = Location.from_data(location)
self.boost = boost
def initialized_on_problem(self, problem, role):
"""Find out what sequence it is that we are supposed to conserve."""
return self._copy_with_full_span_if_no_location(problem)
# if self.location is None:
# result = self.copy_with_changes()
# result.location = Location(0, len(problem.sequence), 1)
# return result
# else:
# return self
def evaluate(self, problem):
"""Return a score equal to -number_of modifications.
Locations are "binned" modifications regions. Each bin has a length
in nucleotides equal to ``localization_interval_length`.`
"""
sequence = self.location.extract_sequence(problem.sequence)
discrepancies = np.array(
[
i
for i, nuc in enumerate(sequence)
if nuc not in IUPAC_NOTATION[self.sequence[i]]
]
)
if self.location.strand == -1:
discrepancies = self.location.end - discrepancies
else:
discrepancies = discrepancies + self.location.start
intervals = [
(r[0], r[-1] + 1)
for r in group_nearby_indices(
discrepancies, max_group_spread=self.localization_interval_length
)
]
locations = [Location(start, end, 1) for start, end in intervals]
return SpecEvaluation(
self, problem, score=-len(discrepancies), locations=locations
)
def localized(self, location, problem=None):
"""Localize the spec to the overlap of its location and the new."""
start, end = location.start, location.end
new_location = self.location.overlap_region(location)
if new_location is None:
return None
else:
if self.location.strand == -1:
start = self.location.end - new_location.end
end = self.location.end - new_location.start
else:
start = new_location.start - self.location.start
end = new_location.end - self.location.start
new_sequence = self.sequence[start:end]
return self.copy_with_changes(location=new_location, sequence=new_sequence)
def restrict_nucleotides(self, sequence, location=None):
"""When localizing, forbid any nucleotide but the one already there."""
if location is not None:
new_location = self.location.overlap_region(location)
if new_location is None:
return []
else:
new_location = self.location
start, end = new_location.start, new_location.end
if self.location.strand == -1:
lend = self.location.end
return [
(
i,
set(
reverse_complement(n)
for n in IUPAC_NOTATION[self.sequence[lend - i - 1]]
),
)
for i in range(start, end)
]
else:
lstart = self.location.start
return [
(i, IUPAC_NOTATION[self.sequence[i - lstart]])
for i in range(start, end)
]
def __repr__(self):
"""Represent."""
return "EnforceSequence(%s)" % str(self.location)
def __str__(self):
"""Represent."""
return "EnforceSequence(%s)" % str(self.location)
DnaChisel-3.2.16/dnachisel/builtin_specifications/EnforceTerminalGCContent.py 0000664 0000000 0000000 00000002166 15007617443 0027330 0 ustar 00root root 0000000 0000000 """Implement EnforceTerminalGCContent"""
from .TerminalSpecification import TerminalSpecification
from ..biotools import gc_content
class EnforceTerminalGCContent(TerminalSpecification):
"""Enforce bounds for the GC content at the sequence's terminal ends.
Parameters
----------
window_size
Size in basepair of the two terminal ends to consider
mini
A float between 0 and 1, minimal proportion of GC that the ends should
contain
maxi
Float between 0 and 1, maximal proportion of GC that the ends should
contain
boost
Multiplicatory factor applied to this specification.
"""
def __init__(self, window_size, mini=0, maxi=1, boost=1.0):
self.mini = mini
self.maxi = maxi
self.window_size = window_size
self.boost = boost
def evaluate_end(self, sequence):
gc = gc_content(sequence)
return -(max(0, self.mini - gc) + max(0, gc - self.maxi))
def __repr__(self):
return "Terminal(%.02f < gc < %.02f, window: %d)" % (
self.mini,
self.maxi,
self.window_size,
)
DnaChisel-3.2.16/dnachisel/builtin_specifications/EnforceTranslation.py 0000664 0000000 0000000 00000021312 15007617443 0026300 0 ustar 00root root 0000000 0000000 "Implement EnforceTranslation."
from ..Specification import SpecEvaluation
from ..biotools import (
translate,
reverse_complement,
get_backtranslation_table,
)
from ..Location import Location
from .CodonSpecification import CodonSpecification
class EnforceTranslation(CodonSpecification):
"""Enforce a specific amino-acid sequence translation.
This class enforces the standard translation, but it is also possible to
change the class' `codons_sequences` and `codons_translations`
dictionaries for more exotic kind of translations
Shorthand for annotations: "cds".
Note: always use a codon optimisation specification with EnforceTranslation.
Parameters
-----------
location
Either a DnaChisel Location or a tuple of the form (start, end, strand)
or just (start, end), with strand defaulting to +1, indicating the
position of the gene to codon-optimize. If not provided, the whole
sequence is considered as the gene. The location should have a length
that is a multiple of 3. The location strand is either 1 if the gene is
encoded on the (+) strand, or -1 for antisense.
genetic_table
Either "Standard", "Bacterial", or any other Biopython genetic table name
(see dnachisel.biotools.CODON_TABLE_NAMES for a list of accepted names).
start_codon
Signals that the first codon is a start codon and provides a policy for
changing it. It is very important that this parameter be set when dealing
with full coding sequences in organisms with different start codons, e.g.
GTG in bacteria. If it is not set, then a GTG (start codon or Valine)
could be changed into GTA (also valine but NOT a start codon).
Can be False (the first codon is not considered a start codon, but can
still naturally code for methionine), ``keep`` (freezes the original
sub-sequence at this location, or a codon e.g. ``ATG`` or ``GTG`` or
``["ATG", "GTG"]``.
translation
String representing the protein sequence that the DNA segment should
translate to, eg. "MKY...LL*" ("*" stands for stop codon).
Can be omitted if the sequence initially encodes the right sequence.
boost
Score multiplicator (=weight) for when the specification is used as an
optimization objective alongside competing objectives.
"""
best_possible_score = 0
enforced_by_nucleotide_restrictions = True
shorthand_name = "cds"
default_genetic_table = "Standard"
def __init__(
self,
genetic_table="default",
start_codon=None,
translation=None,
location=None,
boost=1.0,
):
"""Initialize."""
self.translation = translation
location = Location.from_data(location)
if (location is not None) and (location.strand not in [-1, 1]):
location = Location(location.start, location.end, 1)
self.set_location(location)
self.start_codon = start_codon
self.boost = boost
if genetic_table == "default":
genetic_table = self.default_genetic_table
self.genetic_table = genetic_table
self.initialize_translation_from_problem = translation is None
self.initialize_location_from_problem = location is None
self.backtranslation_table = get_backtranslation_table(genetic_table)
def set_location(self, location):
"""Check that the location length is valid before setting it."""
if location is not None:
if len(location) % 3:
raise ValueError(
"Location length in Codon Specifications should be a 3x. "
"Location %s has length %d" % (location, len(location))
)
if (self.translation is not None) and (
len(location) != 3 * len(self.translation)
):
raise ValueError(
("Window size (%d bp) incompatible with translation " "(%d aa)")
% (len(location), len(self.translation))
)
self.location = location
def initialized_on_problem(self, problem, role):
"""Get translation from the sequence if it is not already set."""
result = self._copy_with_full_span_if_no_location(problem)
if result.translation is None:
subsequence = result.location.extract_sequence(problem.sequence)
translation = translate(
subsequence,
table=result.genetic_table,
assume_start_codon=result.start_codon is not None,
)
result = result.copy_with_changes(translation=translation)
if len(result.location) != 3 * len(result.translation):
raise ValueError(
("Window size (%d bp) incompatible with translation " "(%d aa)")
% (len(result.location), len(result.translation))
)
if (result.start_codon is not None) and result.translation[0] != "M":
raise ValueError(
(
"Spec. %s specificies a start_codon parameter, but the "
"translation at this location does not start with Met. "
"Maybe you should provide a 'translation' parameter or "
"set up a different Genetic Code."
)
% result.label(use_short_form=True)
)
return result
def evaluate(self, problem):
"""Score is the number of wrong-translation codons."""
# Note: this method is actually very little used as this specification
# class sets the enforced_by_nucleotide_restrictions attribute.
location = (
self.location
if self.location is not None
else Location(0, len(problem.sequence), 1)
)
subsequence = location.extract_sequence(problem.sequence)
translation = translate(
subsequence,
table=self.genetic_table,
assume_start_codon=self.start_codon is not None,
)
errors_locations = [
self.codon_index_to_location(index)
for (index, amino_acid) in enumerate(translation)
if amino_acid != self.translation[index]
]
return SpecEvaluation(
self,
problem,
score=-len(errors_locations),
locations=errors_locations,
message=(
"All OK."
if len(errors_locations) == 0
else "Wrong translation at locations %s" % errors_locations
),
)
def localized_on_window(self, new_location, start_codon, end_codon):
new_translation = self.translation[start_codon:end_codon]
if self.location.strand == -1:
location_is_at_start = new_location.end >= self.location.end
else:
location_is_at_start = new_location.start <= self.location.start
return self.__class__(
location=new_location,
translation=new_translation,
boost=self.boost,
genetic_table=self.genetic_table,
start_codon=self.start_codon if location_is_at_start else None,
# has_start_codon=self.has_start_codon and location_is_at_start,
)
def restrict_nucleotides(self, sequence, location=None):
if self.backtranslation_table is None:
return []
def get_first_codon_choices(first_codon):
if self.start_codon is None:
return self.backtranslation_table[self.translation[0]]
elif isinstance(self.start_codon, (list, tuple)):
return list(self.start_codon)
elif self.start_codon == "keep":
return [first_codon]
else:
return [self.start_codon] # "ATG"
first_codon_location = self.codon_index_to_location(0)
first_codon = first_codon_location.extract_sequence(sequence)
choices = [(first_codon_location, get_first_codon_choices(first_codon))] + [
(self.codon_index_to_location(i), self.backtranslation_table[aa])
for i, aa in list(enumerate(self.translation))[1:]
]
def standardize_choice(choice):
location, choices_list = choice
if location.strand == -1:
choices_list = [reverse_complement(c) for c in choices_list]
return (location.to_tuple()[:2], choices_list)
return sorted([standardize_choice(choice) for choice in choices])
def __repr__(self):
return "EnforceTranslation(%s)" % str(self.location)
def __str__(self):
return "EnforceTranslation(%s)" % str(self.location)
def short_label(self):
return "cds"
def breach_label(self):
return "protein sequence changed"
DnaChisel-3.2.16/dnachisel/builtin_specifications/SequenceLengthBounds.py 0000664 0000000 0000000 00000002247 15007617443 0026573 0 ustar 00root root 0000000 0000000 """Implement SequenceLengthBounds."""
from ..Specification import Specification, SpecEvaluation
class SequenceLengthBounds(Specification):
"""Checks that the sequence length is between bounds.
Quite an uncommon specification as it can't really be solved or optimized.
But practical as part of a list of constraints to verify.
Parameters
----------
min_length
Minimal allowed sequence length in nucleotides
max_length
Maximal allowed sequence length in nucleotides. None means no bound.
"""
best_possible_score = 0
def __init__(self, min_length=0, max_length=None, boost=1.0):
self.min_length = min_length
self.max_length = max_length
self.boost = boost
def evaluate(self, problem):
"""Return 0 if the sequence length is between the bounds, else -1"""
L, mini, maxi = len(problem.sequence), self.min_length, self.max_length
if maxi is None:
score = L >= mini
else:
score = mini <= L <= maxi
return SpecEvaluation(self, problem, score - 1)
def __repr__(self):
return "Length(%d < L < %d)" % (self.min_length, self.max_length)
DnaChisel-3.2.16/dnachisel/builtin_specifications/TerminalSpecification.py 0000664 0000000 0000000 00000004170 15007617443 0026757 0 ustar 00root root 0000000 0000000 """Implements TerminalSpecification."""
from ..Location import Location
from ..Specification import Specification, SpecEvaluation
class TerminalSpecification(Specification):
"""Specifications that apply in the same way to both ends of the sequence.
These are particularly useful for modeling constraints from providers
who have terminal-ends constraints.
Subclasses of these specifications should have a `location_size` and a
`evaluate_end` method"""
def evaluate(self, problem):
"""Apply method ``evaluate_end`` to both sides and compile results."""
sequence = problem.sequence
ends_evaluations = [
self.evaluate_end(location.extract_sequence(sequence))
for location in self.ends_locations
]
locations = [
location
for (score, location) in zip(ends_evaluations, self.ends_locations)
if score < 0
]
score = sum(ends_evaluations)
if locations == []:
message = "Passed (no breach at the ends)"
else:
message = "Failed: breaches at ends %s" % str(locations)
return SpecEvaluation(
self, problem, score=score, locations=locations, message=message
)
def initialized_on_problem(self, problem, role):
"""Find out what sequence it is that we are supposed to conserve."""
if not hasattr(self, "ends_locations") or self.ends_locations is None:
L = len(problem.sequence)
wsize = self.window_size
ends_locations = [Location(0, wsize), Location(L - wsize, L)]
return self.copy_with_changes(ends_locations=ends_locations)
else:
return self
def localized(self, location, problem=None):
new_end_locations = []
for end_location in self.ends_locations:
overlap = location.overlap_region(end_location)
if overlap is not None:
new_end_locations.append(end_location)
if len(new_end_locations) == 2:
return self
else:
return self.copy_with_changes(ends_locations=new_end_locations)
DnaChisel-3.2.16/dnachisel/builtin_specifications/UniquifyAllKmers.py 0000664 0000000 0000000 00000025317 15007617443 0025755 0 ustar 00root root 0000000 0000000 """Implement UniquifyAllKmers(Specification)"""
from collections import defaultdict
from ..Specification import Specification
# from .VoidSpecification import VoidSpecification
from ..Specification.SpecEvaluation import SpecEvaluation
from ..biotools import reverse_complement
from ..Location import Location
from functools import lru_cache
def get_kmer_extractor(sequence, include_reverse_complement=True, k=1):
"""Return a function (i => standardized_kmer_string)."""
if include_reverse_complement:
rev_comp_sequence = reverse_complement(sequence)
L = len(sequence)
def extract_kmer(i):
subsequence = sequence[i : i + k]
rev_comp = rev_comp_sequence[L - i - k : L - i]
return min(subsequence, rev_comp)
else:
def extract_kmer(i):
return sequence[i : i + k]
return extract_kmer
@lru_cache(maxsize=1)
def get_kmer_extractor_cached(sequence, include_reverse_complement=True, k=1):
"""Kmer extractor with memoization.
This globally cached method enables much faster computations when
several UniquifyAllKmers functions with equal k are used.
"""
L = len(sequence)
if include_reverse_complement:
rev_comp_sequence = reverse_complement(sequence)
@lru_cache(maxsize=L)
def extract_kmer(i):
subsequence = sequence[i : i + k]
rev_comp = rev_comp_sequence[L - i - k : L - i]
return min(subsequence, rev_comp)
else:
@lru_cache(maxsize=L)
def extract_kmer(i):
return sequence[i : i + k]
return extract_kmer
class UniquifyAllKmers(Specification):
"""Avoid sub-sequence of length k with homologies elsewhere.
NOTE: For sequences with subsequences appearing more than 2 times, the
specification may not work as a problem constraint, but will work as a
problem optimization objective.
You can define a location L and an reference L* (by default they
are both the full sequence)
>>> [=== L ===]
>>>
>>> [=========== L* ==========]
>>>
>>> --------- Sequence --------------------------
This Specification class specifies that "No sub-sequence in L of length
above k has more than 1 occurence in L*".
Some specific cases
- L = L* = Sequence. In this case the full sequence will have only unique
kmers above a certain size (no self-homology).
- L < L*, L* = Sequence. The segment L will have no self-homology and no
homology to the rest of the sequence above a certain size. But there
can be self-homologies elsewhere in the sequence.
- L = L*. segment L will have no self-homology.
Parameters
----------
k
Minimal length of sequences to be considered repeats
reference
The default None indicates that the specification's location should have
no homologies anywhere in the whole sequence. If reference="here", then
the specification's location should have no homology inside that same
location. Reference can also be any location of the sequence that the
specification's location should have no homologies with.
location
Segment of the sequence in which to look for repeats. If None, repeats
are searched in the full sequence.
include_reverse_complement
If True, the sequence repeats are also searched for in the reverse
complement of the sequence (or sub sequence if `location` is not None).
Examples
--------
>>> from dnachisel import *
>>> sequence = random_dna_sequence(50000)
>>> constraint= UniquifyAllKmers(10, include_reverse_complement=True)
>>> problem = DnaOptimizationProblem(sequence, constraints= [constraint])
>>> print (problem.constraints_summary())
"""
best_possible_score = 0
use_cache = True
shorthand_name = "all_unique_kmers"
def __init__(
self,
k,
reference=None,
location=None,
include_reverse_complement=True,
boost=1.0,
localization_data=None,
):
"""Initialize."""
self.k = k
self.location = Location.from_data(location)
if reference in ["here", "same"]:
reference = location
if isinstance(reference, tuple):
reference = Location.from_tuple(reference)
self.reference = reference
self.include_reverse_complement = include_reverse_complement
self.boost = boost
self.localization_data = localization_data
def initialized_on_problem(self, problem, role="constraint"):
"""Location is the full sequence by default."""
def location_or_default(location):
default = Location(0, len(problem.sequence), 1)
return default if location is None else location
location = location_or_default(self.location)
reference = location_or_default(self.reference)
return self.copy_with_changes(location=location, reference=reference)
def evaluate(self, problem):
"""Return 0 if the sequence has no repeats, else -number_of_repeats."""
if self.localization_data is not None:
return self.local_evaluation(problem)
else:
return self.global_evaluation(problem)
def local_evaluation(self, problem):
extract_kmer = self.get_kmer_extractor(problem.sequence)
variable_kmers = {}
for label in ("location", "extended"):
variable_kmers[label] = d = {}
for i in self.localization_data[label]["changing_indices"]:
kmer = extract_kmer(i)
if kmer not in d:
d[kmer] = [i]
else:
d[kmer].append(i)
nonunique_locations = []
for kmer, indices in variable_kmers["location"].items():
if len(indices) > 1:
nonunique_locations += indices
location_variable_kmers = set(variable_kmers["location"].keys())
extended_variable_kmers = set(variable_kmers["extended"].keys())
fixed_location_kmers = self.localization_data["location"]["fixed_kmers"]
extended_fixed_kmers = self.localization_data["extended"]["fixed_kmers"]
for c in [
extended_variable_kmers,
fixed_location_kmers,
extended_fixed_kmers,
]:
nonunique_locations += [
i
for kmer in location_variable_kmers.intersection(c)
for i in variable_kmers["location"][kmer]
]
for c in [location_variable_kmers, fixed_location_kmers]:
nonunique_locations += [
i
for kmer in extended_variable_kmers.intersection(c)
for i in variable_kmers["extended"][kmer]
]
nonunique_locations = [Location(i, i + self.k) for i in nonunique_locations]
return SpecEvaluation(
self,
problem,
score=-len(nonunique_locations),
locations=nonunique_locations,
message="Failed, the following positions are the first occurences"
"of local non-unique segments %s" % nonunique_locations,
)
def get_kmer_extractor(self, sequence):
if self.use_cache:
getter = get_kmer_extractor_cached
else:
getter = get_kmer_extractor
return getter(
sequence,
k=self.k,
include_reverse_complement=self.include_reverse_complement,
)
def global_evaluation(self, problem):
extract_kmer = self.get_kmer_extractor(problem.sequence)
kmers_locations = defaultdict(lambda: [])
start, end = self.reference.start, self.reference.end
for i in range(start, end - self.k + 1):
location = (i, i + self.k)
kmer_sequence = extract_kmer(i)
kmers_locations[kmer_sequence].append(location)
locations = sorted(
[
Location(start_, end_)
for locations_list in kmers_locations.values()
for start_, end_ in locations_list
if len(locations_list) > 1
and (self.location.start <= start_ < end_ <= self.location.end)
],
key=lambda l: l.start,
)
if locations == []:
return SpecEvaluation(
self,
problem,
score=0,
locations=[],
message="Passed: no nonunique %d-mer found." % self.k,
)
return SpecEvaluation(
self,
problem,
score=-len(locations),
locations=locations,
message="Failed, the following positions are the first occurences "
"of non-unique segments %s" % locations,
)
def localized(self, location, problem=None, with_righthand=True):
"""Localize the evaluation."""
if location.overlap_region(self.reference) is None:
return None
if problem is None:
return self
extract_kmer = self.get_kmer_extractor(problem.sequence)
k = self.k
reference = location.extended(k - 1, right=with_righthand)
changing_kmers_zone = reference.overlap_region(self.reference)
changing_kmer_indices = set(changing_kmers_zone.indices[: -k + 1])
localization_data = {}
for loc, label in [
(self.location, "location"),
(self.reference, "extended"),
]:
kmer_indices = set(loc.indices[: -self.k])
fixed_kmer_indices = kmer_indices.difference(changing_kmer_indices)
fixed_kmers = set([extract_kmer(i) for i in fixed_kmer_indices])
changing_inds = kmer_indices.intersection(changing_kmer_indices)
localization_data[label] = {
"fixed_kmers": fixed_kmers,
"changing_indices": changing_inds,
}
localization_data["extended"]["changing_indices"].difference_update(
localization_data["location"]["changing_indices"]
)
return self.copy_with_changes(
localization_data=localization_data, location=changing_kmers_zone
)
def shifted(self, shift):
"""Shift the location of the specification.
This will also shift the reference.
"""
new_location = None if self.location is None else self.location + shift
reference = None if self.reference is None else self.reference + shift
return self.copy_with_changes(
location=new_location,
reference=reference,
derived_from=self,
)
def label_parameters(self):
return [("k", str(self.k))]
def short_label(self):
return "All %dbp unique" % self.k
def breach_label(self):
return "%dbp homologies" % self.k
DnaChisel-3.2.16/dnachisel/builtin_specifications/__init__.py 0000664 0000000 0000000 00000005302 15007617443 0024240 0 ustar 00root root 0000000 0000000 """Built-in genetic specifications."""
from .AllowPrimer import AllowPrimer
from .AvoidBlastMatches import AvoidBlastMatches
from .AvoidChanges import AvoidChanges
from .AvoidHairpins import AvoidHairpins
from .AvoidHeterodimerization import AvoidHeterodimerization
from .AvoidMatches import AvoidMatches
from .UniquifyAllKmers import UniquifyAllKmers
from .AvoidPattern import AvoidPattern
from .AvoidStopCodons import AvoidStopCodons
from .EnforceChanges import EnforceChanges
from .EnforceChoice import EnforceChoice
from .EnforceSequence import EnforceSequence
from .EnforceGCContent import EnforceGCContent
from .EnforceMeltingTemperature import EnforceMeltingTemperature
from .EnforcePatternOccurence import EnforcePatternOccurence
from .EnforceTranslation import EnforceTranslation
from .EnforceRegionsCompatibility import EnforceRegionsCompatibility
from .EnforceTerminalGCContent import EnforceTerminalGCContent
from .SequenceLengthBounds import SequenceLengthBounds
from .codon_optimization import (
AvoidRareCodons,
CodonOptimize,
HarmonizeRCA,
MaximizeCAI,
MatchTargetCodonUsage,
)
DEFAULT_SPECIFICATIONS_DICT = {
"AvoidRareCodons": AvoidRareCodons,
"AllowPrimer": AllowPrimer,
"AvoidBlastMatches": AvoidBlastMatches,
"AvoidChanges": AvoidChanges,
"AvoidHairpins": AvoidHairpins,
"UniquifyAllKmers": UniquifyAllKmers,
"AvoidPattern": AvoidPattern,
"CodonOptimize": CodonOptimize,
"EnforceGCContent": EnforceGCContent,
"EnforceMeltingTemperature": EnforceMeltingTemperature,
"EnforcePatternOccurence": EnforcePatternOccurence,
"EnforceTranslation": EnforceTranslation,
"EnforceRegionsCompatibility": EnforceRegionsCompatibility,
"EnforceSequence": EnforceSequence,
"EnforceChoice": EnforceChoice,
"EnforceChanges": EnforceChanges,
"HarmonizeRCA": HarmonizeRCA,
"MaximizeCAI": MaximizeCAI,
"MatchTargetCodonUsage": MatchTargetCodonUsage,
}
# Add the shorthands to the specifications dict.
for spec in list(DEFAULT_SPECIFICATIONS_DICT.values()):
if spec.__dict__.get("shorthand_name", None) is not None:
DEFAULT_SPECIFICATIONS_DICT[spec.shorthand_name] = spec
__all__ = [
"AllowPrimer",
"AvoidBlastMatches",
"AvoidChanges",
"AvoidHairpins",
"AvoidHeterodimerization",
"AvoidMatches",
"UniquifyAllKmers",
"AvoidPattern",
"AvoidStopCodons",
"CodonOptimize",
"EnforceChanges",
"EnforceChoice",
"EnforceSequence",
"EnforceGCContent",
"EnforceMeltingTemperature",
"EnforcePatternOccurence",
"EnforceTranslation",
"EnforceRegionsCompatibility",
"EnforceTerminalGCContent",
"EnforceSequence",
"SequenceLengthBounds",
"DEFAULT_SPECIFICATIONS_DICT",
]
DnaChisel-3.2.16/dnachisel/builtin_specifications/codon_optimization/ 0000775 0000000 0000000 00000000000 15007617443 0026037 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/builtin_specifications/codon_optimization/AvoidRareCodons.py 0000664 0000000 0000000 00000011620 15007617443 0031433 0 ustar 00root root 0000000 0000000 "Implement AvoidRareCodons."
from ...Specification import SpecEvaluation
from ...biotools import reverse_complement
from .BaseCodonOptimizationClass import BaseCodonOptimizationClass
class AvoidRareCodons(BaseCodonOptimizationClass):
"""Avoid the use of codons with low frequency.
This can be seen as a "mild" form of codon optimization where only rare
codons (which slow down protein synthesis) are considered.
Warning: always use this specification with an EnforceTranslation constraint
defined over the same location, to preserve the amino acid sequence.
Shorthand for annotations: "no_rare_codons".
Parameters
-----------
min_frequency
Minimal frequency accepted for a given codon.
species
Name or TaxID of the species for which to optimize the sequence. A custom
codon_usage_table can be provided instead (or in addition, for species
names whose codon usage table cannot be imported).
codon_usage_table
Optional codon usage table of the species for which the sequence will be
codon-optimized, which can be provided instead of ``species``. A dict of
the form ``{'*': {"TGA": 0.112, "TAA": 0.68}, 'K': ...}`` giving the codon
frequency table (relative usage of each codon; frequencies add up to 1,
separately for each amino acid). See parameter ``species`` above.
location
Either a DnaChisel Location or a tuple of the form (start, end, strand)
or just (start, end), with strand defaulting to +1, indicating the
position of the gene to codon-optimize. If not provided, the whole
sequence is considered as the gene. The location should have a length
that is a multiple of 3. The location strand is either 1 if the gene is
encoded on the (+) strand, or -1 for antisense.
boost
Score multiplicator (=weight) for when the specification is used as an
optimization objective alongside competing objectives.
"""
best_possible_score = 0
enforced_by_nucleotide_restrictions = True
shorthand_name = "no_rare_codons"
def __init__(
self,
min_frequency,
species=None,
codon_usage_table=None,
location=None,
boost=1.0,
):
"""Initialize."""
BaseCodonOptimizationClass.__init__(
self,
species=species,
codon_usage_table=codon_usage_table,
location=location,
boost=boost,
)
self.min_frequency = min_frequency
self.codons_frequencies = {
codon: freq
for aa, aa_data in self.codon_usage_table.items()
if len(aa) == 1
for codon, freq in aa_data.items()
}
self.rare_codons = sorted(
[
codon
for codon, frequency in self.codons_frequencies.items()
if frequency < min_frequency
]
)
self.nonrare_codons = sorted(
[
codon
for codon, frequency in self.codons_frequencies.items()
if frequency >= min_frequency
]
)
def evaluate(self, problem):
"""Score is the sum of (freq - min_frequency) for all rare codons."""
# Note: this method is actually very little used as this specification
# class sets the enforced_by_nucleotide_restrictions attribute.
codons = self.get_codons(problem)
rare_codons_indices = [
i for i, codon in enumerate(codons) if codon in self.rare_codons
]
locations = self.codons_indices_to_locations(rare_codons_indices)
score = (
0
if (len(locations) == 0)
else sum(
(self.codons_frequencies[codons[i]] - self.min_frequency)
for i in rare_codons_indices
)
)
return SpecEvaluation(
self,
problem,
score=score,
locations=locations,
message=(
"All OK."
if len(locations) == 0
else "Rare codons at locations %s" % locations
),
)
def restrict_nucleotides(self, sequence, location=None):
nonrare_codons = list(self.nonrare_codons)
if self.location.strand == -1:
nonrare_codons = sorted([reverse_complement(c) for c in nonrare_codons])
return [
((i, i + 3), nonrare_codons)
for i in range(self.location.start, self.location.end, 3)
]
def _params_string(self):
"""Parameters representation used in __repr__, __str__, etc."""
return "%d%%, %s" % (100 * self.min_frequency, str(self.species))
def __repr__(self):
return "AvoidRareCodons(%s)" % self._params_string()
def __str__(self):
return "AvoidRareCodons(%s)" % self._params_string()
def short_label(self):
return "no_rare_codons(%s)" % self._params_string()
DnaChisel-3.2.16/dnachisel/builtin_specifications/codon_optimization/BaseCodonOptimizationClass.py 0000664 0000000 0000000 00000006401 15007617443 0033644 0 ustar 00root root 0000000 0000000 from ..CodonSpecification import CodonSpecification
from python_codon_tables import get_codons_table
import numpy as np
from ...Location import Location
from ...biotools import group_nearby_indices
class BaseCodonOptimizationClass(CodonSpecification):
best_possible_score = 0 # Don't forget to change in subclasses if needed
localization_group_spread = 3
def __init__(self, species=None, location=None, codon_usage_table=None, boost=1.0):
self.boost = boost
self.location = Location.from_data(location)
self.species = species
self.codon_usage_table = self.get_codons_table(species, codon_usage_table)
def get_codons(self, problem):
subsequence = self.location.extract_sequence(problem.sequence)
if len(subsequence) % 3:
raise ValueError(
"Spec. %s is on a window/sequence with size not multiple of 3)"
% (self.label())
)
return [
subsequence[3 * i : 3 * (i + 1)] for i in range(int(len(subsequence) / 3))
]
@staticmethod
def get_codons_table(species, codon_usage_table):
if codon_usage_table is None:
if species is None:
raise ValueError(
"Provide either an species name or a codon usage table"
)
else:
codon_usage_table = get_codons_table(species)
return codon_usage_table
def initialized_on_problem(self, problem, role):
"""Get location from sequence if no location provided."""
return self._copy_with_full_span_if_no_location(problem)
def codons_indices_to_locations(self, indices):
"""Convert a list of codon positions to a list of Locations"""
indices = np.array(indices)
if self.location.strand == -1:
indices = sorted(self.location.end - 3 * indices)
return [
Location(group[0] - 3, group[-1], strand=-1)
for group in group_nearby_indices(
indices, max_group_spread=self.localization_group_spread
)
]
else:
indices = self.location.start + 3 * indices
return [
Location(group[0], group[-1] + 3)
for group in group_nearby_indices(
indices, max_group_spread=self.localization_group_spread
)
]
def get_codons_synonyms(self):
"""Return a dict {"GTG": [GTG, GTC, ...]} of synonymous codons."""
return {
codon: [c for c in aa_codons]
for aa, aa_codons in self.codon_usage_table.items()
if len(aa) == 1
for codon in aa_codons
}
def get_codons_translations(self):
"""Return a dict {"ATG": "M", "TAG": "*", ...}."""
return {
codon: aa
for aa, aa_codons in self.codon_usage_table.items()
if len(aa) == 1
for codon in aa_codons.keys()
}
def localized_on_window(self, new_location, start_codon, end_codon):
"""Relocate without changing much."""
# The "new_location" already has exactly the right span and strand
# thanks to superclass CodonSpecification
return self.copy_with_changes(location=new_location)
DnaChisel-3.2.16/dnachisel/builtin_specifications/codon_optimization/CodonOptimize.py 0000664 0000000 0000000 00000011066 15007617443 0031200 0 ustar 00root root 0000000 0000000 from .HarmonizeRCA import HarmonizeRCA
from .MatchTargetCodonUsage import MatchTargetCodonUsage
from .MaximizeCAI import MaximizeCAI
def CodonOptimize(
species=None,
method="use_best_codon",
location=None,
codon_usage_table=None,
original_species=None,
original_codon_usage_table=None,
boost=1.0,
):
"""Codon-optimize a coding sequence using a user-selected method.
This pseudo-specification is actually a function which returns an instance
of another specification class depending on the selected "method":
- For method="use_best_codon", every codon will be replaced by the "best"
(i.e. most frequent) synonymous codon in the target organism. This is
equivalent to Codon Adaptation Index (CAI) optimization.
- For method="match_codon_usage", the final sequence's codon usage will
match as much as possible the codon usage profile of the target species
(this method is used throughout the literature, see for instance Hale
and Thomson 1998).
- For method="harmonize_rca", Each codon will be replaced by a synonymous
codon whose usage in the target organism matches the usage of the
original codon in its host organism (as per Claassens 2017).
Warning: always use this specification with an EnforceTranslation constraint
defined over the same location, to preserve the amino acid sequence.
Parameters
==========
species
Species for which the sequence will be codon-optimized.
Either a TaxID (this requires a web connection as the corresponding table
will be downloaded from the internet) or the name of the species to
codon-optimize for (the name must be supported by ``python_codon_tables``
e.g. ``e_coli``, ``s_cerevisiae``, ``h_sapiens``, ``c_elegans``,
``b_subtilis``, ``d_melanogaster``).
Note that a ``codon_usage_table`` can be provided instead, or even in
addition, for species whose codon usage table cannot be auto-imported.
method
Either 'use_best_codon', 'match_codon_usage', or 'harmonize_rca'
(see above for details)
location
Either a DnaChisel Location or a tuple of the form (start, end, strand)
or just (start, end), with strand defaulting to +1, indicating the
position of the gene to codon-optimize. If not provided, the whole
sequence is considered as the gene. The location should have a length
that is a multiple of 3. The location strand is either 1 if the gene is
encoded on the (+) strand, or -1 for antisense.
codon_usage_table
Optional codon usage table of the species for which the sequence will be
codon-optimized, which can be provided instead of ``species``. A dict of
the form ``{'*': {"TGA": 0.112, "TAA": 0.68}, 'K': ...}`` giving the codon
frequency table (relative usage of each codon; frequencies add up to 1,
separately for each amino acid). See parameter ``species`` above.
original_species
When the method is 'harmonize_rca', this is the native species of the
original coding sequence. Same characteristics as parameter ``species``
above.
original_codon_usage_table
Optional codon usage table of the original sequence's native species.
A dict of the form ``{'*': {"TGA": 0.112, "TAA": 0.68}, 'K': ...}``
giving the codon usage table.
References
==========
Claassens et. al., Improving heterologous membrane protein
production in Escherichia coli by combining transcriptional tuning and
codon usage algorithms. PLOS One, 2017
Hale and Thompson, Codon Optimization of the Gene Encoding a
Domain from Human Type 1 Neurofibromin Protein... Protein Expression and
Purification 1998.
"""
if method == "use_best_codon":
return MaximizeCAI(
species=species,
location=location,
codon_usage_table=codon_usage_table,
boost=boost,
)
elif method == "match_codon_usage":
return MatchTargetCodonUsage(
species=species,
location=location,
codon_usage_table=codon_usage_table,
boost=boost,
)
elif method == "harmonize_rca":
return HarmonizeRCA(
species=species,
location=location,
codon_usage_table=codon_usage_table,
original_species=original_species,
original_codon_usage_table=original_codon_usage_table,
boost=boost,
)
raise ValueError(
"`method` must be 'use_best_codon', 'match_codon_usage' "
f"or 'harmonize_rca', not {method!r}"
)
DnaChisel-3.2.16/dnachisel/builtin_specifications/codon_optimization/HarmonizeRCA.py 0000664 0000000 0000000 00000016003 15007617443 0030673 0 ustar 00root root 0000000 0000000 import numpy as np
from ...Specification.SpecEvaluation import SpecEvaluation
from .BaseCodonOptimizationClass import BaseCodonOptimizationClass
class HarmonizeRCA(BaseCodonOptimizationClass):
"""Codon-Harmonize a native sequence for a new host (Claassens method).
This specification will optimize a Sequence 1 from Host 1 into a Sequence
2 for target Host 2.
In simple, rare Host 1 codons will be replaced by rare Host 2 codons, and
high-frequency Host 1 codons will get replaced by codons that are
high-frequency in Host 2.
In more specific, each codon along Sequence 1 gets replaced by the codon
whose Relative Codon Adaptiveness (RCA) in Host 2 is the closest from the
RCA of the original codon in Host 1. A codon's RCA in a given organism is
defined by f/fmax where f is the codon's frequency in the organism and fmax
is the highest frequency of all synonymous codons.
The minimized quantity is sum_i abs(RCA(c_i, H1) - RCA(c'_i, H2))
where c_i, c'_i represent the i-th codon before and after optimization
This method is taken from Claassens 2017, where they simplify a previous
algorithm (Angov 2008), which was much more complicated as it involved
predicting "ribosome pausing" sites in the sequence.
Warning: always use this specification with an EnforceTranslation constraint
defined over the same location, to preserve the amino acid sequence.
Parameters
----------
species
Name or TaxID of the species for which to optimize the sequence. A custom
codon_usage_table can be provided instead (or in addition, for species
names whose codon usage table cannot be imported).
codon_usage_table
Optional - can be provided instead of ``species``. A dict of the form
``{'*': {"TGA": 0.112, "TAA": 0.68}, 'K': ...}`` giving the codon
frequency table (relative usage of each codon; frequencies add up to 1,
separately for each amino acid).
original_species
Name or TaxID of the species the original sequence was taken from. This
information will be used to spot codons which are supposed to be rare
or common. A codon_usage_table can be provided instead (or in addition,
for species names whose codon usage table cannot be imported).
original_codon_usage_table
A dict of the form ``{'*': {"TGA": 0.112, "TAA": 0.68}, 'K': ...}``
giving the codon frequency table (relative usage of each codon;
frequencies add up to 1, separately for each amino acid).
location
Location on which the specification applies
boost
Score multiplicator (=weight) for when the specification is used as an
optimization objective alongside competing objectives.
References
----------
Claassens et. al., Improving heterologous membrane protein
production in Escherichia coli by combining transcriptional tuning and
codon usage algorithms. PLOS One, 2017
"""
shorthand_name = "harmonize_rca"
def __init__(
self,
species=None,
codon_usage_table=None,
original_species=None,
original_codon_usage_table=None,
location=None,
boost=1,
):
if isinstance(species, str) and "->" in species:
original_species, species = species.split("->")
species = species.strip()
original_species = original_species.strip()
BaseCodonOptimizationClass.__init__(
self,
species=species,
codon_usage_table=codon_usage_table,
location=location,
boost=boost,
)
self.codons_synonyms = self.get_codons_synonyms()
self.original_species = original_species
self.original_codon_usage_table = self.get_codons_table(
original_species, original_codon_usage_table
)
for table in [self.codon_usage_table, self.original_codon_usage_table]:
if "RCA" not in table:
table["RCA"] = {
codon: frequency / max(codons_frequencies.values())
for aa, codons_frequencies in table.items()
for codon, frequency in codons_frequencies.items()
if len(aa) == 1
}
def initialized_on_problem(self, problem, role):
new_spec = self._copy_with_full_span_if_no_location(problem)
new_spec.original_codons = new_spec.get_codons(problem)
rca = new_spec.codon_usage_table["RCA"]
rca_o = new_spec.original_codon_usage_table["RCA"]
new_spec.smallest_possible_discrepancies = [
min([abs(rca[c] - rca_o[c]) for c in self.codons_synonyms[codon]])
for codon in new_spec.original_codons
]
return new_spec
def evaluate(self, problem):
"""Return the evaluation for method=="harmonize_rca"."""
codons = self.get_codons(problem)
if len(codons) == 1:
# We are evaluating a single codon. Easy!
codon = codons[0]
original = self.original_codons[0]
rca_codon = self.codon_usage_table["RCA"][codon]
rca_original = self.original_codon_usage_table["RCA"][original]
score = -abs(rca_codon - rca_original)
return SpecEvaluation(
self,
problem,
score=score,
locations=[] if (score == 0) else [self.location],
message="Codon harmonization on window %s scored %.02E"
% (self.location, score),
)
# print (len(codons))
rca_in_original_species = [
self.original_codon_usage_table["RCA"][original_codon]
for original_codon in self.original_codons
]
rca_in_target_species = [
self.codon_usage_table["RCA"][codon] for codon in codons
]
discrepancies = abs(
np.array(rca_in_original_species) - np.array(rca_in_target_species)
)
non_optimality = self.smallest_possible_discrepancies - discrepancies
nonoptimal_indices = np.nonzero(non_optimality)[0]
locations = self.codons_indices_to_locations(nonoptimal_indices)
score = -discrepancies.sum()
return SpecEvaluation(
self,
problem,
score=score,
locations=locations,
message="Codon harmonization on %s scored %.02E" % (self.location, score),
)
def label_parameters(self):
if self.species is None:
return ["(custom table)"]
else:
return [self.original_species + " -> " + self.species]
def short_label(self):
short_label = "harmonize-rca ("
if self.species is not None:
target_species = self.species
else:
target_species = "custom table"
if self.original_species is not None:
original_species = self.original_species
else:
original_species = "custom table"
# short_label += " (%s)" % self.species
return short_label + original_species + " -> " + target_species + ")"
DnaChisel-3.2.16/dnachisel/builtin_specifications/codon_optimization/MatchTargetCodonUsage.py 0000664 0000000 0000000 00000020060 15007617443 0032562 0 ustar 00root root 0000000 0000000 import numpy as np
from ...Specification.SpecEvaluation import SpecEvaluation
from ...biotools import dict_to_pretty_string
from .BaseCodonOptimizationClass import BaseCodonOptimizationClass
class MatchTargetCodonUsage(BaseCodonOptimizationClass):
"""Codon-optimize a sequence so it has the same codon usage as a target.
The objective minimized here is the sum of the discrepancies, over every
possible triplet ATG, CCG, etc. between the codon frequency of this triplet
in the sequence, and its frequency in the target organism.
This method has had several names through the ages. It may have been first
proposed by Hale and Thompson, 1998. It is called Individual Codon Usage
Optimization in Chung 2012, Global CAI Harmonization in Mignon 2018, and
Codon Harmonization in Jayaral 2005. We didn't call it "harmonization"
in DNA Chisel to avoid any confusion with the now more common
host-to-target codon harmonization. See DnaChisel's HarmonizeRCA class
for Codon Harmonization.
Warning: always use this specification with an EnforceTranslation constraint
defined over the same location, to preserve the amino acid sequence.
Parameters
----------
species
Species for which the sequence will be codon-optimized.
Either a TaxID (this requires a web connection as the corresponding table
will be downloaded from the internet) or the name of the species to
codon-optimize for (the name must be supported by ``python_codon_tables``
e.g. ``e_coli``, ``s_cerevisiae``, ``h_sapiens``, ``c_elegans``,
``b_subtilis``, ``d_melanogaster``).
Note that a ``codon_usage_table`` can be provided instead, or even in
addition, for species whose codon usage table cannot be auto-imported.
location
Either a DnaChisel Location or a tuple of the form (start, end, strand)
or just (start, end), with strand defaulting to +1, indicating the
position of the gene to codon-optimize. If not provided, the whole
sequence is considered as the gene. The location should have a length
that is a multiple of 3. The location strand is either 1 if the gene is
encoded on the (+) strand, or -1 for antisense.
codon_usage_table
A dict of the form ``{'*': {"TGA": 0.112, "TAA": 0.68}, 'K': ...}``
giving the codon frequency table (relative usage of each codon;
frequencies add up to 1, separately for each amino acid). Only
provide if no ``species`` parameter was provided.
boost
Score multiplicator (=weight) for when the specification is used as an
optimization objective alongside competing objectives.
References
----------
Hale and Thompson, Codon Optimization of the Gene Encoding a
Domain from Human Type 1 Neurofibromin Protein... Protein Expression and
Purification 1998.
Jayaraj et. al. GeMS: an advanced software package for designing synthetic
genes, Nucleic Acids Research, 2005
Mignon et. al. Codon harmonization – going beyond the speed limit for
protein expression. FEBS Lett, 2018
Chung BK, Lee DY. Computational codon optimization of synthetic gene for
protein expression. BMC Syst Biol. 2012
"""
shorthand_name = "match_codon_usage"
def __init__(self, species=None, location=None, codon_usage_table=None, boost=1.0):
BaseCodonOptimizationClass.__init__(
self,
species=species,
location=location,
codon_usage_table=codon_usage_table,
boost=boost,
)
self.codons_translations = self.get_codons_translations()
def codon_usage_matching_stats(self, problem):
"""Return a codon harmonisation score and a suboptimal locations list.
Parameters
----------
sequence
An ATGC string
species
Any species name from the DnaChisel codon tables, such as ``e_coli``.
Returns
-------
score, list_of_over_represented_codons_positions
``score`` is a negative number equals to sum(fi - ei) where for the
i-th codon in the sequence fi is the relative frequency of this
triplet in the sequence and ei is the relative frequency in the
reference species. The ``list_of_suboptimal_codons_positions`` is
of the form [1, 4, 5, 6...] a number k in that list indicates that
the k-th codon is over-represented, and that a synonymous mutation
of this codon can improve the harmonization score.
"""
codons = self.get_codons(problem)
codons_positions, aa_comparisons = self.compare_frequencies(codons)
score = 0
nonoptimal_aa_indices = []
for aa, data in aa_comparisons.items():
total = data.pop("total")
for codon, codon_freq in data.items():
frequency_diff = codon_freq["sequence"] - codon_freq["table"]
score -= total * abs(frequency_diff)
if codon_freq["sequence"] > codon_freq["table"]:
nonoptimal_aa_indices += codons_positions[codon]
return score, nonoptimal_aa_indices
def evaluate(self, problem):
"""Evaluate on a problem"""
score, nonoptimal_indices = self.codon_usage_matching_stats(problem)
locations = self.codons_indices_to_locations(nonoptimal_indices)
np.random.shuffle(locations)
return SpecEvaluation(
self,
problem,
score=score,
locations=locations,
message="Codon opt. on window %s scored %.02E" % (self.location, score),
)
def localized_on_window(self, new_location, start_codon, end_codon):
"""Relocate without changing much."""
return self
def label_parameters(self):
return ["(custom table)" if self.species is None else self.species]
def compare_frequencies(self, codons, text_mode=False):
"""Return a dict indicating differences between codons frequencies in
the sequence and in this specifications's codons usage table.
Examples
--------
>>> codons = spec.get_codons(problem)
>>> print(spec.compare_frequencies(codons)
Returns
-------
positions, comparisons
(if text_mode = False)
a formatted print-ready string
(if text_mode = True)
>>> {
>>> "K": {
>>> "total": 6,
>>> "AAA": {
>>> "sequence": 1.0,
>>> "table": 0.7
>>> },
>>> ...
>>> },
>>> "D": ...
>>> }
"""
codons_positions = {cod: [] for cod in self.codons_translations}
for i, codon in enumerate(codons):
codons_positions[codon].append(i)
# aa: amino-acid
codons_frequencies = {aa: {"total": 0} for aa in self.codon_usage_table}
for codon, positions in codons_positions.items():
count = len(positions)
aa = self.codons_translations[codon]
codons_frequencies[aa][codon] = count
codons_frequencies[aa]["total"] += count
for aa, data in codons_frequencies.items():
total = max(1, data["total"])
for codon, value in data.items():
if codon != "total":
data[codon] = 1.0 * value / total
codons_frequencies = {
aa: data for aa, data in codons_frequencies.items() if data["total"]
}
comparisons = {
aa: {
"total": seq_data["total"],
**{
codon: {"sequence": seq_data[codon], "table": table_data}
for codon, table_data in self.codon_usage_table[aa].items()
},
}
for aa, seq_data in codons_frequencies.items()
}
if text_mode:
return dict_to_pretty_string(comparisons)
else:
return codons_positions, comparisons
def short_label(self):
result = "match-codon-usage"
if self.species is not None:
result += " (%s)" % self.species
return result
DnaChisel-3.2.16/dnachisel/builtin_specifications/codon_optimization/MaximizeCAI.py 0000664 0000000 0000000 00000012630 15007617443 0030513 0 ustar 00root root 0000000 0000000 import numpy as np
from .BaseCodonOptimizationClass import BaseCodonOptimizationClass
from ...Specification.SpecEvaluation import SpecEvaluation
class MaximizeCAI(BaseCodonOptimizationClass):
"""Codon-optimize a coding sequence for a given species. Maximizes the CAI.
To be precise, the score computed by this specification is N*log(CAI) where
N is the number of codons. Maximizing this score also maximizes the CAI.
Index (CAI). For a sequence with N codons, the CAI is the geometric mean
of the Relative Codon Adaptiveness (RCA) of the different codons. The RCA
of a codon is (f_i/fmax_i) were fi is the frequency of an oligo in the
codon usage table, and fmax is the maximal frequency of the synonymous
codons.
So N*log(CAI) = sum_i ( log(f_i) - log(fmax_i) )
This score is between -inf. and 0 (0 meaning a perfectly optimal sequence).
Warning: always use this specification with an EnforceTranslation constraint
defined over the same location, to preserve the amino acid sequence.
Parameters
----------
species
Species for which the sequence will be codon-optimized.
Either a TaxID (this requires a web connection as the corresponding table
will be downloaded from the internet) or the name of the species to
codon-optimize for (the name must be supported by ``python_codon_tables``
e.g. ``e_coli``, ``s_cerevisiae``, ``h_sapiens``, ``c_elegans``,
``b_subtilis``, ``d_melanogaster``).
Note that a ``codon_usage_table`` can be provided instead, or even in
addition, for species whose codon usage table cannot be auto-imported.
location
Either a DnaChisel Location or a tuple of the form (start, end, strand)
or just (start, end), with strand defaulting to +1, indicating the
position of the gene to codon-optimize. If not provided, the whole
sequence is considered as the gene. The location should have a length
that is a multiple of 3. The location strand is either 1 if the gene is
encoded on the (+) strand, or -1 for antisense.
codon_usage_table
A dict of the form ``{'*': {"TGA": 0.112, "TAA": 0.68}, 'K': ...}``
giving the codon frequency table (relative usage of each codon;
frequencies add up to 1, separately for each amino acid). Only
provide if no ``species`` parameter was provided.
boost
Score multiplicator (=weight) for when the specification is used as an
optimization objective alongside competing objectives.
Examples
--------
>>> objective = MaximizeCAI(
>>> species = "E. coli",
>>> location = (150, 300), # coordinates of a gene
>>> strand = -1
>>> )
"""
shorthand_name = "use_best_codon"
def __init__(self, species=None, location=None, codon_usage_table=None, boost=1.0):
BaseCodonOptimizationClass.__init__(
self,
species=species,
location=location,
codon_usage_table=codon_usage_table,
boost=boost,
)
self.codons_translations = self.get_codons_translations()
if "log_best_frequencies" not in self.codon_usage_table:
self.codon_usage_table["log_best_frequencies"] = {
aa: np.log(max(aa_data.values()))
for aa, aa_data in self.codon_usage_table.items()
if len(aa) == 1
}
if "log_codons_frequencies" not in self.codon_usage_table:
self.codon_usage_table["log_codons_frequencies"] = {
codon: np.log(frequency or 0.001)
for aa, frequencies in self.codon_usage_table.items()
for codon, frequency in frequencies.items()
if len(aa) == 1
}
def evaluate(self, problem):
"""Evaluate!"""
codons = self.get_codons(problem)
ct = self.codons_translations
if len(codons) == 1:
# We are evaluating a single codon. Easy!
codon = codons[0]
freq = self.codon_usage_table["log_codons_frequencies"][codon]
optimal = self.codon_usage_table["log_best_frequencies"][ct[codon]]
score = freq - optimal
return SpecEvaluation(
self,
problem,
score=freq - optimal,
locations=[] if (freq == optimal) else [self.location],
message="Codon opt. on window %s scored %.02E" % (self.location, score),
)
current_usage = [
self.codon_usage_table["log_codons_frequencies"][codon] for codon in codons
]
optimal_usage = [
self.codon_usage_table["log_best_frequencies"][ct[codon]]
for codon in codons
]
non_optimality = np.array(optimal_usage) - np.array(current_usage)
nonoptimal_indices = np.nonzero(non_optimality)[0]
locations = self.codons_indices_to_locations(nonoptimal_indices)
score = -non_optimality.sum()
return SpecEvaluation(
self,
problem,
score=score,
locations=locations,
message="Codon opt. on window %s scored %.02E" % (self.location, score),
)
def label_parameters(self):
return ["(custom table)" if self.species is None else self.species]
def short_label(self):
result = "best-codon-optimize"
if self.species is not None:
result += " (%s)" % self.species
return result
DnaChisel-3.2.16/dnachisel/builtin_specifications/codon_optimization/README.md 0000664 0000000 0000000 00000001616 15007617443 0027322 0 ustar 00root root 0000000 0000000 # Codon Optimization specifications
This submodule contains various classes implementing different flavours of codon
optimization that one can find in the literature.
- ``MaximizeCAI`` will (attempt to) replace every codon by the
"best synonymous codon", i.e. the most frequent in the target species.
- ``MatchTargetCodonUsage`` will optimize the sequence so it has the same codon
usage (=frequencies profile) as the target species.
- ``HarmonizeRCA`` will make so that codons that were rare in the original sequence's
"original species" will also be rare in the final sequence for the target species.
Finally, ``CodonOptimize`` is a generic pseudo-specification-class which uses a "mode"
parameter to return a specification of one of the above classes.
Warning: always use this specification with an EnforceTranslation constraint defined
over the same location, to preserve the amino acid sequence.
DnaChisel-3.2.16/dnachisel/builtin_specifications/codon_optimization/__init__.py 0000664 0000000 0000000 00000000532 15007617443 0030150 0 ustar 00root root 0000000 0000000 from .AvoidRareCodons import AvoidRareCodons
from .CodonOptimize import CodonOptimize
from .HarmonizeRCA import HarmonizeRCA
from .MaximizeCAI import MaximizeCAI
from .MatchTargetCodonUsage import MatchTargetCodonUsage
__all__ = [
"AvoidRareCodons",
"CodonOptimize",
"HarmonizeRCA",
"MaximizeCAI",
"MatchTargetCodonUsage",
]
DnaChisel-3.2.16/dnachisel/cli.py 0000775 0000000 0000000 00000005237 15007617443 0016531 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
DNA Chisel Command Line Interface
Usage:
dnachisel [--circular] [--mute] [--with_sequence_edits]
Where ``source`` is a fasta or Genbank file, and target can be one of:
- A folder name or a zip name (extension .zip). In this case a complete report
along with the sequence will be generated.
- A Genbank file. In this case, only the optimized sequence file is created.
The with_sequence_edits option specifies that edits are also annotated in the
Genbank file. Note that the filename must end with '.gb'.
Note: this CLI will be developed on a per-request basis, so don't hesitate to
ask for more handles and options on Github
(https://github.com/Edinburgh-Genome-Foundry/DnaChisel/issues)
Example to output the result to a genbank:
>>> dnachisel annotated_record.gb optimized_record.gb
Example to output the result to a folder:
>>> dnachisel annotated_record.gb optimization_report/
Example to output the result to a zip archive:
>>> dnachisel annotated_record.gb optimization_report.zip
"""
def main():
import os
from docopt import docopt
from dnachisel import DnaOptimizationProblem, CircularDnaOptimizationProblem
params = docopt(__doc__)
source, target = params[""], params[""]
# VERBOSE PRINT
def verbose_print(*args):
if not params["--mute"]:
print(*args)
# PARSE THE CIRCULARITY
if params["--circular"]:
problem_class = CircularDnaOptimizationProblem
else:
problem_class = DnaOptimizationProblem
problem = problem_class.from_record(
source, logger=None if params["--mute"] else "bar"
)
problem.max_random_iters = 10000
verbose_print(
"\n\nBefore optimization:\n\n",
problem.constraints_text_summary(),
problem.objectives_text_summary(),
)
if target.lower().endswith(".gb"): # save genbank
if params["--with_sequence_edits"]:
annotate_edits = True
else:
annotate_edits = False
verbose_print("Resolving Constraints...")
problem.resolve_constraints()
verbose_print("Optimizing...")
problem.optimize()
problem.to_record(filepath=target, with_sequence_edits=annotate_edits)
else: # save into zip or folder
project_name = os.path.basename(source)
problem.optimize_with_report(
target, project_name=project_name, file_path=source
)
verbose_print(
"\n\nAfter optimization:\n\n",
problem.constraints_text_summary(),
problem.objectives_text_summary(),
"\n\n",
"Result written to %s" % target,
)
if __name__ == "__main__":
main()
DnaChisel-3.2.16/dnachisel/reports/ 0000775 0000000 0000000 00000000000 15007617443 0017074 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/reports/README.md 0000664 0000000 0000000 00000000235 15007617443 0020353 0 ustar 00root root 0000000 0000000 # Reports module
This module is contains templates and methods for report generation.
It notably implements the end-to-end method ``optimize_with_report``.
DnaChisel-3.2.16/dnachisel/reports/SpecAnnotationsTranslator.py 0000664 0000000 0000000 00000003405 15007617443 0024632 0 ustar 00root root 0000000 0000000 from ..biotools import find_specification_label_in_feature
from .tools import install_extras_message
DFV_AVAILABLE = False
try:
from dna_features_viewer import BiopythonTranslator
DFV_AVAILABLE = True
except ImportError:
class BiopythonTranslator:
"Class unavailable. Install DNA Features Viewer."
def __init__(self):
raise ImportError(install_extras_message("DNA Features Viewer"))
class SpecAnnotationsTranslator(BiopythonTranslator):
"""Translator of DnaChisel feature-constraints for DNA Features Viewer"""
feature_prefixes_colors = {
"@": "#ce5454",
"~": "#e5be54",
"#": "#8edfff",
"!": "#fcff75",
}
def compute_filtered_features(self, features):
"""Do not display edits."""
return [
feature
for feature in features
if "".join(feature.qualifiers.get("is_edit", "false")) != "true"
]
def compute_feature_color(self, f):
color = f.qualifiers.get("color", None)
if color is not None:
if isinstance(color, list):
color = color[0]
return color
if f.type == "misc_feature":
specification = find_specification_label_in_feature(f)
if specification is not None:
return self.feature_prefixes_colors.get(specification[0], "#f4df42")
return "#eeeafa"
def compute_feature_label(self, f):
is_edit = f.qualifiers.get("is_edit", "false")
if "true" in [is_edit, is_edit[0]]:
return None
default = BiopythonTranslator.compute_feature_label(self, f)
label = None if (f.type != "misc_feature") else default
if label == "misc_feature":
label = None
return label
DnaChisel-3.2.16/dnachisel/reports/__init__.py 0000664 0000000 0000000 00000000346 15007617443 0021210 0 ustar 00root root 0000000 0000000 """Reports for DNA Chisel"""
from .optimization_reports import (
write_no_solution_report,
write_optimization_report,
plot_optimization_changes,
SpecAnnotationsTranslator,
)
from .colors_cycle import colors_cycle
DnaChisel-3.2.16/dnachisel/reports/assets/ 0000775 0000000 0000000 00000000000 15007617443 0020376 5 ustar 00root root 0000000 0000000 DnaChisel-3.2.16/dnachisel/reports/assets/logo.png 0000664 0000000 0000000 00000050041 15007617443 0022044 0 ustar 00root root 0000000 0000000 ‰PNG
IHDR ° ÷þ
¶ sBIT|dˆ pHYs Á Á…mt2 tEXtSoftware www.inkscape.org›î< IDATxœìÝwœTåÕÀñß�º�*ˆ¢ˆ ØP±ƒ
""X’X‰-X[ИÄCì
jÔE%¢–XH|%j,±E”&
Ò¶ÎÜ÷�33{çÞ;»³»wÚÎù~>£;w§<»ÌÎÜó<Ï9Ç ýØ8 ØØ
¨ ª�µÀ;À¿�¿›s5@¥”RJ)¥Tá;ø0Ó¸ü ü(ÏÉH•RJ)¥”R+üˆ’^ða½|ެ˜(¥”RJ)¥T³Ê�W±•••æYg�e¾üòËæŠ+LÓ4Í
6˜}ô‘yç�wš»ì²‹=©ÏÙO ”RJ)¥”*ð–`â”SN1WZe6%�š3gÎ4;uêd
B6ûæîGQJ)¥”RJ廉†a˜·ÜrK“�‡Ý7ß|cî°ÃÖ ä $—?�RJ)¥”R*?u6®ºêªq_~ù¥ÙµkWkrm¦T`w`’h_•Ûá(¥”RJ)U|~O,h4h�YWWת Ä4MsúôéÖ äGòkädà+’sVj��.9—RJ)¥”REå;b'äO=õT«ƒÓ”œ�XOð–ÓŸ¬Ñiº‚× gÎF§”RJ)¥T‘èKì$¼[·nfCCC›Ó4Í)S¦XOìÌéO'Æa 6ºuëfŽ;ÖÜwß}íAÈ¿s8F¥”RJ)¥ŠÂ)ÄNÀÇ�׿àÃ4MóÍ7ß´žÔ¿“ÓŸN|Dl<Çwœ¹eË–ÄX_xá3
YÇ;,§#UJ)¥”Rª¹@$rvÜqGO°_¿~Ö«ƒ�¥žJ¥”RJ)¥Ú ��pü‹P(äÉú|>ëU?ÐÛ“n£PUå,z5tèPk ¢y J)¥”Rªàøš¿IÞØÿbåÊ•ž<àúõë=y¯-[¶Œ††Çñ%K–X¯®ËÚ€”RJ)¥”òH!€,ˆñÉ'Ÿxò€ï½÷žõê?�³¥”RJ)¥ŠJPC¬úâŋۜ„~ 'X“º¯ÈéO'.ÁRíjäÈ‘æ�7Þh^|ñÅfÇŽcýðfšRJ)¥”R*¥'‰�„_xá…m
>–,Ybúýþø }Ø)§?™ ¯Ðt�-À~¹ RJ)¥”RÅäb'â~¿ß|ÿý÷[|D"ó°Ã³žÔ?ŸÓŸ*Y¸¨Ç|ÌöÉÝДRJ)¥”*>Ï;!ßvÛmÍo¾ù¦ÅÈå—_n=©¯GJðæ›ÞÀ¹Àà·Àp¤R—RJ)¥”R*‹ú LÀìÝ»·ùæ›o¦xlÚ´É<ûì³í«
WåîGQJ)¥”RJ‚€ÍX¶c�uÖYæ§Ÿ~êxlܸÑ|ðÁÍÞ½{Ûƒ�')¬RÄJ)¥”RJ4#×hƒáÀã@7ëÁ¾}û2dÈ***hhhàË/¿äÃ?dóæÍöû? œ‡$ +¥”RJ)¥Ú‘®À0àh$pØÚ£Çí¼HÓU£ì—ï€=z~¥”RJ)¥Tž à|\Tzð\ƒ‘Õ��)žË>&"ýD”RJ)¥”R9�©-Xƒ�‡�]Ò¸í·ÀiÀ\ž×ì{Þr
|
|
¬òàñ•RJ)¥”Rm�‰ dð P^^Î AƒèÑ£?üðï¿ÿ>7n´Þ'Š!330ånk¤ùbøX’ÓÑ(¥”RJ©¢àu Òø¨ èÔ©×_=§Ÿ~:‰ÕÕÕ1sæL~ó›ß°jUba¢ ^æz<¦¦TÑØ[£�±:!?CeìÿU±ïWÄ.��¼–*¤{¹W¾ ÎððñÜìü8˜ä
`��lJ)¥”RJ„wˆå\ôíÛ×\²dI“}9V¯^m4ÈžÎÐØz ÿÖ[H�+j‘2¿›�¼’M±ëµMÜϋ˷úÙãΚÃhib¥”RJ)U 'v[ZZj.X° 怫V2»víj=þU†Æ÷w ŽÌM]j�j$€Ù»Xƒk0´:C?;Àh$¸23›x 9bijcÇŽö1_“Áq(¥”RJ©"æå¬¿?¸ì²Ë¸õÖ[Ó¾ã]wÝÅE]¿úp�‡ãŠûØ1öu
²Š]ìê
Èûý†aÊÿ}u†ˆ€iøý�zŸÏg†Œ`0 ‡Â5ÁP¨6
×UVTFƒ¡P}UeUuiiymYIY]EEemYYy�ð™å¥eõsÿý¯]>üè½=êH‡÷ÎøÙÀ¢øÏÐAñøã�Ó³gO 6mÚÄÅ_̃>¿}
ÐX–�±(¥”RJ©"æe ò°
ÀÂ…Ùyç�Ó¾ãºuëèÖ
�ÁÞHNˆ—æÝ�`´ßŽ6~ØQëûôÙÑ(+-
vîÒÕß¡²CieU§`çN�áP˜²²òæÏ}è.¦Üø[ª«·€¬ŒÎÀÓèÖŸþ9;vLºA4eذa¼ñÆñCW 7g`,J)¥”Rªˆy€T *++ùé§Ÿ0Œ–=tÿþýY¼x±GÃq
ñ ‡:òß·¿¤ªªc3÷ʬú†z†°ßXžµç¼à‚¸ûî»]¿7cÆÆ�¿:‹ØŠ–RJ)¥”R^ñ*Ù8‘8^QQÑâà#~¿ÌjSMM
gœ}|" É•iwý‰µ?|ŸÕçÜzëÔMèmßë�ñÁ(¥”RJ©¢ãU Ù�ÈÖ©À÷ßOmm-ápËŠYYÊñ‚äkxºË4£‰¦ˆµµ5|øÑ{œvú1ÜïãTUfÿ\û?oÏåÎi¢¡Áñc.ÌÀÓuBª€1oÞ¼”7úä“O¬WWd`J)¥”RªÈy™²Ø`Μ9qÄißñ³Ï>³æŒü l…TcòŒßhˆDü¥¥eD"êêj ‡Kèܹ+ÏÌz�>ÛõõòéšôÙçŸrÔ˜¡ÔÔTcšŽÓ�4fôÒŽHPG à·ÞbÈ�!I7X½z5ƒ
båÊ•ñC§�y<¥”RJ)Uä¼ì÷ðBü‹;ï¼³Ew´Ýþ5<> â»ÂÃ`Ÿ}††¨áûïW1ú˜ýY²äs¯ŸÒÕâ/2î¸C¨©©&¹¾d¢Ç—Äþ}1bþóŸYµj?ýôÏ<óp€5øX
<��q(¥”RJ)å™þXšÜ=ôÐCiõyå•WLŸÏgíAq@&ë³¼¬Ü|ø¡gÍ뮹̓&`úý³CUGóõÍ3W.oÈØåßÿ÷?³CUGÓ03šÓÿú´¹Ãöýì=8ÜÊ{¡7°Šæû–ÔÃ24¥”RJ)U伜mÿH”Wš0a÷Ýw_“wxöÙg9î¸ãˆF;Žžþãá˜yñ†A4aÂÙ“¸iÊŸ ‚D"
lÜ´‘1ãäÃ�Þã§ŸÖYÇä‰Ï/d̸ƒÙ°ñ'þ Sn˜Æ‘#Çâó9þ 2Õ…|X4Ujl0x=CcPJ)¥”RÊSÀûXfÔ:è óñÇ7—/_nF"sÕªUæìٳͣŽ:Ê>ó¾è’©�ƒ¡Ø
H…ù×ûŸL¬JüåÞ'+!¶•Ï/>ŸÏ‚æ·=˜xþ~ýv¶ß®,S¿ƒ˜0p>d¬B‚Ž€kÉàï_)¥”RJ©LéŠ-Iãò9°]&•@Ê+Ì¿ÜûDÒÖ¨‡zÖŒu;Ïè%šG�:6é¹ôh¿]¦ë+¥”RJ)•3^•áµZ‹äqü¸(iâ¶Qà~à*`}Æ’`mM‰F’¾wذQ‰jTáp •tð¨IáæÍ›XÿÓ:jkk‚œrÒYIßÏâ,¥”RJ)¥r.H"óÀmÀÙHîÁ±ç3�O�ÿ¾ÊÐR²—¾
ƒ!êëëðù|L¼`2çüêOžëÑ÷sÝ
“¨®Þ ='}ß04 QJ)¥”RÅ#SHÜ`Jì’c†D&D#ÇwËËÊYÿS‘H„êê-ž=ëªÕ+©¯—fƒ�:u¡S§ä4ŸÏo¿‹ J)¥”RªÝ*¾“]×
Weå’z�F¨©©õìé–-ÿšúú: ºwëáø¾nÁRJ)¥”RÅ$Ý»ôE¶X-GJæ¾GzÍ0ÇhϨª¬bÄV@6{ö¼ë×ÿ˜øºG�žŽïk ¢”RJ)¥ŠI:È/€?"�‡›ùÀ5À³^
*£LÓu¤2Ö‘Ü4M¶lñ. ±>Wï^}ß/� äPäu°/Ð
Ø€T.{xðnÉH)¥”RJµkM !`:pr3�±ððWà<¤z’2X&`º ;vJ|½iÓFÏŸ=
Ó½»s–?¿�m€‡�#\¾·p,Ò?d0'k£*,à G¢�Ƕ 5H%¹¤¿‹·�43g`«Ø×ë€s8–L9Œø�X|�£±¤«3°—åú'H…C•�‘÷Ô]€ž@9P ÄË1®�]6 ?!}³ß Î%ýì*E*_ZÍVç`,^±ÿÍ~‰ü®Ûê ¤WÜrd2/W|ÀöÀÎ@?`[d’q+äs«ù=lª�MÀ·Èëïkàc`eÖG-vúäè¹½² ù¼n©}�*Ëõ5Àÿ<Q5€<œ¿Ò[7Ž?þxˆa|òÉ'Ìš5‹õëÕsÏŽ=Þ™në–mbn+ �:6&‡oñpV¼â–ßïO¬²$�+���f…Û4s»í€—€_!«Jör"‘MµÈ‡Ã¤óý;À¿‘7©|s=pLìëp$ð¯Ü
'#^%9ý²ªœÏ® &[®O.ÎÑXÚ£îÀ(à(ä¤tëV>Îàmäïû9`‘'£k™�ÚŽ�<‘ƒ±xeɟſśb:3‘“ü¸»�‰=qÞy:ò‡û¸÷Cm#K#�¨é€tê‚Ïç#�²y³w± Äg”—;{æé¬rd{Õ6 ¡Pˆ &pòÉ'Ó§OÖ[Çœ9s¸å–[X±bÈÉÕýÀgÈÉ®Ê00 v
\‚à‹�“áG€y9]j~ä¤e¼™mTSŽL(X�\�œðªÖ '";ö%y†½µª� ýHàOȪÈ_�÷ël�œ×�GV:¼Ô;v9Y�˜¼éñó¨vÆíd7\¿rÉ%—p÷Ýw;‚€ÊÊJzè!&L˜`=|™ßrÒ�Iè.ex«ªª‚ ž–á5ãOkT”;'Âó´ï%@€ŠŠ
^{í5îºë.†J�=Øe—]¸øâ‹™7oƒ'úš‘7/>T•÷dæëd;ÍGÈÌ›ó;·º ùde¹Ht²«"OW·@Wd¥o²¥u™{ŸÜ¸Ù"óGäïI¯^Èöø¥Hsh¯ƒ»dòú
à¿È
ŸR®ÜNv�!6óݧOn¼ñÆfäöÛog›m;uzáž3�šHB÷û%¨®ñ0 ‰€˜¦éºâ÷çÝ
ˆ8?~åž{îáÀt½a×®]™={6••‰Àj¤RšÊƒ�€ñ+à2òë„OdWeŸ\”â{’û÷§B.¾ ~�ìOÇ&d_ýHNÔBà;Z¶µ¥Ùžò92óCÅ%€¬X.ÎB&–›³É#ø0Y)ÿKìòpìØKHÞÂz÷‡H2ø’#Ü«E£WEÁmÖAñ/Î<óLB¡æ_·åååœvÚiÜtÓMñCÒú=a²*“* ½ªªCb;TMuµgÏ_v‰F£TT8W@ü~Ç?ÁÏÈmò^o @Ïž=9õÔS›¼ñ6ÛlÃøñã¹çž{⇎Döƒ*wöø1Ë‘™§Èlkߨ¥?²
«©“�nÀÀ¯c—�µNAN¾îÈõ@ŠÌHä5ãfGdïøóÙNÁ…ìÓÞ¾™Û^Af‹!'wM%ûwvvöF&ûœõÝu@>WN#?óÀ”·úG^©¬D�w€÷‘`÷§>OwdÒq²Õw¯·;8YA}º…ÏÑ›�C2øø™�ïÅH2Ê- Ù!þÅÞ{7õúMf»íèØ%�Ī`™¦kH•%A¼¦¶Æ»§�€D"ÊËÒÊi~É)K†â6>‡ý÷ß߀4÷�[ìV’ÝJO]‘
5Ñ“g)6ÑÙ7~p.²]$×nF¶‹½žë�‘æÍ'¡HSJ�[�UäT�ÿȶ˜§�÷‚–ôÑú˜»ÄíŠ<� ÑͤbÑÉHºjŸFNò»º|¯).ô0x´µRâjd…ãÈjKä5x.Î-œ•È
ÊÍHB&*·EhŸUÛT9 ƒÁ´(�•’|á–RYÙ!±5«¶Ö»�Æçl ´´Ôq<�ü\I÷ßÔv»Ây!‡µHuœIÈ2ø(àIR ŒBJhŽÍÊèš@fòzçz EbgäDµ)ÑÒëÊigäè܃�%ÈÉYoàJà¼iâ» zúãH`l¼†ä�©öçd5Í|Ô!+ɽ‘âÿ!3eÚ¿A¶ýõnˆ=¯•�T×ûù™'¬²Ìm$1óùÙgŸqÄé¥s,Z”Týoðn›Fæ½Óˆý¼n9 UUˆD¤…I]ýï¦õâ9 Ñh” Ë ½- Ýf“ÞþÊLéFlõjÁ‚iÝaþüùÖ«ßz?$å‘òõ
’ŒøàT¤€€U²o÷÷He/N’Zk«ØXBfðTæ\„óÄy
ÎR—“�¼ÕèPäujŸù©FuRrÕ»§(2ÙðÒèœÛpüÀíÈJ�eeU~Ø
Y™´Ïr.@VµÓû0÷Æ&ä³ãqdUÝþ<éw:¹ïa£rÈ- ù€Ø‡ËO<Áĉ镨~≤’ß7 õ®óÉ/€€™2 ½Š††X Rïe Òøu0è@l�£HWùùŽfOø¨\°`o½õVÊ$t€êêjkfÐí2…âàL$ÿã>,¹_1>¤ŠN?$‰1[�
¿Cš¯Y«sí�Œñ—YC1êŒT¿²š�ô,CòªÇÉȾ6&§ ¹]n«¿O 9‘?duDò><¸©~¶}ÿ�ÈçÿõY—ò^9²R\e;>ymzWU§e"Ÿ+ÄÆau*|œIá4ÍUsÛÿ3idÆÛo¿Í£�>ÚìƒÜ{ï½Ì›—h+°iF“WŒD“hÔ¥¯eV½‡ˆuò8pnióù+ ¹Þ“U‹¥�Ô駟ΪUî�:£Ñ(çŸ>Ë—/�úŽö×D®½[ˆ,ÝŸƒûÕéÀ½d¯ŠÎRÜg×Ç“º:“j»_ᬄ6yOšf;^м^”Ll=‚3øø Yu?‰ìqdää½Ùî:ôoª=˜‚lÿ³z)‡›«à#® 6npùÞN8cUDÜNvÄò�3aÂûêF’éÓ§3iÒ$ë¡»È^'Ì–0AV$RmÁJÜÐ4©««õæI-+ �€sÁÉŸŸ}@®#VcÉ’%<˜'žx‚ÚÚÆßÉûï¿Ïˆ#xøá‡÷»iD¤
‹‰”ZÜ)Ëk7¸-‹ãyY™±»•«rRHÞ‚Õ÷4®b?†ó$ú<œ[÷ŠÍ(d‹‰ýMü+dõ¡5Ž3á=äoûs—ïÝ
ì—Ýá(
@þ>DV‹ói{Óï‘qqó‘Þº¶ˆ¥:Ù½ŽØžÁÚÚZN:é$FŽÉÌ™3™?>óçÏgÆŒ6Œ3Ï<“úúúøý>À=Ò͹Æé[“hÔ¹¥=%Ôz€4;n[°’“Ð�|X™-; ¨øöÛo9é¤“èØ±#}ûö¥K—.ì»ï¾¼öÚkÖûü™üùÀU3©ÝîÖÍþd¹<[®Ä¹šDfö´¦¼·ŽÇù;½ŸÆ“ƒjd…UO¤ªZ±Ú©beSÿ/ÒÉíd?—–!Á»}\ñ¿)·ªI*ÿýŽä‰€$øÈljÀ+‘&™K�rßër;•kn9 õ”G"' ;Ì™3‡9sæ4õX!m&“ìZÍðùb+ î9 eeelØ ¥°ëjkÁY5·Å¬¡Žkºe–a€iæE ð2Rçqb=]jjjøê+Çy²ÇøY�Ê”‘^.¯àl*y¤d£Ôa ‚ßGÊ;ÆuCÊLL~~È¢I¶ëuȶ;«?#Mõ¬Ÿ‘¹~ÙÐ >ì[Öþ†$áæ£ÕHß�w‰5Žé…ü{s@Yˆ¶B¶YY=|šƒ±¤ëRdËØ÷¹ˆÊ½¦NvW ³¡÷› O¡Ùžq0ùÝä¨É-X áPIâk¯¶`Y÷`œMÝŽåK Rk~'dùÔžÿ=RË~W$‘1—•’”·6 AÈG¶ã%HŽ˜¡yZÖÇá\¦Œ÷͋վ8Í'‘÷«åH•'«!.÷mï|ȶ«>¶ã‹� ¸|
>â–#+^ö¸ãÑX…æ8œ9wåb -`¢Á‡Šiîdw3ÒPi{¤dç‹ÈìçÇÀ³Àd`;dflsæ†é%3i[”U|…Â0϶`ÅŸÏgI„oäó'ýäË,«�ȶºÝ‘j; ËõÝ�ÄÕ|Ûj ¼±é`kŸTظ6‹ãøy�Ù��¼7©¶qk<85ÅmíÉèP|IÌçãl²» Ø¥*Ø»8K+$ïÑWùÏ޳紟* éžì~‡tèìì…Dß· ��FS[°â
õü~¿‡Ièfì1Ýw»åiz*[€¯É]U•]Ë�%þÛñIÀYÇcÈÞa»;�Ô5¢UszâÜÂñ’Ëçæ?8Op޶õx\ùj[œ½3¢Hµ«eΛçµ;�7mÇŽÊÁXTë`»>7ƒPªµòùd×S†a$r@âý>ìBAYÍôùüÔÖx´[õðûÝšnexµC¨Ê'o"]t¸™ôkœ=f‚È–°žYK{q>ÎJV©V?R}?Hñ¬DÝTÚŽÝ„äKš(0g¥¤?QDç¬#ÐÝv̾eV©¼VLo4‰ Ä4ÝÓBa @Ãð¬¡[m‘\Óvq4"„ìõ[P*]× _ÚŽJvW�žö™æ‘„`'ß2n½<–"[k›ò$`o4gæöf8pŒíØçHÅÈB5ˆw‘ýÙÑpnZ ¯Ë1·êJå¢ @ÈB|–{yìp(¾â£¶Ö‹;��Ž?ྰá+¬-Xª8U#ÕKì~›åq|�lý´ÿqîÜ�屺S�.¶cwÓ|ï€Z¤D¯U—Øãµgöòò&r²îÑRyÎ\‰t¹ï‡ät¾ƒv¦.ö®ç [£U�)¢“Ýæ’V@Z•â\áˆÆV[¬=F¬·0|Žqé
ˆÊG/âÌ
8©‚–MâÞ…ûìÇ•“�³ôî&¤ª]:îÃYn}í÷½k4Îj_³h{î ÖóK”r—cÚÔO”¢ @ËrD$Å
HI¸$~Û4«`9ŽT�€ë\*cÚ‡x R[þL¤"Î He4Õ¾˜¸o7Ÿí� ePÝÊMNC:>«¦´{XŸæýW·ˆôÀh�&Û®G)ìWªð¹�Äè6TUPÜ�0R~ón$¹îŸH/�“(äxl€éÒ C:¤p$ß*¾â^Ë0{R(È6ÈVŒµÀàA$Auð
Ò@Î^®R¶—�*hV'“›‰ŒË€7lÇBH>H�ì§ ØW?¢´¼€[I^ûã¶‘êPVOs0¥âÜÚtÌú(”jû‰ÃÏ�D¦g€�nè‡#{]g"'–¹××0#†T}@Â%öF„ 8lßsÙ‚•4.g¼Q«R£‘Æ[p_
)×ü"0Y%Q…/ŠÌ”[õDÒ³ø9ðíx$ e}D…a'`”íØKÀâ>ÎÀÛ¶cG!¹íÉy.ÇnÏú(”J¶Úå˜[bºRyËz²ûdfg›&n¿5rBÙ\©Æ<äKÄÑU°JJb…\L³É-Xéo¼j¼A0U â+¸�1ÀsX’àvß}wÎ>ûl&MšÄ˜1c(++³ÞþT$ ÕòÂíÃ#8_öGçb ȇðñ8“Ò÷§ ߣ²â"œ“ý]ÙïçCJ»¶!àÛ±yÈê®R¹ô2 c58QªµâDg¿‹ìÕ«×_=/½ô¯¼ò
S¦L¡OŸ>Öû]„Ôå/>ŸuÄzþÔJ”Æ’’Ð[pØ$¶`cˆ‘|)°�ÞHS8?ÀŽ;îÈo¼Á¼yóxà�¸óÎ;yþùçY¾|9眓”<
ËëK´¥8ëÍ™‹�ÄüYµ;ILW�:¿´[ ¼ÖÊÇ{ç
Ôé@‡V>^¾9ç¶–s1¥lê�ù¶c£Ð‰>U@|@W,MÅŽ;î8-ZÄïÿ{F�ÅÈ‘#ùÍo~ÃÂ…9å”¤É ë€>Ùnë�H4Š[8—àóù0M“ÚÚÚV©ƒ!×ЀĚ%æó¬ë‰5âÚi§�x÷Ýw9è ƒ7êܹ3÷Ýw×^{õðdš^]S…ã¶ë;“ÛÂv9~70$ËcÉgg¶cSiýÛ\Îß{%R�¢=°È. IDAT÷ý0‘7¥ò�½f/œ¯Y¥ò–™)¬ Øm·Ý˜9s&ååÎmý¥¥¥<òÈ#š¨FèÖÈ*�ˆ½H<Ð…Ãø|>¢Ñh+ËðâXሯ¶ƒî[°¤Sz"Iº’gª€A‚¦G}”.]ìm’]}õÕì·ß~ñ«ehþ�rxÉå˜3Í®K€ÿØŽ…�§‘£ÅÎ\h;¶YÑl‹p–ÿœHû˜‰µÑøgF¥råI—c·�:/S©¼âÃ’�xõÕW§.tå÷û¹îº¤êƒã274o–æJQÓtÝR†ðùüD£Ñ4Ëðâ8ìâH(äžk``Û…•¯ÈpbUІÂ�!ÍO,†Á¤II…qìɯª0}„3ïbP.bQ‡”€^a;Þ™µN1P4Æá\¥ºŸ¶÷pb¶GrÅ
Ù6ÈÏaåx+•+ó€×mÇú"eÊÛÀjçX‰~øáÍÞá�C!ÐÐÐ RQåf
sª‰™HšNU†7
cÑh$õ
H+Ã`0E ’¼@f)óñÃ{·øûïŸ~«…8ÀzÕþ�®
S|ÿ±5éq¯�Åj’”>—ä’áwà\(&Û®×á¾m5î™o3 ˜íÑãç‚ÛË›Y…RM»é^o
8Žž@ò½¶äbPJ¥#€l¥ ²²²Ù;„B!JJJØ´iÈ
ÊÞ¤@:à}NC§VÝËlü9S5"…dV]]„ÚÚØo›×#b[°RTÁòù|Ö?Rú8¯UTØ·‘§}Ûôï¨ò݇$ {æj 6ï"[€þb;~R:öál(ì�aV³p®µÖÿ�™Øa–c‡"¯‰OºÍ†‘<
/•’~�Ž�?,]º4í'øæ›o¬Wuÿtûñ™ízÇØ%ÝnÚ™ô Ò‡f‚íø½À§_U·�wºk‹i$ Õ5!ÝþAø%°.Qª¿EfÚË¡Dšµ¾ŠT¡ü ËãRªI>¤Œ% 3fÌhö¶Û¼KÂÒ ÝÞˆ0Hž†55mÝ.ùÀo
@3P¤zÒ]RÕ"€^^V õÄÓ¹$*n¼üòËÔÔØS Ü=óÌ3Ö«öòªp-s9¶mÖG‘ÚDdk‚U ’”Þ-ûÃÉ™À/lÇþƒ÷'#/ _ÛŽ�Dáþ®íÛE?ÏÉ(”j^iÊú\Šï�DÎóÞB
µn‰Ró!�¸ãŽ;xï½÷RÞxþüùÜpÃ
ÖCgld3,�R$uM…B‰²¸Õ5^m�Œ÷ àVú×¥ a¾&¡|°fÍn¾ùæfï°lÙ2¦M›f=ôtFF¦ra¹Ë±^YEjñ¤ô•¶ã½�ê1Å’”~ήð^¯~€œÝm;VB!UJLf@¾ÉÅ ”JS5ò~—*'× îC&Ÿ~ì˜*eçCf®Þ¨©©aĈ<øàƒÔ×76ÙŒD"Ìœ9“aÆY·_ýyÃ×øG™¢z(Ô˜·šî¿»ÆŽÄ¬T�Г·`åsHDŸ×]wÓ§OOyãåË—3zôh6lØ?ô òZSíƒ[þ@÷¬�¢i+€Ÿ!ÁˆÕ!ȾéöÎ- XJæ’Ã6ÙŽ¹@ù®g¾ZúûN•Ê�$äš^á,AŠuüøYÍ~i"º3ù; ªÚrbùKb3…6làì³Ïf›m¶aøðáŒ5Šž={rÊ)§ðÃ?Äï·Y^¯w}Ô