From 41a7f701aabf56fba4e48dc04c2859b3e9cf470d Mon Sep 17 00:00:00 2001 From: David Doty Date: Fri, 4 Sep 2020 10:40:40 -0700 Subject: [PATCH 01/17] updated paper URL now that DNA 2020 paper is published --- README.md | 2 +- scadnano/scadnano.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c43f0914..674b2a29 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ If you find scadnano useful in a scientific project, please cite its associated > scadnano: A browser-based, scriptable tool for designing DNA nanostructures. David Doty, Benjamin L Lee, and Tristan Stérin. DNA 2020: *Proceedings of the 26th International Conference on DNA Computing and Molecular Programming* - [ [paper](https://arxiv.org/abs/2005.11841) | [BibTeX](https://web.cs.ucdavis.edu/~doty/papers/scadnano.bib) ] + [ [paper](https://doi.org/10.4230/LIPIcs.DNA.2020.9) | [BibTeX](https://web.cs.ucdavis.edu/~doty/papers/scadnano.bib) ] ## Overview diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index d859c87c..678244c7 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -16,7 +16,7 @@ | scadnano: A browser-based, scriptable tool for designing DNA nanostructures. | David Doty, Benjamin L Lee, and Tristan Stérin. | DNA 2020: *Proceedings of the 26th International Conference on DNA Computing and Molecular Programming* - | [ `paper `_ | `BibTeX `_ ] + | [ `paper `_ | `BibTeX `_ ] This library uses typing hints from the Python typing library. (https://docs.python.org/3/library/typing.html) From 304e120c1a0de989ce375cacbe3ab83c3c918cd5 Mon Sep 17 00:00:00 2001 From: David Doty Date: Fri, 4 Sep 2020 15:40:55 -0700 Subject: [PATCH 02/17] added rotate_domains method to Strand to "rotate" domains of strand (i.e., like adding a crossover between the 5' and 3' ends, and removing another crossover) --- scadnano/scadnano.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 678244c7..3afb671e 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -2553,6 +2553,29 @@ def __hash__(self) -> int: def __str__(self) -> str: return repr(self) if self.name is None else self.name + def rotate_domains(self, rotation: int, forward: bool = True) -> None: + """ + "Rotates" the strand by replacing domains with a circular rotation, e.g., if the domains are + + A, B, C, D, E, F + + then ``strand.rotate(2)`` makes the :any:`Strand` have the same domains, but in this order: + + C, D, E, F, A, B + + and ``strand.rotate(2, forward=False)`` makes + + E, F, A, B, C, D + + :param rotation: + Amount to rotate domains. + :param forward: + Whether to move domains forward (wrapping off 3' end back to 5' end) or backward (wrapping off + 5' end back to 3' end). + """ + idx = rotation if not forward else len(self.domains) - rotation + self.domains = self.domains[idx:] + self.domains[:idx] + def set_scaffold(self, is_scaf: bool = True) -> None: """Sets this :any:`Strand` as a scaffold. Alters color to default scaffold color. From b7e41d0de73d5b94cbf398f8e2ec0a9d6efcef89 Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 5 Sep 2020 16:56:49 -0700 Subject: [PATCH 03/17] Design.write_scadnano_file now warns if a Loopout is the first or last substrand on a Strand (still allowed in intermediate designs) --- scadnano/scadnano.py | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 3afb671e..8bd9abb8 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -3039,14 +3039,14 @@ def __init__(self, strand: Strand, the_cause: str): first_domain = strand.first_bound_domain() last_domain = strand.last_bound_domain() - msg = (f'{the_cause}\n' - f'strand length = {strand.dna_length()}\n' - f'DNA length = {len(strand.dna_sequence) if strand.dna_sequence else "N/A"}\n' - f'DNA sequence = {strand.dna_sequence}' - f"strand 5' helix = {first_domain.helix if first_domain else 'N/A'}\n" - f"strand 5' end offset = {first_domain.offset_5p() if first_domain else 'N/A'}\n" - f"strand 3' helix = {last_domain.helix if last_domain else 'N/A'}\n" - f"strand 3' end offset = {last_domain.offset_3p() if last_domain else 'N/A'}\n") + msg = (f'''{the_cause} + strand length = {strand.dna_length()} + DNA length = {len(strand.dna_sequence) if strand.dna_sequence else "N/A"} + DNA sequence = {strand.dna_sequence} + strand 5' helix = {first_domain.helix if first_domain else 'N/A'} + strand 5' end offset = {first_domain.offset_5p() if first_domain else 'N/A'} + strand 3' helix = {last_domain.helix if last_domain else 'N/A'} + strand 3' end offset = {last_domain.offset_3p() if last_domain else 'N/A'}\n''') super().__init__(msg) # super(IllegalDesignError, self).__init__(msg) @@ -4206,6 +4206,7 @@ def _check_legal_design(self) -> None: self._check_helix_offsets() self._check_strands_reference_helices_legally() self._check_loopouts_not_consecutive_or_singletons_or_zero_length() + self._check_loopouts_not_first_or_last_substrand() self._check_strands_overlap_legally() self._warn_if_strand_names_not_unique() @@ -4268,27 +4269,34 @@ def err_msg(d1: Domain, d2: Domain, h_idx: int) -> str: if d_first.forward == d_second.forward: raise IllegalDesignError(err_msg(d_first, d_second, helix_idx)) - def _check_loopouts_not_consecutive_or_singletons_or_zero_length(self): + def _check_loopouts_not_consecutive_or_singletons_or_zero_length(self) -> None: for strand in self.strands: Design._check_loopout_not_singleton(strand) Design._check_two_consecutive_loopouts(strand) Design._check_loopouts_length(strand) + def _check_loopouts_not_first_or_last_substrand(self) -> None: + for strand in self.strands: + if isinstance(strand.first_domain(), Loopout): + raise StrandError(strand, 'strand cannot have a Loopout as its first domain') + if isinstance(strand.last_domain(), Loopout): + raise StrandError(strand, 'strand cannot have a Loopout as its last domain') + @staticmethod - def _check_loopout_not_singleton(strand: Strand): - if len(strand.domains) == 1 and strand.first_domain().is_loopout(): + def _check_loopout_not_singleton(strand: Strand) -> None: + if len(strand.domains) == 1 and isinstance(strand.first_domain(), Loopout): raise StrandError(strand, 'strand cannot have a single Loopout as its only domain') @staticmethod - def _check_two_consecutive_loopouts(strand: Strand): + def _check_two_consecutive_loopouts(strand: Strand) -> None: for domain1, domain2 in _pairwise(strand.domains): if domain1.is_loopout() and domain2.is_loopout(): raise StrandError(strand, 'cannot have two consecutive Loopouts in a strand') @staticmethod - def _check_loopouts_length(strand: Strand): + def _check_loopouts_length(strand: Strand) -> None: for loopout in strand.domains: - if loopout.is_loopout() and loopout.length <= 0: + if isinstance(loopout, Loopout) and loopout.length <= 0: raise StrandError(strand, f'loopout length must be positive but is {loopout.length}') def _check_strands_reference_helices_legally(self): @@ -4308,12 +4316,12 @@ def _check_strand_has_legal_offsets_in_helices(self, strand: Strand): for domain in strand.domains: if domain.is_domain(): helix = self.helices[domain.helix] - if domain.start < helix.min_offset: + if helix.min_offset is not None and domain.start < helix.min_offset: err_msg = f"domain {domain} has start offset {domain.start}, " \ f"beyond the end of " \ f"Helix {domain.helix} that has min_offset = {helix.min_offset}" raise StrandError(strand, err_msg) - if domain.end > helix.max_offset: + if helix.max_offset is not None and domain.end > helix.max_offset: err_msg = f"domain {domain} has end offset {domain.end}, " \ f"beyond the end of " \ f"Helix {domain.helix} that has max_offset = {helix.max_offset}" @@ -4880,6 +4888,7 @@ def write_scadnano_file(self, directory: str = '.', filename: str = None, extens :param extension: extension for filename (default: ``.{default_extension}``) Mutually exclusive with `filename` """ + self._check_legal_design() contents = self.to_json() if filename is not None and extension is not None: raise ValueError('at least one of filename or extension must be None') From 903510d7f27b19b6275ef6c1d9475cc7e72b9ba7 Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 7 Sep 2020 14:27:47 -0700 Subject: [PATCH 04/17] fixed all mypy warnings; closes #109 --- scadnano/scadnano.py | 877 ++++++++++++++++++++++------------------ tests/scadnano_tests.py | 356 ++++++++-------- 2 files changed, 670 insertions(+), 563 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 8bd9abb8..6496e4a1 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -41,7 +41,7 @@ """ # needed to use forward annotations: https://docs.python.org/3/whatsnew/3.7.html#whatsnew37-pep563 -# commented out for now to support Python 3.6, which does not support this feature +# commented out for now to support Py3.6, which does not support this feature # from __future__ import annotations __version__ = "0.12.0" # version line; WARNING: do not remove or change this line or comment @@ -52,17 +52,19 @@ import enum import itertools import re -import copy +from builtins import ValueError from dataclasses import dataclass, field, InitVar, replace -from typing import Tuple, List, Iterable, Set, Dict, Union, Optional, FrozenSet, Type, cast, Any +from typing import Tuple, List, Sequence, Iterable, Set, Dict, Union, Optional, FrozenSet, Type, cast, Any from collections import defaultdict, OrderedDict, Counter import sys import os.path default_scadnano_file_extension = 'sc' +VStrands = Dict[int, Dict[str, Any]] -def _pairwise(iterable): + +def _pairwise(iterable: Iterable) -> Iterable: """s -> (s0,s1), (s1,s2), (s2, s3), ...""" a, b = itertools.tee(iterable) next(b, None) @@ -71,8 +73,8 @@ def _pairwise(iterable): # for putting evaluated expressions in docstrings # https://stackoverflow.com/questions/10307696/how-to-put-a-variable-into-python-docstring -def _docstring_parameter(*sub, **kwargs): - def dec(obj): +def _docstring_parameter(*sub: Any, **kwargs: Any) -> Any: + def dec(obj: Any) -> Any: obj.__doc__ = obj.__doc__.format(*sub, **kwargs) return obj @@ -89,7 +91,7 @@ def dec(obj): class _JSONSerializable(ABC): @abstractmethod - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Any: raise NotImplementedError() @@ -103,19 +105,19 @@ class NoIndent: # Value wrapper. Placing a value in this will stop it from being indented when converting to JSON # using _SuppressableIndentEncoder - def __init__(self, value): + def __init__(self, value: Any) -> None: self.value = value class _SuppressableIndentEncoder(json.JSONEncoder): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: self.unique_id = 0 super(_SuppressableIndentEncoder, self).__init__(*args, **kwargs) self.kwargs = dict(kwargs) del self.kwargs['indent'] - self._replacement_map = {} + self._replacement_map: Dict[int, Any] = {} - def default(self, obj): + def default(self, obj: Any) -> Any: if isinstance(obj, NoIndent): # key = uuid.uuid1().hex # this caused problems with Brython. key = self.unique_id @@ -125,10 +127,10 @@ def default(self, obj): else: return super().default(obj) - def encode(self, obj): + def encode(self, obj: Any) -> Any: result = super().encode(obj) for k, v in self._replacement_map.items(): - result = result.replace('"@@%s@@"' % (k,), v) + result = result.replace(f'"@@{k}@@"', v) return result @@ -176,16 +178,16 @@ def __post_init__(self, hex_string: str) -> None: self.g = int(hex_string[2:4], 16) self.b = int(hex_string[4:6], 16) - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> str: # Return object representing this Color that is JSON serializable. # return NoIndent(self.__dict__) if suppress_indent else self.__dict__ return f'#{self.r:02x}{self.g:02x}{self.b:02x}' - def to_cadnano_v2_int_hex(self): + def to_cadnano_v2_int_hex(self) -> int: return int(f'{self.r:02x}{self.g:02x}{self.b:02x}', 16) @classmethod - def from_cadnano_v2_int_hex(cls, hex_int): + def from_cadnano_v2_int_hex(cls, hex_int: int) -> 'Color': hex_str = "0x{:06x}".format(hex_int) return Color(hex_string=hex_str[2:]) @@ -227,7 +229,7 @@ class ColorCycler: # _colors = [Color(hex_string=kelly_color) for kelly_color in _kelly_colors] # """List of colors to cycle through.""" - def __init__(self): + def __init__(self) -> None: self._current_color_idx = 0 # random order order = [3, 11, 0, 12, 8, 1, 10, 6, 5, 9, 4, 7, 2] @@ -237,11 +239,11 @@ def __init__(self): colors_shuffled[i] = color self._colors: List[Color] = colors_shuffled - def __iter__(self): + def __iter__(self) -> 'ColorCycler': # need to make ColorCycler an iterator return self - def __next__(self): + def __next__(self) -> Color: color = self.current_color() self._current_color_idx = (self._current_color_idx + 1) % len(self._colors) return color @@ -249,28 +251,28 @@ def __next__(self): def current_color(self) -> Color: return self._colors[self._current_color_idx] - def __hash__(self): + def __hash__(self) -> int: return hash(self.current_color()) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if not isinstance(other, ColorCycler): return False return self._current_color_idx == other._current_color_idx - def __str__(self): - repr(self) + def __str__(self) -> str: + return repr(self) - def __repr__(self): + def __repr__(self) -> str: return f'ColorCycler({self.current_color()})' @property - def colors(self): + def colors(self) -> List[Color]: """The colors that are cycled through when calling ``next()`` on some :any:`ColorCycler`.""" return list(self._colors) @colors.setter - def colors(self, newcolors): - self._colors = newcolors + def colors(self, newcolors: Iterable[Color]) -> None: + self._colors = list(newcolors) self._current_color_idx = 0 @@ -386,7 +388,7 @@ def default_major_tick_distance(grid: Grid) -> int: make its length the same as the length of the strand.""" -def _rotate_string(string: str, rotation: int): +def _rotate_string(string: str, rotation: int) -> str: rotation = rotation % len(string) return string[rotation:] + string[:rotation] @@ -417,7 +419,7 @@ class M13Variant(enum.Enum): """ -def m13(rotation: int = 5587, variant: M13Variant = M13Variant.p7249): +def m13(rotation: int = 5587, variant: M13Variant = M13Variant.p7249) -> str: """ The M13mp18 DNA sequence (commonly called simply M13). @@ -809,7 +811,7 @@ class Modification(_JSONSerializable): idt_text: Optional[str] = None """IDT text string specifying this modification (e.g., '/5Biosg/' for 5' biotin). optional""" - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Dict[str, Any]: ret = {mod_display_text_key: self.display_text} if self.idt_text is not None: ret[mod_idt_text_key] = self.idt_text @@ -817,7 +819,8 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs): return ret @staticmethod - def from_json(json_map: dict) -> 'Modification': # remove quotes when Python 3.6 support dropped + def from_json( + json_map: Dict[str, Any]) -> 'Modification': # remove quotes when Py3.6 support dropped location = json_map[mod_location_key] if location == "5'": return Modification5Prime.from_json(json_map) @@ -833,13 +836,14 @@ def from_json(json_map: dict) -> 'Modification': # remove quotes when Python 3. class Modification5Prime(Modification): """5' modification of DNA sequence, e.g., biotin or Cy3.""" - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Dict[str, Any]: ret = super().to_json_serializable(suppress_indent) ret[mod_location_key] = "5'" return ret + # remove quotes when Py3.6 support dropped @staticmethod - def from_json(json_map: dict) -> 'Modification5Prime': # remove quotes when Python 3.6 support dropped + def from_json(json_map: Dict[str, Any]) -> 'Modification5Prime': display_text = json_map[mod_display_text_key] location = json_map[mod_location_key] assert location == "5'" @@ -851,13 +855,14 @@ def from_json(json_map: dict) -> 'Modification5Prime': # remove quotes when Pyt class Modification3Prime(Modification): """3' modification of DNA sequence, e.g., biotin or Cy3.""" - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Dict[str, Any]: ret = super().to_json_serializable(suppress_indent) ret[mod_location_key] = "3'" return ret + # remove quotes when Py3.6 support dropped @staticmethod - def from_json(json_map: dict) -> 'Modification3Prime': # remove quotes when Python 3.6 support dropped + def from_json(json_map: Dict[str, Any]) -> 'Modification3Prime': display_text = json_map[mod_display_text_key] location = json_map[mod_location_key] assert location == "3'" @@ -876,7 +881,7 @@ class ModificationInternal(Modification): For example, internal biotins for IDT must be at a T. If any base is allowed, it should be ``['A','C','G','T']``.""" - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Dict[str, Any]: ret = super().to_json_serializable(suppress_indent) ret[mod_location_key] = "internal" if self.allowed_bases is not None: @@ -884,8 +889,9 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs): list(self.allowed_bases)) if suppress_indent else list(self.allowed_bases) return ret + # remove quotes when Py3.6 support dropped @staticmethod - def from_json(json_map: dict) -> 'ModificationInternal': # remove quotes when Python 3.6 support dropped + def from_json(json_map: Dict[str, Any]) -> 'ModificationInternal': display_text = json_map[mod_display_text_key] location = json_map[mod_location_key] assert location == "internal" @@ -918,18 +924,18 @@ class Position3D(_JSONSerializable): """z-coordinate of position. Increasing `z` moves right in the side view and out of the screen in the main view.""" - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): - dct = self.__dict__ + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Dict[str, Any]: + dct: Dict[str, Any] = self.__dict__ # return NoIndent(dct) if suppress_indent else dct return dct @staticmethod - def from_json(json_map: dict) -> 'Position3D': # remove quotes when Python 3.6 support dropped + def from_json(json_map: Dict[str, Any]) -> 'Position3D': # remove quotes when Py3.6 support dropped if position_origin_key in json_map: - origin = json_map[position_origin_key] - x = origin[position_x_key] - y = origin[position_y_key] - z = origin[position_z_key] + origin_ = json_map[position_origin_key] + x = origin_[position_x_key] + y = origin_[position_y_key] + z = origin_[position_z_key] else: x = json_map[position_x_key] y = json_map[position_y_key] @@ -979,8 +985,8 @@ class HelixGroup(_JSONSerializable): grid: Grid = Grid.none """Same meaning as :py:data:`Design.grid`, enforced only on the :any:`Helix`'s in the group.""" - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): - dct = dict() + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Dict[str, Any]: + dct: Dict[str, Any] = dict() helix_idxs: List[int] = kwargs['helix_idxs'] @@ -1002,7 +1008,7 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs): return dct - def _assign_default_helices_view_order(self, helices_in_group: Dict[int, 'Helix']): + def _assign_default_helices_view_order(self, helices_in_group: Dict[int, 'Helix']) -> None: if self.helices_view_order is not None: raise AssertionError('should not call _assign_default_helices_view_order if ' 'HelixGroup.helices_view_order is not None, but it is ' @@ -1011,7 +1017,7 @@ def _assign_default_helices_view_order(self, helices_in_group: Dict[int, 'Helix' self.helices_view_order = _check_helices_view_order_and_return(self.helices_view_order, helix_idxs) @staticmethod - def from_json(json_map: dict, **kwargs) -> 'HelixGroup': # remove quotes when Python 3.6 support dropped + def from_json(json_map: dict, **kwargs: Any) -> 'HelixGroup': # remove quotes when Py3.6 support dropped grid = optional_field(Grid.none, json_map, grid_key) num_helices: int = kwargs['num_helices'] @@ -1043,6 +1049,8 @@ def helices_view_order_inverse(self, idx: int) -> int: :return: view order of the :any:`Helix` :raises ValueError: if `idx` is not the index of a :any:`Helix` in this :any:`HelixGroup` """ + if self.helices_view_order is None: + raise ValueError('cannot access helices_view_order_inverse until helices_view_order is set') return self.helices_view_order.index(idx) @@ -1078,7 +1086,7 @@ class Helix(_JSONSerializable): associated to the :any:`Helix` via the integer index :any:`Domain.helix`. """ - max_offset: int = None # type: ignore + max_offset: Optional[int] = None # type: ignore """Maximum offset (exclusive) of :any:`Domain` that can be drawn on this :any:`Helix`. Optional field. @@ -1092,7 +1100,7 @@ class Helix(_JSONSerializable): Optional field. Default value 0. """ - major_tick_start: int = None # type: ignore + major_tick_start: Optional[int] = None # type: ignore """Offset of first major tick when not specifying :py:data:`Helix.major_ticks`. Used in combination with either :py:data:`Helix.major_tick_distance` or @@ -1177,7 +1185,7 @@ class Helix(_JSONSerializable): See https://en.wikipedia.org/wiki/Aircraft_principal_axes Units are degrees.""" - idx: Optional[int] = None # type: ignore + idx: Optional[int] = None """Index of this :any:`Helix`. Optional if no other :any:`Helix` specifies a value for *idx*. @@ -1202,7 +1210,7 @@ def __post_init__(self) -> None: f'outside the range of available offsets since max_offset = ' f'{self.max_offset}') - def to_json_serializable(self, suppress_indent: bool = True, **kwargs) -> dict: + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Dict[str, Any]: dct: Any = dict() grid: Grid = kwargs['grid'] @@ -1258,8 +1266,8 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs) -> dict: return NoIndent(dct) if suppress_indent and use_no_indent_helix else dct @staticmethod - def from_json(json_map: dict, **kwargs) -> 'Helix': # remove quotes when Python 3.6 support dropped - grid_position = None + def from_json(json_map: dict) -> 'Helix': # remove quotes when Py3.6 support dropped + grid_position: Optional[Tuple[int, int]] = None if grid_position_key in json_map: gp_list = json_map[grid_position_key] if len(gp_list) == 3: @@ -1267,7 +1275,7 @@ def from_json(json_map: dict, **kwargs) -> 'Helix': # remove quotes when Python if len(gp_list) != 2: raise IllegalDesignError("list of grid_position coordinates must be length 2, " f"but this is the list: {gp_list}") - grid_position = tuple(gp_list) + grid_position = (gp_list[0], gp_list[1]) major_tick_distance = json_map.get(major_tick_distance_key) major_ticks = json_map.get(major_ticks_key) @@ -1315,6 +1323,8 @@ def calculate_major_ticks(self, grid: Grid) -> List[int]: overrides :py:data:`Helix.major_tick_distance`, which overrides `default_major_tick_distance` from :any:`Design`. """ + if self.max_offset is None: + raise ValueError('cannot calculate major ticks if max_offset is not specified') if self.major_tick_start is None: raise AssertionError('major_tick_start should never be None') if self.major_ticks is not None: @@ -1335,7 +1345,7 @@ def calculate_major_ticks(self, grid: Grid) -> List[int]: return list(range(self.major_tick_start, self.max_offset + 1, distance)) @property - def domains(self): + def domains(self) -> List['Domain']: """ Return :any:`Domain`'s on this :any:`Helix`. Assigned when a :any:`Design` is created using this :any:`Helix`. @@ -1430,14 +1440,15 @@ class Domain(_JSONSerializable): """ # not serialized; for efficiency - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped _parent_strand: Optional['Strand'] = field(init=False, repr=False, compare=False, default=None) def __post_init__(self) -> None: self._check_start_end() - def to_json_serializable(self, suppress_indent: bool = True, **kwargs) -> Union[NoIndent, dict]: - dct = OrderedDict() + def to_json_serializable(self, suppress_indent: bool = True, + **kwargs: Any) -> Union[NoIndent, Dict[str, Any]]: + dct: Dict[str, Any] = OrderedDict() if self.name is not None: dct[domain_name_key] = self.name dct[helix_idx_key] = self.helix @@ -1453,7 +1464,7 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs) -> Union[ return NoIndent(dct) if suppress_indent else dct @staticmethod - def from_json(json_map: dict) -> 'Domain': # remove quotes when Python 3.6 support dropped + def from_json(json_map: Dict[str, Any]) -> 'Domain': # remove quotes when Py3.6 support dropped helix = mandatory_field(Domain, json_map, helix_idx_key) forward = mandatory_field(Domain, json_map, forward_key, *legacy_forward_keys) start = mandatory_field(Domain, json_map, start_key) @@ -1489,7 +1500,9 @@ def __repr__(self) -> str: def __str__(self) -> str: return repr(self) if self.name is None else self.name - def strand(self) -> 'Strand': # remove quotes when Python 3.6 support dropped + def strand(self) -> 'Strand': # remove quotes when Py3.6 support dropped + if self._parent_strand is None: + raise ValueError('_parent_strand has not yet been set') return self._parent_strand def set_name(self, name: str) -> None: @@ -1502,26 +1515,29 @@ def set_label(self, label) -> None: # type: ignore def _check_start_end(self) -> None: if self.start >= self.end: + if self._parent_strand is None: + raise ValueError(f'start = {self.start} must be less than end = {self.end}\n' + f'_parent_strand has not yet been set') raise StrandError(self._parent_strand, f'start = {self.start} must be less than end = {self.end}') - @staticmethod - def is_loopout() -> bool: - """Indicates if this is a :any:`Loopout` (always false) - Useful when object could be either :any:`Loopout` or :any:`Domain`.""" - return False - - @staticmethod - def is_domain() -> bool: - """Indicates if this is a :any:`Domain` (always true) - Useful when object could be either :any:`Loopout` or :any:`Domain`.""" - return True - - def set_start(self, new_start: int): + # @staticmethod + # def is_loopout() -> bool: + # """Indicates if this is a :any:`Loopout` (always false) + # Useful when object could be either :any:`Loopout` or :any:`Domain`.""" + # return False + # + # @staticmethod + # def is_domain() -> bool: + # """Indicates if this is a :any:`Domain` (always true) + # Useful when object could be either :any:`Loopout` or :any:`Domain`.""" + # return True + + def set_start(self, new_start: int) -> None: self.start = new_start self._check_start_end() - def set_end(self, new_end: int): + def set_end(self, new_end: int) -> None: self.end = new_end self._check_start_end() @@ -1549,7 +1565,7 @@ def contains_offset(self, offset: int) -> bool: then it contains the offset 7 even though there is no base 7 positions from the start.""" return self.start <= offset < self.end - def __len__(self): + def __len__(self) -> int: """Same as :meth:`Domain.dna_length`. See also :meth:`Domain.visual_length`.""" @@ -1684,7 +1700,7 @@ def _between_5p_and_offset(self, offset_to_test: int, offset_edge: int) -> bool: # The type hint 'Domain' must be in quotes since Domain is not yet defined. # This is a "forward reference": https://www.python.org/dev/peps/pep-0484/#forward-references - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped # def overlaps(self, other: Domain) -> bool: def overlaps(self, other: 'Domain') -> bool: r"""Indicates if this :any:`Domain`'s set of offsets (the set @@ -1700,9 +1716,9 @@ def overlaps(self, other: 'Domain') -> bool: self.forward == (not other.forward) and self.compute_overlap(other)[0] >= 0) - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped # def overlaps_illegally(self, other: Domain): - def overlaps_illegally(self, other: 'Domain'): + def overlaps_illegally(self, other: 'Domain') -> bool: r"""Indicates if this :any:`Domain`'s set of offsets (the set :math:`\{x \in \mathbb{N} \mid` ``self.start`` @@ -1716,7 +1732,7 @@ def overlaps_illegally(self, other: 'Domain'): self.forward == other.forward and self.compute_overlap(other)[0] >= 0) - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped # def compute_overlap(self, other: Domain) -> Tuple[int, int]: def compute_overlap(self, other: 'Domain') -> Tuple[int, int]: """Return [left,right) offset indicating overlap between this Domain and `other`. @@ -1734,21 +1750,6 @@ def insertion_offsets(self) -> List[int]: return [ins_off for (ins_off, _) in self.insertions] -''' - var forward = util.get_value(json_map, constants.forward_key, name); - var helix = util.get_value(json_map, constants.helix_idx_key, name); - var start = util.get_value(json_map, constants.start_key, name); - var end = util.get_value(json_map, constants.end_key, name); -// List deletions = -// json_map.containsKey(constants.deletions_key) ? List.from(json_map[constants.deletions_key]) : []; -// List> insertions = -// json_map.containsKey(constants.insertions_key) ? parse_json_insertions(json_map[constants.insertions_key]) : []; - var deletions = List.from(util.get_value_with_default(json_map, constants.deletions_key, [])); - var insertions = - parse_json_insertions(util.get_value_with_default(json_map, constants.insertions_key, [])); -''' - - @dataclass class Loopout(_JSONSerializable): """Represents a single-stranded loopout on a :any:`Strand`. @@ -1797,11 +1798,12 @@ class Loopout(_JSONSerializable): """ # not serialized; for efficiency - # remove quotes when Python 3.6 support dropped - _parent_strand: 'Strand' = field(init=False, repr=False, compare=False, default=None) + # remove quotes when Py3.6 support dropped + _parent_strand: Optional['Strand'] = field(init=False, repr=False, compare=False, default=None) - def to_json_serializable(self, suppress_indent: bool = True, **kwargs: dict) -> Union[dict, NoIndent]: - dct = {loopout_key: self.length} + def to_json_serializable(self, suppress_indent: bool = True, + **kwargs: Any) -> Union[Dict[str, Any], NoIndent]: + dct: Dict[str, Any] = {loopout_key: self.length} if self.name is not None: dct[domain_name_key] = self.name if self.label is not None: @@ -1809,7 +1811,7 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: dict) -> return NoIndent(dct) if suppress_indent else dct @staticmethod - def from_json(json_map: dict) -> 'Loopout': # remove quotes when Python 3.6 support dropped + def from_json(json_map: Dict[str, Any]) -> 'Loopout': # remove quotes when Py3.6 support dropped # XXX: this should never fail since we detect whether to call this from_json by the presence # of a length key in json_map length_str = mandatory_field(Loopout, json_map, loopout_key) @@ -1822,7 +1824,7 @@ def __repr__(self) -> str: return f'Loopout(' + \ (f'{self.name}, ' if self.name is not None else '') + \ f'{self.length}, ' + \ - ((f'{self.label}, ' if self.label is not None else '')) + \ + (f'{self.label}, ' if self.label is not None else '') + \ f')' def __str__(self) -> str: @@ -1836,19 +1838,19 @@ def set_label(self, label: Any) -> None: """Sets label of this :any:`Loopout`.""" self.label = label - @staticmethod - def is_loopout() -> bool: - """Indicates if this is a :any:`Loopout` (always true). - Useful when object could be either :any:`Loopout` or :any:`Domain`.""" - return True - - @staticmethod - def is_domain() -> bool: - """Indicates if this is a :any:`Domain` (always false) - Useful when object could be either :any:`Loopout` or :any:`Domain`.""" - return False - - def __len__(self): + # @staticmethod + # def is_loopout() -> bool: + # """Indicates if this is a :any:`Loopout` (always true). + # Useful when object could be either :any:`Loopout` or :any:`Domain`.""" + # return True + # + # @staticmethod + # def is_domain() -> bool: + # """Indicates if this is a :any:`Domain` (always false) + # Useful when object could be either :any:`Loopout` or :any:`Domain`.""" + # return False + + def __len__(self) -> int: """Same as :any:`Loopout.dna_length`""" return self.dna_length() @@ -1859,6 +1861,8 @@ def dna_length(self) -> int: def dna_sequence(self) -> Optional[str]: """Return DNA sequence of this :any:`Loopout`, or ``None`` if no DNA sequence has been assigned to the :any:`Strand` of this :any:`Loopout`.""" + if self._parent_strand is None: + raise ValueError('_parent_strand has not been set') strand_seq = self._parent_strand.dna_sequence if strand_seq is None: return None @@ -1871,6 +1875,8 @@ def dna_sequence(self) -> Optional[str]: def get_seq_start_idx(self) -> int: """Starting DNA subsequence index for first base of this :any:`Loopout` on its :any:`Strand`'s DNA sequence.""" + if self._parent_strand is None: + raise ValueError('_parent_strand has not been set') domains = self._parent_strand.domains # index of self in parent strand's list of domains self_domain_idx = domains.index(self) @@ -1931,7 +1937,7 @@ class IDTFields(_JSONSerializable): Optional field, but non-optional if :py:data:`IDTField.plate` is not ``None``. """ - def __post_init__(self): + def __post_init__(self) -> None: _check_idt_string_not_none_or_empty(self.name, 'name') _check_idt_string_not_none_or_empty(self.scale, 'scale') _check_idt_string_not_none_or_empty(self.purification, 'purification') @@ -1942,8 +1948,9 @@ def __post_init__(self): raise IllegalDesignError(f'IDTFields.well cannot be None if IDTFields.plate is not None\n' f'IDTFields.plate = {self.plate}') - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): - dct = self.__dict__ + def to_json_serializable(self, suppress_indent: bool = True, + **kwargs: Any) -> Union[NoIndent, Dict[str, Any]]: + dct: Dict[str, Any] = self.__dict__ if self.plate is None: del dct['plate'] if self.well is None: @@ -1951,7 +1958,7 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs): return NoIndent(dct) -def _check_idt_string_not_none_or_empty(value: str, field_name: str): +def _check_idt_string_not_none_or_empty(value: str, field_name: str) -> None: if value is None: raise IllegalDesignError(f'field {field_name} in IDTFields cannot be None') if len(value) == 0: @@ -1973,7 +1980,7 @@ class StrandBuilder: of method chaining described above, or else errors could result. """ - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def __init__(self, design: 'Design', helix: int, offset: int): self.design: Design = design self.current_helix: int = helix @@ -1986,10 +1993,10 @@ def __init__(self, design: 'Design', helix: int, offset: int): @property def strand(self) -> 'Strand': if self._strand is None: - raise ValueError('no strand has been created yet') + raise ValueError('no Strand created yet; make at least one domain first') return self._strand - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def cross(self, helix: int, offset: Optional[int] = None, move: Optional[int] = None) -> 'StrandBuilder': """ Add crossover. Must be followed by call to :py:meth:`StrandBuilder.to` to have any effect. @@ -2004,6 +2011,8 @@ def cross(self, helix: int, offset: Optional[int] = None, move: Optional[int] = Mutually excusive with `offset`. :return: self """ + if self._strand is None: + raise ValueError('no Strand created yet; make at least one domain first') if move is not None and offset is not None: raise IllegalDesignError('move and offset cannot both be specified:\n' f'move: {move}\n' @@ -2016,7 +2025,7 @@ def cross(self, helix: int, offset: Optional[int] = None, move: Optional[int] = self.current_offset += move return self - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def loopout(self, helix: int, length: int, offset: Optional[int] = None, move: Optional[int] = None) \ -> 'StrandBuilder': """ @@ -2033,11 +2042,13 @@ def loopout(self, helix: int, length: int, offset: Optional[int] = None, move: O Mutually excusive with `offset`. :return: self """ + if self._strand is None: + raise ValueError('no Strand created yet; make at least one domain first') self.cross(helix, offset=offset, move=move) self.design.append_domain(self._strand, Loopout(length)) return self - def move(self, delta: int) -> 'StrandBuilder': # remove quotes when Python 3.6 support dropped + def move(self, delta: int) -> 'StrandBuilder': # remove quotes when Py3.6 support dropped """ Extends this :any:`StrandBuilder` on the current helix to offset given by the current offset plus `delta`, which adds a new :any:`Domain` to the :any:`Strand` being built. This is a @@ -2059,7 +2070,7 @@ def move(self, delta: int) -> 'StrandBuilder': # remove quotes when Python 3.6 """ return self.to(self.current_offset + delta) - def to(self, offset: int) -> 'StrandBuilder': # remove quotes when Python 3.6 support dropped + def to(self, offset: int) -> 'StrandBuilder': # remove quotes when Py3.6 support dropped """ Extends this :any:`StrandBuilder` on the current helix to offset `offset`, which adds a new :any:`Domain` to the :any:`Strand` being built. This is an @@ -2110,7 +2121,7 @@ def to(self, offset: int) -> 'StrandBuilder': # remove quotes when Python 3.6 s return self - def update_to(self, offset: int) -> 'StrandBuilder': # remove quotes when Python 3.6 support dropped + def update_to(self, offset: int) -> 'StrandBuilder': # remove quotes when Py3.6 support dropped """ Like :py:meth:`StrandBuilder.to`, but changes the current offset without creating a new :any:`Domain`. So unlike :py:meth:`StrandBuilder.to`, several consecutive calls to @@ -2142,16 +2153,18 @@ def update_to(self, offset: int) -> 'StrandBuilder': # remove quotes when Pytho return self - def as_scaffold(self) -> 'StrandBuilder': # remove quotes when Python 3.6 support dropped + def as_scaffold(self) -> 'StrandBuilder': # remove quotes when Py3.6 support dropped """ Makes Strand being built a scaffold. :return: self """ + if self._strand is None: + raise ValueError('no Strand created yet; make at least one domain first') self._strand.set_scaffold(True) return self - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def with_modification_5p(self, mod: Modification5Prime) -> 'StrandBuilder': """ Sets Strand being built to have given 5' modification. @@ -2159,10 +2172,12 @@ def with_modification_5p(self, mod: Modification5Prime) -> 'StrandBuilder': :param mod: 5' modification :return: self """ + if self._strand is None: + raise ValueError('no Strand created yet; make at least one domain first') self._strand.set_modification_5p(mod) return self - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def with_modification_3p(self, mod: Modification3Prime) -> 'StrandBuilder': """ Sets Strand being built to have given 3' modification. @@ -2170,10 +2185,12 @@ def with_modification_3p(self, mod: Modification3Prime) -> 'StrandBuilder': :param mod: 3' modification :return: self """ + if self._strand is None: + raise ValueError('no Strand created yet; make at least one domain first') self._strand.set_modification_3p(mod) return self - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def with_modification_internal(self, idx: int, mod: ModificationInternal, warn_on_no_dna: bool) -> 'StrandBuilder': """ @@ -2184,10 +2201,12 @@ def with_modification_internal(self, idx: int, mod: ModificationInternal, :param warn_on_no_dna: whether to print warning to screen if DNA has not been assigned :return: self """ + if self._strand is None: + raise ValueError('no Strand created yet; make at least one domain first') self._strand.set_modification_internal(idx, mod, warn_on_no_dna) return self - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def with_color(self, color: Color) -> 'StrandBuilder': """ Sets Strand being built to have given color. @@ -2195,10 +2214,12 @@ def with_color(self, color: Color) -> 'StrandBuilder': :param color: color to set for Strand :return: self """ + if self._strand is None: + raise ValueError('no Strand created yet; make at least one domain first') self._strand.set_color(color) return self - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def with_sequence(self, sequence: str, assign_complement: bool = True) -> 'StrandBuilder': """ Assigns `sequence` as DNA sequence of the :any:`Strand` being built. @@ -2214,10 +2235,12 @@ def with_sequence(self, sequence: str, assign_complement: bool = True) -> 'Stran :py:meth:`Design.assign_dna`. :return: self """ + if self._strand is None: + raise ValueError('no Strand created yet; make at least one domain first') self.design.assign_dna(strand=self._strand, sequence=sequence, assign_complement=assign_complement) return self - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def with_domain_sequence(self, sequence: str, assign_complement: bool = True) -> 'StrandBuilder': """ Assigns `sequence` as DNA sequence of the most recently created :any:`Domain` in @@ -2241,12 +2264,14 @@ def with_domain_sequence(self, sequence: str, assign_complement: bool = True) -> :py:meth:`Design.assign_dna`. :return: self """ + if self._strand is None: + raise ValueError('no Strand created yet; make at least one domain first') last_domain = self._strand.domains[-1] self.design.assign_dna(strand=self._strand, sequence=sequence, domain=last_domain, assign_complement=assign_complement) return self - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def with_name(self, name: str) -> 'StrandBuilder': """ Assigns `name` as name of the :any:`Strand` being built. @@ -2259,11 +2284,11 @@ def with_name(self, name: str) -> 'StrandBuilder': :return: self """ if self._strand is None: - raise AssertionError('_strand cannot be None') + raise ValueError('no Strand created yet; make at least one domain first') self._strand.set_name(name) return self - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def with_label(self, label) -> 'StrandBuilder': # type: ignore """ Assigns `label` as label of the :any:`Strand` being built. @@ -2280,7 +2305,7 @@ def with_label(self, label) -> 'StrandBuilder': # type: ignore self._strand.set_label(label) return self - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def with_domain_name(self, name: str) -> 'StrandBuilder': """ Assigns `name` as of the most recently created :any:`Domain` or :any:`Loopout` in @@ -2299,12 +2324,12 @@ def with_domain_name(self, name: str) -> 'StrandBuilder': :return: self """ if self._strand is None: - raise AssertionError('_strand cannot be None') + raise ValueError('no Strand created yet; make at least one domain first') last_domain = self._strand.domains[-1] last_domain.set_name(name) return self - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped def with_domain_label(self, label) -> 'StrandBuilder': # type: ignore """ Assigns `label` as label of the most recently created :any:`Domain` or :any:`Loopout` in @@ -2326,7 +2351,7 @@ def with_domain_label(self, label) -> 'StrandBuilder': # type: ignore :return: self """ if self._strand is None: - raise AssertionError('_strand cannot be None') + raise ValueError('no Strand created yet; make at least one domain first') last_domain = self._strand.domains[-1] last_domain.set_label(label) return self @@ -2443,24 +2468,24 @@ class Strand(_JSONSerializable): # not serialized; efficient way to see a list of all domains on a given helix _helix_idx_domain_map: Dict[int, List[Domain]] = field( - init=False, repr=False, compare=False, default=None) + init=False, repr=False, compare=False, default_factory=dict) def __post_init__(self) -> None: self._helix_idx_domain_map = defaultdict(list) for domain in self.domains: - if domain.is_domain(): + if isinstance(domain, Domain): self._helix_idx_domain_map[domain.helix].append(domain) for domain in self.domains: domain._parent_strand = self if len(self.domains) == 1: - if self.first_domain().is_loopout(): + if isinstance(self.domains[0], Loopout): raise StrandError(self, 'strand cannot have a single Loopout as its only domain') for domain1, domain2 in _pairwise(self.domains): - if domain1.is_loopout() and domain2.is_loopout(): + if isinstance(domain1, Loopout) and isinstance(domain2, Loopout): raise StrandError(self, 'cannot have two consecutive Loopouts in a strand') if self.use_default_idt: @@ -2469,8 +2494,8 @@ def __post_init__(self) -> None: self._ensure_modifications_legal() self._ensure_domains_nonoverlapping() - def to_json_serializable(self, suppress_indent: bool = True, **kwargs) -> dict: - dct = OrderedDict() + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Dict[str, Any]: + dct: Dict[str, Any] = OrderedDict() if self.name is not None: dct[strand_name_key] = self.name if self.color is not None: @@ -2499,12 +2524,12 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs) -> dict: return dct @staticmethod - def from_json(json_map: dict) -> 'Strand': # remove quotes when Python 3.6 support dropped + def from_json(json_map: dict) -> 'Strand': # remove quotes when Py3.6 support dropped domain_jsons = mandatory_field(Strand, json_map, domains_key, *legacy_domains_keys) if len(domain_jsons) == 0: raise IllegalDesignError(f'{domains_key} list cannot be empty') - domains = [] + domains: List[Union[Domain, Loopout]] = [] for domain_json in domain_jsons: if loopout_key in domain_json: domains.append(Loopout.from_json(domain_json)) @@ -2524,7 +2549,7 @@ def from_json(json_map: dict) -> 'Strand': # remove quotes when Python 3.6 supp default_scaffold_color if is_scaffold else default_strand_color) if isinstance(color_str, int): def decimal_int_to_hex(d: int) -> str: - return "#" + "{0:#08x}".format(d, 8)[2:] + return "#" + "{0:#08x}".format(d, 8)[2:] # type: ignore color_str = decimal_int_to_hex(color_str) color = Color(hex_string=color_str) @@ -2542,7 +2567,7 @@ def decimal_int_to_hex(d: int) -> str: label=label, ) - def __eq__(self, other: 'Strand') -> bool: # remove quotes when Python 3.6 support dropped + def __eq__(self, other: Any) -> bool: # remove quotes when Py3.6 support dropped if not isinstance(other, Strand): return False return self.domains == other.domains @@ -2588,7 +2613,7 @@ def set_name(self, name: str) -> None: """Sets name of this :any:`Strand`.""" self.name = name - def set_label(self, label) -> None: + def set_label(self, label: Any) -> None: """Sets label of this :any:`Strand`.""" self.label = label @@ -2610,23 +2635,24 @@ def set_default_idt(self, use_default_idt: bool = True, skip_scaffold: bool = Tr else: self.idt = None - def set_modification_5p(self, mod: Modification5Prime = None): + def set_modification_5p(self, mod: Modification5Prime = None) -> None: """Sets 5' modification to be `mod`.""" self.modification_5p = mod - def set_modification_3p(self, mod: Modification3Prime = None): + def set_modification_3p(self, mod: Modification3Prime = None) -> None: """Sets 3' modification to be `mod`.""" self.modification_3p = mod - def remove_modification_5p(self): + def remove_modification_5p(self) -> None: """Removes 5' modification.""" self.modification_5p = None - def remove_modification_3p(self): + def remove_modification_3p(self) -> None: """Removes 3' modification.""" self.modification_3p = None - def set_modification_internal(self, idx: int, mod: ModificationInternal, warn_on_no_dna: bool = True): + def set_modification_internal(self, idx: int, mod: ModificationInternal, + warn_on_no_dna: bool = True) -> None: """Adds internal modification `mod` at given DNA index `idx`.""" if idx < 0: raise IllegalDesignError('idx of modification must be nonnegative') @@ -2644,20 +2670,26 @@ def set_modification_internal(self, idx: int, mod: ModificationInternal, warn_on 'modification were not done. To be safe, first assign DNA, then add the modifications.') self.modifications_int[idx] = mod - def remove_modification_internal(self, idx: int): + def remove_modification_internal(self, idx: int) -> None: """Removes internal modification at given DNA index `idx`.""" if idx in self.modifications_int: del self.modifications_int[idx] - def first_domain(self) -> Union[Domain, Loopout]: - """First domain (of type either :any:`Domain` or :any:`Loopout`) on this :any:`Strand`.""" - return self.domains[0] - - def last_domain(self) -> Union[Domain, Loopout]: - """Last domain (of type either :any:`Domain` or :any:`Loopout`) on this :any:`Strand`.""" - return self.domains[-1] - - def set_dna_sequence(self, sequence: str): + def first_domain(self) -> Domain: + """First domain on this :any:`Strand`.""" + domain = self.domains[0] + if isinstance(domain, Loopout): + raise StrandError(self, 'cannot have loopout as first domain on strand') + return domain + + def last_domain(self) -> Domain: + """Last domain on this :any:`Strand`.""" + domain = self.domains[-1] + if isinstance(domain, Loopout): + raise StrandError(self, 'cannot have loopout as last domain on strand') + return domain + + def set_dna_sequence(self, sequence: str) -> None: """Set this :any:`Strand`'s DNA sequence to `seq` WITHOUT checking for complementarity with overlapping :any:`Strand`'s or automatically assigning their sequences. @@ -2689,7 +2721,7 @@ def dna_length(self) -> int: def bound_domains(self) -> List[Domain]: """:any:`Domain`'s of this :any:`Strand` that are not :any:`Loopout`'s.""" - return [domain for domain in self.domains if domain.is_domain()] + return [domain for domain in self.domains if isinstance(domain, Domain)] def offset_5p(self) -> int: """5' offset of this entire :any:`Strand`, INCLUSIVE.""" @@ -2699,7 +2731,7 @@ def offset_3p(self) -> int: """3' offset of this entire :any:`Strand`, INCLUSIVE.""" return self.last_domain().offset_3p() - def overlaps(self, other: 'Strand') -> bool: # remove quotes when Python 3.6 support dropped + def overlaps(self, other: 'Strand') -> bool: # remove quotes when Py3.6 support dropped """Indicates whether `self` overlaps `other_strand`, meaning that the set of offsets occupied by `self` has nonempty intersection with those occupied by `other_strand`.""" for domain_self in self.bound_domains(): @@ -2708,7 +2740,7 @@ def overlaps(self, other: 'Strand') -> bool: # remove quotes when Python 3.6 su return True return False - def assign_dna_complement_from(self, other: 'Strand'): # remove quotes when Python 3.6 support dropped + def assign_dna_complement_from(self, other: 'Strand') -> None: # remove quotes when Py3.6 support dropped """Assuming a DNA sequence has been assigned to `other`, assign its Watson-Crick complement to the portions of this Strand that are bound to `other`. @@ -2728,17 +2760,20 @@ def assign_dna_complement_from(self, other: 'Strand'): # remove quotes when Pyt already_assigned = self.dna_sequence is not None # put DNA sequences to assign to domains in List, one position per domain - strand_complement_builder = [] + strand_complement_builder: List[str] = [] if already_assigned: for domain in self.domains: - strand_complement_builder.append(domain.dna_sequence()) + domain_seq = domain.dna_sequence() + if domain_seq is None: + raise ValueError(f'no DNA sequence has been assigned to {self}') + strand_complement_builder.append(domain_seq) else: for domain in self.domains: wildcards = DNA_base_wildcard * domain.dna_length() strand_complement_builder.append(wildcards) for (domain_idx, domain_self) in enumerate(self.domains): - if domain_self.is_loopout(): + if isinstance(domain_self, Loopout): domain_self_dna_sequence = DNA_base_wildcard * domain_self.dna_length() else: helix = domain_self.helix @@ -2763,6 +2798,8 @@ def assign_dna_complement_from(self, other: 'Strand'): # remove quotes when Pyt wildcards = DNA_base_wildcard * num_wildcard_bases other_seq = domain_other.dna_sequence_in(overlap_left, overlap_right - 1) + if other_seq is None: + raise ValueError(f'no DNA sequence has been assigned to strand {other}') overlap_complement = wc(other_seq) domain_complement_builder.append(wildcards) domain_complement_builder.append(overlap_complement) @@ -2813,11 +2850,11 @@ def assign_dna_complement_from(self, other: 'Strand'): # remove quotes when Pyt self.set_dna_sequence(new_dna_sequence) # self.dna_sequence = _pad_dna(new_dna_sequence, self.dna_length()) - def insert_domain(self, order, domain): + def insert_domain(self, order: int, domain: Union[Domain, Loopout]) -> None: # Only intended to be called by Design.insert_domain self.domains.insert(order, domain) domain._parent_strand = self - if domain.is_domain(): + if isinstance(domain, Domain): self._helix_idx_domain_map[domain.helix].append(domain) if self.use_default_idt: self.set_default_idt() @@ -2831,7 +2868,7 @@ def insert_domain(self, order, domain): new_wildcards = DNA_base_wildcard * (end_idx - start_idx) self.dna_sequence = prefix + new_wildcards + suffix - def remove_domain(self, domain: Union[Domain, Loopout]): + def remove_domain(self, domain: Union[Domain, Loopout]) -> None: # Only intended to be called by Design.remove_domain # remove relevant portion of DNA sequence to maintain its length @@ -2844,12 +2881,12 @@ def remove_domain(self, domain: Union[Domain, Loopout]): self.domains.remove(domain) domain._parent_strand = None - if domain.is_domain(): + if isinstance(domain, Domain): self._helix_idx_domain_map[domain.helix].remove(domain) if self.use_default_idt: self.set_default_idt() - def dna_index_start_domain(self, domain: Domain): + def dna_index_start_domain(self, domain: Union[Domain, Loopout]) -> int: """ Returns index in DNA sequence of domain, e.g., if there are five domains @@ -2867,7 +2904,7 @@ def dna_index_start_domain(self, domain: Domain): def contains_loopouts(self) -> bool: for domain in self.domains: - if domain.is_loopout(): + if isinstance(domain, Loopout): return True return False @@ -2878,8 +2915,9 @@ def first_bound_domain(self) -> Domain: domain as :py:meth:`Strand.first_domain`, but in case an initial or final :any:`Loopout` is supported in the future, this method is provided.""" for domain in self.domains: - if domain.is_domain(): + if isinstance(domain, Domain): return domain + raise StrandError(self, 'should not be able to have a Strand with no (bound) Domains') def last_bound_domain(self) -> Domain: """Last :any:`Domain` (i.e., not a :any:`Loopout`) on this :any:`Strand`. @@ -2890,10 +2928,11 @@ def last_bound_domain(self) -> Domain: domain_rev = list(self.domains) domain_rev.reverse() for domain in domain_rev: - if domain.is_domain(): + if isinstance(domain, Domain): return domain + raise AssertionError('should not be able to have a Strand with no (bound) Domains') - def reverse(self): + def reverse(self) -> None: """ Reverses "polarity" of this :any:`Strand`. @@ -2905,7 +2944,7 @@ def reverse(self): for domain in self.bound_domains(): domain.forward = not domain.forward - def _ensure_modifications_legal(self, check_offsets_legal=False): + def _ensure_modifications_legal(self, check_offsets_legal: bool = False) -> None: if check_offsets_legal: if self.dna_sequence is None: raise IllegalDesignError(f"must assign DNA sequence first") @@ -2920,50 +2959,57 @@ def _ensure_modifications_legal(self, check_offsets_legal=False): f"{len(self.dna_sequence)}: " f"{self.modifications_int}") - def _ensure_domains_nonoverlapping(self): + def _ensure_domains_nonoverlapping(self) -> None: for d1, d2 in itertools.combinations(self.domains, 2): if isinstance(d1, Domain) and isinstance(d2, Domain) and d1.overlaps_illegally(d2): raise StrandError(self, f'two domains on strand overlap:' f'\n{d1}' f'\n{d2}') - def idt_dna_sequence(self): + def idt_dna_sequence(self) -> str: self._ensure_modifications_legal(check_offsets_legal=True) - ret_list = [] - if self.modification_5p is not None: + if self.dna_sequence is None: + raise ValueError('DNA sequence has not been assigned yet') + + ret_list: List[str] = [] + if self.modification_5p is not None and self.modification_5p.idt_text is not None: ret_list.append(self.modification_5p.idt_text) for offset, base in enumerate(self.dna_sequence): ret_list.append(base) if offset in self.modifications_int: # if internal mod attached to base, replace base mod = self.modifications_int[offset] - if mod.allowed_bases is not None: - if base not in mod.allowed_bases: - msg = f'internal modification {mod} can only replace one of these bases: ' \ - f'{",".join(mod.allowed_bases)}, but the base at offset {offset} is {base}' - raise IllegalDesignError(msg) - ret_list[-1] = mod.idt_text # replace base with modified base - else: - ret_list.append(mod.idt_text) # append modification between two bases + if mod.idt_text is not None: + if mod.allowed_bases is not None: + if base not in mod.allowed_bases: + msg = f'internal modification {mod} can only replace one of these bases: ' \ + f'{",".join(mod.allowed_bases)}, but the base at offset {offset} is {base}' + raise IllegalDesignError(msg) + ret_list[-1] = mod.idt_text # replace base with modified base + else: + ret_list.append(mod.idt_text) # append modification between two bases - if self.modification_3p is not None: + if self.modification_3p is not None and self.modification_3p.idt_text is not None: ret_list.append(self.modification_3p.idt_text) return ''.join(ret_list) - def unmodified_version(self): + def no_modifications_version(self) -> 'Strand': + """ + :return: version of this :any:`Strand` with no DNA modifications. + """ strand_nomods = replace(self, modification_3p=None, modification_5p=None, modifications_int={}) return strand_nomods -def _pad_and_remove_whitespace_and_uppercase(sequence: str, strand: Strand, start: int = 0): +def _pad_and_remove_whitespace_and_uppercase(sequence: str, strand: Strand, start: int = 0) -> str: sequence = _remove_whitespace_and_uppercase(sequence) padded_sequence = _pad_dna(sequence, strand.dna_length(), start) return padded_sequence -def _remove_whitespace_and_uppercase(sequence): +def _remove_whitespace_and_uppercase(sequence: str) -> str: sequence = re.sub(r'\s*', '', sequence) sequence = sequence.upper() return sequence @@ -3022,11 +3068,11 @@ def _string_merge_wildcard(s1: str, s2: str, wildcard: str) -> str: class IllegalDesignError(ValueError): """Indicates that some aspect of the :any:`Design` object is illegal.""" - def __init__(self, the_cause: str): + def __init__(self, the_cause: str) -> None: self.cause = the_cause # __str__ is to print() the value - def __str__(self): + def __str__(self) -> str: return repr(self.cause) @@ -3035,29 +3081,38 @@ class StrandError(IllegalDesignError): Information about the :any:`Strand` is embedded in the error message when this exception is raised that helps to identify which :any:`Strand` caused the problem.""" - def __init__(self, strand: Strand, the_cause: str): - first_domain = strand.first_bound_domain() - last_domain = strand.last_bound_domain() + def __init__(self, strand: Strand, the_cause: str) -> None: + # need to avoid calling first_bound_domain here to avoid infinite mutual recursion + first_domain: Optional[Domain] + last_domain: Optional[Domain] + if len(strand.domains) > 0 and isinstance(strand.domains[0], Domain): + first_domain = strand.domains[0] + else: + first_domain = None + if len(strand.domains) > 0 and isinstance(strand.domains[-1], Domain): + last_domain = strand.domains[-1] + else: + last_domain = None msg = (f'''{the_cause} strand length = {strand.dna_length()} DNA length = {len(strand.dna_sequence) if strand.dna_sequence else "N/A"} DNA sequence = {strand.dna_sequence} - strand 5' helix = {first_domain.helix if first_domain else 'N/A'} - strand 5' end offset = {first_domain.offset_5p() if first_domain else 'N/A'} - strand 3' helix = {last_domain.helix if last_domain else 'N/A'} - strand 3' end offset = {last_domain.offset_3p() if last_domain else 'N/A'}\n''') + strand 5' helix = {first_domain.helix if first_domain is not None else 'N/A'} + strand 5' end offset = {first_domain.offset_5p() if first_domain is not None else 'N/A'} + strand 3' helix = {last_domain.helix if last_domain is not None else 'N/A'} + strand 3' end offset = {last_domain.offset_3p() if last_domain is not None else 'N/A'}\n''') super().__init__(msg) # super(IllegalDesignError, self).__init__(msg) -def _plates(idt_strands): - plates = set() - for strand in idt_strands: - if strand.idt is not None and strand.idt.plate is not None: - plates.add(strand.idt.plate) - return list(plates) +# def _plates(idt_strands) -> List[str]: +# plates: Set[str] = set() +# for strand in idt_strands: +# if strand.idt is not None and strand.idt.plate is not None: +# plates.add(strand.idt.plate) +# return list(plates) _96WELL_PLATE_ROWS: List[str] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] @@ -3088,13 +3143,13 @@ def cols(self) -> List[int]: class _PlateCoordinate: - def __init__(self, plate_type: PlateType): + def __init__(self, plate_type: PlateType) -> None: self._plate_type = plate_type self._plate: int = 1 self._row_idx: int = 0 self._col_idx: int = 0 - def increment(self): + def increment(self) -> None: self._row_idx += 1 if self._row_idx == len(self._plate_type.rows()): self._row_idx = 0 @@ -3116,7 +3171,7 @@ def well(self) -> str: return f'{self.row()}{self.col()}' -def remove_helix_idxs_if_default(helices: List[Dict]): +def remove_helix_idxs_if_default(helices: List[Dict]) -> None: # removes indices from each helix if they are the default (order of appearance in list) default = True for expected_idx, helix in enumerate(helices): @@ -3135,7 +3190,7 @@ def add_quotes(string: str) -> str: return f'"{string}"' -def mandatory_field(ret_type: Type, json_map: dict, main_key: str, *legacy_keys: str): +def mandatory_field(ret_type: Type, json_map: Dict[str, Any], main_key: str, *legacy_keys: str) -> Any: # should be called from function whose return type is the type being constructed from JSON, e.g., # Design or Strand, given by ret_type. This helps give a useful error message for key in (main_key,) + legacy_keys: @@ -3151,7 +3206,7 @@ def mandatory_field(ret_type: Type, json_map: dict, main_key: str, *legacy_keys: raise IllegalDesignError(msg) -def optional_field(default_value, json_map: dict, main_key: str, *legacy_keys: str): +def optional_field(default_value: Any, json_map: Dict[str, Any], main_key: str, *legacy_keys: str) -> Any: # like dict.get, except that it checks for multiple keys for key in (main_key,) + legacy_keys: if key in json_map: @@ -3181,11 +3236,11 @@ class Geometry(_JSONSerializable): def distance_between_helices(self) -> float: return 2 * self.helix_radius + self.inter_helix_gap - def is_default(self): + def is_default(self) -> bool: return self == _default_geometry @staticmethod - def from_json(json_map: dict) -> 'Geometry': # remove quotes when Python 3.6 support dropped + def from_json(json_map: dict) -> 'Geometry': # remove quotes when Py3.6 support dropped geometry = Geometry() geometry.rise_per_base_pair = optional_field(_default_geometry.rise_per_base_pair, json_map, rise_per_base_pair_key, *legacy_rise_per_base_pair_keys) @@ -3211,8 +3266,8 @@ def values(self) -> List[float]: def default_values() -> List[float]: return _default_geometry.values() - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): - dct = OrderedDict() + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Dict[str, Any]: + dct: Dict[str, Any] = OrderedDict() for name, val, val_default in zip(Geometry.keys(), self.values(), Geometry.default_values()): if val != val_default: dct[name] = val @@ -3232,7 +3287,7 @@ def _check_helices_view_order_and_return( return helices_view_order -def _check_helices_grid_legal(grid: Grid, helices: Iterable[Helix]): +def _check_helices_grid_legal(grid: Grid, helices: Iterable[Helix]) -> None: for helix in helices: if grid == Grid.none and helix.grid_position is not None: raise IllegalDesignError( @@ -3242,7 +3297,7 @@ def _check_helices_grid_legal(grid: Grid, helices: Iterable[Helix]): f'grid is not none, but Helix {helix.idx} has position = ${helix.position}') -def _check_helices_view_order_is_bijection(helices_view_order: List[int], helix_idxs: Iterable[int]): +def _check_helices_view_order_is_bijection(helices_view_order: List[int], helix_idxs: Iterable[int]) -> None: if not (sorted(helices_view_order) == sorted(helix_idxs)): raise IllegalDesignError( f"The specified helices view order: {helices_view_order}\n " @@ -3288,7 +3343,7 @@ def __init__(self, *, strands: List[Strand] = None, grid: Optional[Grid] = None, helices_view_order: List[int] = None, - geometry: Geometry = None): + geometry: Geometry = None) -> None: """ :param helices: List of :any:`Helix`'s; if missing, set based on `strands`. :param groups: Dict mapping group name to :any:`HelixGroup`. @@ -3297,9 +3352,6 @@ def __init__(self, *, keys of this dict. :param strands: List of :any:`Strand`'s. If missing, will be empty. :param grid: :any:`Grid` to use. - :param major_tick_distance: regularly spaced major ticks between all helices. - :any:`Helix.major_tick_distance` overrides this value for any :any:`Helix` in which it is - specified. :param helices_view_order: order in which to view helices from top to bottom in web interface main view. Mutually exclusive with `groups`. @@ -3365,14 +3417,15 @@ def __init__(self, *, grid_for_group = group.grid group.helices_view_order = _check_helices_view_order_and_return(helices_view_order_for_group, helix_idxs_in_group) - if grid_for_group is None: raise AssertionError() + if grid_for_group is None: + raise AssertionError() group.grid = grid_for_group helices_in_group = [self.helices[idx] for idx in helix_idxs_in_group] _check_helices_grid_legal(group.grid, helices_in_group) self.__post_init__() - def __post_init__(self): + def __post_init__(self) -> None: # XXX: exact order of these calls is important self._ensure_helices_distinct_objects() self._ensure_strands_distinct_objects() @@ -3395,6 +3448,8 @@ def helices_view_order(self) -> List[int]: :return: helices_view_order of this :any:`Design` """ group = self._get_default_group() + if group.helices_view_order is None: + raise ValueError(f'group {group} does not have helices_view_order defined') return group.helices_view_order @property @@ -3408,7 +3463,7 @@ def grid(self) -> Grid: group = self._get_default_group() return group.grid - def _get_default_group(self): + def _get_default_group(self) -> HelixGroup: # Gets default group and raise exception if default group is not being used if not self._has_default_groups(): raise ValueError('The default group is not being used for this design.') @@ -3427,13 +3482,13 @@ def helices_idxs_in_group(self, group_name: str) -> List[int]: """ return [idx for idx, helix in self.helices.items() if helix.group == group_name] - def _assign_colors_to_strands(self): + def _assign_colors_to_strands(self) -> None: # if color not specified, pick one by cycling through list of staple colors, # unless caller specified not to for strand in self.strands: self._assign_color_to_strand(strand) - def _assign_color_to_strand(self, strand: Strand): + def _assign_color_to_strand(self, strand: Strand) -> None: if strand.color is None and self.automatically_assign_color: if strand.is_scaffold: strand.color = default_scaffold_color @@ -3441,7 +3496,7 @@ def _assign_color_to_strand(self, strand: Strand): strand.color = next(self.color_cycler) @staticmethod - def from_scadnano_file(filename: str) -> 'Design': # remove quotes when Python 3.6 support dropped + def from_scadnano_file(filename: str) -> 'Design': # remove quotes when Py3.6 support dropped """ Loads a :any:`Design` from the file with the given name. @@ -3453,7 +3508,7 @@ def from_scadnano_file(filename: str) -> 'Design': # remove quotes when Python return Design.from_scadnano_json_str(json_str) @staticmethod - def from_scadnano_json_str(json_str: str) -> 'Design': # remove quotes when Python 3.6 support dropped + def from_scadnano_json_str(json_str: str) -> 'Design': # remove quotes when Py3.6 support dropped """ Loads a :any:`Design` from the given JSON string. @@ -3468,7 +3523,7 @@ def from_scadnano_json_str(json_str: str) -> 'Design': # remove quotes when Pyt raise IllegalDesignError(f'I was expecting a JSON key but did not find it: {e}') @staticmethod - def _check_mutually_exclusive_fields(json_map: dict): + def _check_mutually_exclusive_fields(json_map: dict) -> None: exclusive_pairs = [ (grid_key, groups_key), (helices_view_order_key, groups_key), @@ -3479,7 +3534,7 @@ def _check_mutually_exclusive_fields(json_map: dict): @staticmethod def from_scadnano_json_map( - json_map: dict) -> 'Design': # remove quotes when Python 3.6 support dropped + json_map: dict) -> 'Design': # remove quotes when Py3.6 support dropped """ Loads a :any:`Design` from the given JSON object (i.e., Python object obtained by calling json.loads(json_str) from a string representing contents of a JSON file. @@ -3564,7 +3619,7 @@ def from_scadnano_json_map( geometry=geometry, ) - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Dict[str, Any]: dct: Any = OrderedDict() dct[version_key] = __version__ @@ -3616,9 +3671,11 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs): dct[strands_key] = [strand.to_json_serializable(suppress_indent) for strand in self.strands] for helix_list_order, helix in enumerate(self.helices.values()): - helix_json = dct[helices_key][helix_list_order] - if suppress_indent and hasattr(helix_json, 'value'): - helix_json = helix_json.value # get past NoIndent surrounding helix, if it is there + helix_json_maybe: Union[NoIndent, Dict[str, Any]] = dct[helices_key][helix_list_order] + if isinstance(helix_json_maybe, NoIndent): + helix_json = helix_json_maybe.value # get past NoIndent surrounding helix, if it is there + else: + helix_json = helix_json_maybe # XXX: no need to check here because key was already deleted by Helix.to_json_serializable # max_offset still needs to be checked here since it requires global knowledge of Strands # if 0 == helix_json[min_offset_key]: @@ -3640,7 +3697,7 @@ def scaffold(self) -> Optional[Strand]: @staticmethod def _normalize_helices_as_dict(helices: Union[List[Helix], Dict[int, Helix]]) -> Dict[int, Helix]: - def idx_of(helix: Helix, order: int): + def idx_of(helix: Helix, order: int) -> int: return order if helix.idx is None else helix.idx if isinstance(helices, list): @@ -3659,24 +3716,24 @@ def idx_of(helix: Helix, order: int): @staticmethod def assign_modifications_to_strands(strands: List[Strand], strand_jsons: List[dict], - all_mods: Dict[str, Modification]): + all_mods: Dict[str, Modification]) -> None: for strand, strand_json in zip(strands, strand_jsons): if modification_5p_key in strand_json: mod_name = strand_json[modification_5p_key] - strand.modification_5p = all_mods[mod_name] + strand.modification_5p = cast(Modification5Prime, all_mods[mod_name]) if modification_3p_key in strand_json: mod_name = strand_json[modification_3p_key] - strand.modification_3p = all_mods[mod_name] + strand.modification_3p = cast(Modification3Prime, all_mods[mod_name]) if modifications_int_key in strand_json: mod_names_by_offset = strand_json[modifications_int_key] for offset_str, mod_name in mod_names_by_offset.items(): offset = int(offset_str) - strand.modifications_int[offset] = all_mods[mod_name] + strand.modifications_int[offset] = cast(ModificationInternal, all_mods[mod_name]) @staticmethod - def _cadnano_v2_import_find_5_end(vstrands, strand_type: str, helix_num: int, base_id: int, + def _cadnano_v2_import_find_5_end(vstrands: VStrands, strand_type: str, helix_num: int, base_id: int, id_from: int, - base_from: int): + base_from: int) -> Tuple[int, int]: """ Routine which finds the 5' end of a strand in a cadnano v2 import. It returns the helix and the base of the 5' end. """ @@ -3689,11 +3746,13 @@ def _cadnano_v2_import_find_5_end(vstrands, strand_type: str, helix_num: int, ba return id_from_before, base_from_before @staticmethod - def _cadnano_v2_import_find_strand_color(vstrands, strand_type: str, strand_5_end_base: int, - strand_5_end_helix: int): - """ Routines which finds the color of a cadnano v2 strand. """ - color = default_scaffold_color + def _cadnano_v2_import_find_strand_color(vstrands: VStrands, strand_type: str, strand_5_end_base: int, + strand_5_end_helix: int) -> Color: + """Routine that finds the color of a cadnano v2 strand.""" + color: Color = default_scaffold_color if strand_type == 'stap': + base_id: int + stap_color: int for base_id, stap_color in vstrands[strand_5_end_helix]['stap_colors']: if base_id == strand_5_end_base: color = Color.from_cadnano_v2_int_hex(stap_color) @@ -3701,30 +3760,33 @@ def _cadnano_v2_import_find_strand_color(vstrands, strand_type: str, strand_5_en return color @staticmethod - def _cadnano_v2_import_extract_deletions(skip_table, start, end): + def _cadnano_v2_import_extract_deletions(skip_table: Dict[int, Any], start: int, end: int) -> List[int]: """ Routines which converts cadnano skips to scadnano deletions """ - to_return = [] + to_return: List[int] = [] for base_id in range(start, end): if skip_table[base_id] == -1: to_return.append(base_id) return to_return @staticmethod - def _cadnano_v2_import_extract_insertions(loop_table, start, end): + def _cadnano_v2_import_extract_insertions(loop_table: Dict[int, Any], + start: int, end: int) -> List[Tuple[int, int]]: """ Routines which converts cadnano skips to scadnano insertions """ - to_return = [] + to_return: List[Tuple[int, int]] = [] for base_id in range(start, end): if loop_table[base_id] != 0: - to_return.append([base_id, loop_table[base_id]]) + to_return.append((base_id, loop_table[base_id])) return to_return @staticmethod - def _cadnano_v2_import_explore_domains(vstrands, seen, strand_type: str, strand_5_end_base: int, - strand_5_end_helix: int): + def _cadnano_v2_import_explore_domains(vstrands: VStrands, seen: Dict[Tuple[int, int], bool], + strand_type: str, + strand_5_end_base: int, + strand_5_end_helix: int) -> List[Domain]: """Finds all domains of a cadnano v2 strand. """ curr_helix = strand_5_end_helix curr_base = strand_5_end_base - domains = [] + domains: List[Domain] = [] direction_forward = (strand_type == 'scaf' and curr_helix % 2 == 0) or ( (strand_type == 'stap' and curr_helix % 2 == 1)) @@ -3766,9 +3828,10 @@ def _cadnano_v2_import_explore_domains(vstrands, seen, strand_type: str, strand_ return domains @staticmethod - def _cadnano_v2_import_explore_strand(vstrands, strand_type: str, seen, + def _cadnano_v2_import_explore_strand(vstrands: VStrands, + strand_type: str, seen: Dict[Tuple[int, int], bool], helix_num: int, - base_id: int): + base_id: int) -> Optional[Strand]: """ Routine that will follow a cadnano v2 strand accross helices and create cadnano domains and strand accordingly. """ @@ -3788,27 +3851,32 @@ def _cadnano_v2_import_explore_strand(vstrands, strand_type: str, seen, strand_color = Design._cadnano_v2_import_find_strand_color(vstrands, strand_type, strand_5_end_base, strand_5_end_helix) - domains = Design._cadnano_v2_import_explore_domains(vstrands, seen, strand_type, - strand_5_end_base, - strand_5_end_helix) - strand = Strand(domains=domains, is_scaffold=(strand_type == 'scaf'), color=strand_color) + domains: Sequence[Domain] = Design._cadnano_v2_import_explore_domains(vstrands, seen, strand_type, + strand_5_end_base, + strand_5_end_helix) + domains_loopouts = cast(List[Union[Domain, Loopout]], # noqa + domains) # type: ignore + strand = Strand(domains=domains_loopouts, is_scaffold=(strand_type == 'scaf'), color=strand_color) return strand - # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped @staticmethod - def from_cadnano_v2(directory: str = None, filename: str = None, json_dict: dict = None) -> 'Design': + def from_cadnano_v2(directory: Optional[str] = None, filename: Optional[str] = None, + json_dict: Optional[dict] = None) -> 'Design': """ Creates a Design from a cadnano v2 file. """ - if json_dict is None: + if json_dict is None and filename is not None and directory is not None: file_path = os.path.join(directory, filename) f = open(file_path, 'r') cadnano_v2_design = json.load(f) f.close() - else: + elif json_dict is not None: cadnano_v2_design = json_dict + else: + raise ValueError('must have json_dict None and filename/directory not None, or vice versa') num_bases = len(cadnano_v2_design['vstrands'][0]['scaf']) grid_type = Grid.square @@ -3831,7 +3899,7 @@ def from_cadnano_v2(directory: str = None, filename: str = None, json_dict: dict helices[num] = helix # We do a DFS on strands - seen = {'scaf': {}, 'stap': {}} + seen: Dict[str, dict] = {'scaf': {}, 'stap': {}} strands: List[Strand] = [] cadnano_helices = OrderedDict({}) for cadnano_helix in cadnano_v2_design['vstrands']: @@ -3930,7 +3998,7 @@ def strand(self, helix: int, offset: int) -> StrandBuilder: """ return StrandBuilder(self, helix, offset) - def assign_m13_to_scaffold(self, rotation: int = 5587, variant: M13Variant = M13Variant.p7249): + def assign_m13_to_scaffold(self, rotation: int = 5587, variant: M13Variant = M13Variant.p7249) -> None: """Assigns the scaffold to be the sequence of M13: :py:func:`m13` with the given `rotation`. Raises :any:`IllegalDesignError` if the number of scaffolds is not exactly 1. @@ -3944,13 +4012,14 @@ def assign_m13_to_scaffold(self, rotation: int = 5587, variant: M13Variant = M13 scaffold = strand if num_scafs == 0: raise IllegalDesignError( - 'Tried to assign DNA to scaffold, but there is no scaffold strand. ' + 'Tried to assign DNA to scaffold, but there is no scaffold strand.\n' 'You must set strand.is_scaffold to True for exactly one strand.') elif num_scafs > 1: raise IllegalDesignError( - 'Tried to assign DNA to scaffold, but there are multiple scaffold ' - 'strands. You must set strand.is_scaffold to True for exactly one ' - 'strand.') + 'Tried to assign DNA to scaffold, but there are multiple scaffold strands.\n' + 'You must set strand.is_scaffold to True for exactly one strand.') + if scaffold is None: + raise AssertionError('we counted; there is exactly one scaffold') self.assign_dna(scaffold, m13(rotation, variant)) @staticmethod @@ -3958,7 +4027,7 @@ def _get_multiple_of_x_sup_closest_to_y(x: int, y: int) -> int: return y if y % x == 0 else y + (x - y % x) @staticmethod - def _cadnano_v2_place_strand_segment(helix_dct, domain: Domain, + def _cadnano_v2_place_strand_segment(helix_dct: Dict[str, Any], domain: Domain, strand_type: str = 'scaf') -> None: """Converts a strand region with no crossover to cadnano v2. """ @@ -3994,7 +4063,7 @@ def _cadnano_v2_place_strand_segment(helix_dct, domain: Domain, return @staticmethod - def _cadnano_v2_place_crossover(helix_from_dct: dict, helix_to_dct: dict, + def _cadnano_v2_place_crossover(helix_from_dct: Dict[str, Any], helix_to_dct: Dict[str, Any], domain_from: Domain, domain_to: Domain, strand_type: str = 'scaf') -> None: """Converts a crossover to cadnano v2 format. @@ -4030,34 +4099,45 @@ def _cadnano_v2_place_strand(self, strand: Strand, dct: dict, strand_type = 'scaf' for i, domain in enumerate(strand.domains): + if isinstance(domain, Loopout): + raise ValueError(f'cannot convert Strand {strand} to cadnanov2 format, since it has Loopouts') + which_helix_id = helices_ids_reverse[domain.helix] which_helix = dct['vstrands'][which_helix_id] if strand_type == 'stap': - which_helix['stap_colors'].append(self._cadnano_v2_color_of_stap(strand.color, domain)) + color = strand.color if strand.color is not None else Color(0, 0, 0) + which_helix['stap_colors'].append(self._cadnano_v2_color_of_stap(color, domain)) self._cadnano_v2_place_strand_segment(which_helix, domain, strand_type) if i != len(strand.domains) - 1: next_domain = strand.domains[i + 1] + if isinstance(next_domain, Loopout): + raise ValueError( + f'cannot convert Strand {strand} to cadnanov2 format, since it has Loopouts') next_helix_id = helices_ids_reverse[next_domain.helix] next_helix = dct['vstrands'][next_helix_id] self._cadnano_v2_place_crossover(which_helix, next_helix, domain, next_domain, strand_type) - def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int) -> dict: + def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int) -> Dict[int, int]: """Creates blank cadnanov2 helices in and initialized all their fields. """ helices_ids_reverse = {} for i, helix in self.helices.items(): - helix_dct = OrderedDict() + helix_dct: Dict[str, Any] = OrderedDict() helix_dct['num'] = helix.idx if self.grid == Grid.square: + if helix.grid_position is None: + raise ValueError('cannot have grid_position == None if grid is square') helix_dct['row'] = helix.grid_position[1] helix_dct['col'] = helix.grid_position[0] - if self.grid == Grid.honeycomb: + elif self.grid == Grid.honeycomb: + if helix.grid_position is None: + raise ValueError('cannot have grid_position == None if grid is honeycomb') helix_dct['row'], helix_dct['col'] = helix.grid_position[1], helix.grid_position[0] helix_dct['scaf'] = [] @@ -4079,11 +4159,11 @@ def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int) -> dict: dct['vstrands'].append(helix_dct) return helices_ids_reverse - def to_cadnano_v2(self): + def to_cadnano_v2(self) -> Dict[str, Any]: """Converts the design to the cadnano v2 format. Please see the spec `misc/cadnano-format-specs/v2.txt` for more info on that format. """ - dct = OrderedDict() + dct: Dict[str, Any] = OrderedDict() dct['vstrands'] = [] if self.__class__ != Design: @@ -4098,6 +4178,8 @@ def to_cadnano_v2(self): ''' num_bases = 0 for helix in self.helices.values(): + if helix.max_offset is None: + raise ValueError('must have helix.max_offset set') num_bases = max(num_bases, helix.max_offset) if self.grid == Grid.square: @@ -4115,17 +4197,17 @@ def to_cadnano_v2(self): for strand in self.strands: if hasattr(strand, is_scaffold_key) and strand.is_scaffold: for domain in strand.domains: - if type(domain) == Loopout: + if isinstance(domain, Loopout): raise ValueError( 'We cannot handle designs with Loopouts as it is not a cadnano v2 concept') if domain.helix % 2 != int(not domain.forward): - raise ValueError('We can only convert designs where even helices have the scaffold \ - going forward and odd helices have the scaffold going backward see the spec v2.txt Note 4. {}'.format( - domain)) + raise ValueError('We can only convert designs where even helices have the scaffold' + 'going forward and odd helices have the scaffold going backward see ' + f'the spec v2.txt Note 4. {domain}') '''Filling the helices with blank. ''' - helices_ids_reverse = self._cadnano_v2_fill_blank(dct, num_bases) + helices_ids_reverse: Dict[int, int] = self._cadnano_v2_fill_blank(dct, num_bases) '''Putting the scaffold in place. ''' @@ -4134,7 +4216,7 @@ def to_cadnano_v2(self): return dct - def set_helices_view_order(self, helices_view_order: List[int]): + def set_helices_view_order(self, helices_view_order: List[int]) -> None: """ Sets helices_view_order. @@ -4195,12 +4277,14 @@ def set_default_idt(self, use_default_idt: bool = True) -> None: def strands_starting_on_helix(self, helix: int) -> List[Strand]: """Return list of :any:`Strand`'s that begin (have their 5' end) on the :any:`Helix` with index `helix`.""" - return [strand for strand in self.strands if strand.domains[0].helix == helix] + return [strand for strand in self.strands + if isinstance(strand.domains[0], Domain) and strand.domains[0].helix == helix] def strands_ending_on_helix(self, helix: int) -> List[Strand]: """Return list of :any:`Strand`'s that finish (have their 3' end) on the :any:`Helix` with index `helix`.""" - return [strand for strand in self.strands if strand.domains[-1].helix == helix] + return [strand for strand in self.strands + if isinstance(strand.domains[-1], Domain) and strand.domains[-1].helix == helix] def _check_legal_design(self) -> None: self._check_helix_offsets() @@ -4221,7 +4305,7 @@ def _check_helix_offsets(self) -> None: f'helix.max_offset = {helix.max_offset}' raise IllegalDesignError(err_msg) - def _check_strands_overlap_legally(self, domain_to_check: Domain = None): + def _check_strands_overlap_legally(self, domain_to_check: Optional[Domain] = None) -> None: """If `Domain_to_check` is None, check all. Otherwise only check pairs where one is domain_to_check.""" @@ -4290,7 +4374,7 @@ def _check_loopout_not_singleton(strand: Strand) -> None: @staticmethod def _check_two_consecutive_loopouts(strand: Strand) -> None: for domain1, domain2 in _pairwise(strand.domains): - if domain1.is_loopout() and domain2.is_loopout(): + if isinstance(domain1, Loopout) and isinstance(domain2, Loopout): raise StrandError(strand, 'cannot have two consecutive Loopouts in a strand') @staticmethod @@ -4299,22 +4383,22 @@ def _check_loopouts_length(strand: Strand) -> None: if isinstance(loopout, Loopout) and loopout.length <= 0: raise StrandError(strand, f'loopout length must be positive but is {loopout.length}') - def _check_strands_reference_helices_legally(self): + def _check_strands_reference_helices_legally(self) -> None: # ensure each strand refers to an existing helix for strand in self.strands: self._check_strand_references_legal_helices(strand) self._check_strand_has_legal_offsets_in_helices(strand) - def _check_strand_references_legal_helices(self, strand: Strand): + def _check_strand_references_legal_helices(self, strand: Strand) -> None: for domain in strand.domains: - if domain.is_domain() and domain.helix not in self.helices: + if isinstance(domain, Domain) and domain.helix not in self.helices: err_msg = f"domain {domain} refers to nonexistent Helix index {domain.helix}; " \ f"here is the list of valid helices: {self._helices_to_string()}" raise StrandError(strand, err_msg) - def _check_strand_has_legal_offsets_in_helices(self, strand: Strand): + def _check_strand_has_legal_offsets_in_helices(self, strand: Strand) -> None: for domain in strand.domains: - if domain.is_domain(): + if isinstance(domain, Domain): helix = self.helices[domain.helix] if helix.min_offset is not None and domain.start < helix.min_offset: err_msg = f"domain {domain} has start offset {domain.start}, " \ @@ -4329,12 +4413,12 @@ def _check_strand_has_legal_offsets_in_helices(self, strand: Strand): # ensure helix_idx's are never negative twice in a row for domain1, domain2 in _pairwise(strand.domains): - if domain1.is_loopout() and domain2.is_loopout(): + if isinstance(domain1, Loopout) and isinstance(domain2, Loopout): err_msg = f"Loopouts {domain1} and {domain2} are consecutive on strand {strand}. " \ f"At least one of any consecutive pair must be a Domain, not a Loopout." raise StrandError(strand, err_msg) - def set_helix_idx(self, old_idx: int, new_idx: int): + def set_helix_idx(self, old_idx: int, new_idx: int) -> None: if new_idx in self.helices: raise IllegalDesignError(f'cannot assign idx {new_idx} to helix {old_idx}; ' 'another helix already has that index') @@ -4345,7 +4429,7 @@ def set_helix_idx(self, old_idx: int, new_idx: int): for domain in helix.domains: domain.helix = new_idx - def domain_at(self, helix: int, offset: int, forward: bool): + def domain_at(self, helix: int, offset: int, forward: bool) -> Optional[Domain]: """ Return :any:`Domain` that overlaps `offset` on helix with idx `helix` and has :py:data:`Domain.forward` = ``True``, or ``None`` if there is no such :any:`Domain`. @@ -4374,7 +4458,7 @@ def domains_at(self, helix: int, offset: int) -> List[Domain]: return domains_on_helix # TODO: add_strand and insert_domain should check for existing deletions/insertion parallel strands - def add_strand(self, strand: Strand): + def add_strand(self, strand: Strand) -> None: """Add `strand` to this design.""" self._check_strand_references_legal_helices(strand) self.strands.append(strand) @@ -4385,14 +4469,14 @@ def add_strand(self, strand: Strand): if self.automatically_assign_color: self._assign_color_to_strand(strand) - def remove_strand(self, strand: Strand): + def remove_strand(self, strand: Strand) -> None: """Remove `strand` from this design.""" self.strands.remove(strand) for domain in strand.domains: if isinstance(domain, Domain): self.helices[domain.helix].domains.remove(domain) - def append_domain(self, strand: Strand, domain: Union[Domain, Loopout]): + def append_domain(self, strand: Strand, domain: Union[Domain, Loopout]) -> None: """ Same as :any:`Design.insert_domain`, but inserts at end. @@ -4401,11 +4485,11 @@ def append_domain(self, strand: Strand, domain: Union[Domain, Loopout]): """ self.insert_domain(strand, len(strand.domains), domain) - def insert_domain(self, strand: Strand, order: int, domain: Union[Domain, Loopout]): + def insert_domain(self, strand: Strand, order: int, domain: Union[Domain, Loopout]) -> None: """Insert `Domain` into `strand` at index given by `order`. Uses same indexing as Python lists, e.g., ``design.insert_domain(strand, domain, 0)`` inserts ``domain`` as the new first :any:`Domain`.""" - if domain.is_domain() and domain.helix not in self.helices: + if isinstance(domain, Domain) and domain.helix not in self.helices: err_msg = f"domain {domain} refers to nonexistent Helix index {domain.helix}; " \ f"here is the list of valid helices: {self._helices_to_string()}" raise StrandError(strand, err_msg) @@ -4414,23 +4498,23 @@ def insert_domain(self, strand: Strand, order: int, domain: Union[Domain, Loopou strand.insert_domain(order, domain) self._check_strand_references_legal_helices(strand) self._check_loopouts_not_consecutive_or_singletons_or_zero_length() - if domain.is_domain(): + if isinstance(domain, Domain): self.helices[domain.helix].domains.append(domain) self._check_strands_overlap_legally(domain_to_check=domain) - def remove_domain(self, strand: Strand, domain: Union[Domain, Loopout]): + def remove_domain(self, strand: Strand, domain: Union[Domain, Loopout]) -> None: """Remove `Domain` from `strand`.""" assert strand in self.strands strand.remove_domain(domain) - if domain.is_domain(): + if isinstance(domain, Domain): self.helices[domain.helix].domains.remove(domain) - def _build_domains_on_helix_lists(self): + def _build_domains_on_helix_lists(self) -> None: for helix in self.helices.values(): helix._domains = [] for strand in self.strands: for domain in strand.domains: - if domain.is_domain(): + if isinstance(domain, Domain): if domain.helix in self.helices: self.helices[domain.helix].domains.append(domain) else: @@ -4438,7 +4522,7 @@ def _build_domains_on_helix_lists(self): f"is the list of helix indices: {self._helices_to_string()}" raise StrandError(strand=strand, the_cause=msg) - def _helices_to_string(self): + def _helices_to_string(self) -> str: return ', '.join(map(str, self.helices.keys())) @_docstring_parameter(default_extension=default_scadnano_file_extension) @@ -4465,7 +4549,7 @@ def to_json(self, suppress_indent: bool = True) -> str: # on the helix at that position, as well as updating the end offset of the domain (and subsequent # domains on the same helix) - def add_deletion(self, helix: int, offset: int): + def add_deletion(self, helix: int, offset: int) -> None: """Adds a deletion to every :class:`scadnano.Strand` at the given helix and base offset.""" domains = self.domains_at(helix, offset) if len(domains) == 0: @@ -4474,7 +4558,7 @@ def add_deletion(self, helix: int, offset: int): if domain.contains_offset(offset): domain.deletions.append(offset) - def add_insertion(self, helix: int, offset: int, length: int): + def add_insertion(self, helix: int, offset: int, length: int) -> None: """Adds an insertion with the given length to every :class:`scadnano.Strand` at the given helix and base offset, with the given length.""" domains = self.domains_at(helix, offset) @@ -4484,36 +4568,36 @@ def add_insertion(self, helix: int, offset: int, length: int): if domain.contains_offset(offset): domain.insertions.append((offset, length)) - def set_start(self, domain: Domain, start: int): + def set_start(self, domain: Domain, start: int) -> None: """Sets ``Domain.start`` to `start`.""" assert domain in (domain for strand in self.strands for domain in strand.domains) domain.set_start(start) self._check_strands_overlap_legally(domain) - def set_end(self, domain: Domain, end: int): + def set_end(self, domain: Domain, end: int) -> None: """Sets ``Domain.end`` to `end`.""" assert domain in (domain for strand in self.strands for domain in strand.domains) domain.set_end(end) self._check_strands_overlap_legally(domain) - def move_strand_offsets(self, delta: int): + def move_strand_offsets(self, delta: int) -> None: """Moves all strands backward (if `delta` < 0) or forward (if `delta` > 0) by `delta`.""" for strand in self.strands: - for domain in strand.domains: + for domain in strand.bound_domains(): domain.start += delta domain.end += delta self._check_strands_overlap_legally() - def move_strands_on_helices(self, delta: int): + def move_strands_on_helices(self, delta: int) -> None: """Moves all strands up (if `delta` < 0) or down (if `delta` > 0) by the number of helices given by `delta`.""" for strand in self.strands: - for domain in strand.domains: + for domain in strand.bound_domains(): domain.helix += delta self._check_strands_reference_helices_legally() def assign_dna(self, strand: Strand, sequence: str, assign_complement: bool = True, - domain: Union[Domain, Loopout] = None, check_length: bool = False): + domain: Union[Domain, Loopout] = None, check_length: bool = False) -> None: """ Assigns `sequence` as DNA sequence of `strand`. @@ -4611,7 +4695,9 @@ def assign_dna(self, strand: Strand, sequence: str, assign_complement: bool = Tr # allow a partial assignment to one domain to automatically assign the complement to the # bound domain. # However, if there are no wildcards in the assigned sequence we can safely skip strand. - if strand == other_strand and DNA_base_wildcard not in strand.dna_sequence: + if strand == other_strand \ + and strand.dna_sequence is not None \ + and DNA_base_wildcard not in strand.dna_sequence: continue if other_strand.overlaps(strand): # we do this even if other_strand has a complete DNA sequence, @@ -4642,10 +4728,13 @@ def to_idt_bulk_input_format(self, delimiter: str = ',', warn_duplicate_name: bo added_strands = self._idt_strands(warn_duplicate_name, warn_on_non_idt_strands, export_non_modified_strand_version) - idt_lines = [ - delimiter.join( - [strand.idt.name, strand.idt_dna_sequence(), strand.idt.scale, strand.idt.purification]) - for strand in added_strands.values()] + idt_lines: List[str] = [] + for strand in added_strands.values(): + if strand.idt is None: + raise ValueError(f'cannot export strand {strand} to IDT because it has no IDT field') + idt_lines.append(delimiter.join( + [strand.idt.name, strand.idt_dna_sequence(), strand.idt.scale, strand.idt.purification] + )) idt_string = '\n'.join(idt_lines) return idt_string @@ -4658,6 +4747,9 @@ def _idt_strands(self, warn_duplicate_name: bool, warn_on_non_idt_strands: bool, name = strand.idt.name if name in added_strands: existing_strand = added_strands[name] + if existing_strand.idt is None: + raise ValueError(f'cannot export strand {existing_strand} ' + f'to IDT because it has no IDT field') assert existing_strand.idt.name == name domain = strand.first_domain() existing_domain = existing_strand.first_domain() @@ -4693,7 +4785,7 @@ def _idt_strands(self, warn_duplicate_name: bool, warn_on_non_idt_strands: bool, f'{existing_domain.offset_5p()}\n') added_strands[name] = strand if export_non_modified_strand_version: - added_strands[name + '_nomods'] = strand.unmodified_version() + added_strands[name + '_nomods'] = strand.no_modifications_version() elif warn_on_non_idt_strands and not strand.is_scaffold: print(f"WARNING: strand with 5' end at (helix, offset) " f"({strand.first_domain().helix}, {strand.first_domain().offset_5p()}) " @@ -4703,7 +4795,7 @@ def _idt_strands(self, warn_duplicate_name: bool, warn_on_non_idt_strands: bool, def write_idt_bulk_input_file(self, directory: str = '.', filename: str = None, extension: str = None, delimiter: str = ',', warn_duplicate_name: bool = True, warn_on_non_idt_strands: bool = True, - export_non_modified_strand_version: bool = False): + export_non_modified_strand_version: bool = False) -> None: """Write ``.idt`` text file encoding the strands of this :any:`Design` with the field :any:`Strand.idt`, suitable for pasting into the "Bulk Input" field of IDT (Integrated DNA Technologies, Coralville, IA, https://www.idtdna.com/), @@ -4740,7 +4832,7 @@ def write_idt_plate_excel_file(self, directory: str = '.', filename: str = None, warn_duplicate_name: bool = False, warn_on_non_idt_strands: bool = False, use_default_plates: bool = False, warn_using_default_plates: bool = True, plate_type: PlateType = PlateType.wells96, - export_non_modified_strand_version: bool = False): + export_non_modified_strand_version: bool = False) -> None: """Write ``.xls`` (Microsoft Excel) file encoding the strands of this :any:`Design` with the field :py:data:`Strand.idt`, suitable for uploading to IDT (Integrated DNA Technologies, Coralville, IA, https://www.idtdna.com/) @@ -4784,7 +4876,7 @@ def write_idt_plate_excel_file(self, directory: str = '.', filename: str = None, warn_using_default_plates=warn_using_default_plates) def _write_plates_assuming_explicit_in_each_strand(self, directory: str, filename: Optional[str], - idt_strands: List[Strand]): + idt_strands: List[Strand]) -> None: plates = list({strand.idt.plate for strand in idt_strands if strand.idt is not None if strand.idt.plate is not None}) if len(plates) == 0: @@ -4801,9 +4893,11 @@ def _write_plates_assuming_explicit_in_each_strand(self, directory: str, filenam strands_in_plate = [strand for strand in idt_strands if strand.idt is not None and strand.idt.plate == plate] - strands_in_plate.sort(key=lambda s: (int(s.idt.well[1:]), s.idt.well[0])) + strands_in_plate.sort(key=lambda s: (int(s.idt.well[1:]), s.idt.well[0])) # type: ignore for row, strand in enumerate(strands_in_plate): + if strand.idt is None: + raise ValueError(f'cannot export strand {strand} to IDT because it has no idt field') worksheet.write(row + 1, 0, strand.idt.well) worksheet.write(row + 1, 1, strand.idt.name) worksheet.write(row + 1, 2, strand.idt_dna_sequence()) @@ -4811,7 +4905,7 @@ def _write_plates_assuming_explicit_in_each_strand(self, directory: str, filenam workbook.save(filename_plate) @staticmethod - def _add_new_excel_plate_sheet(plate_name: str, workbook): + def _add_new_excel_plate_sheet(plate_name: str, workbook: Any) -> Any: worksheet = workbook.add_sheet(plate_name) worksheet.write(0, 0, 'Well Position') worksheet.write(0, 1, 'Name') @@ -4819,8 +4913,8 @@ def _add_new_excel_plate_sheet(plate_name: str, workbook): return worksheet @staticmethod - def _setup_excel_file(directory: str, filename: Optional[str]): - import xlwt + def _setup_excel_file(directory: str, filename: Optional[str]) -> Tuple[str, Any]: + import xlwt # type: ignore plate_extension = f'xls' if filename is None: filename_plate = _get_filename_same_name_as_running_python_script( @@ -4832,7 +4926,7 @@ def _setup_excel_file(directory: str, filename: Optional[str]): def _write_plates_default(self, directory: str, filename: Optional[str], idt_strands: List[Strand], plate_type: PlateType = PlateType.wells96, - warn_using_default_plates: bool = True): + warn_using_default_plates: bool = True) -> None: plate_coord = _PlateCoordinate(plate_type=plate_type) plate = 1 excel_row = 1 @@ -4840,6 +4934,8 @@ def _write_plates_default(self, directory: str, filename: Optional[str], idt_str worksheet = self._add_new_excel_plate_sheet(f'plate{plate}', workbook) for strand in idt_strands: + if strand.idt is None: + raise ValueError(f'cannot export strand {strand} to IDT because it has no idt field') if warn_using_default_plates and strand.idt.plate is not None: print( f"WARNING: strand {strand} has plate entry {strand.idt.plate}, which is being ignored " @@ -4865,7 +4961,7 @@ def _write_plates_default(self, directory: str, filename: Optional[str], idt_str workbook.save(filename_plate) @_docstring_parameter(default_extension=default_scadnano_file_extension) - def write_scadnano_file(self, directory: str = '.', filename: str = None, extension: str = None): + def write_scadnano_file(self, directory: str = '.', filename: str = None, extension: str = None) -> None: """Write ``.{default_extension}`` file representing this :any:`Design`, suitable for reading by scadnano, with the output file having the same name as the running script but with ``.py`` changed to @@ -4896,7 +4992,7 @@ def write_scadnano_file(self, directory: str = '.', filename: str = None, extens extension = default_scadnano_file_extension _write_file_same_name_as_running_python_script(contents, extension, directory, filename) - def export_cadnano_v2(self, directory: str = '.', filename=None): + def export_cadnano_v2(self, directory: str = '.', filename: Optional[str] = None) -> None: """Write ``.json`` file representing this :any:`Design`, suitable for reading by cadnano v2, with the output file having the same name as the running script but with ``.py`` changed to ``.json``, unless `filename` is explicitly specified. @@ -4919,7 +5015,7 @@ def export_cadnano_v2(self, directory: str = '.', filename=None): _write_file_same_name_as_running_python_script(contents, 'json', directory, filename) - def add_nick(self, helix: int, offset: int, forward: bool, new_color: bool = True): + def add_nick(self, helix: int, offset: int, forward: bool, new_color: bool = True) -> None: """Add nick to :any:`Domain` on :any:`Helix` with index `helix`, in direction given by `forward`, at offset `offset`. The two :any:`Domain`'s created by this nick will have 5'/3' ends at offsets `offset` and `offset-1`. @@ -4973,39 +5069,45 @@ def add_nick(self, helix: int, offset: int, forward: bool, new_color: bool = Tru domain_to_add_before = domain_right domain_to_add_after = domain_left - if strand.dna_sequence: - dna_sequence_before: str = ''.join( - domain.dna_sequence() for domain in domains_before) # ignore: typing - dna_sequence_after: str = ''.join( - domain.dna_sequence() for domain in domains_after) # ignore: typing - dna_sequence_on_domain_left = domain_to_remove.dna_sequence_in( + seq_before_whole: Optional[str] + seq_after_whole: Optional[str] + if strand.dna_sequence is not None: + seq_before: str = ''.join(domain.dna_sequence() for domain in domains_before) # type: ignore + seq_after: str = ''.join(domain.dna_sequence() for domain in domains_after) # type: ignore + seq_on_domain_left: str = domain_to_remove.dna_sequence_in( # type: ignore domain_to_remove.start, offset - 1) - dna_sequence_on_domain_right = domain_to_remove.dna_sequence_in(offset, - domain_to_remove.end - 1) + seq_on_domain_right: str = domain_to_remove.dna_sequence_in(offset, # type: ignore + domain_to_remove.end - 1) if domain_to_remove.forward: - dna_sequence_on_domain_before = dna_sequence_on_domain_left - dna_sequence_on_domain_after = dna_sequence_on_domain_right + seq_on_domain_before = seq_on_domain_left + seq_on_domain_after = seq_on_domain_right else: - dna_sequence_on_domain_before = dna_sequence_on_domain_right - dna_sequence_on_domain_after = dna_sequence_on_domain_left - dna_sequence_before_whole = dna_sequence_before + dna_sequence_on_domain_before - dna_sequence_after_whole = dna_sequence_on_domain_after + dna_sequence_after + seq_on_domain_before = seq_on_domain_right + seq_on_domain_after = seq_on_domain_left + seq_before_whole = seq_before + seq_on_domain_before + seq_after_whole = seq_on_domain_after + seq_after else: - dna_sequence_before_whole = None - dna_sequence_after_whole = None + seq_before_whole = None + seq_after_whole = None self.strands.remove(strand) idt_present = strand.idt is not None - strand_before = Strand(domains=domains_before + [domain_to_add_before], - dna_sequence=dna_sequence_before_whole, - color=strand.color, idt=strand.idt if idt_present else None) + strand_before = Strand( + domains=domains_before + cast(List[Union[Domain, Loopout]], [domain_to_add_before]), # noqa + dna_sequence=seq_before_whole, + color=strand.color, + idt=strand.idt if idt_present else None, + ) color_after = next(self.color_cycler) if new_color else strand.color - strand_after = Strand(domains=[domain_to_add_after] + domains_after, - dna_sequence=dna_sequence_after_whole, - color=color_after, use_default_idt=idt_present) + strand_after = Strand( + domains=cast(List[Union[Domain, Loopout]], [domain_to_add_after]) + domains_after, # noqa + dna_sequence=seq_after_whole, + color=color_after, + use_default_idt=idt_present, + ) self.helices[helix].domains.remove(domain_to_remove) self.helices[helix].domains.extend([domain_to_add_before, domain_to_add_after]) @@ -5013,7 +5115,7 @@ def add_nick(self, helix: int, offset: int, forward: bool, new_color: bool = Tru self.strands.extend([strand_before, strand_after]) def add_half_crossover(self, helix: int, helix2: int, offset: int, forward: bool, - offset2: int = None, forward2: bool = None): + offset2: int = None, forward2: bool = None) -> None: """ Add a half crossover from helix `helix` at offset `offset` to `helix2`, on the strand with :py:data:`Strand.forward` = `forward`. @@ -5087,7 +5189,7 @@ def add_half_crossover(self, helix: int, helix2: int, offset: int, forward: bool self.strands.append(new_strand) def add_full_crossover(self, helix: int, helix2: int, offset: int, forward: bool, - offset2: int = None, forward2: bool = None): + offset2: int = None, forward2: bool = None) -> None: """ Adds two half-crossovers, one at `offset` and another at `offset`-1. Other arguments have the same meaning as in :py:meth:`Design.add_half_crossover`. @@ -5115,7 +5217,8 @@ def add_full_crossover(self, helix: int, helix2: int, offset: int, forward: bool self.add_half_crossover(helix=helix, helix2=helix2, offset=offset, offset2=offset2, forward=forward, forward2=forward2) - def add_crossovers(self, crossovers: List['Crossover']): # remove quotes when Python 3.6 support dropped + # remove quotes when Py3.6 support dropped + def add_crossovers(self, crossovers: List['Crossover']) -> None: """ Adds a list of :any:`Crossover`'s in batch. @@ -5147,7 +5250,7 @@ def add_crossovers(self, crossovers: List['Crossover']): # remove quotes when P forward=crossover.forward, forward2=crossover.forward2, offset=crossover.offset, offset2=crossover.offset2) - def _prepare_nicks_for_full_crossover(self, helix, forward, offset): + def _prepare_nicks_for_full_crossover(self, helix: int, forward: bool, offset: int) -> None: domain_right = self.domain_at(helix, offset, forward) if domain_right is None: raise IllegalDesignError(f'You tried to create a full crossover at ' @@ -5163,7 +5266,7 @@ def _prepare_nicks_for_full_crossover(self, helix, forward, offset): else: assert domain_left.end == domain_right.start - def inline_deletions_insertions(self): + def inline_deletions_insertions(self) -> None: """ Converts deletions and insertions by "inlining" them. Insertions and deletions are removed, and their domains have their lengths altered. Also, major tick marks on the helices will be @@ -5191,7 +5294,7 @@ def inline_deletions_insertions(self): for helix in self.helices.values(): self._inline_deletions_insertions_on_helix(helix) - def _inline_deletions_insertions_on_helix(self, helix: Helix): + def _inline_deletions_insertions_on_helix(self, helix: Helix) -> None: ################################################### # first gather information before changing anything @@ -5220,7 +5323,9 @@ def _inline_deletions_insertions_on_helix(self, helix: Helix): ################################################### # now that info is gathered, start changing things - helix.max_offset += delta_length + if helix.max_offset is not None: + helix.max_offset += delta_length + if len(major_ticks) > 0: major_tick_idx = 0 delta_acc = 0 # accumulated delta; insertions add to this and deletions subtract from it @@ -5247,7 +5352,7 @@ def _inline_deletions_insertions_on_helix(self, helix: Helix): domain.deletions = [] domain.insertions = [] - def reverse_all(self): + def reverse_all(self) -> None: """ Reverses "polarity" of every :any:`Strand` in this :any:`Design`. @@ -5258,38 +5363,39 @@ def reverse_all(self): for strand in self.strands: strand.reverse() - def set_major_tick_distance(self, major_tick_distance: int): - self.major_tick_distance = major_tick_distance + def set_major_tick_distance(self, major_tick_distance: int) -> None: + for helix in self.helices.values(): + helix.major_tick_distance = major_tick_distance - def _ensure_helices_distinct_objects(self): + def _ensure_helices_distinct_objects(self) -> None: pair = _find_index_pair_same_object(self.helices) - if pair: + if pair is not None: i, j = pair raise IllegalDesignError('helices must all be distinct objects, but those at indices ' f'{i} and {j} are the same object') - def _ensure_strands_distinct_objects(self): + def _ensure_strands_distinct_objects(self) -> None: pair = _find_index_pair_same_object(self.strands) - if pair: + if pair is not None: i, j = pair raise IllegalDesignError('strands must all be distinct objects, but those at indices ' f'{i} and {j} are the same object') - def _ensure_helix_groups_exist(self): + def _ensure_helix_groups_exist(self) -> None: for helix in self.helices.values(): if helix.group not in self.groups.keys(): raise IllegalDesignError(f'helix {helix.idx} has group {helix.group}, which does not ' f'exist in the design. The valid groups are ' f'{", ".join(self.groups.keys())}') - def _has_default_groups(self): + def _has_default_groups(self) -> bool: return len(self.groups) == 1 and default_group_name in self.groups - def _assign_default_helices_view_orders_to_groups(self): + def _assign_default_helices_view_orders_to_groups(self) -> None: for name, group in self.groups.items(): if group.helices_view_order is None: - helices_in_group = {idx: helix for idx, helix in self.helices if helix.group == name} - group._assign_default_helices_view_order(helices_in_group) + helices_in_group = {idx: helix for idx, helix in self.helices.items() if helix.group == name} + group._assign_default_helices_view_order(helices_in_group) # noqa def _warn_if_strand_names_not_unique(self) -> None: names = [strand.name for strand in self.strands if strand.name is not None] @@ -5327,20 +5433,21 @@ def _name_of_this_script() -> str: def _write_file_same_name_as_running_python_script(contents: str, extension: str, directory: str = '.', - filename=None): + filename: Optional[str] = None) -> None: relative_filename = _get_filename_same_name_as_running_python_script(directory, extension, filename) with open(relative_filename, 'w') as out_file: out_file.write(contents) -def _get_filename_same_name_as_running_python_script(directory, extension, filename): +def _get_filename_same_name_as_running_python_script(directory: str, extension: str, + filename: Optional[str]) -> str: if filename is None: filename = _name_of_this_script() + f'.{extension}' relative_filename = _create_directory_and_set_filename(directory, filename) return relative_filename -def _create_directory_and_set_filename(directory, filename): +def _create_directory_and_set_filename(directory: str, filename: str) -> str: if not os.path.exists(directory): os.makedirs(directory) relative_filename = os.path.join(directory, filename) @@ -5373,13 +5480,13 @@ class Crossover: forward: bool """direction of :any:`Strand` on `helix` to which to add half crossover""" - offset2: Optional[int] = None + offset2: int """ offset on `helix2` at which to add half crossover. If not specified, defaults to `offset` """ - forward2: Optional[bool] = None + forward2: bool """ direction of :any:`Strand` on `helix2` to which to add half crossover. If not specified, defaults to the negation of `forward` @@ -5391,7 +5498,7 @@ class Crossover: If not specified, defaults to ``False``. """ - def __post_init__(self): + def __post_init__(self) -> None: if self.offset2 is None: self.offset2 = self.offset if self.forward2 is None: diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index 8bcacdcd..76ce86cc 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -34,11 +34,11 @@ class TestCreateStrandChainedMethods(unittest.TestCase): # tests methods for creating strands using chained method notation as in this issue: # https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/85 - def setUp(self): + def setUp(self) -> None: helices = [sc.Helix(max_offset=100) for _ in range(6)] self.design_6helix = sc.Design(helices=helices, strands=[], grid=sc.square) - def test_strand__loopouts_with_labels(self): + def test_strand__loopouts_with_labels(self) -> None: design = self.design_6helix sb = design.strand(0, 0) sb.to(10) @@ -63,7 +63,7 @@ def test_strand__loopouts_with_labels(self): self.assertEqual(1, len(design.strands)) self.assertEqual(expected_strand, design.strands[0]) - def test_strand__loopouts_with_labels_to_json(self): + def test_strand__loopouts_with_labels_to_json(self) -> None: design = self.design_6helix sb = design.strand(0, 0) sb.to(10) @@ -90,7 +90,7 @@ def test_strand__loopouts_with_labels_to_json(self): self.assertEqual(1, len(design_from_json.strands)) self.assertEqual(expected_strand, design_from_json.strands[0]) - def test_strand__0_0_to_10_cross_1_to_5(self): + def test_strand__0_0_to_10_cross_1_to_5(self) -> None: design = self.design_6helix sb = design.strand(0, 0) sb.to(10) @@ -109,7 +109,7 @@ def test_strand__0_0_to_10_cross_1_to_5(self): self.assertEqual(0, len(design.helices[4].domains)) self.assertEqual(0, len(design.helices[5].domains)) - def test_strand__0_0_to_10_cross_1_to_5__reverse(self): + def test_strand__0_0_to_10_cross_1_to_5__reverse(self) -> None: design = self.design_6helix design.strand(1, 5).to(10).cross(0).to(0) expected_strand = sc.Strand([ @@ -125,7 +125,7 @@ def test_strand__0_0_to_10_cross_1_to_5__reverse(self): self.assertEqual(0, len(design.helices[4].domains)) self.assertEqual(0, len(design.helices[5].domains)) - def test_strand__h0_off0_to_off10_cross_h1_to_off5_loopout_length3_h2_to_off15(self): + def test_strand__h0_off0_to_off10_cross_h1_to_off5_loopout_length3_h2_to_off15(self) -> None: design = self.design_6helix sb = design.strand(0, 0) sb.to(10) @@ -148,7 +148,7 @@ def test_strand__h0_off0_to_off10_cross_h1_to_off5_loopout_length3_h2_to_off15(s self.assertEqual(0, len(design.helices[4].domains)) self.assertEqual(0, len(design.helices[5].domains)) - def test_strand__two_forward_paranemic_crossovers(self): + def test_strand__two_forward_paranemic_crossovers(self) -> None: design = self.design_6helix design.strand(0, 0).to(10).cross(1).to(15).cross(2).to(20) expected_strand = sc.Strand([ @@ -165,7 +165,7 @@ def test_strand__two_forward_paranemic_crossovers(self): self.assertEqual(0, len(design.helices[4].domains)) self.assertEqual(0, len(design.helices[5].domains)) - def test_strand__two_reverse_paranemic_crossovers(self): + def test_strand__two_reverse_paranemic_crossovers(self) -> None: design = self.design_6helix design.strand(0, 20).to(10).cross(1).to(5).cross(2).to(0) expected_strand = sc.Strand([ @@ -182,7 +182,7 @@ def test_strand__two_reverse_paranemic_crossovers(self): self.assertEqual(0, len(design.helices[4].domains)) self.assertEqual(0, len(design.helices[5].domains)) - def test_strand__multiple_strands(self): + def test_strand__multiple_strands(self) -> None: design = self.design_6helix design.strand(0, 0).to(10).cross(1).to(0) design.strand(0, 20).to(10).cross(1).to(20) @@ -204,7 +204,7 @@ def test_strand__multiple_strands(self): self.assertEqual(0, len(design.helices[4].domains)) self.assertEqual(0, len(design.helices[5].domains)) - def test_strand__multiple_strands_other_order(self): + def test_strand__multiple_strands_other_order(self) -> None: design = self.design_6helix design.strand(0, 20).to(10).cross(1).to(20) design.strand(0, 0).to(10).cross(1).to(0) @@ -226,7 +226,7 @@ def test_strand__multiple_strands_other_order(self): self.assertEqual(0, len(design.helices[4].domains)) self.assertEqual(0, len(design.helices[5].domains)) - def test_strand__multiple_strands_overlap_no_error(self): + def test_strand__multiple_strands_overlap_no_error(self) -> None: design = self.design_6helix design.strand(0, 0).to(10).cross(1).to(0) \ .as_scaffold() \ @@ -263,13 +263,13 @@ def test_strand__multiple_strands_overlap_no_error(self): self.assertEqual(0, len(design.helices[4].domains)) self.assertEqual(0, len(design.helices[5].domains)) - def test_strand__multiple_strands_overlap_error(self): + def test_strand__multiple_strands_overlap_error(self) -> None: design = self.design_6helix design.strand(0, 0).to(10).cross(1).to(0) with self.assertRaises(sc.IllegalDesignError): design.strand(0, 2).to(8) - def test_strand__call_to_twice_legally(self): + def test_strand__call_to_twice_legally(self) -> None: design = self.design_6helix sb = design.strand(0, 0) sb.to(10) @@ -290,7 +290,7 @@ def test_strand__call_to_twice_legally(self): self.assertEqual(0, len(design.helices[4].domains)) self.assertEqual(0, len(design.helices[5].domains)) - def test_strand__call_update_to_twice_legally(self): + def test_strand__call_update_to_twice_legally(self) -> None: design = self.design_6helix sb = design.strand(0, 0) sb.to(10) @@ -310,7 +310,7 @@ def test_strand__call_update_to_twice_legally(self): self.assertEqual(0, len(design.helices[4].domains)) self.assertEqual(0, len(design.helices[5].domains)) - def test_strand__call_to_then_update_to_legally(self): + def test_strand__call_to_then_update_to_legally(self) -> None: design = self.design_6helix sb = design.strand(0, 0) sb.to(10) @@ -330,14 +330,14 @@ def test_strand__call_to_then_update_to_legally(self): self.assertEqual(0, len(design.helices[4].domains)) self.assertEqual(0, len(design.helices[5].domains)) - def test_strand__call_to_twice_increase_decrease_forward(self): + def test_strand__call_to_twice_increase_decrease_forward(self) -> None: design = self.design_6helix sb = design.strand(0, 0) sb.to(10) with self.assertRaises(sc.IllegalDesignError): sb.to(5) - def test_strand__call_to_twice_decrease_increase_reverse(self): + def test_strand__call_to_twice_decrease_increase_reverse(self) -> None: design = self.design_6helix sb = design.strand(0, 10) sb.to(0) @@ -347,14 +347,14 @@ def test_strand__call_to_twice_decrease_increase_reverse(self): class TestCreateHelix(unittest.TestCase): - def test_helix_constructor_no_max_offset_with_major_ticks(self): + def test_helix_constructor_no_max_offset_with_major_ticks(self) -> None: # tests bug where an exception is raised if major ticks is defined but not max_offset helix = sc.Helix(major_ticks=[0, 5, 10]) class TestM13(unittest.TestCase): - def test_p7249(self): + def test_p7249(self) -> None: p7249 = sc.m13() self.assertEqual('TTCCCTTCCTTTCTCG', p7249[:16]) self.assertEqual(7249, len(p7249)) @@ -362,12 +362,12 @@ def test_p7249(self): self.assertEqual('AATGCTACTACTATTA', p7249[:16]) self.assertEqual(7249, len(p7249)) - def test_p7560(self): + def test_p7560(self) -> None: p7560 = sc.m13(rotation=0, variant=sc.M13Variant.p7560) self.assertEqual('AGCTTGGCACTGGCCG', p7560[:16]) self.assertEqual(7560, len(p7560)) - def test_p8064(self): + def test_p8064(self) -> None: p8064 = sc.m13(rotation=0, variant=sc.M13Variant.p8064) self.assertEqual('GGCAATGACCTGATAG', p8064[:16]) self.assertEqual(8064, len(p8064)) @@ -375,7 +375,7 @@ def test_p8064(self): class TestModifications(unittest.TestCase): - def test_mod_illegal_exceptions_raised(self): + def test_mod_illegal_exceptions_raised(self) -> None: strand = sc.Strand(domains=[sc.Domain(0, True, 0, 5)], dna_sequence='AATGC') strand.set_modification_internal(2, mod.biotin_int) with self.assertRaises(sc.IllegalDesignError): @@ -417,7 +417,7 @@ def test_mod_illegal_exceptions_raised(self): # modifications=[cy3I_offset_off_end]) # seq = strand.idt_dna_sequence() - def test_Cy3(self): + def test_Cy3(self) -> None: cy3_5 = mod.cy3_5p self.assertEqual(r'/5Cy3/', cy3_5.idt_text) self.assertEqual(r'/5Cy3/', cy3_5.id) @@ -455,7 +455,7 @@ def test_Cy3(self): self.assertEqual(r'AT/iCy3/TG/iCy3/C', strandI.idt_dna_sequence()) self.assertEqual(r'/5Cy3/AT/iCy3/TG/iCy3/C/3Cy3Sp/', strand53I.idt_dna_sequence()) - def test_biotin(self): + def test_biotin(self) -> None: biotin5 = mod.biotin_5p self.assertEqual(r'/5Biosg/', biotin5.idt_text) self.assertEqual(r'/5Biosg/', biotin5.id) @@ -492,7 +492,7 @@ def test_biotin(self): self.assertEqual(r'/5Biosg/ATTGC/3Bio/', strand53.idt_dna_sequence()) self.assertEqual(r'/5Biosg/A/iBiodT//iBiodT/GC/3Bio/', strand53I.idt_dna_sequence()) - def test_to_json_serializable(self): + def test_to_json_serializable(self) -> None: biotin5 = mod.biotin_5p self.assertEqual(r'/5Biosg/', biotin5.idt_text) self.assertEqual(r'/5Biosg/', biotin5.id) @@ -565,32 +565,32 @@ class TestImportCadnanoV2(unittest.TestCase): input_path = os.path.join('tests_inputs', folder) output_path = os.path.join('tests_outputs', folder) - def test_32_helix_rectangle(self): + def test_32_helix_rectangle(self) -> None: design = sc.Design.from_cadnano_v2(directory=self.input_path, filename='test_32_helix_rectangle.json') design.write_scadnano_file(directory=self.output_path, filename=f'test_32_helix_rectangle.{sc.default_scadnano_file_extension}') - def test_helices_order(self): + def test_helices_order(self) -> None: design = sc.Design.from_cadnano_v2(directory=self.input_path, filename='test_helices_order.json') design.write_scadnano_file(directory=self.output_path, filename=f'test_helices_order.{sc.default_scadnano_file_extension}') - def test_huge_hex(self): + def test_huge_hex(self) -> None: design = sc.Design.from_cadnano_v2(directory=self.input_path, filename='test_huge_hex.json') design.write_scadnano_file(directory=self.output_path, filename=f'test_huge_hex.{sc.default_scadnano_file_extension}') - def test_Science09_prot120_98_v3(self): + def test_Science09_prot120_98_v3(self) -> None: file_name = "test_Science09_prot120_98_v3" design = sc.Design.from_cadnano_v2(directory=self.input_path, filename=file_name + ".json") design.write_scadnano_file(directory=self.output_path, filename=f'{file_name}.{sc.default_scadnano_file_extension}') - def test_Nature09_monolith(self): + def test_Nature09_monolith(self) -> None: file_name = "test_Nature09_monolith" design = sc.Design.from_cadnano_v2(directory=self.input_path, filename=file_name + ".json") @@ -607,7 +607,7 @@ class TestExportCadnanoV2(unittest.TestCase): output_path = os.path.join('tests_outputs', folder) ext = sc.default_scadnano_file_extension - def test_2_staple_2_helix_origami_extremely_simple(self): + def test_2_staple_2_helix_origami_extremely_simple(self) -> None: helices = [sc.Helix(max_offset=32), sc.Helix(max_offset=32)] scaf_part = sc.Domain(helix=0, forward=True, start=0, end=32) scaf = sc.Strand(domains=[scaf_part], is_scaffold=True) @@ -617,7 +617,7 @@ def test_2_staple_2_helix_origami_extremely_simple(self): design.export_cadnano_v2(directory=self.output_path, filename='test_2_stape_2_helix_origami_extremely_simple.json') - def test_2_staple_2_helix_origami_extremely_simple_2(self): + def test_2_staple_2_helix_origami_extremely_simple_2(self) -> None: helices = [sc.Helix(max_offset=32), sc.Helix(max_offset=32)] scaf_part1 = sc.Domain(helix=0, forward=True, start=0, end=32) scaf_part2 = sc.Domain(helix=1, forward=False, start=0, end=32) @@ -628,7 +628,7 @@ def test_2_staple_2_helix_origami_extremely_simple_2(self): design.export_cadnano_v2(directory=self.output_path, filename='test_2_stape_2_helix_origami_extremely_simple_2.json') - def test_2_staple_2_helix_origami_deletions_insertions(self): + def test_2_staple_2_helix_origami_deletions_insertions(self) -> None: # left staple stap_left_ss1 = sc.Domain(helix=1, forward=True, start=0, end=16) stap_left_ss0 = sc.Domain(helix=0, forward=False, start=0, end=16) @@ -668,7 +668,7 @@ def test_2_staple_2_helix_origami_deletions_insertions(self): design.export_cadnano_v2(directory=self.output_path, filename='test_2_stape_2_helix_origami_deletions_insertions.json') - def test_6_helix_origami_rectangle(self): + def test_6_helix_origami_rectangle(self) -> None: design = rect.create(num_helices=6, num_cols=10, nick_pattern=rect.staggered, twist_correction_deletion_spacing=3) design.write_scadnano_file(directory=self.input_path, @@ -677,13 +677,13 @@ def test_6_helix_origami_rectangle(self): filename='test_6_helix_origami_rectangle.json') @unittest.skip('DD: I cannot find where this file is. Is it supposed to be generated by some code?') - def test_6_helix_bundle_honeycomb(self): + def test_6_helix_bundle_honeycomb(self) -> None: design = sc.Design.from_scadnano_file( os.path.join(self.input_path, f'test_6_helix_bundle_honeycomb.{self.ext}')) design.export_cadnano_v2(directory=self.output_path, filename='test_6_helix_bundle_honeycomb.json') - def test_16_helix_origami_rectangle_no_twist(self): + def test_16_helix_origami_rectangle_no_twist(self) -> None: design = rect.create(num_helices=16, num_cols=26, assign_seq=True, twist_correction_deletion_spacing=3) design.write_scadnano_file(directory=self.input_path, @@ -691,7 +691,7 @@ def test_16_helix_origami_rectangle_no_twist(self): design.export_cadnano_v2(directory=self.output_path, filename='test_16_helix_origami_rectangle_no_twist.json') - def test_bad_cases(self): + def test_bad_cases(self) -> None: """ We do not handle Loopouts and design where the parity of the helix does not correspond to the direction. """ @@ -746,7 +746,7 @@ class TestDesignFromJson(unittest.TestCase): Tests reading a design from a dict derived from JSON. """ - def setUp(self): + def setUp(self) -> None: """ 0 8 16 | | | @@ -776,13 +776,13 @@ def setUp(self): self.design_pre_json = sc.Design(strands=[st_l, st_r, scaf], grid=sc.square) self.design_pre_json.assign_dna(scaf, 'A' * 36) - def test_from_json__from_and_to_file_contents(self): + def test_from_json__from_and_to_file_contents(self) -> None: json_str = self.design_pre_json.to_json() json_map = json.loads(json_str) design = sc.Design.from_scadnano_json_map(json_map) design.to_json_serializable() - def test_from_json__three_strands(self): + def test_from_json__three_strands(self) -> None: json_str = self.design_pre_json.to_json() json_map = json.loads(json_str) design = sc.Design.from_scadnano_json_map(json_map) @@ -887,7 +887,7 @@ def test_from_json__three_strands(self): self.assertEqual(None, scaf.modification_3p) self.assertDictEqual({}, scaf.modifications_int) - def test_from_json__helices_non_default_indices(self): + def test_from_json__helices_non_default_indices(self) -> None: h2 = sc.Helix(idx=2) h3 = sc.Helix(idx=3) h5 = sc.Helix(idx=5) @@ -912,7 +912,7 @@ def test_from_json__helices_non_default_indices(self): self.assertEqual(3, design.helices[3].idx) self.assertEqual(5, design.helices[5].idx) - def test_from_json__helices_non_default_indices_mixed_with_default(self): + def test_from_json__helices_non_default_indices_mixed_with_default(self) -> None: h2 = sc.Helix(idx=2) h3 = sc.Helix() h5 = sc.Helix(idx=5) @@ -938,7 +938,7 @@ def test_from_json__helices_non_default_indices_mixed_with_default(self): self.assertEqual(1, design.helices[1].idx) self.assertEqual(5, design.helices[5].idx) - def test_from_json__helices_non_default_error_if_some_have_idx_not_others(self): + def test_from_json__helices_non_default_error_if_some_have_idx_not_others(self) -> None: h2 = sc.Helix(idx=2) h3 = sc.Helix(idx=3) h3_2 = sc.Helix(idx=3) @@ -946,7 +946,7 @@ def test_from_json__helices_non_default_error_if_some_have_idx_not_others(self): with self.assertRaises(sc.IllegalDesignError): self.design_pre_json = sc.Design(helices=helices, strands=[], grid=sc.square) - def test_from_json__helices_non_default_error_if_some_have_idx_not_others_mixed_default(self): + def test_from_json__helices_non_default_error_if_some_have_idx_not_others_mixed_default(self) -> None: h0 = sc.Helix() h0_2 = sc.Helix(idx=0) helices = [h0, h0_2] @@ -959,7 +959,7 @@ class TestStrandReversePolarity(unittest.TestCase): Tests reversing polarity of all strands in the system. """ - def test_reverse_all__one_strand(self): + def test_reverse_all__one_strand(self) -> None: """ before 0 8 @@ -985,7 +985,7 @@ def test_reverse_all__one_strand(self): self.assertEqual(0, ss.start) self.assertEqual(8, ss.end) - def test_reverse_all__three_strands(self): + def test_reverse_all__three_strands(self) -> None: """ before 0 8 16 @@ -1103,7 +1103,7 @@ class TestInlineInsDel(unittest.TestCase): Tests inlining of insertions/deletions. """ - def test_inline_deletions_insertions__one_deletion(self): + def test_inline_deletions_insertions__one_deletion(self) -> None: """ before 0 4 8 16 24 @@ -1134,7 +1134,7 @@ def helix0_strand0_inlined_test(self, design, max_offset, major_ticks, start, en self.assertListEqual([], strand.domains[0].deletions) self.assertListEqual([], strand.domains[0].insertions) - def test_inline_deletions_insertions__two_deletions(self): + def test_inline_deletions_insertions__two_deletions(self) -> None: """ before 0 2 4 8 16 24 @@ -1153,7 +1153,7 @@ def test_inline_deletions_insertions__two_deletions(self): design.inline_deletions_insertions() self.helix0_strand0_inlined_test(design, max_offset=22, major_ticks=[0, 6, 14, 22], start=0, end=6) - def test_inline_deletions_insertions__one_insertion(self): + def test_inline_deletions_insertions__one_insertion(self) -> None: """ before 0 4 8 16 24 @@ -1172,7 +1172,7 @@ def test_inline_deletions_insertions__one_insertion(self): design.inline_deletions_insertions() self.helix0_strand0_inlined_test(design, max_offset=25, major_ticks=[0, 9, 17, 25], start=0, end=9) - def test_inline_deletions_insertions__two_insertions(self): + def test_inline_deletions_insertions__two_insertions(self) -> None: """ before 0 2 4 8 16 24 @@ -1191,7 +1191,7 @@ def test_inline_deletions_insertions__two_insertions(self): design.inline_deletions_insertions() self.helix0_strand0_inlined_test(design, max_offset=28, major_ticks=[0, 12, 20, 28], start=0, end=12) - def test_inline_deletions_insertions__one_deletion_one_insertion(self): + def test_inline_deletions_insertions__one_deletion_one_insertion(self) -> None: """ before 0 2 4 8 16 24 @@ -1210,7 +1210,7 @@ def test_inline_deletions_insertions__one_deletion_one_insertion(self): design.inline_deletions_insertions() self.helix0_strand0_inlined_test(design, max_offset=26, major_ticks=[0, 10, 18, 26], start=0, end=10) - def test_inline_deletions_insertions__one_deletion_right_of_major_tick(self): + def test_inline_deletions_insertions__one_deletion_right_of_major_tick(self) -> None: """ before 0 89 16 24 @@ -1229,7 +1229,7 @@ def test_inline_deletions_insertions__one_deletion_right_of_major_tick(self): design.inline_deletions_insertions() self.helix0_strand0_inlined_test(design, max_offset=23, major_ticks=[0, 8, 15, 23], start=0, end=11) - def test_inline_deletions_insertions__one_deletion_on_major_tick(self): + def test_inline_deletions_insertions__one_deletion_on_major_tick(self) -> None: """ | is major tick, and . is minor tick before @@ -1249,7 +1249,7 @@ def test_inline_deletions_insertions__one_deletion_on_major_tick(self): design.inline_deletions_insertions() self.helix0_strand0_inlined_test(design, max_offset=23, major_ticks=[0, 8, 15, 23], start=0, end=11) - def test_inline_deletions_insertions__one_deletion_left_of_major_tick(self): + def test_inline_deletions_insertions__one_deletion_left_of_major_tick(self) -> None: """ before 0 78 16 24 @@ -1268,7 +1268,7 @@ def test_inline_deletions_insertions__one_deletion_left_of_major_tick(self): design.inline_deletions_insertions() self.helix0_strand0_inlined_test(design, max_offset=23, major_ticks=[0, 7, 15, 23], start=0, end=11) - def test_inline_deletions_insertions__one_insertion_right_of_major_tick(self): + def test_inline_deletions_insertions__one_insertion_right_of_major_tick(self) -> None: """ before 0 89 16 24 @@ -1287,7 +1287,7 @@ def test_inline_deletions_insertions__one_insertion_right_of_major_tick(self): design.inline_deletions_insertions() self.helix0_strand0_inlined_test(design, max_offset=25, major_ticks=[0, 8, 17, 25], start=0, end=13) - def test_inline_deletions_insertions__one_insertion_on_major_tick(self): + def test_inline_deletions_insertions__one_insertion_on_major_tick(self) -> None: """ before 0 8 16 24 @@ -1306,7 +1306,7 @@ def test_inline_deletions_insertions__one_insertion_on_major_tick(self): design.inline_deletions_insertions() self.helix0_strand0_inlined_test(design, max_offset=25, major_ticks=[0, 8, 17, 25], start=0, end=13) - def test_inline_deletions_insertions__one_insertion_left_of_major_tick(self): + def test_inline_deletions_insertions__one_insertion_left_of_major_tick(self) -> None: """ before 0 78 16 24 @@ -1325,7 +1325,7 @@ def test_inline_deletions_insertions__one_insertion_left_of_major_tick(self): design.inline_deletions_insertions() self.helix0_strand0_inlined_test(design, max_offset=25, major_ticks=[0, 9, 17, 25], start=0, end=13) - def test_inline_deletions_insertions__deletions_insertions_in_multiple_domains(self): + def test_inline_deletions_insertions__deletions_insertions_in_multiple_domains(self) -> None: """ before 0 5 8 11 16 19 24 @@ -1344,7 +1344,7 @@ def test_inline_deletions_insertions__deletions_insertions_in_multiple_domains(s design.inline_deletions_insertions() self.helix0_strand0_inlined_test(design, max_offset=26, major_ticks=[0, 10, 19, 26], start=0, end=26) - def test_inline_deletions_insertions__deletions_insertions_in_multiple_domains_two_strands(self): + def test_inline_deletions_insertions__deletions_insertions_in_multiple_domains_two_strands(self) -> None: """ | is major tick, . is minor tick before @@ -1422,7 +1422,7 @@ class TestNickAndCrossover(unittest.TestCase): <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] """ - def setUp(self): + def setUp(self) -> None: strands_small_design = [ sc.Strand([sc.Domain(0, True, 0, 16)]), sc.Strand([sc.Domain(0, False, 0, 16)]), @@ -1445,7 +1445,7 @@ def setUp(self): staps.append(stap) self.design: sc.Design = sc.Design(strands=scafs + staps, grid=sc.square) - def test_add_nick__twice_on_same_domain(self): + def test_add_nick__twice_on_same_domain(self) -> None: """ before 0 8 16 24 @@ -1465,7 +1465,7 @@ def test_add_nick__twice_on_same_domain(self): self.assertIn(sc.Strand([sc.Domain(0, True, 8, 16)]), design.strands) self.assertIn(sc.Strand([sc.Domain(0, True, 16, 24)]), design.strands) - def test_add_nick__small_design_no_nicks_added(self): + def test_add_nick__small_design_no_nicks_added(self) -> None: """ 0 8 16 ACGTACGA AACCGGTA @@ -1493,7 +1493,7 @@ def test_add_nick__small_design_no_nicks_added(self): strand = strand_matching(self.small_design.strands, 1, False, 0, 16) self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - def test_add_nick__small_design_H0_forward(self): + def test_add_nick__small_design_H0_forward(self) -> None: """ 0 8 16 ACGTACGA AACCGGTA @@ -1527,7 +1527,7 @@ def test_add_nick__small_design_H0_forward(self): strand = strand_matching(self.small_design.strands, 1, False, 0, 16) self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - def test_add_nick__small_design_H0_reverse(self): + def test_add_nick__small_design_H0_reverse(self) -> None: """ 0 8 16 ACGTACGA AACCGGTA @@ -1561,7 +1561,7 @@ def test_add_nick__small_design_H0_reverse(self): strand = strand_matching(self.small_design.strands, 1, False, 0, 16) self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - def test_add_nick__small_design_H1_forward(self): + def test_add_nick__small_design_H1_forward(self) -> None: """ 0 8 16 ACGTACGA AACCGGTA @@ -1595,7 +1595,7 @@ def test_add_nick__small_design_H1_forward(self): strand = strand_matching(self.small_design.strands, 1, False, 0, 16) self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - def test_add_nick__small_design_H1_reverse(self): + def test_add_nick__small_design_H1_reverse(self) -> None: """ 0 8 16 ACGTACGA AACCGGTA @@ -1629,7 +1629,7 @@ def test_add_nick__small_design_H1_reverse(self): strand = strand_matching(self.small_design.strands, 1, False, 8, 16) self.assertEqual(remove_whitespace('GGCCCAAA'), strand.dna_sequence) - def test_add_full_crossover__small_design_H0_forward(self): + def test_add_full_crossover__small_design_H0_forward(self) -> None: """ 0 8 16 ACGTACGA AACCGGTA @@ -1666,7 +1666,7 @@ def test_add_full_crossover__small_design_H0_forward(self): strand = strand_matching(self.small_design.strands, 1, False, 8, 16) self.assertEqual(remove_whitespace('GGCCCAAA AACCGGTA'), strand.dna_sequence) - def test_add_full_crossover__small_design_H0_reverse(self): + def test_add_full_crossover__small_design_H0_reverse(self) -> None: """ 0 8 16 ACGTACGA AACCGGTA @@ -1703,7 +1703,7 @@ def test_add_full_crossover__small_design_H0_reverse(self): strand = strand_matching(self.small_design.strands, 1, False, 0, 16) self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - def test_add_half_crossover__small_design_H0_reverse_8(self): + def test_add_half_crossover__small_design_H0_reverse_8(self) -> None: """ 0 8 16 ACGTACGA AACCGGTA @@ -1746,7 +1746,7 @@ def test_add_half_crossover__small_design_H0_reverse_8(self): strand = strand_matching(self.small_design.strands, 1, False, 0, 16) self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - def test_add_half_crossover__small_design_H0_reverse_0(self): + def test_add_half_crossover__small_design_H0_reverse_0(self) -> None: """ 0 8 16 ACGTACGA AACCGGTA @@ -1777,7 +1777,7 @@ def test_add_half_crossover__small_design_H0_reverse_0(self): strand = strand_matching(self.small_design.strands, 1, False, 0, 16) self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - def test_add_half_crossover__small_design_H0_reverse_15(self): + def test_add_half_crossover__small_design_H0_reverse_15(self) -> None: """ 0 8 16 ACGTACGA AACCGGTA @@ -1808,7 +1808,7 @@ def test_add_half_crossover__small_design_H0_reverse_15(self): strand = strand_matching(self.small_design.strands, 1, False, 0, 16) self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - def test_add_half_crossover__small_design_illegal(self): + def test_add_half_crossover__small_design_illegal(self) -> None: """ 0 8 16 0 [------- -------> @@ -1820,7 +1820,7 @@ def test_add_half_crossover__small_design_illegal(self): with self.assertRaises(sc.IllegalDesignError): self.small_design.add_half_crossover(helix=0, helix2=1, offset=16, forward=False) - def test_add_full_crossover__small_design_illegal(self): + def test_add_full_crossover__small_design_illegal(self) -> None: """ 0 8 16 0 [------- -------> @@ -1832,7 +1832,7 @@ def test_add_full_crossover__small_design_illegal(self): with self.assertRaises(sc.IllegalDesignError): self.small_design.add_full_crossover(helix=0, helix2=1, offset=16, forward=False) - def test_add_full_crossover__small_design_illegal_only_one_helix_has_domain(self): + def test_add_full_crossover__small_design_illegal_only_one_helix_has_domain(self) -> None: """ 0 8 16 0 [------- -------> @@ -1886,7 +1886,7 @@ def add_nicks(self, design: sc.Design): design.add_nick(helix=5, offset=24, forward=True) design.add_nick(helix=5, offset=56, forward=True) - def test_add_nick__6_helix_rectangle(self): + def test_add_nick__6_helix_rectangle(self) -> None: self.add_nicks(self.design) self.assertEqual(25, len(self.design.strands)) for helix in range(0, len(self.design.helices), 2): @@ -1960,7 +1960,7 @@ def add_crossovers_after_nicks(self, design: sc.Design): design.add_half_crossover(helix=2, helix2=3, offset=95, forward=True) design.add_half_crossover(helix=4, helix2=5, offset=95, forward=True) - def test_add_nick_then_add_crossovers__6_helix_rectangle(self): + def test_add_nick_then_add_crossovers__6_helix_rectangle(self) -> None: self.add_nicks(self.design) self.add_crossovers_after_nicks(self.design) @@ -2143,19 +2143,19 @@ def test_add_nick_then_add_crossovers__6_helix_rectangle(self): class TestAutocalculatedData(unittest.TestCase): - def test_helix_min_max_offsets_illegal_explicitly_specified(self): + def test_helix_min_max_offsets_illegal_explicitly_specified(self) -> None: helices = [sc.Helix(min_offset=5, max_offset=5)] with self.assertRaises(sc.IllegalDesignError): design = sc.Design(helices=helices, strands=[], grid=sc.square) - def test_helix_min_max_offsets_illegal_autocalculated(self): + def test_helix_min_max_offsets_illegal_autocalculated(self) -> None: helices = [sc.Helix(min_offset=5)] ss = sc.Domain(0, True, 0, 4) strand = sc.Strand([ss]) with self.assertRaises(sc.IllegalDesignError): design = sc.Design(helices=helices, strands=[strand], grid=sc.square) - def test_helix_min_max_offsets(self): + def test_helix_min_max_offsets(self) -> None: helices = [sc.Helix(), sc.Helix(min_offset=-5), sc.Helix(max_offset=5), sc.Helix(min_offset=5, max_offset=10)] ss_0 = sc.Domain(helix=0, forward=True, start=20, end=25) @@ -2173,7 +2173,7 @@ def test_helix_min_max_offsets(self): self.assertEqual(5, design.helices[3].min_offset) self.assertEqual(10, design.helices[3].max_offset) - def test_helix_max_offset(self): + def test_helix_max_offset(self) -> None: helices = [sc.Helix(), sc.Helix(max_offset=8), sc.Helix()] ss_0 = sc.Domain(helix=0, forward=True, start=5, end=10) ss_1 = sc.Domain(helix=1, forward=False, start=2, end=6) @@ -2186,7 +2186,7 @@ def test_helix_max_offset(self): class TestSetHelixIdx(unittest.TestCase): - def test_set_helix_idx(self): + def test_set_helix_idx(self) -> None: helices = [sc.Helix(max_offset=20), sc.Helix(max_offset=20), sc.Helix(max_offset=20)] ss_0 = sc.Domain(helix=0, forward=True, start=0, end=6) ss_1 = sc.Domain(helix=1, forward=True, start=0, end=7) @@ -2220,7 +2220,7 @@ def test_set_helix_idx(self): class TestHelixGroups(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: n = 'north' e = 'east' s = 'south' @@ -2256,10 +2256,10 @@ def setUp(self): self.s = s self.w = w - def test_helix_groups(self): + def test_helix_groups(self) -> None: self._asserts_for_fixture(self.design) - def test_helix_groups_to_from_JSON(self): + def test_helix_groups_to_from_JSON(self) -> None: n = self.n e = self.e s = self.s @@ -2337,7 +2337,7 @@ def test_helix_groups_to_from_JSON(self): design_from_json = sc.Design.from_scadnano_json_str(design_json_str) self._asserts_for_fixture(design_from_json) - def test_helix_groups_fail_nonexistent(self): + def test_helix_groups_fail_nonexistent(self) -> None: helices = [ sc.Helix(max_offset=20, group="north"), sc.Helix(max_offset=21, group="east"), @@ -2379,7 +2379,7 @@ def _asserts_for_fixture(self, design: sc.Design): self.assertSequenceEqual([0, 0], design.helices[13].grid_position) self.assertSequenceEqual([0, 1], design.helices[15].grid_position) - def test_JSON_bad_uses_groups_and_top_level_grid(self): + def test_JSON_bad_uses_groups_and_top_level_grid(self) -> None: json_str = ''' { "grid": "none", @@ -2421,7 +2421,7 @@ def test_JSON_bad_uses_groups_and_top_level_grid(self): with self.assertRaises(sc.IllegalDesignError) as ex: design = sc.Design.from_scadnano_json_str(json_str) - def test_JSON_bad_uses_groups_and_top_level_helices_view_order(self): + def test_JSON_bad_uses_groups_and_top_level_helices_view_order(self) -> None: json_str = ''' { "helices_view_order": [3, 2, 1, 0], @@ -2463,7 +2463,7 @@ def test_JSON_bad_uses_groups_and_top_level_helices_view_order(self): with self.assertRaises(sc.IllegalDesignError) as ex: design = sc.Design.from_scadnano_json_str(json_str) - def test_JSON_bad_no_groups_but_helices_reference_groups(self): + def test_JSON_bad_no_groups_but_helices_reference_groups(self) -> None: json_str = ''' { "grid": "square", @@ -2572,7 +2572,7 @@ def test_strand_names_can_be_nonunique(self) -> None: class TestJSON(unittest.TestCase): - def test_Helix_major_tick_start_default_min_offset(self): + def test_Helix_major_tick_start_default_min_offset(self) -> None: helices = [ sc.Helix(min_offset=10, max_offset=100), sc.Helix(max_offset=100), @@ -2603,7 +2603,7 @@ def test_Helix_major_tick_start_default_min_offset(self): self.assertEqual(0, design.helices[1].major_tick_start) self.assertEqual(15, design.helices[2].major_tick_start) - def test_Helix_major_tick_periodic_distances(self): + def test_Helix_major_tick_periodic_distances(self) -> None: grid = sc.square helices = [ sc.Helix(major_tick_start=10, max_offset=30, major_tick_distance=5), @@ -2654,7 +2654,7 @@ def test_Helix_major_tick_periodic_distances(self): self.assertNotIn(sc.major_tick_periodic_distances_key, h4) self.assertNotIn(sc.major_tick_start_key, h4) - def test_default_helices_view_order_with_nondefault_helix_idxs_in_default_order(self): + def test_default_helices_view_order_with_nondefault_helix_idxs_in_default_order(self) -> None: helices = [sc.Helix(idx=1, max_offset=100), sc.Helix(idx=3, max_offset=100)] design = sc.Design(helices=helices, strands=[]) self.assertListEqual([1, 3], design.helices_view_order) @@ -2663,7 +2663,7 @@ def test_default_helices_view_order_with_nondefault_helix_idxs_in_default_order( design_json_ser = design.to_json_serializable(suppress_indent=False) self.assertFalse(sc.helices_view_order_key in design_json_ser) - def test_default_helices_view_order_with_nondefault_helix_idxs_in_nondefault_order(self): + def test_default_helices_view_order_with_nondefault_helix_idxs_in_nondefault_order(self) -> None: helices = [sc.Helix(idx=1, max_offset=100), sc.Helix(idx=3, max_offset=100)] design = sc.Design(helices=helices, strands=[], helices_view_order=[3, 1]) self.assertListEqual([3, 1], design.helices_view_order) @@ -2673,7 +2673,7 @@ def test_default_helices_view_order_with_nondefault_helix_idxs_in_nondefault_ord actual_view_order = design_json_ser[sc.helices_view_order_key] self.assertListEqual([3, 1], actual_view_order) - def test_strand_labels(self): + def test_strand_labels(self) -> None: helices = [sc.Helix(max_offset=100), sc.Helix(max_offset=100)] strand0_expected = sc.Strand([sc.Domain(0, True, 0, 10)], label={ 'name': 'strand 0', @@ -2692,7 +2692,7 @@ def test_strand_labels(self): self.assertDictEqual(strand0_expected.label, strand0.label) self.assertDictEqual(strand1_expected.label, strand1.label) - def test_domain_labels(self): + def test_domain_labels(self) -> None: helices = [sc.Helix(max_offset=100), sc.Helix(max_offset=100)] dom00_expected = sc.Domain(0, True, 0, 10, label='domain 00') dom10_expected = sc.Domain(0, False, 0, 10) @@ -2710,7 +2710,7 @@ def test_domain_labels(self): self.assertIsNone(dom10.label) self.assertEqual(dom11_expected.label, dom11.label) - def test_nondefault_geometry(self): + def test_nondefault_geometry(self) -> None: geometry_expected = sc.Geometry(rise_per_base_pair=10.0, helix_radius=4.0, bases_per_turn=11.0, minor_groove_angle=10.0, inter_helix_gap=5.0) @@ -2724,7 +2724,7 @@ def test_nondefault_geometry(self): self.assertAlmostEqual(geometry_expected.minor_groove_angle, geometry_actual.minor_groove_angle) self.assertAlmostEqual(geometry_expected.inter_helix_gap, geometry_actual.inter_helix_gap) - def test_nondefault_geometry_some_default(self): + def test_nondefault_geometry_some_default(self) -> None: geometry_expected = sc.Geometry(rise_per_base_pair=10.0, minor_groove_angle=10.0, inter_helix_gap=5.0) design = sc.Design(helices=[], strands=[], geometry=geometry_expected) json_str = design.to_json() @@ -2755,7 +2755,7 @@ def test_NoIndent_on_helix_without_position_or_major_ticks_present(self) -> None helix_json = json_map[sc.helices_key][0] self.assertTrue(isinstance(helix_json, sc.NoIndent)) - def test_error_when_grid_missing(self): + def test_error_when_grid_missing(self) -> None: json_str = """ { "helices": [{"grid_position": [0,0]}], @@ -2772,7 +2772,7 @@ def test_error_when_grid_missing(self): msg = ex.exception.args[0] self.assertTrue('grid' in msg) - def test_error_when_domain_helix_missing(self): + def test_error_when_domain_helix_missing(self) -> None: json_str = """ { "grid": "square", @@ -2790,7 +2790,7 @@ def test_error_when_domain_helix_missing(self): msg = ex.exception.args[0] self.assertTrue('helix' in msg) - def test_error_when_domain_forward_and_right_missing(self): + def test_error_when_domain_forward_and_right_missing(self) -> None: json_str = """ { "grid": "square", @@ -2809,7 +2809,7 @@ def test_error_when_domain_forward_and_right_missing(self): self.assertTrue('forward' in msg) self.assertTrue('right' in msg) - def test_error_when_domain_start_missing(self): + def test_error_when_domain_start_missing(self) -> None: json_str = """ { "grid": "square", @@ -2827,7 +2827,7 @@ def test_error_when_domain_start_missing(self): msg = ex.exception.args[0] self.assertTrue('start' in msg) - def test_error_when_domain_end_missing(self): + def test_error_when_domain_end_missing(self) -> None: json_str = """ { "grid": "square", @@ -2845,7 +2845,7 @@ def test_error_when_domain_end_missing(self): msg = ex.exception.args[0] self.assertTrue('end' in msg) - def test_error_when_strands_missing(self): + def test_error_when_strands_missing(self) -> None: json_str = """ { "grid": "square", @@ -2857,7 +2857,7 @@ def test_error_when_strands_missing(self): msg = ex.exception.args[0] self.assertTrue('strands' in msg) - def test_legacy_right_key(self): + def test_legacy_right_key(self) -> None: json_str = """ { "grid": "square", @@ -2873,7 +2873,7 @@ def test_legacy_right_key(self): d = sc.Design.from_scadnano_json_str(json_str) self.assertEqual(True, d.strands[0].domains[0].forward) - def test_legacy_dna_sequence_key(self): + def test_legacy_dna_sequence_key(self) -> None: json_str = """ { "grid": "square", @@ -2890,7 +2890,7 @@ def test_legacy_dna_sequence_key(self): d = sc.Design.from_scadnano_json_str(json_str) self.assertEqual("ACGTA", d.strands[0].dna_sequence) - def test_legacy_substrands_key(self): + def test_legacy_substrands_key(self) -> None: json_str = """ { "grid": "square", @@ -2909,7 +2909,7 @@ def test_legacy_substrands_key(self): self.assertEqual(0, d.strands[0].domains[0].start) self.assertEqual(5, d.strands[0].domains[0].end) - def test_color_specified_with_integer(self): + def test_color_specified_with_integer(self) -> None: # addresses https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/58 # 0066cc hex is 26316 decimal json_str = """ @@ -2929,7 +2929,7 @@ def test_color_specified_with_integer(self): actual_color_hex = d.strands[0].color.to_json_serializable(False) self.assertEqual(expected_color_hex, actual_color_hex) - def test_position_specified_with_origin_keyword(self): + def test_position_specified_with_origin_keyword(self) -> None: # addresses https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/59 json_str = """ { @@ -2962,7 +2962,7 @@ def test_position_specified_with_origin_keyword(self): self.assertEqual(expected_roll, actual_roll) self.assertEqual(expected_yaw, actual_yaw) - def test_json_tristan_example_issue_32(self): + def test_json_tristan_example_issue_32(self) -> None: json_str = """ { "version": "0.3.0", @@ -2982,7 +2982,7 @@ def test_json_tristan_example_issue_32(self): """ d = sc.Design.from_scadnano_json_str(json_str) - def test_to_json__hairpin(self): + def test_to_json__hairpin(self) -> None: """ 01234 AAACC # helix 0 going forward @@ -3004,7 +3004,7 @@ def test_to_json__hairpin(self): json = design.to_json() # should be no error getting here - def test_to_json__roll(self): + def test_to_json__roll(self) -> None: helix = sc.Helix(roll=90) ss_f = sc.Domain(helix=0, forward=True, start=0, end=5) ss_r = sc.Domain(helix=0, forward=False, start=0, end=5) @@ -3017,7 +3017,7 @@ def test_to_json__roll(self): class TestIllegalStructuresPrevented(unittest.TestCase): - # def test_to_json__error_if_DNAOrigamiDesign_no_scaffold(self): + # def test_to_json__error_if_DNAOrigamiDesign_no_scaffold(self) -> None: # # we are allowed to delay assigning a scaffold to a DNAOrigamiDesign, # # but to_json should fail # st_l = sc.Strand([ @@ -3039,22 +3039,22 @@ class TestIllegalStructuresPrevented(unittest.TestCase): # with self.assertRaises(sc.IllegalDesignError): # design_pre_json.to_json() - def test_strands_not_specified_in_Design_constructor(self): + def test_strands_not_specified_in_Design_constructor(self) -> None: design = sc.Design(helices=[]) self.assertEqual(0, len(design.helices)) self.assertEqual(0, len(design.strands)) - def test_helices_not_specified_in_Design_constructor(self): + def test_helices_not_specified_in_Design_constructor(self) -> None: design = sc.Design(strands=[]) self.assertEqual(0, len(design.helices)) self.assertEqual(0, len(design.strands)) - def test_strands_and_helices_not_specified_in_Design_constructor(self): + def test_strands_and_helices_not_specified_in_Design_constructor(self) -> None: design = sc.Design() self.assertEqual(0, len(design.helices)) self.assertEqual(0, len(design.strands)) - def test_consecutive_domains_loopout(self): + def test_consecutive_domains_loopout(self) -> None: helices = [sc.Helix(max_offset=10)] ss1 = sc.Domain(0, True, 0, 3) ss2 = sc.Loopout(4) @@ -3067,25 +3067,25 @@ def test_consecutive_domains_loopout(self): with self.assertRaises(sc.IllegalDesignError): design = sc.Design(helices=helices, strands=[strand], grid=sc.square) - def test_singleton_loopout(self): + def test_singleton_loopout(self) -> None: helices = [sc.Helix(max_offset=10)] - ss1 = sc.Loopout(4) - with self.assertRaises(sc.IllegalDesignError): - strand = sc.Strand([ss1]) + loopout = sc.Loopout(4) + with self.assertRaises(sc.StrandError): + strand = sc.Strand([loopout]) strand = sc.Strand([]) - strand.domains.append(ss1) - with self.assertRaises(sc.IllegalDesignError): + strand.domains.append(loopout) + with self.assertRaises(sc.StrandError): design = sc.Design(helices=helices, strands=[strand], grid=sc.square) - def test_strand_offset_beyond_maxbases(self): + def test_strand_offset_beyond_maxbases(self) -> None: helices = [sc.Helix(max_offset=10)] ss1 = sc.Domain(0, True, 0, 20) strands = [sc.Strand([ss1])] with self.assertRaises(sc.StrandError): design = sc.Design(helices=helices, strands=strands) - def test_to_idt_bulk_input_format__duplicate_names_same_sequence(self): + def test_to_idt_bulk_input_format__duplicate_names_same_sequence(self) -> None: length = 8 helices = [sc.Helix(max_offset=length)] ss1_r = sc.Domain(0, True, 0, 4) @@ -3106,7 +3106,7 @@ def test_to_idt_bulk_input_format__duplicate_names_same_sequence(self): # should not raise exception idt_str = design.to_idt_bulk_input_format() - def test_to_idt_bulk_input_format__duplicate_names_different_sequences(self): + def test_to_idt_bulk_input_format__duplicate_names_different_sequences(self) -> None: ss1_r = sc.Domain(0, True, 0, 4) ss2_r = sc.Domain(0, True, 4, 8) ss_l = sc.Domain(0, False, 0, 4) @@ -3125,7 +3125,7 @@ def test_to_idt_bulk_input_format__duplicate_names_different_sequences(self): with self.assertRaises(sc.IllegalDesignError): idt_str = design.to_idt_bulk_input_format() - def test_to_idt_bulk_input_format__duplicate_names_different_scales(self): + def test_to_idt_bulk_input_format__duplicate_names_different_scales(self) -> None: ss1_r = sc.Domain(0, True, 0, 4) ss2_r = sc.Domain(0, True, 4, 8) ss_l = sc.Domain(0, False, 0, 4) @@ -3144,7 +3144,7 @@ def test_to_idt_bulk_input_format__duplicate_names_different_scales(self): with self.assertRaises(sc.IllegalDesignError): idt_str = design.to_idt_bulk_input_format() - def test_to_idt_bulk_input_format__duplicate_names_different_purifications(self): + def test_to_idt_bulk_input_format__duplicate_names_different_purifications(self) -> None: length = 8 ss1_r = sc.Domain(0, True, 0, 4) ss2_r = sc.Domain(0, True, 4, 8) @@ -3164,7 +3164,7 @@ def test_to_idt_bulk_input_format__duplicate_names_different_purifications(self) with self.assertRaises(sc.IllegalDesignError): idt_str = design.to_idt_bulk_input_format() - def test_assign_dna__conflicting_sequences_directly_assigned(self): + def test_assign_dna__conflicting_sequences_directly_assigned(self) -> None: ss_right = sc.Domain(0, True, 0, 5) ss_left = sc.Domain(0, False, 0, 5) strand_right = sc.Strand([ss_right]) @@ -3174,7 +3174,7 @@ def test_assign_dna__conflicting_sequences_directly_assigned(self): with self.assertRaises(sc.IllegalDesignError): design.assign_dna(strand_right, 'TTTTT') - def test_assign_dna__conflicting_sequences_indirectly_assigned(self): + def test_assign_dna__conflicting_sequences_indirectly_assigned(self) -> None: ss_right = sc.Domain(0, True, 0, 5) ss_left = sc.Domain(0, False, 0, 5) strand_right = sc.Strand([ss_right]) @@ -3184,7 +3184,7 @@ def test_assign_dna__conflicting_sequences_indirectly_assigned(self): with self.assertRaises(sc.IllegalDesignError): design.assign_dna(strand_left, 'GGGGG') - def test_overlapping_caught_in_strange_counterexample(self): + def test_overlapping_caught_in_strange_counterexample(self) -> None: # found this counterexample as a simplified version of something caught in practice s1_left_ss0 = sc.Domain(0, False, 0, 5) s1_ss1 = sc.Domain(0, True, 0, 15) @@ -3200,14 +3200,14 @@ def test_overlapping_caught_in_strange_counterexample(self): with self.assertRaises(sc.IllegalDesignError): design = sc.Design(strands=strands, grid=sc.square) - def test_major_tick_outside_range(self): + def test_major_tick_outside_range(self) -> None: with self.assertRaises(sc.IllegalDesignError): helix = sc.Helix(max_offset=9, major_ticks=[2, 5, 10]) - def test_major_tick_just_inside_range(self): + def test_major_tick_just_inside_range(self) -> None: helix = sc.Helix(max_offset=9, major_ticks=[0, 5, 9]) - def test_two_illegally_overlapping_strands(self): + def test_two_illegally_overlapping_strands(self) -> None: ss_bot = sc.Domain(helix=0, forward=False, start=0, end=9) ss_top = sc.Domain(helix=0, forward=False, start=0, end=9) strand_bot = sc.Strand(domains=[ss_bot]) @@ -3216,7 +3216,7 @@ def test_two_illegally_overlapping_strands(self): with self.assertRaises(sc.IllegalDesignError): sc.Design(grid=sc.square, strands=strands) - def test_two_nonconsecutive_illegally_overlapping_strands(self): + def test_two_nonconsecutive_illegally_overlapping_strands(self) -> None: ss_top1 = sc.Domain(helix=0, forward=False, start=0, end=5) ss_bot = sc.Domain(helix=0, forward=True, start=2, end=9) ss_top2 = sc.Domain(helix=0, forward=False, start=4, end=8) @@ -3227,7 +3227,7 @@ def test_two_nonconsecutive_illegally_overlapping_strands(self): with self.assertRaises(sc.IllegalDesignError): sc.Design(grid=sc.square, strands=strands) - def test_four_legally_leapfrogging_strands(self): + def test_four_legally_leapfrogging_strands(self) -> None: ss_top1 = sc.Domain(helix=0, forward=False, start=0, end=20) ss_bot1 = sc.Domain(helix=0, forward=True, start=10, end=30) ss_top2 = sc.Domain(helix=0, forward=False, start=20, end=40) @@ -3239,7 +3239,7 @@ def test_four_legally_leapfrogging_strands(self): strands = [strand_bot1, strand_bot2, strand_top1, strand_top2] sc.Design(grid=sc.square, strands=strands) - def test_strand_references_nonexistent_helix(self): + def test_strand_references_nonexistent_helix(self) -> None: h1 = sc.Helix(max_offset=9) h2 = sc.Helix(max_offset=9) ss_bot = sc.Domain(helix=2, forward=False, start=0, end=9) @@ -3259,7 +3259,7 @@ def setUp(self) -> None: self.design.strand(0, 0).to(3).cross(1).to(0).cross(2).to(3).with_sequence('ACA TCT GTG') self.strand = self.design.strands[0] - def test_3_helix_before_design(self): + def test_3_helix_before_design(self) -> None: expected_strand_before = sc.Strand([ sc.Domain(0, True, 0, 3), sc.Domain(1, False, 0, 3), @@ -3267,7 +3267,7 @@ def test_3_helix_before_design(self): ], dna_sequence='ACA TCT GTG'.replace(' ', '')) self.assertEqual(expected_strand_before, self.strand) - def test_insert_domain_with_sequence(self): + def test_insert_domain_with_sequence(self) -> None: helices = [sc.Helix(max_offset=100) for _ in range(4)] design = sc.Design(helices=helices, strands=[]) design.strand(0, 0).to(3).cross(1).to(0).cross(3).to(3).with_sequence('ACA TCT GTG') @@ -3290,7 +3290,7 @@ def test_insert_domain_with_sequence(self): ], dna_sequence='ACA TCT ??? GTG'.replace(' ', '')) self.assertEqual(expected_strand, design.strands[0]) - def test_append_domain_with_sequence(self): + def test_append_domain_with_sequence(self) -> None: domain = sc.Domain(3, False, 0, 3) self.design.append_domain(self.strand, domain) expected_strand = sc.Strand([ @@ -3301,7 +3301,7 @@ def test_append_domain_with_sequence(self): ], dna_sequence='ACA TCT GTG ???'.replace(' ', '')) self.assertEqual(expected_strand, self.strand) - def test_remove_first_domain_with_sequence(self): + def test_remove_first_domain_with_sequence(self) -> None: self.design.remove_domain(self.strand, self.strand.domains[0]) expected_strand = sc.Strand([ sc.Domain(1, False, 0, 3), @@ -3309,7 +3309,7 @@ def test_remove_first_domain_with_sequence(self): ], dna_sequence=' TCT GTG'.replace(' ', '')) self.assertEqual(expected_strand, self.strand) - def test_remove_middle_domain_with_sequence(self): + def test_remove_middle_domain_with_sequence(self) -> None: self.design.remove_domain(self.strand, self.strand.domains[1]) expected_strand = sc.Strand([ sc.Domain(0, True, 0, 3), @@ -3317,7 +3317,7 @@ def test_remove_middle_domain_with_sequence(self): ], dna_sequence='ACA GTG'.replace(' ', '')) self.assertEqual(expected_strand, self.strand) - def test_remove_last_domain_with_sequence(self): + def test_remove_last_domain_with_sequence(self) -> None: self.design.remove_domain(self.strand, self.strand.domains[2]) expected_strand = sc.Strand([ sc.Domain(0, True, 0, 3), @@ -3332,7 +3332,7 @@ def setUp(self) -> None: helices = [sc.Helix(max_offset=100) for _ in range(10)] self.design = sc.Design(helices=helices, strands=[], grid=sc.square) - def test_with_label__str(self): + def test_with_label__str(self) -> None: label = 'abc' self.design.strand(0, 0).to(5).cross(1).to(0).with_label(label) actual_strand = self.design.strands[0] @@ -3343,7 +3343,7 @@ def test_with_label__str(self): self.assertEqual(expected_strand.label, actual_strand.label) - def test_with_label__dict(self): + def test_with_label__dict(self) -> None: label = {'name': 'abc', 'type': 3} self.design.strand(0, 0).to(5).cross(1).to(0).with_label(label) actual_strand = self.design.strands[0] @@ -3354,7 +3354,7 @@ def test_with_label__dict(self): self.assertDictEqual(expected_strand.label, actual_strand.label) - def test_with_domain_label(self): + def test_with_domain_label(self) -> None: label0 = 'abc' label1 = {'name': 'abc', 'type': 3} self.design.strand(0, 0).to(5).with_domain_label(label0).cross(1).to(0).with_domain_label(label1) @@ -3367,7 +3367,7 @@ def test_with_domain_label(self): self.assertEqual(expected_strand.domains[0].label, actual_strand.domains[0].label) self.assertDictEqual(expected_strand.domains[1].label, actual_strand.domains[1].label) - def test_with_domain_label__and__with_label(self): + def test_with_domain_label__and__with_label(self) -> None: strand_label = 'xyz' label0 = 'abc' label1 = {'name': 'abc', 'type': 3} @@ -3391,7 +3391,7 @@ def set_colors_black(*strands): class TestAddStrand(unittest.TestCase): - def test_add_strand__with_loopout(self): + def test_add_strand__with_loopout(self) -> None: helices = [sc.Helix(max_offset=10), sc.Helix(max_offset=10)] design = sc.Design(helices=helices, strands=[]) @@ -3407,7 +3407,7 @@ def test_add_strand__with_loopout(self): self.assertEqual(ss1, design.domain_at(0, 0, True)) self.assertEqual(ss2, design.domain_at(1, 0, False)) - def test_add_strand__illegal_overlapping_domains(self): + def test_add_strand__illegal_overlapping_domains(self) -> None: helices = [sc.Helix(max_offset=50), sc.Helix(max_offset=50)] design = sc.Design(helices=helices, strands=[], grid=sc.square) with self.assertRaises(sc.StrandError): @@ -3421,7 +3421,7 @@ def test_add_strand__illegal_overlapping_domains(self): class TestAssignDNA(unittest.TestCase): - def test_assign_dna__hairpin(self): + def test_assign_dna__hairpin(self) -> None: """ 01234 AAACC # helix 0 going forward @@ -3442,7 +3442,7 @@ def test_assign_dna__hairpin(self): design.assign_dna(strand_forward, 'AAACC TGCAC') self.assertEqual('AAACC TGCAC GGTTT'.replace(' ', ''), strand_forward.dna_sequence) - def test_assign_dna__from_strand_with_loopout(self): + def test_assign_dna__from_strand_with_loopout(self) -> None: """ 01234 <-TTTGG-] @@ -3476,7 +3476,7 @@ def test_assign_dna__from_strand_with_loopout(self): self.assertEqual('GGTTT'.replace(' ', ''), strand_single0.dna_sequence) self.assertEqual('CGAAT'.replace(' ', ''), strand_single1.dna_sequence) - def test_assign_dna__to_strand_with_loopout(self): + def test_assign_dna__to_strand_with_loopout(self) -> None: """ 01234 <-TTTGG-] @@ -3515,7 +3515,7 @@ def test_assign_dna__to_strand_with_loopout(self): self.assertEqual('GGTTT'.replace(' ', ''), strand_single0.dna_sequence) self.assertEqual('CGAAT'.replace(' ', ''), strand_single1.dna_sequence) - def test_assign_dna__assign_from_strand_multi_other_single(self): + def test_assign_dna__assign_from_strand_multi_other_single(self) -> None: """ 01234567 <-TTTG----GACA-] @@ -3538,7 +3538,7 @@ def test_assign_dna__assign_from_strand_multi_other_single(self): self.assertEqual('CTGT ATGA TTCG AAAC'.replace(' ', ''), strand_multi.dna_sequence) self.assertEqual('ACAG GTTT'.replace(' ', ''), strand_single.dna_sequence) - def test_assign_dna__assign_to_strand_multi_other_single(self): + def test_assign_dna__assign_to_strand_multi_other_single(self) -> None: """ 01234567 <-TTTG----GACA-] @@ -3561,7 +3561,7 @@ def test_assign_dna__assign_to_strand_multi_other_single(self): self.assertEqual('CTGT ???? ???? AAAC'.replace(' ', ''), strand_multi.dna_sequence) self.assertEqual('ACAG GTTT'.replace(' ', ''), strand_single.dna_sequence) - def test_assign_dna__other_strand_fully_defined_already(self): + def test_assign_dna__other_strand_fully_defined_already(self) -> None: """ 01234567 [------> @@ -3578,7 +3578,7 @@ def test_assign_dna__other_strand_fully_defined_already(self): design.assign_dna(strand_l, 'TTTG') # should not have an error by this point - def test_assign_dna__other_strand_fully_defined_already_and_other_extends_beyond(self): + def test_assign_dna__other_strand_fully_defined_already_and_other_extends_beyond(self) -> None: """ 01234567 [------> @@ -3595,7 +3595,7 @@ def test_assign_dna__other_strand_fully_defined_already_and_other_extends_beyond design.assign_dna(strand_l, 'ACTT') # should not have an error by this point - def test_assign_dna__other_strand_fully_defined_already_and_self_extends_beyond(self): + def test_assign_dna__other_strand_fully_defined_already_and_self_extends_beyond(self) -> None: """ 01234567 [------> @@ -3612,7 +3612,7 @@ def test_assign_dna__other_strand_fully_defined_already_and_self_extends_beyond( design.assign_dna(strand_r, 'CAAAGTCG') # should not have an error by this point - def test_assign_dna__two_equal_length_strands_on_one_helix(self): + def test_assign_dna__two_equal_length_strands_on_one_helix(self) -> None: """ 01234 <---] @@ -3628,7 +3628,7 @@ def test_assign_dna__two_equal_length_strands_on_one_helix(self): design.assign_dna(strand_l, 'AAAAC') self.assertEqual('GTTTT', strand_r.dna_sequence) - def test_assign_dna__assign_seq_with_wildcards(self): + def test_assign_dna__assign_seq_with_wildcards(self) -> None: """ 01234 <---] @@ -3645,7 +3645,7 @@ def test_assign_dna__assign_seq_with_wildcards(self): design.assign_dna(strand_top, 'AA??C') self.assertEqual('G??TT', strand_bot.dna_sequence) - def test_assign_dna__one_strand_assigned_by_complement_from_two_other_strands(self): + def test_assign_dna__one_strand_assigned_by_complement_from_two_other_strands(self) -> None: """ 0123 4567 <-AAAC-] <-GGGA-] @@ -3663,7 +3663,7 @@ def test_assign_dna__one_strand_assigned_by_complement_from_two_other_strands(se design.assign_dna(st_top_right, 'AGGG') self.assertEqual('TTTGCCCT', st_bot.dna_sequence) - def test_assign_dna__adapter_assigned_from_scaffold_and_tiles(self): + def test_assign_dna__adapter_assigned_from_scaffold_and_tiles(self) -> None: """ XXX: it appears the behavior this tests (which the other tests miss) is assigning DNA to tile0 first, then to tile1, and adap is connected to each of them on different helices. @@ -3700,7 +3700,7 @@ def test_assign_dna__adapter_assigned_from_scaffold_and_tiles(self): design.assign_dna(scaf, 'AA TTTG GAAA TG') self.assertEqual('TTTC CATT GGCA CAAA'.replace(' ', ''), adap.dna_sequence) - def test_assign_dna__adapter_assigned_from_scaffold_and_tiles_with_deletions(self): + def test_assign_dna__adapter_assigned_from_scaffold_and_tiles_with_deletions(self) -> None: """ XXX: it appears the behavior this tests (which the other tests miss) is assigning DNA to tile0 first, then to tile1, and adap is connected to each of them on different helices. @@ -3742,7 +3742,7 @@ def test_assign_dna__adapter_assigned_from_scaffold_and_tiles_with_deletions(sel design.assign_dna(scaf, 'AA TTTG GAA TG') self.assertEqual('TTC CAT GCA CAAA'.replace(' ', ''), adap.dna_sequence) - def test_assign_dna__adapter_assigned_from_scaffold_and_tiles_with_insertions(self): + def test_assign_dna__adapter_assigned_from_scaffold_and_tiles_with_insertions(self) -> None: """ XXX: it appears the behavior this tests (which the other tests miss) is assigning DNA to tile0 first, then to tile1, and adap is connected to each of them on different helices. @@ -3783,7 +3783,7 @@ def test_assign_dna__adapter_assigned_from_scaffold_and_tiles_with_insertions(se design.assign_dna(scaf, 'AA TTTG GAAA TG') self.assertEqual('TTTC CATTT GGGCA CAAA'.replace(' ', ''), adap.dna_sequence) - def test_assign_dna__dna_sequence_shorter_than_complementary_strand_right_strand_longer(self): + def test_assign_dna__dna_sequence_shorter_than_complementary_strand_right_strand_longer(self) -> None: """ <---] CAAAA @@ -3799,7 +3799,7 @@ def test_assign_dna__dna_sequence_shorter_than_complementary_strand_right_strand design.assign_dna(strand_short, 'AAAAC') self.assertEqual('GTTTT?????', strand_long.dna_sequence) - def test_assign_dna__dna_sequence_shorter_than_complementary_strand_left_strand_longer(self): + def test_assign_dna__dna_sequence_shorter_than_complementary_strand_left_strand_longer(self) -> None: """ [---> AAAAC @@ -3815,7 +3815,7 @@ def test_assign_dna__dna_sequence_shorter_than_complementary_strand_left_strand_ design.assign_dna(strand_short, 'AAAAC') self.assertEqual('?????GTTTT', strand_long.dna_sequence) - def test_assign_dna__dna_sequence_with_uncomplemented_domain_on_different_helix(self): + def test_assign_dna__dna_sequence_with_uncomplemented_domain_on_different_helix(self) -> None: """ <---] CAAAA @@ -3836,7 +3836,7 @@ def test_assign_dna__dna_sequence_with_uncomplemented_domain_on_different_helix( self.assertEqual('GTTTT????????', strand_long.dna_sequence) def test_assign_dna__dna_sequence_with_uncomplemented_domain_on_different_helix_wildcards_both_ends( - self): + self) -> None: """ <---] CAAAA @@ -3856,7 +3856,7 @@ def test_assign_dna__dna_sequence_with_uncomplemented_domain_on_different_helix_ design.assign_dna(strand_short, 'AAAAC') self.assertEqual('?????GTTTT???', strand_long.dna_sequence) - def test_assign_dna__one_helix_with_one_bottom_strand_and_three_top_strands(self): + def test_assign_dna__one_helix_with_one_bottom_strand_and_three_top_strands(self) -> None: """ 012 345 678 -TTT> -GGG> -CCC> @@ -3878,7 +3878,7 @@ def test_assign_dna__one_helix_with_one_bottom_strand_and_three_top_strands(self self.assertEqual('GGG', strand_top2.dna_sequence) self.assertEqual('TTT', strand_top3.dna_sequence) - def test_assign_dna__two_helices_with_multiple_domain_intersections(self): + def test_assign_dna__two_helices_with_multiple_domain_intersections(self) -> None: """ 012 345 678 901 M13 [-ACC----TAA---GAA----AAC---+ @@ -3907,7 +3907,7 @@ def test_assign_dna__two_helices_with_multiple_domain_intersections(self): self.assertEqual('GGT GAT TTC TTA'.replace(' ', ''), first_stap.dna_sequence) self.assertEqual('AGT GTT TTC ATG'.replace(' ', ''), second_stap.dna_sequence) - def test_assign_dna__upper_left_edge_staple_of_16H_origami_rectangle(self): + def test_assign_dna__upper_left_edge_staple_of_16H_origami_rectangle(self) -> None: """ staple | @@ -3929,7 +3929,7 @@ def test_assign_dna__upper_left_edge_staple_of_16H_origami_rectangle(self): expected_seq_stap_upperleft = 'CTAAAACACTCATCTTGAGGCAAAAGAATACA' self.assertEqual(expected_seq_stap_upperleft, stap.dna_sequence) - def test_assign_dna__2helix_with_deletions(self): + def test_assign_dna__2helix_with_deletions(self) -> None: """ scaf index: 2 3 4 5 offset: 0 D1 2 3 D4 5 @@ -3969,7 +3969,7 @@ def test_assign_dna__2helix_with_deletions(self): self.assertEqual("TTTG", stap_left.dna_sequence) self.assertEqual("GAAC", stap_right.dna_sequence) - def test_assign_dna__wildcards_simple(self): + def test_assign_dna__wildcards_simple(self) -> None: """ 012 345 678 -TTC> -GGA> -CCT> @@ -4027,7 +4027,7 @@ def setUp(self) -> None: self.strand_top_big9, self.strand_top_big6] self.design = sc.Design(grid=sc.square, strands=strands) - def test_assign_dna__wildcards_multiple_overlaps(self): + def test_assign_dna__wildcards_multiple_overlaps(self) -> None: """ 012 345 678 901 234 567 890 +---------------+ @@ -4050,11 +4050,11 @@ def test_assign_dna__wildcards_multiple_overlaps(self): self.design.assign_dna(self.strand_top_big6, 'GGATTGGCA') self.assertEqual('TGC CAA GCA GTT TCC GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence) - def test_assign_dna__domain_sequence_too_long_error(self): + def test_assign_dna__domain_sequence_too_long_error(self) -> None: with self.assertRaises(sc.IllegalDesignError): self.design.assign_dna(self.strand_top_big9, 'AACTTC', domain=self.dom_top9) - def test_assign_dna__to_individual_domains__wildcards_multiple_overlaps(self): + def test_assign_dna__to_individual_domains__wildcards_multiple_overlaps(self) -> None: """ 012 345 678 901 234 567 890 +---------------+ @@ -4164,7 +4164,7 @@ def test_assign_dna__to_individual_domains__wildcards_multiple_overlaps(self): self.assertEqual('GGA TTG GCA'.replace(' ', ''), self.strand_top_big6.dna_sequence) self.assertEqual('TGC CAA GCA GTT TCC GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence) - def test_method_chaining_with_domain_sequence(self): + def test_method_chaining_with_domain_sequence(self) -> None: """ 012 345 678 901 234 567 890 +---------------+ @@ -4253,7 +4253,7 @@ def test_method_chaining_with_domain_sequence(self): class TestSubstrandDNASequenceIn(unittest.TestCase): - def test_dna_sequence_in__right_then_left(self): + def test_dna_sequence_in__right_then_left(self) -> None: ss0 = sc.Domain(0, True, 0, 10) ss1 = sc.Domain(1, False, 0, 10) strand = sc.Strand([ss0, ss1]) @@ -4289,7 +4289,7 @@ def test_dna_sequence_in__right_then_left(self): self.assertEqual("GGTTTTACG", ss1.dna_sequence_in(1, 9)) self.assertEqual("GGTTTTACGT", ss1.dna_sequence_in(0, 9)) - def test_dna_sequence_in__right_then_left_deletions(self): + def test_dna_sequence_in__right_then_left_deletions(self) -> None: ss0 = sc.Domain(0, True, 0, 10, deletions=[2, 5, 6]) ss1 = sc.Domain(1, False, 0, 10, deletions=[2, 6, 7]) strand = sc.Strand([ss0, ss1]) @@ -4331,7 +4331,7 @@ def test_dna_sequence_in__right_then_left_deletions(self): # self.assertEqual("GGT", ss1.dna_sequence_in(6, 10)) # self.assertEqual("GGTTTA", ss1.dna_sequence_in(2, 10)) - def test_dna_sequence_in__right_then_left_insertions(self): + def test_dna_sequence_in__right_then_left_insertions(self) -> None: ss0 = sc.Domain(0, True, 0, 10, insertions=[(2, 1), (6, 2)]) ss1 = sc.Domain(1, False, 0, 10, insertions=[(2, 1), (6, 2)]) strand = sc.Strand([ss0, ss1]) @@ -4373,7 +4373,7 @@ def test_dna_sequence_in__right_then_left_insertions(self): # self.assertEqual("TTTACG", ss1.dna_sequence_in(6, 10)) # self.assertEqual("TTTACGTACGT", ss1.dna_sequence_in(2, 10)) - def test_dna_sequence_in__right_then_left_deletions_and_insertions(self): + def test_dna_sequence_in__right_then_left_deletions_and_insertions(self) -> None: ss0 = sc.Domain(0, True, 0, 10, deletions=[4], insertions=[(2, 1), (6, 2)]) ss1 = sc.Domain(1, False, 0, 10, deletions=[4], insertions=[(2, 1), (6, 2)]) strand = sc.Strand([ss0, ss1]) From b2012e9964437532cc5ee81bd9e0707961f3c50f Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 7 Sep 2020 14:51:52 -0700 Subject: [PATCH 05/17] fixed mypy warnings in origami_rectangle --- scadnano/origami_rectangle.py | 61 ++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/scadnano/origami_rectangle.py b/scadnano/origami_rectangle.py index 35010376..c3915294 100644 --- a/scadnano/origami_rectangle.py +++ b/scadnano/origami_rectangle.py @@ -2,14 +2,13 @@ The :mod:`origami_rectangle` module defines the function :py:func:`origami_rectangle.create` for creating a DNA origami rectangle using the :mod:`scadnano` module. """ - -from dataclasses import dataclass, field - +from typing import List, Union, cast # from . import scadnano as sc import scadnano as sc from enum import Enum, auto -#TODO: write version of origami_rectangle.create that uses add_nick and add_crossover. + +# TODO: write version of origami_rectangle.create that uses add_nick and add_crossover. class NickPattern(Enum): """Represents options for where to place nicks between staples.""" @@ -62,12 +61,11 @@ class NickPattern(Enum): CURRENTLY UNSUPPORTED.""" - -def create(*, num_helices: int, num_cols: int, assign_seq: bool = True, seam_left_column=-1, +def create(*, num_helices: int, num_cols: int, assign_seq: bool = True, seam_left_column: int = -1, nick_pattern: NickPattern = NickPattern.staggered, twist_correction_deletion_spacing: int = 0, twist_correction_start_col: int = 1, - twist_correction_deletion_offset=-1, - num_flanking_columns: int = 1, num_flanking_helices=0, + twist_correction_deletion_offset: int = -1, + num_flanking_columns: int = 1, num_flanking_helices: int = 0, custom_scaffold: str = None, edge_staples: bool = True, scaffold_nick_offset: int = -1, use_idt_defaults: bool = False) -> sc.Design: """ @@ -234,7 +232,7 @@ def create(*, num_helices: int, num_cols: int, assign_seq: bool = True, seam_lef scaffold = _create_scaffold(offset_start, offset_end, offset_mid, num_helices, num_flanking_helices, scaffold_nick_offset) staples = _create_staples(offset_start, offset_end, offset_mid, num_helices, num_flanking_helices, - num_cols, nick_pattern, edge_staples, use_idt_defaults) + num_cols, nick_pattern, edge_staples) design = sc.Design(helices=helices, strands=[scaffold] + staples, grid=sc.square) @@ -262,15 +260,15 @@ def create(*, num_helices: int, num_cols: int, assign_seq: bool = True, seam_lef BASES_PER_COLUMN = 16 -def _create_helices(num_helices: int, num_bases_per_helix: int): +def _create_helices(num_helices: int, num_bases_per_helix: int) -> List[sc.Helix]: return [sc.Helix(max_offset=num_bases_per_helix) for _ in range(num_helices)] def _create_scaffold(offset_start: int, offset_end: int, offset_mid: int, num_helices: int, - num_flanking_helices: int, scaffold_nick_offset: int): + num_flanking_helices: int, scaffold_nick_offset: int) -> sc.Strand: # top domain is continguous top_domain = sc.Domain(helix=0 + num_flanking_helices, forward=True, - start=offset_start, end=offset_end) + start=offset_start, end=offset_end) domains_left = [] domains_right = [] if scaffold_nick_offset < 0: @@ -281,32 +279,33 @@ def _create_scaffold(offset_start: int, offset_end: int, offset_mid: int, num_he center_offset = offset_mid if helix < num_helices + num_flanking_helices - 1 else scaffold_nick_offset forward = (helix % 2 == num_flanking_helices % 2) left_domain = sc.Domain(helix=helix, forward=forward, - start=offset_start, end=center_offset) + start=offset_start, end=center_offset) right_domain = sc.Domain(helix=helix, forward=forward, - start=center_offset, end=offset_end) + start=center_offset, end=offset_end) domains_left.append(left_domain) domains_right.append(right_domain) domains_left.reverse() - domains = domains_left + [top_domain] + domains_right + domains = cast(List[Union[sc.Domain, sc.Loopout]], # noqa + domains_left + [top_domain] + domains_right) # type: ignore return sc.Strand(domains=domains, color=sc.default_scaffold_color, is_scaffold=True) def _create_staples(offset_start: int, offset_end: int, offset_mid: int, num_helices: int, num_flanking_helices: int, num_cols: int, - nick_pattern: NickPattern, edge_staples, idt: bool): + nick_pattern: NickPattern, edge_staples: bool) -> List[sc.Strand]: if edge_staples: - left_edge_staples = _create_left_edge_staples(offset_start, num_helices, num_flanking_helices, idt) - right_edge_staples = _create_right_edge_staples(offset_end, num_helices, num_flanking_helices, idt) + left_edge_staples = _create_left_edge_staples(offset_start, num_helices, num_flanking_helices) + right_edge_staples = _create_right_edge_staples(offset_end, num_helices, num_flanking_helices) else: left_edge_staples = [] right_edge_staples = [] - seam_staples = _create_seam_staples(offset_mid, num_helices, num_flanking_helices, idt) + seam_staples = _create_seam_staples(offset_mid, num_helices, num_flanking_helices) inner_staples = _create_inner_staples(offset_start, offset_end, offset_mid, num_helices, - num_flanking_helices, num_cols, nick_pattern, idt) + num_flanking_helices, num_cols, nick_pattern) return left_edge_staples + right_edge_staples + seam_staples + inner_staples -def _create_seam_staples(offset_mid: int, num_helices: int, num_flanking_helices: int, idt: bool): +def _create_seam_staples(offset_mid: int, num_helices: int, num_flanking_helices: int) -> List[sc.Strand]: staples = [] crossover_left = offset_mid - BASES_PER_COLUMN crossover_right = offset_mid + BASES_PER_COLUMN @@ -339,7 +338,8 @@ def _create_seam_staples(offset_mid: int, num_helices: int, num_flanking_helices return [first_staple] + staples + [last_staple] -def _create_left_edge_staples(offset_start: int, num_helices: int, num_flanking_helices: int, idt: bool): +def _create_left_edge_staples(offset_start: int, num_helices: int, + num_flanking_helices: int) -> List[sc.Strand]: staples = [] crossover_right = offset_start + BASES_PER_COLUMN for helix in range(0 + num_flanking_helices, num_helices + num_flanking_helices, 2): @@ -353,7 +353,8 @@ def _create_left_edge_staples(offset_start: int, num_helices: int, num_flanking_ return staples -def _create_right_edge_staples(offset_end: int, num_helices: int, num_flanking_helices: int, idt: bool): +def _create_right_edge_staples(offset_end: int, num_helices: int, + num_flanking_helices: int) -> List[sc.Strand]: staples = [] crossover_left = offset_end - BASES_PER_COLUMN for helix in range(0 + num_flanking_helices, num_helices + num_flanking_helices, 2): @@ -369,7 +370,7 @@ def _create_right_edge_staples(offset_end: int, num_helices: int, num_flanking_h def _create_inner_staples(offset_start: int, offset_end: int, offset_mid: int, num_helices: int, num_flanking_helices: int, num_cols: int, - nick_pattern: NickPattern, idt: bool): + nick_pattern: NickPattern) -> List[sc.Strand]: if nick_pattern is not NickPattern.staggered: raise NotImplementedError("Currently can only handle staggered nick pattern") # if ((num_cols - 4) // 2) % 2 != 0: @@ -434,11 +435,11 @@ def _create_inner_staples(offset_start: int, offset_end: int, offset_mid: int, n return staples -def add_deletion_in_range(design: sc.Design, helix: int, start: int, end: int, deletion_offset: int): - #Inserts deletion somewhere in given range. +def add_deletion_in_range(design: sc.Design, helix: int, start: int, end: int, deletion_offset: int) -> None: + # Inserts deletion somewhere in given range. - #`offset` is the relative offset within a column at which to put the deletions. - #If negative, chooses first available offset. + # `offset` is the relative offset within a column at which to put the deletions. + # If negative, chooses first available offset. candidate_offsets = [] for candidate_deletion_offset in range(start, end): if valid_deletion_offset(design, helix, candidate_deletion_offset): @@ -455,7 +456,7 @@ def add_deletion_in_range(design: sc.Design, helix: int, start: int, end: int, d design.add_deletion(helix, deletion_absolute_offset) -def valid_deletion_offset(design: sc.Design, helix: int, offset: int): +def valid_deletion_offset(design: sc.Design, helix: int, offset: int) -> bool: domains_at_offset = design.domains_at(helix, offset) if len(domains_at_offset) > 2: raise ValueError(f'Invalid Design; more than two Substrands found at ' @@ -482,7 +483,7 @@ def add_twist_correction_deletions(design: sc.Design, deletion_offset: int, num_helices: int, num_cols: int, - num_flanking_helices: int): + num_flanking_helices: int) -> None: for col in range(deletion_start_col, num_cols): col_start = offset_start + col * BASES_PER_COLUMN col_end = offset_start + (col + 1) * BASES_PER_COLUMN From a3248bca145ddde9725cc0a16b191ed692248c4a Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 7 Sep 2020 19:29:54 -0700 Subject: [PATCH 06/17] made Design, Strand and Domain generic parameterized by StrandLabel and DomainLabel. Made strand label not indented in serialized JSON --- scadnano/scadnano.py | 84 +++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 6496e4a1..b5bdb290 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -54,7 +54,8 @@ import re from builtins import ValueError from dataclasses import dataclass, field, InitVar, replace -from typing import Tuple, List, Sequence, Iterable, Set, Dict, Union, Optional, FrozenSet, Type, cast, Any +from typing import Tuple, List, Sequence, Iterable, Set, Dict, Union, Optional, FrozenSet, Type, cast, Any, \ + TypeVar, Generic from collections import defaultdict, OrderedDict, Counter import sys import os.path @@ -123,7 +124,7 @@ def default(self, obj: Any) -> Any: key = self.unique_id self.unique_id += 1 self._replacement_map[key] = json.dumps(obj.value, **self.kwargs) - return "@@%s@@" % (key,) + return f"@@{key}@@" else: return super().default(obj) @@ -1375,8 +1376,12 @@ def _is_close(x1: float, x2: float) -> bool: return abs(x1 - x2) < 0.00000001 +DomainLabel = TypeVar('DomainLabel') +StrandLabel = TypeVar('StrandLabel') + + @dataclass -class Domain(_JSONSerializable): +class Domain(_JSONSerializable, Generic[DomainLabel]): """ A maximal portion of a :any:`Strand` that is continguous on a single :any:`Helix`. A :any:`Strand` contains a list of :any:`Domain`'s (and also potentially :any:`Loopout`'s). @@ -1428,7 +1433,7 @@ class Domain(_JSONSerializable): This is used to interoperate with the dsd DNA sequence design package.""" - label: Any = None + label: Optional[DomainLabel] = None """ Generic "label" object to associate to this :any:`Domain`. @@ -1509,7 +1514,7 @@ def set_name(self, name: str) -> None: """Sets name of this :any:`Domain`.""" self.name = name - def set_label(self, label) -> None: # type: ignore + def set_label(self, label: DomainLabel) -> None: """Sets label of this :any:`Domain`.""" self.label = label @@ -1965,7 +1970,7 @@ def _check_idt_string_not_none_or_empty(value: str, field_name: str) -> None: raise IllegalDesignError(f'field {field_name} in IDTFields cannot be empty') -class StrandBuilder: +class StrandBuilder(Generic[StrandLabel, DomainLabel]): """ Represents a :any:`Strand` that is being built in an existing :any:`Design`. @@ -1981,23 +1986,24 @@ class StrandBuilder: """ # remove quotes when Py3.6 support dropped - def __init__(self, design: 'Design', helix: int, offset: int): - self.design: Design = design + def __init__(self, design: 'Design[StrandLabel, DomainLabel]', helix: int, offset: int): + self.design: Design[StrandLabel, DomainLabel] = design self.current_helix: int = helix self.current_offset: int = offset # self.loopout_length: Optional[int] = None - self._strand: Optional[Strand] = None + self._strand: Optional[Strand[StrandLabel]] = None self.just_moved_to_helix: bool = True - self.last_domain: Optional[Domain] = None + self.last_domain: Optional[Domain[DomainLabel]] = None @property - def strand(self) -> 'Strand': + def strand(self) -> 'Strand[StrandLabel]': if self._strand is None: raise ValueError('no Strand created yet; make at least one domain first') return self._strand # remove quotes when Py3.6 support dropped - def cross(self, helix: int, offset: Optional[int] = None, move: Optional[int] = None) -> 'StrandBuilder': + def cross(self, helix: int, offset: Optional[int] = None, move: Optional[int] = None) \ + -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Add crossover. Must be followed by call to :py:meth:`StrandBuilder.to` to have any effect. @@ -2027,7 +2033,7 @@ def cross(self, helix: int, offset: Optional[int] = None, move: Optional[int] = # remove quotes when Py3.6 support dropped def loopout(self, helix: int, length: int, offset: Optional[int] = None, move: Optional[int] = None) \ - -> 'StrandBuilder': + -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Like :py:meth:`StrandBuilder.cross`, but creates a :any:`Loopout` instead of a crossover. @@ -2048,7 +2054,8 @@ def loopout(self, helix: int, length: int, offset: Optional[int] = None, move: O self.design.append_domain(self._strand, Loopout(length)) return self - def move(self, delta: int) -> 'StrandBuilder': # remove quotes when Py3.6 support dropped + # remove quotes when Py3.6 support dropped + def move(self, delta: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Extends this :any:`StrandBuilder` on the current helix to offset given by the current offset plus `delta`, which adds a new :any:`Domain` to the :any:`Strand` being built. This is a @@ -2070,7 +2077,8 @@ def move(self, delta: int) -> 'StrandBuilder': # remove quotes when Py3.6 suppo """ return self.to(self.current_offset + delta) - def to(self, offset: int) -> 'StrandBuilder': # remove quotes when Py3.6 support dropped + # remove quotes when Py3.6 support dropped + def to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Extends this :any:`StrandBuilder` on the current helix to offset `offset`, which adds a new :any:`Domain` to the :any:`Strand` being built. This is an @@ -2109,7 +2117,7 @@ def to(self, offset: int) -> 'StrandBuilder': # remove quotes when Py3.6 suppor else: raise IllegalDesignError(f'offset {offset} cannot be equal to current offset') - domain = Domain(helix=self.current_helix, forward=forward, start=start, end=end) + domain: Domain[DomainLabel] = Domain(helix=self.current_helix, forward=forward, start=start, end=end) self.last_domain = domain if self._strand is not None: self.design.append_domain(self._strand, domain) @@ -2121,7 +2129,8 @@ def to(self, offset: int) -> 'StrandBuilder': # remove quotes when Py3.6 suppor return self - def update_to(self, offset: int) -> 'StrandBuilder': # remove quotes when Py3.6 support dropped + # remove quotes when Py3.6 support dropped + def update_to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Like :py:meth:`StrandBuilder.to`, but changes the current offset without creating a new :any:`Domain`. So unlike :py:meth:`StrandBuilder.to`, several consecutive calls to @@ -2153,7 +2162,8 @@ def update_to(self, offset: int) -> 'StrandBuilder': # remove quotes when Py3.6 return self - def as_scaffold(self) -> 'StrandBuilder': # remove quotes when Py3.6 support dropped + # remove quotes when Py3.6 support dropped + def as_scaffold(self) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Makes Strand being built a scaffold. @@ -2165,7 +2175,7 @@ def as_scaffold(self) -> 'StrandBuilder': # remove quotes when Py3.6 support dr return self # remove quotes when Py3.6 support dropped - def with_modification_5p(self, mod: Modification5Prime) -> 'StrandBuilder': + def with_modification_5p(self, mod: Modification5Prime) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Sets Strand being built to have given 5' modification. @@ -2178,7 +2188,7 @@ def with_modification_5p(self, mod: Modification5Prime) -> 'StrandBuilder': return self # remove quotes when Py3.6 support dropped - def with_modification_3p(self, mod: Modification3Prime) -> 'StrandBuilder': + def with_modification_3p(self, mod: Modification3Prime) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Sets Strand being built to have given 3' modification. @@ -2191,8 +2201,8 @@ def with_modification_3p(self, mod: Modification3Prime) -> 'StrandBuilder': return self # remove quotes when Py3.6 support dropped - def with_modification_internal(self, idx: int, mod: ModificationInternal, - warn_on_no_dna: bool) -> 'StrandBuilder': + def with_modification_internal(self, idx: int, mod: ModificationInternal, warn_on_no_dna: bool) \ + -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Sets Strand being built to have given internal modification. @@ -2207,7 +2217,7 @@ def with_modification_internal(self, idx: int, mod: ModificationInternal, return self # remove quotes when Py3.6 support dropped - def with_color(self, color: Color) -> 'StrandBuilder': + def with_color(self, color: Color) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Sets Strand being built to have given color. @@ -2220,7 +2230,8 @@ def with_color(self, color: Color) -> 'StrandBuilder': return self # remove quotes when Py3.6 support dropped - def with_sequence(self, sequence: str, assign_complement: bool = True) -> 'StrandBuilder': + def with_sequence(self, sequence: str, assign_complement: bool = True) \ + -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Assigns `sequence` as DNA sequence of the :any:`Strand` being built. This should be done after the :any:`Strand`'s structure is done being built, e.g., @@ -2241,7 +2252,8 @@ def with_sequence(self, sequence: str, assign_complement: bool = True) -> 'Stran return self # remove quotes when Py3.6 support dropped - def with_domain_sequence(self, sequence: str, assign_complement: bool = True) -> 'StrandBuilder': + def with_domain_sequence(self, sequence: str, assign_complement: bool = True) \ + -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Assigns `sequence` as DNA sequence of the most recently created :any:`Domain` in the :any:`Strand` being built. This should be called immediately after a :any:`Domain` is created @@ -2272,7 +2284,7 @@ def with_domain_sequence(self, sequence: str, assign_complement: bool = True) -> return self # remove quotes when Py3.6 support dropped - def with_name(self, name: str) -> 'StrandBuilder': + def with_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Assigns `name` as name of the :any:`Strand` being built. @@ -2289,7 +2301,7 @@ def with_name(self, name: str) -> 'StrandBuilder': return self # remove quotes when Py3.6 support dropped - def with_label(self, label) -> 'StrandBuilder': # type: ignore + def with_label(self, label: StrandLabel) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Assigns `label` as label of the :any:`Strand` being built. @@ -2306,7 +2318,7 @@ def with_label(self, label) -> 'StrandBuilder': # type: ignore return self # remove quotes when Py3.6 support dropped - def with_domain_name(self, name: str) -> 'StrandBuilder': + def with_domain_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Assigns `name` as of the most recently created :any:`Domain` or :any:`Loopout` in the :any:`Strand` being built. This should be called immediately after a :any:`Domain` is created @@ -2330,7 +2342,7 @@ def with_domain_name(self, name: str) -> 'StrandBuilder': return self # remove quotes when Py3.6 support dropped - def with_domain_label(self, label) -> 'StrandBuilder': # type: ignore + def with_domain_label(self, label: DomainLabel) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ Assigns `label` as label of the most recently created :any:`Domain` or :any:`Loopout` in the :any:`Strand` being built. This should be called immediately after a :any:`Domain` is created @@ -2358,7 +2370,7 @@ def with_domain_label(self, label) -> 'StrandBuilder': # type: ignore @dataclass -class Strand(_JSONSerializable): +class Strand(_JSONSerializable, Generic[StrandLabel, DomainLabel]): """ Represents a single strand of DNA. @@ -2395,7 +2407,7 @@ class Strand(_JSONSerializable): uses for the scaffold. """ - domains: List[Union[Domain, Loopout]] + domains: List[Union[Domain[DomainLabel], Loopout]] """:any:`Domain`'s (or :any:`Loopout`'s) composing this Strand. Each :any:`Domain` is contiguous on a single :any:`Helix` and could be either single-stranded or double-stranded, @@ -2457,7 +2469,7 @@ class Strand(_JSONSerializable): This is used to interoperate with the dsd DNA sequence design package. """ - label: Any = None + label: Optional[DomainLabel] = None """Generic "label" object to associate to this :any:`Strand`. Useful for associating extra information with the Strand that will be serialized, for example, @@ -2467,7 +2479,7 @@ class Strand(_JSONSerializable): """ # not serialized; efficient way to see a list of all domains on a given helix - _helix_idx_domain_map: Dict[int, List[Domain]] = field( + _helix_idx_domain_map: Dict[int, List[Domain[DomainLabel]]] = field( init=False, repr=False, compare=False, default_factory=dict) def __post_init__(self) -> None: @@ -2519,7 +2531,7 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D dct[modifications_int_key] = NoIndent(mods_dict) if suppress_indent else mods_dict if self.label is not None: - dct[strand_label_key] = self.label + dct[strand_label_key] = NoIndent(self.label) if suppress_indent else self.label return dct @@ -3305,10 +3317,10 @@ def _check_helices_view_order_is_bijection(helices_view_order: List[int], helix_ @dataclass -class Design(_JSONSerializable): +class Design(_JSONSerializable, Generic[StrandLabel, DomainLabel]): """Object representing the entire design of the DNA structure.""" - strands: List[Strand] + strands: List[Strand[StrandLabel, DomainLabel]] """All of the :any:`Strand`'s in this :any:`Design`. Required field.""" From 080bf6de7f005cbdde6b6613953a536f03b9e5a4 Mon Sep 17 00:00:00 2001 From: David Doty Date: Tue, 8 Sep 2020 08:02:28 -0700 Subject: [PATCH 07/17] added example with domains names (some mismatching) --- examples/names_domains_strands.py | 36 ++++++++++++ .../output_designs/names_domains_strands.sc | 55 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 examples/names_domains_strands.py create mode 100644 examples/output_designs/names_domains_strands.sc diff --git a/examples/names_domains_strands.py b/examples/names_domains_strands.py new file mode 100644 index 00000000..207ad450 --- /dev/null +++ b/examples/names_domains_strands.py @@ -0,0 +1,36 @@ +import scadnano as sc + + +def create_design(): + num_domains = 5 + helices = [sc.Helix(max_offset=num_domains * 8)] + design = sc.Design(helices=helices, strands=[], grid=sc.square) + design.strand(0, num_domains * 8) \ + .move(-8).with_domain_name('domain 5*') \ + .move(-8).with_domain_name('domain 4*') \ + .move(-8).with_domain_name('domain 3*') \ + .move(-8).with_domain_name('domain 2*') \ + .move(-8).with_domain_name('domain 1*') \ + .with_name('bottom strand') + + # domain names match + design.strand(0, 0) .move(8).with_domain_name('domain 1') .with_name('top strand 1') + + # domains are aligns but names mismatch + design.strand(0, 8) .move(8).with_domain_name('domain2') .with_name('top strand 2') + + # domain names match + design.strand(0, 16) .move(8).with_domain_name('domain 3') .with_name('top strand 3') + + # domain names match but domains are mis-aligned + design.strand(0, 24) .move(9).with_domain_name('domain 4') .with_name('top strand 4') + + # domain names match but domains are mis-aligned + design.strand(0, 33) .move(7).with_domain_name('domain 5') .with_name('top strand 5') + + return design + + +if __name__ == '__main__': + design = create_design() + design.write_scadnano_file(directory='output_designs') diff --git a/examples/output_designs/names_domains_strands.sc b/examples/output_designs/names_domains_strands.sc new file mode 100644 index 00000000..5282e38b --- /dev/null +++ b/examples/output_designs/names_domains_strands.sc @@ -0,0 +1,55 @@ +{ + "version": "0.12.0", + "grid": "square", + "helices": [ + {"grid_position": [0, 0]} + ], + "strands": [ + { + "name": "bottom strand", + "color": "#f74308", + "domains": [ + {"name": "domain 5*", "helix": 0, "forward": false, "start": 32, "end": 40}, + {"name": "domain 4*", "helix": 0, "forward": false, "start": 24, "end": 32}, + {"name": "domain 3*", "helix": 0, "forward": false, "start": 16, "end": 24}, + {"name": "domain 2*", "helix": 0, "forward": false, "start": 8, "end": 16}, + {"name": "domain 1*", "helix": 0, "forward": false, "start": 0, "end": 8} + ] + }, + { + "name": "top strand 1", + "color": "#57bb00", + "domains": [ + {"name": "domain 1", "helix": 0, "forward": true, "start": 0, "end": 8} + ] + }, + { + "name": "top strand 2", + "color": "#888888", + "domains": [ + {"name": "domain2", "helix": 0, "forward": true, "start": 8, "end": 16} + ] + }, + { + "name": "top strand 3", + "color": "#32b86c", + "domains": [ + {"name": "domain 3", "helix": 0, "forward": true, "start": 16, "end": 24} + ] + }, + { + "name": "top strand 4", + "color": "#333333", + "domains": [ + {"name": "domain 4", "helix": 0, "forward": true, "start": 24, "end": 33} + ] + }, + { + "name": "top strand 5", + "color": "#320096", + "domains": [ + {"name": "domain 5", "helix": 0, "forward": true, "start": 33, "end": 40} + ] + } + ] +} \ No newline at end of file From a2a6b072a21f83578c9aea5802eb25cc7e5eb829 Mon Sep 17 00:00:00 2001 From: David Doty Date: Tue, 8 Sep 2020 08:14:52 -0700 Subject: [PATCH 08/17] updated names example to have more kinds of mismatches --- examples/names_domains_strands.py | 27 ++++++++++++------- .../output_designs/names_domains_strands.sc | 23 +++++++++++++--- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/examples/names_domains_strands.py b/examples/names_domains_strands.py index 207ad450..85f1c78a 100644 --- a/examples/names_domains_strands.py +++ b/examples/names_domains_strands.py @@ -1,11 +1,14 @@ import scadnano as sc -def create_design(): - num_domains = 5 - helices = [sc.Helix(max_offset=num_domains * 8)] +def create_design() -> sc.Design: + num_domains = 9 + helices = [sc.Helix(max_offset=(num_domains) * 8)] design = sc.Design(helices=helices, strands=[], grid=sc.square) - design.strand(0, num_domains * 8) \ + design.strand(0, (num_domains - 1) * 8) \ + .move(-8).with_domain_name('domain 8*') \ + .move(-8).with_domain_name('domain 7*') \ + .move(-8).with_domain_name('domain 6*') \ .move(-8).with_domain_name('domain 5*') \ .move(-8).with_domain_name('domain 4*') \ .move(-8).with_domain_name('domain 3*') \ @@ -14,19 +17,25 @@ def create_design(): .with_name('bottom strand') # domain names match - design.strand(0, 0) .move(8).with_domain_name('domain 1') .with_name('top strand 1') + design.strand(0, 0).move(8).with_domain_name('domain 1').with_name('top strand 1') # domains are aligns but names mismatch - design.strand(0, 8) .move(8).with_domain_name('domain2') .with_name('top strand 2') + design.strand(0, 8).move(8).with_domain_name('domain 50').with_name('top strand 2') # domain names match - design.strand(0, 16) .move(8).with_domain_name('domain 3') .with_name('top strand 3') + design.strand(0, 16).move(8).with_domain_name('domain 3').with_name('top strand 3') + + # domain names are the same, not complementary + design.strand(0, 24).move(8).with_domain_name('domain 4*').with_name('top strand 4') + + # domain names match but domains are mis-aligned + design.strand(0, 32).move(9).with_domain_name('domain 5').with_name('top strand 5') # domain names match but domains are mis-aligned - design.strand(0, 24) .move(9).with_domain_name('domain 4') .with_name('top strand 4') + design.strand(0, 48).move(7).with_domain_name('domain 7').with_name('top strand 7') # domain names match but domains are mis-aligned - design.strand(0, 33) .move(7).with_domain_name('domain 5') .with_name('top strand 5') + design.strand(0, 64).move(8).with_domain_name('domain 9').with_name('top strand 9') return design diff --git a/examples/output_designs/names_domains_strands.sc b/examples/output_designs/names_domains_strands.sc index 5282e38b..6a9cdc06 100644 --- a/examples/output_designs/names_domains_strands.sc +++ b/examples/output_designs/names_domains_strands.sc @@ -9,6 +9,9 @@ "name": "bottom strand", "color": "#f74308", "domains": [ + {"name": "domain 8*", "helix": 0, "forward": false, "start": 56, "end": 64}, + {"name": "domain 7*", "helix": 0, "forward": false, "start": 48, "end": 56}, + {"name": "domain 6*", "helix": 0, "forward": false, "start": 40, "end": 48}, {"name": "domain 5*", "helix": 0, "forward": false, "start": 32, "end": 40}, {"name": "domain 4*", "helix": 0, "forward": false, "start": 24, "end": 32}, {"name": "domain 3*", "helix": 0, "forward": false, "start": 16, "end": 24}, @@ -27,7 +30,7 @@ "name": "top strand 2", "color": "#888888", "domains": [ - {"name": "domain2", "helix": 0, "forward": true, "start": 8, "end": 16} + {"name": "domain 50", "helix": 0, "forward": true, "start": 8, "end": 16} ] }, { @@ -41,14 +44,28 @@ "name": "top strand 4", "color": "#333333", "domains": [ - {"name": "domain 4", "helix": 0, "forward": true, "start": 24, "end": 33} + {"name": "domain 4*", "helix": 0, "forward": true, "start": 24, "end": 32} ] }, { "name": "top strand 5", "color": "#320096", "domains": [ - {"name": "domain 5", "helix": 0, "forward": true, "start": 33, "end": 40} + {"name": "domain 5", "helix": 0, "forward": true, "start": 32, "end": 41} + ] + }, + { + "name": "top strand 7", + "color": "#03b6a2", + "domains": [ + {"name": "domain 7", "helix": 0, "forward": true, "start": 48, "end": 55} + ] + }, + { + "name": "top strand 9", + "color": "#7300de", + "domains": [ + {"name": "domain 9", "helix": 0, "forward": true, "start": 64, "end": 72} ] } ] From 27baee2d4e27ee7d8d5995418105acb6b7610ea9 Mon Sep 17 00:00:00 2001 From: David Doty Date: Tue, 8 Sep 2020 08:15:43 -0700 Subject: [PATCH 09/17] annotated variable to quiet mypy --- examples/names_domains_strands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/names_domains_strands.py b/examples/names_domains_strands.py index 85f1c78a..0cb04131 100644 --- a/examples/names_domains_strands.py +++ b/examples/names_domains_strands.py @@ -4,7 +4,7 @@ def create_design() -> sc.Design: num_domains = 9 helices = [sc.Helix(max_offset=(num_domains) * 8)] - design = sc.Design(helices=helices, strands=[], grid=sc.square) + design: sc.Design = sc.Design(helices=helices, strands=[], grid=sc.square) design.strand(0, (num_domains - 1) * 8) \ .move(-8).with_domain_name('domain 8*') \ .move(-8).with_domain_name('domain 7*') \ From c5bdb1c39744612df82f16d8a5cd3b32509a605b Mon Sep 17 00:00:00 2001 From: David Doty Date: Tue, 8 Sep 2020 08:17:26 -0700 Subject: [PATCH 10/17] Update names_domains_strands.py --- examples/names_domains_strands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/names_domains_strands.py b/examples/names_domains_strands.py index 0cb04131..e80e5336 100644 --- a/examples/names_domains_strands.py +++ b/examples/names_domains_strands.py @@ -34,7 +34,7 @@ def create_design() -> sc.Design: # domain names match but domains are mis-aligned design.strand(0, 48).move(7).with_domain_name('domain 7').with_name('top strand 7') - # domain names match but domains are mis-aligned + # no overlap so no mismatch design.strand(0, 64).move(8).with_domain_name('domain 9').with_name('top strand 9') return design From 9b54b1e5fc8d18960aef068fae487862a5d8eac9 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Tue, 8 Sep 2020 17:01:00 +0100 Subject: [PATCH 11/17] Export code supports helix groups and associated unittest. test_6_helix_bundle_honeycomb restored. --- doc/compile.py | 7 +++++ doc/index.rst.in | 52 ++++++++++++++++++++++++++++++++++++ scadnano/scadnano.py | 31 +++++++++++++++------- tests/scadnano_tests.py | 59 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 doc/compile.py create mode 100644 doc/index.rst.in diff --git a/doc/compile.py b/doc/compile.py new file mode 100644 index 00000000..e22882f7 --- /dev/null +++ b/doc/compile.py @@ -0,0 +1,7 @@ +""" Take index.rst.in as input and populate with classes of scadnano.py +""" + +import scadnano as sc + +for elem in dir(sc): + if \ No newline at end of file diff --git a/doc/index.rst.in b/doc/index.rst.in new file mode 100644 index 00000000..c7394eab --- /dev/null +++ b/doc/index.rst.in @@ -0,0 +1,52 @@ +.. scadnano documentation master file, created by + sphinx-quickstart on Tue Jul 16 10:14:04 2019. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +scadnano documentation +==================================== + +.. toctree:: + :maxdepth: 3 + :caption: Contents: + +.. scadnano + +origami_rectangle +===================== +.. automodule:: origami_rectangle + :members: + +Interoperability - cadnano v2 +============================= + +Scadnano provides function to convert design to and from cadnano v2: + +* :py:meth:`DNADesign.from_cadnano_v2` will create a scadnano DNADesign from a ``cadnanov2`` json file. +* :py:meth:`DNADesign.export_cadnano_v2` will produce a ``cadnanov2`` json file from a scadnano design. + +**Important** + +All ``cadnanov2`` designs can be imported to scadnano. However **not all scadnano designs can be imported +to cadnanov2**, to be importable to ``cadnanov2`` a scadnano design need to comply with the following points: + +* The design cannot feature any :py:class:`Loopout` as it is not a concept that exists in ``cadnanov2``. +* Following ``cadnanov2`` conventions, helices with **even** number must have their scaffold going **forward** and helices with **odd** number **backward**. + +Also note that maximum helices offsets can be altered in a ``scadnano`` to ``cadnanov2`` conversion as ``cadnanov2`` needs max offsets to be a multiple of 21 in the hex grid and 32 in the rectangular grid. +The conversion algorithm will choose the lowest multiple of 21 or 32 which fits the entire design. + +The ``cadnanov2`` json format does not embed sequences hence they will be lost after conversion. + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + + +.. |br| raw:: html + +
+ diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index d859c87c..63041280 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -4022,7 +4022,7 @@ def _cadnano_v2_place_strand(self, strand: Strand, dct: dict, self._cadnano_v2_place_crossover(which_helix, next_helix, domain, next_domain, strand_type) - def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int) -> dict: + def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int, design_grid: Grid) -> dict: """Creates blank cadnanov2 helices in and initialized all their fields. """ helices_ids_reverse = {} @@ -4030,11 +4030,11 @@ def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int) -> dict: helix_dct = OrderedDict() helix_dct['num'] = helix.idx - if self.grid == Grid.square: + if design_grid == Grid.square: helix_dct['row'] = helix.grid_position[1] helix_dct['col'] = helix.grid_position[0] - if self.grid == Grid.honeycomb: + if design_grid == Grid.honeycomb: helix_dct['row'], helix_dct['col'] = helix.grid_position[1], helix.grid_position[0] helix_dct['scaf'] = [] @@ -4063,9 +4063,22 @@ def to_cadnano_v2(self): dct = OrderedDict() dct['vstrands'] = [] - if self.__class__ != Design: - raise ValueError( - 'Please export DNAOrigamiDesign only as we need to know which strand is the scaffold.') + '''Check if helix group are used or if only one grid is used''' + design_grid = None + if self._has_default_groups(): + design_grid = self.grid + else: + gridUsed = {} + grid_type = None + for group_name in self.groups: + gridUsed[self.groups[group_name].grid] = True + grid_type = self.groups[group_name].grid + if len(gridUsed) > 1: + raise ValueError('Designs using helix groups can be exported to cadnano v2 \ + only if all groups share the same grid type.') + else: + design_grid = grid_type + '''Figuring out the type of grid. In cadnano v2, all helices have the same max offset @@ -4077,9 +4090,9 @@ def to_cadnano_v2(self): for helix in self.helices.values(): num_bases = max(num_bases, helix.max_offset) - if self.grid == Grid.square: + if design_grid == Grid.square: num_bases = self._get_multiple_of_x_sup_closest_to_y(32, num_bases) - elif self.grid == Grid.honeycomb: + elif design_grid == Grid.honeycomb: num_bases = self._get_multiple_of_x_sup_closest_to_y(21, num_bases) else: raise NotImplementedError('We can export to cadnano v2 `square` and `honeycomb` grids only.') @@ -4102,7 +4115,7 @@ def to_cadnano_v2(self): '''Filling the helices with blank. ''' - helices_ids_reverse = self._cadnano_v2_fill_blank(dct, num_bases) + helices_ids_reverse = self._cadnano_v2_fill_blank(dct, num_bases, design_grid) '''Putting the scaffold in place. ''' diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index 8bcacdcd..7d10c48f 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -607,6 +607,64 @@ class TestExportCadnanoV2(unittest.TestCase): output_path = os.path.join('tests_outputs', folder) ext = sc.default_scadnano_file_extension + def test_export_design_with_helix_group(self): + e = 'east' + s = 'south' + helices = [ + sc.Helix(max_offset=24, group=s), + sc.Helix(max_offset=25, group=s), + ] + helices.extend([ + sc.Helix(max_offset=22, group=e), + sc.Helix(max_offset=23, group=e), + ]) + + group_south = sc.HelixGroup(position=sc.Position3D(x=0, y=10, z=0), + grid=sc.square) + group_east = sc.HelixGroup(position=sc.Position3D(x=10, y=0, z=0), grid=sc.square) + + groups = { + e: group_east, + s: group_south + } + + design = sc.Design(helices=helices, groups=groups, strands=[]) + design.write_scadnano_file(directory=self.input_path, + filename=f'test_export_design_with_helix_group.{self.ext}') + design.export_cadnano_v2(directory=self.output_path, + filename='test_export_design_with_helix_group.json') + + def test_export_design_with_helix_group_not_same_grid(self): + e = 'east' + s = 'south' + helices = [ + sc.Helix(max_offset=24, group=s), + sc.Helix(max_offset=25, group=s), + ] + helices.extend([ + sc.Helix(max_offset=22, group=e), + sc.Helix(max_offset=23, group=e), + ]) + + group_south = sc.HelixGroup(position=sc.Position3D(x=0, y=10, z=0), + grid=sc.square) + group_east = sc.HelixGroup(position=sc.Position3D(x=10, y=0, z=0), grid=sc.honeycomb) + + groups = { + e: group_east, + s: group_south + } + + design = sc.Design(helices=helices, groups=groups, strands=[]) + design.write_scadnano_file(directory=self.input_path, + filename=f'test_export_design_with_helix_group_not_same_grid.{self.ext}') + + with self.assertRaises(ValueError) as context: + design.export_cadnano_v2(directory=self.output_path, + filename='test_export_design_with_helix_group_not_same_grid.json') + self.assertTrue('helix groups' in context.exception.args[0]) + + def test_2_staple_2_helix_origami_extremely_simple(self): helices = [sc.Helix(max_offset=32), sc.Helix(max_offset=32)] scaf_part = sc.Domain(helix=0, forward=True, start=0, end=32) @@ -676,7 +734,6 @@ def test_6_helix_origami_rectangle(self): design.export_cadnano_v2(directory=self.output_path, filename='test_6_helix_origami_rectangle.json') - @unittest.skip('DD: I cannot find where this file is. Is it supposed to be generated by some code?') def test_6_helix_bundle_honeycomb(self): design = sc.Design.from_scadnano_file( os.path.join(self.input_path, f'test_6_helix_bundle_honeycomb.{self.ext}')) From 4d622123c3bbc11bbb99c2c826a9d3504fd9ac49 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Tue, 8 Sep 2020 17:01:39 +0100 Subject: [PATCH 12/17] Export code supports helix groups and associated unittest. test_6_helix_bundle_honeycomb restored. --- doc/compile.py | 7 ------- doc/index.rst.in | 52 ------------------------------------------------ 2 files changed, 59 deletions(-) delete mode 100644 doc/compile.py delete mode 100644 doc/index.rst.in diff --git a/doc/compile.py b/doc/compile.py deleted file mode 100644 index e22882f7..00000000 --- a/doc/compile.py +++ /dev/null @@ -1,7 +0,0 @@ -""" Take index.rst.in as input and populate with classes of scadnano.py -""" - -import scadnano as sc - -for elem in dir(sc): - if \ No newline at end of file diff --git a/doc/index.rst.in b/doc/index.rst.in deleted file mode 100644 index c7394eab..00000000 --- a/doc/index.rst.in +++ /dev/null @@ -1,52 +0,0 @@ -.. scadnano documentation master file, created by - sphinx-quickstart on Tue Jul 16 10:14:04 2019. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -scadnano documentation -==================================== - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -.. scadnano - -origami_rectangle -===================== -.. automodule:: origami_rectangle - :members: - -Interoperability - cadnano v2 -============================= - -Scadnano provides function to convert design to and from cadnano v2: - -* :py:meth:`DNADesign.from_cadnano_v2` will create a scadnano DNADesign from a ``cadnanov2`` json file. -* :py:meth:`DNADesign.export_cadnano_v2` will produce a ``cadnanov2`` json file from a scadnano design. - -**Important** - -All ``cadnanov2`` designs can be imported to scadnano. However **not all scadnano designs can be imported -to cadnanov2**, to be importable to ``cadnanov2`` a scadnano design need to comply with the following points: - -* The design cannot feature any :py:class:`Loopout` as it is not a concept that exists in ``cadnanov2``. -* Following ``cadnanov2`` conventions, helices with **even** number must have their scaffold going **forward** and helices with **odd** number **backward**. - -Also note that maximum helices offsets can be altered in a ``scadnano`` to ``cadnanov2`` conversion as ``cadnanov2`` needs max offsets to be a multiple of 21 in the hex grid and 32 in the rectangular grid. -The conversion algorithm will choose the lowest multiple of 21 or 32 which fits the entire design. - -The ``cadnanov2`` json format does not embed sequences hence they will be lost after conversion. - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - - -.. |br| raw:: html - -
- From 62330752fbe511c95c6a1d1e8688f03467c99639 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Tue, 8 Sep 2020 17:05:02 +0100 Subject: [PATCH 13/17] Export code supports helix groups and associated unittest. test_6_helix_bundle_honeycomb restored. --- scadnano/scadnano.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 24e5bc17..24632248 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -4133,11 +4133,7 @@ def _cadnano_v2_place_strand(self, strand: Strand, dct: dict, self._cadnano_v2_place_crossover(which_helix, next_helix, domain, next_domain, strand_type) -<<<<<<< HEAD - def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int) -> Dict[int, int]: -======= def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int, design_grid: Grid) -> dict: ->>>>>>> master """Creates blank cadnanov2 helices in and initialized all their fields. """ helices_ids_reverse = {} @@ -4145,23 +4141,11 @@ def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int, design_grid: Grid) - helix_dct: Dict[str, Any] = OrderedDict() helix_dct['num'] = helix.idx -<<<<<<< HEAD - if self.grid == Grid.square: - if helix.grid_position is None: - raise ValueError('cannot have grid_position == None if grid is square') - helix_dct['row'] = helix.grid_position[1] - helix_dct['col'] = helix.grid_position[0] - - elif self.grid == Grid.honeycomb: - if helix.grid_position is None: - raise ValueError('cannot have grid_position == None if grid is honeycomb') -======= if design_grid == Grid.square: helix_dct['row'] = helix.grid_position[1] helix_dct['col'] = helix.grid_position[0] if design_grid == Grid.honeycomb: ->>>>>>> master helix_dct['row'], helix_dct['col'] = helix.grid_position[1], helix.grid_position[0] helix_dct['scaf'] = [] @@ -4244,11 +4228,7 @@ def to_cadnano_v2(self) -> Dict[str, Any]: '''Filling the helices with blank. ''' -<<<<<<< HEAD - helices_ids_reverse: Dict[int, int] = self._cadnano_v2_fill_blank(dct, num_bases) -======= helices_ids_reverse = self._cadnano_v2_fill_blank(dct, num_bases, design_grid) ->>>>>>> master '''Putting the scaffold in place. ''' From a274a98999c94f7c5bf11b9c64a1b4af1ef50b0c Mon Sep 17 00:00:00 2001 From: Cosmo Date: Tue, 8 Sep 2020 17:29:18 +0100 Subject: [PATCH 14/17] Forcing the add of test_6_helix_bundle_honeycomb.sc which was ignored --- .../test_6_helix_bundle_honeycomb.sc | 1870 +++++++++++++++++ 1 file changed, 1870 insertions(+) create mode 100644 tests_inputs/cadnano_v2_export/test_6_helix_bundle_honeycomb.sc diff --git a/tests_inputs/cadnano_v2_export/test_6_helix_bundle_honeycomb.sc b/tests_inputs/cadnano_v2_export/test_6_helix_bundle_honeycomb.sc new file mode 100644 index 00000000..482e378f --- /dev/null +++ b/tests_inputs/cadnano_v2_export/test_6_helix_bundle_honeycomb.sc @@ -0,0 +1,1870 @@ +{ + "version": "0.12.0", + "grid": "honeycomb", + "helices": [ + {"grid_position": [1, 1], "max_offset": 1295}, + {"grid_position": [0, 1], "max_offset": 1295}, + {"grid_position": [0, 2], "max_offset": 1295}, + {"grid_position": [1, 2], "max_offset": 1295}, + {"grid_position": [2, 2], "max_offset": 1295}, + {"grid_position": [2, 1], "max_offset": 1295} + ], + "strands": [ + { + "color": "#57bb00", + "sequence": "CATTTCTCCGAAGAGACGCATTTCACATGTGGGCCTTGAATC", + "domains": [ + {"helix": 3, "forward": true, "start": 56, "end": 70}, + {"helix": 2, "forward": false, "start": 56, "end": 70}, + {"helix": 1, "forward": true, "start": 56, "end": 70} + ] + }, + { + "color": "#007200", + "sequence": "ACGCTCGCCCTGCTCAATGTCCCGCCAAGAATTGTCAACCTT", + "domains": [ + {"helix": 2, "forward": false, "start": 70, "end": 84}, + {"helix": 3, "forward": true, "start": 70, "end": 84}, + {"helix": 4, "forward": false, "start": 70, "end": 84} + ] + }, + { + "color": "#7300de", + "sequence": "ATCTCTGACCTCCTAGTCGGGAAACCTGGCACGAATATAGTT", + "domains": [ + {"helix": 4, "forward": false, "start": 56, "end": 70}, + {"helix": 5, "forward": true, "start": 56, "end": 70}, + {"helix": 0, "forward": false, "start": 56, "end": 70} + ] + }, + { + "color": "#03b6a2", + "sequence": "GGGAGTGACTCTATCAACTCGTCGGTGGTCGTGCCAGCTGCA", + "domains": [ + {"helix": 1, "forward": true, "start": 70, "end": 84}, + {"helix": 0, "forward": false, "start": 70, "end": 84}, + {"helix": 5, "forward": true, "start": 70, "end": 84} + ] + }, + { + "color": "#32b86c", + "sequence": "TGTGAATTCATGGGGATGTTCTTCTAAGGGAGGAGAAGCCAG", + "domains": [ + {"helix": 3, "forward": true, "start": 98, "end": 112}, + {"helix": 2, "forward": false, "start": 98, "end": 112}, + {"helix": 1, "forward": true, "start": 98, "end": 112} + ] + }, + { + "color": "#f7931e", + "sequence": "GCTGCAAGGCGATGCCTCTTCGCTATTAAAGGGCGATCGGTG", + "domains": [ + {"helix": 2, "forward": false, "start": 112, "end": 126}, + {"helix": 3, "forward": true, "start": 112, "end": 126}, + {"helix": 4, "forward": false, "start": 112, "end": 126} + ] + }, + { + "color": "#b8056c", + "sequence": "CGCGCACGACTTAAAACGCGCGGGGAGACCAAGCTTTCTCCC", + "domains": [ + {"helix": 4, "forward": false, "start": 98, "end": 112}, + {"helix": 5, "forward": true, "start": 98, "end": 112}, + {"helix": 0, "forward": false, "start": 98, "end": 112} + ] + }, + { + "color": "#aaaa00", + "sequence": "GGTTAAGTTGGGTAAACGACGGCCAGTGGGCGGTTTGCGTAT", + "domains": [ + {"helix": 1, "forward": true, "start": 112, "end": 126}, + {"helix": 0, "forward": false, "start": 112, "end": 126}, + {"helix": 5, "forward": true, "start": 112, "end": 126} + ] + }, + { + "color": "#320096", + "sequence": "AAAGCGCCATTCCGTGGTGCCGGAAACCCCTTTCCGGCACCG", + "domains": [ + {"helix": 3, "forward": true, "start": 140, "end": 154}, + {"helix": 2, "forward": false, "start": 140, "end": 154}, + {"helix": 1, "forward": true, "start": 140, "end": 154} + ] + }, + { + "color": "#57bb00", + "sequence": "ATGGGCGCATCGTCGCGGATTGACCGTATCTCCGTGGGAACA", + "domains": [ + {"helix": 2, "forward": false, "start": 154, "end": 168}, + {"helix": 3, "forward": true, "start": 154, "end": 168}, + {"helix": 4, "forward": false, "start": 154, "end": 168} + ] + }, + { + "color": "#cc0000", + "sequence": "AAGCCATTCAGGCTGTTTTTCTTTTCACACTCCAGCCAGCAG", + "domains": [ + {"helix": 4, "forward": false, "start": 140, "end": 154}, + {"helix": 5, "forward": true, "start": 140, "end": 154}, + {"helix": 0, "forward": false, "start": 140, "end": 154} + ] + }, + { + "color": "#007200", + "sequence": "CTTAACCGTGCATCCTCAGGAAGATCGCCAGTGAGACGGGCA", + "domains": [ + {"helix": 1, "forward": true, "start": 154, "end": 168}, + {"helix": 0, "forward": false, "start": 154, "end": 168}, + {"helix": 5, "forward": true, "start": 154, "end": 168} + ] + }, + { + "color": "#7300de", + "sequence": "ATCAACATTAAACCTTCCTGTAGCCAGCGATAATTCGCGTCT", + "domains": [ + {"helix": 3, "forward": true, "start": 182, "end": 196}, + {"helix": 2, "forward": false, "start": 182, "end": 196}, + {"helix": 1, "forward": true, "start": 182, "end": 196} + ] + }, + { + "color": "#32b86c", + "sequence": "AAACGTTAATATCCAAAAACAGGAAGATGATAATCAGAAAAG", + "domains": [ + {"helix": 2, "forward": false, "start": 196, "end": 210}, + {"helix": 3, "forward": true, "start": 196, "end": 210}, + {"helix": 4, "forward": false, "start": 196, "end": 210} + ] + }, + { + "color": "#f74308", + "sequence": "CCTGTGAGCGAGTATTCACCGCCTGGCCCGCCATCAAAAACG", + "domains": [ + {"helix": 4, "forward": false, "start": 182, "end": 196}, + {"helix": 5, "forward": true, "start": 182, "end": 196}, + {"helix": 0, "forward": false, "start": 182, "end": 196} + ] + }, + { + "color": "#f7931e", + "sequence": "GGTTTGTTAAAATTTTAACCAATAGGAACTGAGAGAGTTGCA", + "domains": [ + {"helix": 1, "forward": true, "start": 196, "end": 210}, + {"helix": 0, "forward": false, "start": 196, "end": 210}, + {"helix": 5, "forward": true, "start": 196, "end": 210} + ] + }, + { + "color": "#b8056c", + "sequence": "ATCGTAAAACTAAAGAGAATCGATGAACGTAGTCTGGAGCAA", + "domains": [ + {"helix": 3, "forward": true, "start": 224, "end": 238}, + {"helix": 2, "forward": false, "start": 224, "end": 238}, + {"helix": 1, "forward": true, "start": 224, "end": 238} + ] + }, + { + "color": "#320096", + "sequence": "AACCGTTCTAGCAAAGGCCGGAGACAGTGATTCAAAAGGGTG", + "domains": [ + {"helix": 2, "forward": false, "start": 238, "end": 252}, + {"helix": 3, "forward": true, "start": 238, "end": 252}, + {"helix": 4, "forward": false, "start": 238, "end": 252} + ] + }, + { + "color": "#888888", + "sequence": "AGGCATGTCAATCACTGGTTTGCCCCAGTCATTGCCTGAGTA", + "domains": [ + {"helix": 4, "forward": false, "start": 224, "end": 238}, + {"helix": 5, "forward": true, "start": 224, "end": 238}, + {"helix": 0, "forward": false, "start": 224, "end": 238} + ] + }, + { + "color": "#57bb00", + "sequence": "ACTGATAAATTAATCAAAGGCTATCAGGCAGGCGAAAATCCT", + "domains": [ + {"helix": 1, "forward": true, "start": 238, "end": 252}, + {"helix": 0, "forward": false, "start": 238, "end": 252}, + {"helix": 5, "forward": true, "start": 238, "end": 252} + ] + }, + { + "color": "#cc0000", + "sequence": "ATATTTTAAATGAAAATTTTTAGAACCCCTTCAACGCAAGGA", + "domains": [ + {"helix": 3, "forward": true, "start": 266, "end": 280}, + {"helix": 2, "forward": false, "start": 266, "end": 280}, + {"helix": 1, "forward": true, "start": 266, "end": 280} + ] + }, + { + "color": "#7300de", + "sequence": "TAAAGCCTCAGAAATCATACAGGCAAGGGCATTAACATCCAA", + "domains": [ + {"helix": 2, "forward": false, "start": 280, "end": 294}, + {"helix": 3, "forward": true, "start": 280, "end": 294}, + {"helix": 4, "forward": false, "start": 280, "end": 294} + ] + }, + { + "color": "#333333", + "sequence": "TACAATGCCTGAGTCCGAAATCGGCAAAGAAGCCTTTATTAT", + "domains": [ + {"helix": 4, "forward": false, "start": 266, "end": 280}, + {"helix": 5, "forward": true, "start": 266, "end": 280}, + {"helix": 0, "forward": false, "start": 266, "end": 280} + ] + }, + { + "color": "#32b86c", + "sequence": "TAGCATAAAGCTAAATACTTTTGCGGGAATCCCTTATAAATC", + "domains": [ + {"helix": 1, "forward": true, "start": 280, "end": 294}, + {"helix": 0, "forward": false, "start": 280, "end": 294}, + {"helix": 5, "forward": true, "start": 280, "end": 294} + ] + }, + { + "color": "#f74308", + "sequence": "GAGCTGAAAAGGCATATTTTCATTTGGGAAATAACCTGTTTA", + "domains": [ + {"helix": 3, "forward": true, "start": 308, "end": 322}, + {"helix": 2, "forward": false, "start": 308, "end": 322}, + {"helix": 1, "forward": true, "start": 308, "end": 322} + ] + }, + { + "color": "#b8056c", + "sequence": "TTCATTCCATATTATGTTTTAAATATGCTAATGCTGTAGCTC", + "domains": [ + {"helix": 2, "forward": false, "start": 322, "end": 336}, + {"helix": 3, "forward": true, "start": 322, "end": 336}, + {"helix": 4, "forward": false, "start": 322, "end": 336} + ] + }, + { + "color": "#03b6a2", + "sequence": "AATGGCATCAATTCAGATAGGGTTGAGTCGCAAATGGTCAAC", + "domains": [ + {"helix": 4, "forward": false, "start": 308, "end": 322}, + {"helix": 5, "forward": true, "start": 308, "end": 322}, + {"helix": 0, "forward": false, "start": 308, "end": 322} + ] + }, + { + "color": "#320096", + "sequence": "GCAACAGTTGATTCCATTAGATACATTTGTTGTTCCAGTTTG", + "domains": [ + {"helix": 1, "forward": true, "start": 322, "end": 336}, + {"helix": 0, "forward": false, "start": 322, "end": 336}, + {"helix": 5, "forward": true, "start": 322, "end": 336} + ] + }, + { + "color": "#888888", + "sequence": "TCATTTTTGCGGCAGCTCCTTTTGATAAGAGAGAGTACCTTT", + "domains": [ + {"helix": 3, "forward": true, "start": 350, "end": 364}, + {"helix": 2, "forward": false, "start": 350, "end": 364}, + {"helix": 1, "forward": true, "start": 350, "end": 364} + ] + }, + { + "color": "#cc0000", + "sequence": "CCCGAAAGACTTTTGAAGCAAAGCGGATCCCTGACTATTATA", + "domains": [ + {"helix": 2, "forward": false, "start": 364, "end": 378}, + {"helix": 3, "forward": true, "start": 364, "end": 378}, + {"helix": 4, "forward": false, "start": 364, "end": 378} + ] + }, + { + "color": "#aaaa00", + "sequence": "GTATGGCTTAGAGCTATTAAAGAACGTGAGGTCAGGATTAGT", + "domains": [ + {"helix": 4, "forward": false, "start": 350, "end": 364}, + {"helix": 5, "forward": true, "start": 350, "end": 364}, + {"helix": 0, "forward": false, "start": 350, "end": 364} + ] + }, + { + "color": "#7300de", + "sequence": "AACAAATATCGCGTAAGCAAACTCCAACGACTCCAACGTCAA", + "domains": [ + {"helix": 1, "forward": true, "start": 364, "end": 378}, + {"helix": 0, "forward": false, "start": 364, "end": 378}, + {"helix": 5, "forward": true, "start": 364, "end": 378} + ] + }, + { + "color": "#333333", + "sequence": "AAAACGAGAATGCCATGCTTTAAACAGTCAATTGAATCCCCC", + "domains": [ + {"helix": 3, "forward": true, "start": 392, "end": 406}, + {"helix": 2, "forward": false, "start": 392, "end": 406}, + {"helix": 1, "forward": true, "start": 392, "end": 406} + ] + }, + { + "color": "#f74308", + "sequence": "AAAAGAAGTTTTAAAGACGACGATAAAATCATAACCCTCGTT", + "domains": [ + {"helix": 2, "forward": false, "start": 406, "end": 420}, + {"helix": 3, "forward": true, "start": 406, "end": 420}, + {"helix": 4, "forward": false, "start": 406, "end": 420} + ] + }, + { + "color": "#007200", + "sequence": "TAACCATAAATCAATCTATCAGGGCGATTCATAAATATTCAA", + "domains": [ + {"helix": 4, "forward": false, "start": 392, "end": 406}, + {"helix": 5, "forward": true, "start": 392, "end": 406}, + {"helix": 0, "forward": false, "start": 392, "end": 406} + ] + }, + { + "color": "#b8056c", + "sequence": "TCGCCAGAGGGGGTATACTGCGGAATCGGGCCCACTACGTGA", + "domains": [ + {"helix": 1, "forward": true, "start": 406, "end": 420}, + {"helix": 0, "forward": false, "start": 406, "end": 420}, + {"helix": 5, "forward": true, "start": 406, "end": 420} + ] + }, + { + "color": "#03b6a2", + "sequence": "GCCAAAAGGAATATCTAATGCAGATACATAGGAATACCACAT", + "domains": [ + {"helix": 3, "forward": true, "start": 434, "end": 448}, + {"helix": 2, "forward": false, "start": 434, "end": 448}, + {"helix": 1, "forward": true, "start": 434, "end": 448} + ] + }, + { + "color": "#888888", + "sequence": "TTGGGAAGAAAAAAGCGATTTTAAGAACCATTGTGAATTACC", + "domains": [ + {"helix": 2, "forward": false, "start": 448, "end": 462}, + {"helix": 3, "forward": true, "start": 448, "end": 462}, + {"helix": 4, "forward": false, "start": 448, "end": 462} + ] + }, + { + "color": "#f7931e", + "sequence": "TTTACGAGGCATAGCAAGTTTTTTGGGGAGTTGAGATTTAGA", + "domains": [ + {"helix": 4, "forward": false, "start": 434, "end": 448}, + {"helix": 5, "forward": true, "start": 434, "end": 448}, + {"helix": 0, "forward": false, "start": 434, "end": 448} + ] + }, + { + "color": "#cc0000", + "sequence": "TCATCTACGTTAATTAGAAAGATTCATCTCGAGGTGCCGTAA", + "domains": [ + {"helix": 1, "forward": true, "start": 448, "end": 462}, + {"helix": 0, "forward": false, "start": 448, "end": 462}, + {"helix": 5, "forward": true, "start": 448, "end": 462} + ] + }, + { + "color": "#aaaa00", + "sequence": "GTAAATTGGGCTGAGAAACACCAGAACGGAAGGCTTGCCCTG", + "domains": [ + {"helix": 3, "forward": true, "start": 476, "end": 490}, + {"helix": 2, "forward": false, "start": 476, "end": 490}, + {"helix": 1, "forward": true, "start": 476, "end": 490} + ] + }, + { + "color": "#333333", + "sequence": "GACCTTCATCAAGACAGATGAACGGTGTACCAACTTTGAAAG", + "domains": [ + {"helix": 2, "forward": false, "start": 490, "end": 504}, + {"helix": 3, "forward": true, "start": 490, "end": 504}, + {"helix": 4, "forward": false, "start": 490, "end": 504} + ] + }, + { + "color": "#57bb00", + "sequence": "AGTGAGATGGTTTAACCCTAAAGGGAGCATTCAGTGAATAAC", + "domains": [ + {"helix": 4, "forward": false, "start": 476, "end": 490}, + {"helix": 5, "forward": true, "start": 476, "end": 490}, + {"helix": 0, "forward": false, "start": 476, "end": 490} + ] + }, + { + "color": "#f74308", + "sequence": "ACGAGTAATCTTGATAACAAAGCTGCTCCCCCGATTTAGAGC", + "domains": [ + {"helix": 1, "forward": true, "start": 490, "end": 504}, + {"helix": 0, "forward": false, "start": 490, "end": 504}, + {"helix": 5, "forward": true, "start": 490, "end": 504} + ] + }, + { + "color": "#007200", + "sequence": "ACGAGGCGCAGACACCATGTTACTTAGCTCAAATCCGCGACC", + "domains": [ + {"helix": 3, "forward": true, "start": 518, "end": 532}, + {"helix": 2, "forward": false, "start": 518, "end": 532}, + {"helix": 1, "forward": true, "start": 518, "end": 532} + ] + }, + { + "color": "#03b6a2", + "sequence": "TCATCTTTGACCCTACCTAAAACGAAAGGCCACTACGAAGGC", + "domains": [ + {"helix": 2, "forward": false, "start": 532, "end": 546}, + {"helix": 3, "forward": true, "start": 532, "end": 546}, + {"helix": 4, "forward": false, "start": 532, "end": 546} + ] + }, + { + "color": "#32b86c", + "sequence": "ACCGGTCAATCATACGGCGAACGTGGCGTAAATTGTGTCGAT", + "domains": [ + {"helix": 4, "forward": false, "start": 518, "end": 532}, + {"helix": 5, "forward": true, "start": 518, "end": 532}, + {"helix": 0, "forward": false, "start": 518, "end": 532} + ] + }, + { + "color": "#888888", + "sequence": "TGCCCAGCGATTATGTATCATCGCCTGAAGAAAGGAAGGGAA", + "domains": [ + {"helix": 1, "forward": true, "start": 532, "end": 546}, + {"helix": 0, "forward": false, "start": 532, "end": 546}, + {"helix": 5, "forward": true, "start": 532, "end": 546} + ] + }, + { + "color": "#f7931e", + "sequence": "CATGAGGAAGTTAAGAGGACTAAAGACTAACGGCTACAGAGG", + "domains": [ + {"helix": 3, "forward": true, "start": 560, "end": 574}, + {"helix": 2, "forward": false, "start": 560, "end": 574}, + {"helix": 1, "forward": true, "start": 560, "end": 574} + ] + }, + { + "color": "#aaaa00", + "sequence": "GAGGCTTGCAGGTTCAACCATCGCCCACTTGCGCCGACAATG", + "domains": [ + {"helix": 2, "forward": false, "start": 574, "end": 588}, + {"helix": 3, "forward": true, "start": 574, "end": 588}, + {"helix": 4, "forward": false, "start": 574, "end": 588} + ] + }, + { + "color": "#320096", + "sequence": "ACTCCATTAAACGGCGGGCGCTAGGGCGCGAGGGTAGCAAAG", + "domains": [ + {"helix": 4, "forward": false, "start": 560, "end": 574}, + {"helix": 5, "forward": true, "start": 560, "end": 574}, + {"helix": 0, "forward": false, "start": 560, "end": 574} + ] + }, + { + "color": "#333333", + "sequence": "CTGAGTTAAAGGCCAGACAGCATCGGAACTGGCAAGTGTAGC", + "domains": [ + {"helix": 1, "forward": true, "start": 574, "end": 588}, + {"helix": 0, "forward": false, "start": 574, "end": 588}, + {"helix": 5, "forward": true, "start": 574, "end": 588} + ] + }, + { + "color": "#57bb00", + "sequence": "TTTCGAGGTGAAGTATCGGTTTATCAGCGTAGGAGCCTTTAA", + "domains": [ + {"helix": 3, "forward": true, "start": 602, "end": 616}, + {"helix": 2, "forward": false, "start": 602, "end": 616}, + {"helix": 1, "forward": true, "start": 602, "end": 616} + ] + }, + { + "color": "#007200", + "sequence": "AGTGAGAATAGAGTATGGGATTTTGCTAAGTAAATGAATTTT", + "domains": [ + {"helix": 2, "forward": false, "start": 616, "end": 630}, + {"helix": 3, "forward": true, "start": 616, "end": 630}, + {"helix": 4, "forward": false, "start": 616, "end": 630} + ] + }, + { + "color": "#7300de", + "sequence": "CTTTTCTTAAACAGTAACCACCACACCCAAAGGCTCCAAACA", + "domains": [ + {"helix": 4, "forward": false, "start": 602, "end": 616}, + {"helix": 5, "forward": true, "start": 602, "end": 616}, + {"helix": 0, "forward": false, "start": 602, "end": 616} + ] + }, + { + "color": "#03b6a2", + "sequence": "TTAAGGAACAACTAAAAATCTCCAAAAAGCCGCGCTTAATGC", + "domains": [ + {"helix": 1, "forward": true, "start": 616, "end": 630}, + {"helix": 0, "forward": false, "start": 616, "end": 630}, + {"helix": 5, "forward": true, "start": 616, "end": 630} + ] + }, + { + "color": "#32b86c", + "sequence": "GCGTAACGATCTTCAGACAGCCCTCATAAAGCCTGTAGCATT", + "domains": [ + {"helix": 3, "forward": true, "start": 644, "end": 658}, + {"helix": 2, "forward": false, "start": 644, "end": 658}, + {"helix": 1, "forward": true, "start": 644, "end": 658} + ] + }, + { + "color": "#f7931e", + "sequence": "CACCCTCATTTTACAGAACCGCCACCCTTTTAGTACCGCCAC", + "domains": [ + {"helix": 2, "forward": false, "start": 658, "end": 672}, + {"helix": 3, "forward": true, "start": 658, "end": 672}, + {"helix": 4, "forward": false, "start": 658, "end": 672} + ] + }, + { + "color": "#b8056c", + "sequence": "CCAAAGTTTTGTCGCGTACTATGGTTGCACAAACTACAACTA", + "domains": [ + {"helix": 4, "forward": false, "start": 644, "end": 658}, + {"helix": 5, "forward": true, "start": 644, "end": 658}, + {"helix": 0, "forward": false, "start": 644, "end": 658} + ] + }, + { + "color": "#aaaa00", + "sequence": "CCCAGGGATAGCAAGTTTCGTCACCAGTTTTGACGAGCACGT", + "domains": [ + {"helix": 1, "forward": true, "start": 658, "end": 672}, + {"helix": 0, "forward": false, "start": 658, "end": 672}, + {"helix": 5, "forward": true, "start": 658, "end": 672} + ] + }, + { + "color": "#320096", + "sequence": "GTATAGCCCGGACCTCGAGAGGGTTGATCAAGGCGGATAAGT", + "domains": [ + {"helix": 3, "forward": true, "start": 686, "end": 700}, + {"helix": 2, "forward": false, "start": 686, "end": 700}, + {"helix": 1, "forward": true, "start": 686, "end": 700} + ] + }, + { + "color": "#57bb00", + "sequence": "TATTATTCTGAACGCGTATAAACAGTTAGCCTTGAGTAACAG", + "domains": [ + {"helix": 2, "forward": false, "start": 700, "end": 714}, + {"helix": 3, "forward": true, "start": 700, "end": 714}, + {"helix": 4, "forward": false, "start": 700, "end": 714} + ] + }, + { + "color": "#cc0000", + "sequence": "TGATAGGTGTATCATCGTTAGAATCAGATTGCTCAGTACCTG", + "domains": [ + {"helix": 4, "forward": false, "start": 686, "end": 700}, + {"helix": 5, "forward": true, "start": 686, "end": 700}, + {"helix": 0, "forward": false, "start": 686, "end": 700} + ] + }, + { + "color": "#007200", + "sequence": "GCACATGAAAGTATGGATTAGCGGGGTTGCGGGAGCTAAACA", + "domains": [ + {"helix": 1, "forward": true, "start": 700, "end": 714}, + {"helix": 0, "forward": false, "start": 700, "end": 714}, + {"helix": 5, "forward": true, "start": 700, "end": 714} + ] + }, + { + "color": "#7300de", + "sequence": "ACAGGAGTGTACCCTACATGGCTTTTGATCGTTCCAGTAAGC", + "domains": [ + {"helix": 3, "forward": true, "start": 728, "end": 742}, + {"helix": 2, "forward": false, "start": 728, "end": 742}, + {"helix": 1, "forward": true, "start": 728, "end": 742} + ] + }, + { + "color": "#32b86c", + "sequence": "GGTCAGACGATTCAACCAGAGCCGCCGCGCCGCCACCAGAAC", + "domains": [ + {"helix": 2, "forward": false, "start": 742, "end": 756}, + {"helix": 3, "forward": true, "start": 742, "end": 756}, + {"helix": 4, "forward": false, "start": 742, "end": 756} + ] + }, + { + "color": "#f74308", + "sequence": "CATGGTAATAAGTTGGGATTTTAGACAGTCTGAATTTACCCT", + "domains": [ + {"helix": 4, "forward": false, "start": 728, "end": 742}, + {"helix": 5, "forward": true, "start": 728, "end": 742}, + {"helix": 0, "forward": false, "start": 728, "end": 742} + ] + }, + { + "color": "#f7931e", + "sequence": "GTGGCCTTGATATTTGGAAAGCGCAGTCGAACGGTACGCCAG", + "domains": [ + {"helix": 1, "forward": true, "start": 742, "end": 756}, + {"helix": 0, "forward": false, "start": 742, "end": 756}, + {"helix": 5, "forward": true, "start": 742, "end": 756} + ] + }, + { + "color": "#b8056c", + "sequence": "CCCTCAGAACCGGTCCTCCCTCAGAGCCTCGCCACCACCGGA", + "domains": [ + {"helix": 3, "forward": true, "start": 770, "end": 784}, + {"helix": 2, "forward": false, "start": 770, "end": 784}, + {"helix": 1, "forward": true, "start": 770, "end": 784} + ] + }, + { + "color": "#320096", + "sequence": "GCGCGTTTTCATCGAGCGACAGAATCAAAGCAGCACCGTAAT", + "domains": [ + {"helix": 2, "forward": false, "start": 784, "end": 798}, + {"helix": 3, "forward": true, "start": 784, "end": 798}, + {"helix": 4, "forward": false, "start": 784, "end": 798} + ] + }, + { + "color": "#888888", + "sequence": "CACCACCCTCAGAGTTTTTATAATCAGTACCGGAACCAGACT", + "domains": [ + {"helix": 4, "forward": false, "start": 770, "end": 784}, + {"helix": 5, "forward": true, "start": 770, "end": 784}, + {"helix": 0, "forward": false, "start": 770, "end": 784} + ] + }, + { + "color": "#57bb00", + "sequence": "ACCGGCATTTTCGGTCATAATCAAAATCGAGGCCACCGAGTA", + "domains": [ + {"helix": 1, "forward": true, "start": 784, "end": 798}, + {"helix": 0, "forward": false, "start": 784, "end": 798}, + {"helix": 5, "forward": true, "start": 784, "end": 798} + ] + }, + { + "color": "#cc0000", + "sequence": "TTAGCAAGGCCGTTCCAGTAGCACCATTTTAGAGCCAGCAAA", + "domains": [ + {"helix": 3, "forward": true, "start": 812, "end": 826}, + {"helix": 2, "forward": false, "start": 812, "end": 826}, + {"helix": 1, "forward": true, "start": 812, "end": 826} + ] + }, + { + "color": "#7300de", + "sequence": "GAGGGAGGGAAGCAACCAGCGCCAAAGAAGAAAATTCATATG", + "domains": [ + {"helix": 2, "forward": false, "start": 826, "end": 840}, + {"helix": 3, "forward": true, "start": 826, "end": 840}, + {"helix": 4, "forward": false, "start": 826, "end": 840} + ] + }, + { + "color": "#333333", + "sequence": "GTGAAACGTCACCATCACGCAAATTAACCATTTGGGAATTAG", + "domains": [ + {"helix": 4, "forward": false, "start": 812, "end": 826}, + {"helix": 5, "forward": true, "start": 812, "end": 826}, + {"helix": 0, "forward": false, "start": 812, "end": 826} + ] + }, + { + "color": "#32b86c", + "sequence": "ATGTAAATATTGACTCACCGACTTGAGCCGTTGTAGCAATAC", + "domains": [ + {"helix": 1, "forward": true, "start": 826, "end": 840}, + {"helix": 0, "forward": false, "start": 826, "end": 840}, + {"helix": 5, "forward": true, "start": 826, "end": 840} + ] + }, + { + "color": "#f74308", + "sequence": "AAGACACCACGGCAACATATAAAAGAAAAAATACATAAAGGT", + "domains": [ + {"helix": 3, "forward": true, "start": 854, "end": 868}, + {"helix": 2, "forward": false, "start": 854, "end": 868}, + {"helix": 1, "forward": true, "start": 854, "end": 868} + ] + }, + { + "color": "#b8056c", + "sequence": "CGCAATAATAACCAGATAGCCGAACAAATTTTAAGAAAAGTA", + "domains": [ + {"helix": 2, "forward": false, "start": 868, "end": 882}, + {"helix": 3, "forward": true, "start": 868, "end": 882}, + {"helix": 4, "forward": false, "start": 868, "end": 882} + ] + }, + { + "color": "#03b6a2", + "sequence": "AGAATAAGTTTATTATAACATCACTTGCCGTAGAAAATACAG", + "domains": [ + {"helix": 4, "forward": false, "start": 854, "end": 868}, + {"helix": 5, "forward": true, "start": 854, "end": 868}, + {"helix": 0, "forward": false, "start": 854, "end": 868} + ] + }, + { + "color": "#320096", + "sequence": "GGGGAATACCCAAAAGTATGTTAGCAAACTGAGTAGAAGAAC", + "domains": [ + {"helix": 1, "forward": true, "start": 868, "end": 882}, + {"helix": 0, "forward": false, "start": 868, "end": 882}, + {"helix": 5, "forward": true, "start": 868, "end": 882} + ] + }, + { + "color": "#888888", + "sequence": "AAACAATGAAATAACCCAATAATAAGAGTACAAGAATTGAGT", + "domains": [ + {"helix": 3, "forward": true, "start": 896, "end": 910}, + {"helix": 2, "forward": false, "start": 896, "end": 910}, + {"helix": 1, "forward": true, "start": 896, "end": 910} + ] + }, + { + "color": "#cc0000", + "sequence": "AGCGCATTAGACAGTAGCAGCCTTTACATAACGTCAAAAATG", + "domains": [ + {"helix": 2, "forward": false, "start": 910, "end": 924}, + {"helix": 3, "forward": true, "start": 910, "end": 924}, + {"helix": 4, "forward": false, "start": 910, "end": 924} + ] + }, + { + "color": "#aaaa00", + "sequence": "AAAGCAATAGCTATTTGCTGGTAATATCAGAGATAACCCAAG", + "domains": [ + {"helix": 4, "forward": false, "start": 896, "end": 910}, + {"helix": 5, "forward": true, "start": 896, "end": 910}, + {"helix": 0, "forward": false, "start": 896, "end": 910} + ] + }, + { + "color": "#7300de", + "sequence": "TAGGGAGAATTAACAGCGCTAATATCAGCAGAACAATATTAC", + "domains": [ + {"helix": 1, "forward": true, "start": 910, "end": 924}, + {"helix": 0, "forward": false, "start": 910, "end": 924}, + {"helix": 5, "forward": true, "start": 910, "end": 924} + ] + }, + { + "color": "#333333", + "sequence": "TTATTTATCCCAGTACAAAATAAACAGCAAGCCTAATTTGCC", + "domains": [ + {"helix": 3, "forward": true, "start": 938, "end": 952}, + {"helix": 2, "forward": false, "start": 938, "end": 952}, + {"helix": 1, "forward": true, "start": 938, "end": 952} + ] + }, + { + "color": "#f74308", + "sequence": "TTAAATCAAGATTTTTTAGCGAACCTCCTCTAAGAACGCGAG", + "domains": [ + {"helix": 2, "forward": false, "start": 952, "end": 966}, + {"helix": 3, "forward": true, "start": 952, "end": 966}, + {"helix": 4, "forward": false, "start": 952, "end": 966} + ] + }, + { + "color": "#007200", + "sequence": "GCATCCAAATAAGAACAGGAAAAACGCTCGTCTTTCCAGAAG", + "domains": [ + {"helix": 4, "forward": false, "start": 938, "end": 952}, + {"helix": 5, "forward": true, "start": 938, "end": 952}, + {"helix": 0, "forward": false, "start": 938, "end": 952} + ] + }, + { + "color": "#b8056c", + "sequence": "AGTAGTTGCTATTTCCAACGCTAACGAGCATGGAAATACCTA", + "domains": [ + {"helix": 1, "forward": true, "start": 952, "end": 966}, + {"helix": 0, "forward": false, "start": 952, "end": 966}, + {"helix": 5, "forward": true, "start": 952, "end": 966} + ] + }, + { + "color": "#03b6a2", + "sequence": "ATAGCAAGCAAAATGAATCATTACCGCGATTTTATTTTCATC", + "domains": [ + {"helix": 3, "forward": true, "start": 980, "end": 994}, + {"helix": 2, "forward": false, "start": 980, "end": 994}, + {"helix": 1, "forward": true, "start": 980, "end": 994} + ] + }, + { + "color": "#888888", + "sequence": "CAATAATCGGCTAGAATATCCCATCCTAGTCCTGAACAAGAA", + "domains": [ + {"helix": 2, "forward": false, "start": 994, "end": 1008}, + {"helix": 3, "forward": true, "start": 994, "end": 1008}, + {"helix": 4, "forward": false, "start": 994, "end": 1008} + ] + }, + { + "color": "#f7931e", + "sequence": "AATCAGATATAGAAATCGTCTGAAATGGAAGCAAGCCGTTTT", + "domains": [ + {"helix": 4, "forward": false, "start": 980, "end": 994}, + {"helix": 5, "forward": true, "start": 980, "end": 994}, + {"helix": 0, "forward": false, "start": 980, "end": 994} + ] + }, + { + "color": "#cc0000", + "sequence": "GTGTCTTTCCTTATCACTCATCGAGAACATTATTTACATTGG", + "domains": [ + {"helix": 1, "forward": true, "start": 994, "end": 1008}, + {"helix": 0, "forward": false, "start": 994, "end": 1008}, + {"helix": 5, "forward": true, "start": 994, "end": 1008} + ] + }, + { + "color": "#aaaa00", + "sequence": "GCTAATGCAGAACGCAATAAACAACATGGTTCTGTCCAGACG", + "domains": [ + {"helix": 3, "forward": true, "start": 1022, "end": 1036}, + {"helix": 2, "forward": false, "start": 1022, "end": 1036}, + {"helix": 1, "forward": true, "start": 1022, "end": 1036} + ] + }, + { + "color": "#333333", + "sequence": "CAACGCCAACATGACTCAACAGTAGGGCACCAGTATAAAGCC", + "domains": [ + {"helix": 2, "forward": false, "start": 1036, "end": 1050}, + {"helix": 3, "forward": true, "start": 1036, "end": 1050}, + {"helix": 4, "forward": false, "start": 1036, "end": 1050} + ] + }, + { + "color": "#57bb00", + "sequence": "AACGCGCCTGTTTAACACGACCAGTAATAGGTAAAGTAATAT", + "domains": [ + {"helix": 4, "forward": false, "start": 1022, "end": 1036}, + {"helix": 5, "forward": true, "start": 1022, "end": 1036}, + {"helix": 0, "forward": false, "start": 1022, "end": 1036} + ] + }, + { + "color": "#f74308", + "sequence": "ACGTAATTTAGGCAAAAGTACCGACAAAAAAAGGGACATTCT", + "domains": [ + {"helix": 1, "forward": true, "start": 1036, "end": 1050}, + {"helix": 0, "forward": false, "start": 1036, "end": 1050}, + {"helix": 5, "forward": true, "start": 1036, "end": 1050} + ] + }, + { + "color": "#007200", + "sequence": "AAAGCCTGTTTAGCGAATCATAATTACTCCATAAGAATAAAC", + "domains": [ + {"helix": 3, "forward": true, "start": 1064, "end": 1078}, + {"helix": 2, "forward": false, "start": 1064, "end": 1078}, + {"helix": 1, "forward": true, "start": 1064, "end": 1078} + ] + }, + { + "color": "#03b6a2", + "sequence": "TCAAATATATTTCGAAATCCAATCGCAAATATGTAAATGCTG", + "domains": [ + {"helix": 2, "forward": false, "start": 1078, "end": 1092}, + {"helix": 3, "forward": true, "start": 1078, "end": 1092}, + {"helix": 4, "forward": false, "start": 1078, "end": 1092} + ] + }, + { + "color": "#32b86c", + "sequence": "ATGTATCATATGCGGAACCCTTCTGACCATAAGGCGTTAAAG", + "domains": [ + {"helix": 4, "forward": false, "start": 1064, "end": 1078}, + {"helix": 5, "forward": true, "start": 1064, "end": 1078}, + {"helix": 0, "forward": false, "start": 1064, "end": 1078} + ] + }, + { + "color": "#888888", + "sequence": "ACTAGTTAATTTCACGACCGTGTGATAATGAAAGCGTAAGAA", + "domains": [ + {"helix": 1, "forward": true, "start": 1078, "end": 1092}, + {"helix": 0, "forward": false, "start": 1078, "end": 1092}, + {"helix": 5, "forward": true, "start": 1078, "end": 1092} + ] + }, + { + "color": "#f7931e", + "sequence": "ACCTTTTTAACCCATCATAGGTCTGAGATTAGTGAATTTATC", + "domains": [ + {"helix": 3, "forward": true, "start": 1106, "end": 1120}, + {"helix": 2, "forward": false, "start": 1106, "end": 1120}, + {"helix": 1, "forward": true, "start": 1106, "end": 1120} + ] + }, + { + "color": "#aaaa00", + "sequence": "AATCGTCGCTATAATAAATCAATATATGTTTAATGGAAACAG", + "domains": [ + {"helix": 2, "forward": false, "start": 1120, "end": 1134}, + {"helix": 3, "forward": true, "start": 1120, "end": 1134}, + {"helix": 4, "forward": false, "start": 1120, "end": 1134} + ] + }, + { + "color": "#320096", + "sequence": "TATCCGGCTTAGGTAATATTTTTGAATGAGAAGAGTCAATTA", + "domains": [ + {"helix": 4, "forward": false, "start": 1106, "end": 1120}, + {"helix": 5, "forward": true, "start": 1106, "end": 1120}, + {"helix": 0, "forward": false, "start": 1106, "end": 1120} + ] + }, + { + "color": "#333333", + "sequence": "AATAATTAATTTTCAGATTAAGACGCTGGCTATTAGTCTTTA", + "domains": [ + {"helix": 1, "forward": true, "start": 1120, "end": 1134}, + {"helix": 0, "forward": false, "start": 1120, "end": 1134}, + {"helix": 5, "forward": true, "start": 1120, "end": 1134} + ] + }, + { + "color": "#57bb00", + "sequence": "AATTAATTACATATCAAACATCAAGAAAAAAAAGAAGATGAT", + "domains": [ + {"helix": 3, "forward": true, "start": 1148, "end": 1162}, + {"helix": 2, "forward": false, "start": 1148, "end": 1162}, + {"helix": 1, "forward": true, "start": 1148, "end": 1162} + ] + }, + { + "color": "#007200", + "sequence": "AATAACGGATTCAAGAATATACAGTAACTCAGGTTTAACGTC", + "domains": [ + {"helix": 2, "forward": false, "start": 1162, "end": 1176}, + {"helix": 3, "forward": true, "start": 1162, "end": 1176}, + {"helix": 4, "forward": false, "start": 1162, "end": 1176} + ] + }, + { + "color": "#7300de", + "sequence": "AGTTAACAATTTCAAGCCCTAAAACATCATTACCTGAGCAAA", + "domains": [ + {"helix": 4, "forward": false, "start": 1148, "end": 1162}, + {"helix": 5, "forward": true, "start": 1148, "end": 1162}, + {"helix": 0, "forward": false, "start": 1148, "end": 1162} + ] + }, + { + "color": "#03b6a2", + "sequence": "GAGCCTGATTGCTTAATTATTCATTTCAGCCATTAAAAATAC", + "domains": [ + {"helix": 1, "forward": true, "start": 1162, "end": 1176}, + {"helix": 0, "forward": false, "start": 1162, "end": 1176}, + {"helix": 5, "forward": true, "start": 1162, "end": 1176} + ] + }, + { + "color": "#32b86c", + "sequence": "TTGCACGTAAAACACTACCATATCAAAACAATGGAAGGGTTA", + "domains": [ + {"helix": 3, "forward": true, "start": 1190, "end": 1204}, + {"helix": 2, "forward": false, "start": 1190, "end": 1204}, + {"helix": 1, "forward": true, "start": 1190, "end": 1204} + ] + }, + { + "color": "#f7931e", + "sequence": "GGAATTATCATCACTTATCATTTTGCGGTTAAAAGTTTGAGT", + "domains": [ + {"helix": 2, "forward": false, "start": 1204, "end": 1218}, + {"helix": 3, "forward": true, "start": 1204, "end": 1218}, + {"helix": 4, "forward": false, "start": 1204, "end": 1218} + ] + }, + { + "color": "#b8056c", + "sequence": "AACAGAAATAAAGAGCAGAAGATAAAACTACTTCTGAATAAA", + "domains": [ + {"helix": 4, "forward": false, "start": 1190, "end": 1204}, + {"helix": 5, "forward": true, "start": 1190, "end": 1204}, + {"helix": 0, "forward": false, "start": 1190, "end": 1204} + ] + }, + { + "color": "#aaaa00", + "sequence": "GAATATTCCTGATTGATTGTTTGGATTAAGAGGTGAGGCGGT", + "domains": [ + {"helix": 1, "forward": true, "start": 1204, "end": 1218}, + {"helix": 0, "forward": false, "start": 1204, "end": 1218}, + {"helix": 5, "forward": true, "start": 1204, "end": 1218} + ] + }, + { + "color": "#cc0000", + "sequence": "TTAATGACTGTAAGGATACCGACAGTGCTGTCTAATCTATTT", + "domains": [ + {"helix": 5, "forward": true, "start": 84, "end": 91}, + {"helix": 0, "forward": false, "start": 84, "end": 91}, + {"helix": 1, "forward": true, "start": 84, "end": 98}, + {"helix": 2, "forward": false, "start": 84, "end": 98} + ] + }, + { + "color": "#888888", + "sequence": "CTGCCATATCGGCCGTGTCCTTAGTGCTAATAACCCCGCTGT", + "domains": [ + {"helix": 0, "forward": false, "start": 91, "end": 98}, + {"helix": 5, "forward": true, "start": 91, "end": 98}, + {"helix": 4, "forward": false, "start": 84, "end": 98}, + {"helix": 3, "forward": true, "start": 84, "end": 98} + ] + }, + { + "color": "#f74308", + "sequence": "TGGGCGCGTTGTAAACGCCAGGGTTTTCAGAAAGGGGGATGT", + "domains": [ + {"helix": 5, "forward": true, "start": 126, "end": 133}, + {"helix": 0, "forward": false, "start": 126, "end": 133}, + {"helix": 1, "forward": true, "start": 126, "end": 140}, + {"helix": 2, "forward": false, "start": 126, "end": 140} + ] + }, + { + "color": "#333333", + "sequence": "TCACGACCAGGGTGGCGCAACTGTTGGGCGCCAGCTGGCGGC", + "domains": [ + {"helix": 0, "forward": false, "start": 133, "end": 140}, + {"helix": 5, "forward": true, "start": 133, "end": 140}, + {"helix": 4, "forward": false, "start": 126, "end": 140}, + {"helix": 3, "forward": true, "start": 126, "end": 140} + ] + }, + { + "color": "#888888", + "sequence": "ACAGCTGTATCGGCTGCCAGTTTGAGGGTTACGTTGGTGTAG", + "domains": [ + {"helix": 5, "forward": true, "start": 168, "end": 175}, + {"helix": 0, "forward": false, "start": 168, "end": 175}, + {"helix": 1, "forward": true, "start": 168, "end": 182}, + {"helix": 2, "forward": false, "start": 168, "end": 182} + ] + }, + { + "color": "#03b6a2", + "sequence": "ACGACAGATTGCCCACAACCCGTCGGATATGGGATAGGTCTC", + "domains": [ + {"helix": 0, "forward": false, "start": 175, "end": 182}, + {"helix": 5, "forward": true, "start": 175, "end": 182}, + {"helix": 4, "forward": false, "start": 168, "end": 182}, + {"helix": 3, "forward": true, "start": 168, "end": 182} + ] + }, + { + "color": "#333333", + "sequence": "GCAAGCGTCATTTTCGCATTAAATTTTTGGTATTTAAATTGT", + "domains": [ + {"helix": 5, "forward": true, "start": 210, "end": 217}, + {"helix": 0, "forward": false, "start": 210, "end": 217}, + {"helix": 1, "forward": true, "start": 210, "end": 224}, + {"helix": 2, "forward": false, "start": 210, "end": 224} + ] + }, + { + "color": "#aaaa00", + "sequence": "AATCAGCGTCCACGTATGTACCCCGGTTTGTATAAGCAAATA", + "domains": [ + {"helix": 0, "forward": false, "start": 217, "end": 224}, + {"helix": 5, "forward": true, "start": 217, "end": 224}, + {"helix": 4, "forward": false, "start": 210, "end": 224}, + {"helix": 3, "forward": true, "start": 210, "end": 224} + ] + }, + { + "color": "#03b6a2", + "sequence": "GTTTGATAGATCTAGCCGGAGAGGGTAGTCAATATGATATTC", + "domains": [ + {"helix": 5, "forward": true, "start": 252, "end": 259}, + {"helix": 0, "forward": false, "start": 252, "end": 259}, + {"helix": 1, "forward": true, "start": 252, "end": 266}, + {"helix": 2, "forward": false, "start": 252, "end": 266} + ] + }, + { + "color": "#007200", + "sequence": "TTTTGAGGGTGGTTAATGTGTAGGTAAACAAATCACCATCAT", + "domains": [ + {"helix": 0, "forward": false, "start": 259, "end": 266}, + {"helix": 5, "forward": true, "start": 259, "end": 266}, + {"helix": 4, "forward": false, "start": 252, "end": 266}, + {"helix": 3, "forward": true, "start": 252, "end": 266} + ] + }, + { + "color": "#aaaa00", + "sequence": "AAAAGAACCCTGTAATCGGTTGTACCAAGCAAAATTAAGCAA", + "domains": [ + {"helix": 5, "forward": true, "start": 294, "end": 301}, + {"helix": 0, "forward": false, "start": 294, "end": 301}, + {"helix": 1, "forward": true, "start": 294, "end": 308}, + {"helix": 2, "forward": false, "start": 294, "end": 308} + ] + }, + { + "color": "#f7931e", + "sequence": "ATTATGATAGCCCGTACTAATAGTAGTACAAAGAATTAGCGC", + "domains": [ + {"helix": 0, "forward": false, "start": 301, "end": 308}, + {"helix": 5, "forward": true, "start": 301, "end": 308}, + {"helix": 4, "forward": false, "start": 294, "end": 308}, + {"helix": 3, "forward": true, "start": 294, "end": 308} + ] + }, + { + "color": "#007200", + "sequence": "GAACAAGGTTTGACCCAATTCTGCGAACGAGTGTCTGGAAGT", + "domains": [ + {"helix": 5, "forward": true, "start": 336, "end": 343}, + {"helix": 0, "forward": false, "start": 336, "end": 343}, + {"helix": 1, "forward": true, "start": 336, "end": 350}, + {"helix": 2, "forward": false, "start": 336, "end": 350} + ] + }, + { + "color": "#57bb00", + "sequence": "AGATTTAAGTCCACTTAATTGCTGAATAAACTAAAGTACGGG", + "domains": [ + {"helix": 0, "forward": false, "start": 343, "end": 350}, + {"helix": 5, "forward": true, "start": 343, "end": 350}, + {"helix": 4, "forward": false, "start": 336, "end": 350}, + {"helix": 3, "forward": true, "start": 336, "end": 350} + ] + }, + { + "color": "#f7931e", + "sequence": "AGGGCGAAGACCGGTTTAATTCGAGCTTTCATTAAGAGGAAG", + "domains": [ + {"helix": 5, "forward": true, "start": 378, "end": 385}, + {"helix": 0, "forward": false, "start": 378, "end": 385}, + {"helix": 1, "forward": true, "start": 378, "end": 392}, + {"helix": 2, "forward": false, "start": 378, "end": 392} + ] + }, + { + "color": "#32b86c", + "sequence": "GCGAACCAAAACCGAAATCAGGTCTTTATGCATCAAAAAGAG", + "domains": [ + {"helix": 0, "forward": false, "start": 385, "end": 392}, + {"helix": 5, "forward": true, "start": 385, "end": 392}, + {"helix": 4, "forward": false, "start": 378, "end": 392}, + {"helix": 3, "forward": true, "start": 378, "end": 392} + ] + }, + { + "color": "#57bb00", + "sequence": "ACCATCAGCGTCCAAATAGTAAAATGTTTAAGAGGCTTTTGC", + "domains": [ + {"helix": 5, "forward": true, "start": 420, "end": 427}, + {"helix": 0, "forward": false, "start": 420, "end": 427}, + {"helix": 1, "forward": true, "start": 420, "end": 434}, + {"helix": 2, "forward": false, "start": 420, "end": 434} + ] + }, + { + "color": "#320096", + "sequence": "CTGGATACCCAAATTAAGAGCAACACTAACCAAAATAGCGAC", + "domains": [ + {"helix": 0, "forward": false, "start": 427, "end": 434}, + {"helix": 5, "forward": true, "start": 427, "end": 434}, + {"helix": 4, "forward": false, "start": 420, "end": 434}, + {"helix": 3, "forward": true, "start": 420, "end": 434} + ] + }, + { + "color": "#32b86c", + "sequence": "AGCACTATTACAGGAAAACGAACTAACGAGCCAGTCAGGACG", + "domains": [ + {"helix": 5, "forward": true, "start": 462, "end": 469}, + {"helix": 0, "forward": false, "start": 462, "end": 469}, + {"helix": 1, "forward": true, "start": 462, "end": 476}, + {"helix": 2, "forward": false, "start": 462, "end": 476} + ] + }, + { + "color": "#7300de", + "sequence": "AACATTAAATCGGAATTTCAACTTTAATTGGCTCATTATATA", + "domains": [ + {"helix": 0, "forward": false, "start": 469, "end": 476}, + {"helix": 5, "forward": true, "start": 469, "end": 476}, + {"helix": 4, "forward": false, "start": 462, "end": 476}, + {"helix": 3, "forward": true, "start": 462, "end": 476} + ] + }, + { + "color": "#320096", + "sequence": "TTGACGGATCAACGCAAGAACCGGATATCGCATAGGCTGGCT", + "domains": [ + {"helix": 5, "forward": true, "start": 504, "end": 511}, + {"helix": 0, "forward": false, "start": 504, "end": 511}, + {"helix": 1, "forward": true, "start": 504, "end": 518}, + {"helix": 2, "forward": false, "start": 504, "end": 518} + ] + }, + { + "color": "#b8056c", + "sequence": "TACCCAAGGAAAGCAGGGAACCGAACTGACAGACCAGGCGGA", + "domains": [ + {"helix": 0, "forward": false, "start": 511, "end": 518}, + {"helix": 5, "forward": true, "start": 511, "end": 518}, + {"helix": 4, "forward": false, "start": 504, "end": 518}, + {"helix": 3, "forward": true, "start": 504, "end": 518} + ] + }, + { + "color": "#7300de", + "sequence": "GAAAGCGGAGATTTACCAAGCGCGAAACTTACACTAAAACAC", + "domains": [ + {"helix": 5, "forward": true, "start": 546, "end": 553}, + {"helix": 0, "forward": false, "start": 546, "end": 553}, + {"helix": 1, "forward": true, "start": 546, "end": 560}, + {"helix": 2, "forward": false, "start": 546, "end": 560} + ] + }, + { + "color": "#cc0000", + "sequence": "TACAACGAAAGGAGGTAAAATACGTAATAGGCAAAAGAATTT", + "domains": [ + {"helix": 0, "forward": false, "start": 553, "end": 560}, + {"helix": 5, "forward": true, "start": 553, "end": 560}, + {"helix": 4, "forward": false, "start": 546, "end": 560}, + {"helix": 3, "forward": true, "start": 546, "end": 560} + ] + }, + { + "color": "#b8056c", + "sequence": "GGTCACGCAGCGAAGCTTTTGCGGGATCTTTATTCGGTCGCT", + "domains": [ + {"helix": 5, "forward": true, "start": 588, "end": 595}, + {"helix": 0, "forward": false, "start": 588, "end": 595}, + {"helix": 1, "forward": true, "start": 588, "end": 602}, + {"helix": 2, "forward": false, "start": 588, "end": 602} + ] + }, + { + "color": "#f74308", + "sequence": "CCCTCAGCTGCGCGCTTGATACCGATAGGCATAACCGATAGC", + "domains": [ + {"helix": 0, "forward": false, "start": 595, "end": 602}, + {"helix": 5, "forward": true, "start": 595, "end": 602}, + {"helix": 4, "forward": false, "start": 588, "end": 602}, + {"helix": 3, "forward": true, "start": 588, "end": 602} + ] + }, + { + "color": "#cc0000", + "sequence": "GCCGCTACACGTTGAAGGAATTGCGAATGTCAGTTTCAGCGG", + "domains": [ + {"helix": 5, "forward": true, "start": 630, "end": 637}, + {"helix": 0, "forward": false, "start": 630, "end": 637}, + {"helix": 1, "forward": true, "start": 630, "end": 644}, + {"helix": 2, "forward": false, "start": 630, "end": 644} + ] + }, + { + "color": "#888888", + "sequence": "ATTTTTTCAGGGCGTCTTTCCAGACGTTAACAACTTTCAATA", + "domains": [ + {"helix": 0, "forward": false, "start": 637, "end": 644}, + {"helix": 5, "forward": true, "start": 637, "end": 644}, + {"helix": 4, "forward": false, "start": 630, "end": 644}, + {"helix": 3, "forward": true, "start": 630, "end": 644} + ] + }, + { + "color": "#f74308", + "sequence": "ATAACGTACACTGAGCCCAATAGGAACCATCCTCAGAGCCAC", + "domains": [ + {"helix": 5, "forward": true, "start": 672, "end": 679}, + {"helix": 0, "forward": false, "start": 672, "end": 679}, + {"helix": 1, "forward": true, "start": 672, "end": 686}, + {"helix": 2, "forward": false, "start": 672, "end": 686} + ] + }, + { + "color": "#333333", + "sequence": "TACCGTAGCTTTCCCCGTACTCAGGAGGCAGAACCGCCACAA", + "domains": [ + {"helix": 0, "forward": false, "start": 679, "end": 686}, + {"helix": 5, "forward": true, "start": 679, "end": 686}, + {"helix": 4, "forward": false, "start": 672, "end": 686}, + {"helix": 3, "forward": true, "start": 672, "end": 686} + ] + }, + { + "color": "#888888", + "sequence": "GGAGGCCAGGATTATAAGAGGCTGAGACTGTATTTCGGAACC", + "domains": [ + {"helix": 5, "forward": true, "start": 714, "end": 721}, + {"helix": 0, "forward": false, "start": 714, "end": 721}, + {"helix": 1, "forward": true, "start": 714, "end": 728}, + {"helix": 2, "forward": false, "start": 714, "end": 728} + ] + }, + { + "color": "#03b6a2", + "sequence": "CAAGAGAGATTAAATTAACGGGGTCAGTATGCCCCCTGCCAT", + "domains": [ + {"helix": 0, "forward": false, "start": 721, "end": 728}, + {"helix": 5, "forward": true, "start": 721, "end": 728}, + {"helix": 4, "forward": false, "start": 714, "end": 728}, + {"helix": 3, "forward": true, "start": 714, "end": 728} + ] + }, + { + "color": "#333333", + "sequence": "AATCCTGGCCAGAACACAAACAAATAAAGCGAGGTTGAGGCA", + "domains": [ + {"helix": 5, "forward": true, "start": 756, "end": 763}, + {"helix": 0, "forward": false, "start": 756, "end": 763}, + {"helix": 1, "forward": true, "start": 756, "end": 770}, + {"helix": 2, "forward": false, "start": 756, "end": 770} + ] + }, + { + "color": "#aaaa00", + "sequence": "CATTAAAAGAAGTGCCACCACCCTCAGACAGCATTGACAGCA", + "domains": [ + {"helix": 0, "forward": false, "start": 763, "end": 770}, + {"helix": 5, "forward": true, "start": 763, "end": 770}, + {"helix": 4, "forward": false, "start": 756, "end": 770}, + {"helix": 3, "forward": true, "start": 756, "end": 770} + ] + }, + { + "color": "#03b6a2", + "sequence": "AAAGAGTCATCTTTTCATAGCCCCCTTAACCGTCAGACTGTA", + "domains": [ + {"helix": 5, "forward": true, "start": 798, "end": 805}, + {"helix": 0, "forward": false, "start": 798, "end": 805}, + {"helix": 1, "forward": true, "start": 798, "end": 812}, + {"helix": 2, "forward": false, "start": 798, "end": 812} + ] + }, + { + "color": "#007200", + "sequence": "CGTTTGCCTGTCCAATGAAACCATCGATGTTTGCCTTTAGCA", + "domains": [ + {"helix": 0, "forward": false, "start": 805, "end": 812}, + {"helix": 5, "forward": true, "start": 805, "end": 812}, + {"helix": 4, "forward": false, "start": 798, "end": 812}, + {"helix": 3, "forward": true, "start": 798, "end": 812} + ] + }, + { + "color": "#aaaa00", + "sequence": "TTCTTTGATCACCGGGAAATTATTCATTCGATTCAACCGATT", + "domains": [ + {"helix": 5, "forward": true, "start": 840, "end": 847}, + {"helix": 0, "forward": false, "start": 840, "end": 847}, + {"helix": 1, "forward": true, "start": 840, "end": 854}, + {"helix": 2, "forward": false, "start": 840, "end": 854} + ] + }, + { + "color": "#f7931e", + "sequence": "GTGAATTATTAGTATTGTCACAATCAATCAAAAGGGCGACCA", + "domains": [ + {"helix": 0, "forward": false, "start": 847, "end": 854}, + {"helix": 5, "forward": true, "start": 847, "end": 854}, + {"helix": 4, "forward": false, "start": 840, "end": 854}, + {"helix": 3, "forward": true, "start": 840, "end": 854} + ] + }, + { + "color": "#007200", + "sequence": "TCAAACTATTACGCAGAACTGGCATGATCAAAACCGAGGAAA", + "domains": [ + {"helix": 5, "forward": true, "start": 882, "end": 889}, + {"helix": 0, "forward": false, "start": 882, "end": 889}, + {"helix": 1, "forward": true, "start": 882, "end": 896}, + {"helix": 2, "forward": false, "start": 882, "end": 896} + ] + }, + { + "color": "#57bb00", + "sequence": "ACTCCTTATCGGCCCTTACCGAAGCCCTGTTACCAGAAGGAG", + "domains": [ + {"helix": 0, "forward": false, "start": 889, "end": 896}, + {"helix": 5, "forward": true, "start": 889, "end": 896}, + {"helix": 4, "forward": false, "start": 882, "end": 896}, + {"helix": 3, "forward": true, "start": 882, "end": 896} + ] + }, + { + "color": "#f7931e", + "sequence": "CGCCAGCGTAATTGTGAACACCCTGAACCATAAAAACAGGGA", + "domains": [ + {"helix": 5, "forward": true, "start": 924, "end": 931}, + {"helix": 0, "forward": false, "start": 924, "end": 931}, + {"helix": 1, "forward": true, "start": 924, "end": 938}, + {"helix": 2, "forward": false, "start": 924, "end": 938} + ] + }, + { + "color": "#32b86c", + "sequence": "TCAGAGGCATTGCAAACGATTTTTTGTTGAGAGAATAACATA", + "domains": [ + {"helix": 0, "forward": false, "start": 931, "end": 938}, + {"helix": 5, "forward": true, "start": 931, "end": 938}, + {"helix": 4, "forward": false, "start": 924, "end": 938}, + {"helix": 3, "forward": true, "start": 924, "end": 938} + ] + }, + { + "color": "#57bb00", + "sequence": "CATTTTGAATCTTATGCACCCAGCTACACCGGTTTTGAAGCC", + "domains": [ + {"helix": 5, "forward": true, "start": 966, "end": 973}, + {"helix": 0, "forward": false, "start": 966, "end": 973}, + {"helix": 1, "forward": true, "start": 966, "end": 980}, + {"helix": 2, "forward": false, "start": 966, "end": 980} + ] + }, + { + "color": "#320096", + "sequence": "TATCCTGACGCTCAGGCTTATCCGGTATCGACTTGCGGGACA", + "domains": [ + {"helix": 0, "forward": false, "start": 973, "end": 980}, + {"helix": 5, "forward": true, "start": 973, "end": 980}, + {"helix": 4, "forward": false, "start": 966, "end": 980}, + {"helix": 3, "forward": true, "start": 966, "end": 980} + ] + }, + { + "color": "#32b86c", + "sequence": "CAGATTCAGTACCGCATTCCAAGAACGGTTGTAGAAACCAAT", + "domains": [ + {"helix": 5, "forward": true, "start": 1008, "end": 1015}, + {"helix": 0, "forward": false, "start": 1008, "end": 1015}, + {"helix": 1, "forward": true, "start": 1008, "end": 1022}, + {"helix": 2, "forward": false, "start": 1008, "end": 1022} + ] + }, + { + "color": "#7300de", + "sequence": "TAAACCAACCAGTCTCAACAATAGATAAATTTACGAGCATCA", + "domains": [ + {"helix": 0, "forward": false, "start": 1015, "end": 1022}, + {"helix": 5, "forward": true, "start": 1015, "end": 1022}, + {"helix": 4, "forward": false, "start": 1008, "end": 1022}, + {"helix": 3, "forward": true, "start": 1008, "end": 1022} + ] + }, + { + "color": "#320096", + "sequence": "GGCCAACAGAATATGAGGCATTTTCGAGAGCGCCATATTTAA", + "domains": [ + {"helix": 5, "forward": true, "start": 1050, "end": 1057}, + {"helix": 0, "forward": false, "start": 1050, "end": 1057}, + {"helix": 1, "forward": true, "start": 1050, "end": 1064}, + {"helix": 2, "forward": false, "start": 1050, "end": 1064} + ] + }, + { + "color": "#b8056c", + "sequence": "TAATAAGAGAGATATTATACAAATTCTTTTAATTGAGAATAA", + "domains": [ + {"helix": 0, "forward": false, "start": 1057, "end": 1064}, + {"helix": 5, "forward": true, "start": 1057, "end": 1064}, + {"helix": 4, "forward": false, "start": 1050, "end": 1064}, + {"helix": 3, "forward": true, "start": 1050, "end": 1064} + ] + }, + { + "color": "#7300de", + "sequence": "TACGTGGGAAATACTCTTCTGACCTAAAGAGAGAAAACTTTT", + "domains": [ + {"helix": 5, "forward": true, "start": 1092, "end": 1099}, + {"helix": 0, "forward": false, "start": 1092, "end": 1099}, + {"helix": 1, "forward": true, "start": 1092, "end": 1106}, + {"helix": 2, "forward": false, "start": 1092, "end": 1106} + ] + }, + { + "color": "#cc0000", + "sequence": "ATGGTTTCACAGACTGGGTTATATAACTGACAAAGAACGCCT", + "domains": [ + {"helix": 0, "forward": false, "start": 1099, "end": 1106}, + {"helix": 5, "forward": true, "start": 1099, "end": 1106}, + {"helix": 4, "forward": false, "start": 1092, "end": 1106}, + {"helix": 3, "forward": true, "start": 1092, "end": 1106} + ] + }, + { + "color": "#b8056c", + "sequence": "ATGCGCGATAGCTTCCTTAGAATCCTTGACCTTGCTTCTGTA", + "domains": [ + {"helix": 5, "forward": true, "start": 1134, "end": 1141}, + {"helix": 0, "forward": false, "start": 1134, "end": 1141}, + {"helix": 1, "forward": true, "start": 1134, "end": 1148}, + {"helix": 2, "forward": false, "start": 1134, "end": 1148} + ] + }, + { + "color": "#f74308", + "sequence": "CATAGCGAACTGATTTTGAATTACCTTTTGAGTGAATAACAA", + "domains": [ + {"helix": 0, "forward": false, "start": 1141, "end": 1148}, + {"helix": 5, "forward": true, "start": 1141, "end": 1148}, + {"helix": 4, "forward": false, "start": 1134, "end": 1148}, + {"helix": 3, "forward": true, "start": 1134, "end": 1148} + ] + }, + { + "color": "#cc0000", + "sequence": "CGAACGAAGAGGCGTGAATACCAAGTTATTATCGGGAGAAAC", + "domains": [ + {"helix": 5, "forward": true, "start": 1176, "end": 1183}, + {"helix": 0, "forward": false, "start": 1176, "end": 1183}, + {"helix": 1, "forward": true, "start": 1176, "end": 1190}, + {"helix": 2, "forward": false, "start": 1176, "end": 1190} + ] + }, + { + "color": "#888888", + "sequence": "ATCGCGCACCACCAAATTGCGTAGATTTAGTACCTTTTACAT", + "domains": [ + {"helix": 0, "forward": false, "start": 1183, "end": 1190}, + {"helix": 5, "forward": true, "start": 1183, "end": 1190}, + {"helix": 4, "forward": false, "start": 1176, "end": 1190}, + {"helix": 3, "forward": true, "start": 1176, "end": 1190} + ] + }, + { + "color": "#f74308", + "sequence": "CAGTATTTAATCCTATCAGATGATGGCATCACCAGAAGGAGC", + "domains": [ + {"helix": 5, "forward": true, "start": 1218, "end": 1225}, + {"helix": 0, "forward": false, "start": 1218, "end": 1225}, + {"helix": 1, "forward": true, "start": 1218, "end": 1232}, + {"helix": 2, "forward": false, "start": 1218, "end": 1232} + ] + }, + { + "color": "#333333", + "sequence": "ATCAATAAACACCGGAACGTTATTAATTAACAAAGAAACCGA", + "domains": [ + {"helix": 0, "forward": false, "start": 1225, "end": 1232}, + {"helix": 5, "forward": true, "start": 1225, "end": 1232}, + {"helix": 4, "forward": false, "start": 1218, "end": 1232}, + {"helix": 3, "forward": true, "start": 1218, "end": 1232} + ] + }, + { + "color": "#cc0000", + "sequence": "ACTGCCCGAAATTGTCATGGTCATAGCTAAACGGAGGATCCC", + "domains": [ + {"helix": 5, "forward": true, "start": 42, "end": 49}, + {"helix": 0, "forward": false, "start": 42, "end": 49}, + {"helix": 1, "forward": true, "start": 42, "end": 56}, + {"helix": 2, "forward": false, "start": 42, "end": 56} + ] + }, + { + "color": "#f74308", + "sequence": "CCTGTGTGCTTTCCGGTTGGTGTAATGAACCTCGATAAAGAT", + "domains": [ + {"helix": 0, "forward": false, "start": 49, "end": 56}, + {"helix": 5, "forward": true, "start": 49, "end": 56}, + {"helix": 4, "forward": false, "start": 42, "end": 56}, + {"helix": 3, "forward": true, "start": 42, "end": 56} + ] + }, + { + "color": "#007200", + "sequence": "CTAATCCTTTGCCCCCTGCAACAGTGCCAATACATTTGAGTC", + "domains": [ + {"helix": 4, "forward": false, "start": 1232, "end": 1246}, + {"helix": 5, "forward": true, "start": 1232, "end": 1246}, + {"helix": 0, "forward": false, "start": 1232, "end": 1246} + ] + }, + { + "color": "#320096", + "sequence": "CAACTCGTATTACAACTTTACAAACAATATGATTTAGAAGTA", + "domains": [ + {"helix": 3, "forward": true, "start": 1232, "end": 1246}, + {"helix": 2, "forward": false, "start": 1232, "end": 1246}, + {"helix": 1, "forward": true, "start": 1232, "end": 1246} + ] + }, + { + "color": "#0066cc", + "sequence": "TGATAGACGGTTTTTCGCCCTTTGACGTTGGAGTCCACGTTCTTTAATAGTGGACTCTTGTTCCAAACTGGAACAACACTCAACCCTATCTCGGGCTATTCTTTTGATTTATAAGGGATTTTGCCGATTTCGGAACCACCATCAAACAGGATTTTCGCCTGCTGGGGCAAACCAGCGTGGACCGCTTGCTGCAACTCTCTCAGGGCCAGGCGGTGAAGGGCAATCAGCTGTTGCCCGTCTCACTGGTGAAAAGAAAAACCACCCTGGCGCCCAATACGCAAACCGCCTCTCCCCGCGCGTTGGCCGATTCATTAATGCAGCTGGCACGACAGGTTTCCCGACTGGAAAGCGGGCAGTGAGCGCAACGCAATTAATGTGAGTTAGCTCACTCATTAGGCACCCCAGGCTTTACACTTTATGCTTCCGGCTCGTATGTTGTGTGGAATTGTGAGCGGATAACAATTTCACACAGGAAACAGCTATGACCATGATTACGAATTCGAGCTCGGTACCCGGGGATCCTCCGTCTTTATCGAGGTAACAAGCACCACGTAGCTTAAGCCCTGTTTACTCATTACACCAACCAGGAGGTCAGAGTTCGGAGAAATGATTTATGTGAAATGCGTCAGCCGATTCAAGGCCCCTATATTCGTGCCCACCGACGAGTTGCTTACAGATGGCAGGGCCGCACTGTCGGTATCATAGAGTCACTCCAGGGCGAGCGTAAATAGATTAGAAGCGGGGTTATTTTGGCGGGACATTGTCATAAGGTTGACAATTCAGCACTAAGGACACTTAAGTCGTGCGCATGAATTCACAACCACTTAGAAGAACATCCACCCTGGCTTCTCCTGAGAAAGCTTGGCACTGGCCGTCGTTTTACAACGTCGTGACTGGGAAAACCCTGGCGTTACCCAACTTAATCGCCTTGCAGCACATCCCCCTTTCGCCAGCTGGCGTAATAGCGAAGAGGCCCGCACCGATCGCCCTTCCCAACAGTTGCGCAGCCTGAATGGCGAATGGCGCTTTGCCTGGTTTCCGGCACCAGAAGCGGTGCCGGAAAGCTGGCTGGAGTGCGATCTTCCTGAGGCCGATACTGTCGTCGTCCCCTCAAACTGGCAGATGCACGGTTACGATGCGCCCATCTACACCAACGTGACCTATCCCATTACGGTCAATCCGCCGTTTGTTCCCACGGAGAATCCGACGGGTTGTTACTCGCTCACATTTAATGTTGATGAAAGCTGGCTACAGGAAGGCCAGACGCGAATTATTTTTGATGGCGTTCCTATTGGTTAAAAAATGAGCTGATTTAACAAAAATTTAATGCGAATTTTAACAAAATATTAACGTTTACAATTTAAATATTTGCTTATACAATCTTCCTGTTTTTGGGGCTTTTCTGATTATCAACCGGGGTACATATGATTGACATGCTAGTTTTACGATTACCGTTCATCGATTCTCTTGTTTGCTCCAGACTCTCAGGCAATGACCTGATAGCCTTTGTAGATCTCTCAAAAATAGCTACCCTCTCCGGCATTAATTTATCAGCTAGAACGGTTGAATATCATATTGATGGTGATTTGACTGTCTCCGGCCTTTCTCACCCTTTTGAATCTTTACCTACACATTACTCAGGCATTGCATTTAAAATATATGAGGGTTCTAAAAATTTTTATCCTTGCGTTGAAATAAAGGCTTCTCCCGCAAAAGTATTACAGGGTCATAATGTTTTTGGTACAACCGATTTAGCTTTATGCTCTGAGGCTTTATTGCTTAATTTTGCTAATTCTTTGCCTTGCCTGTATGATTTATTGGATGTTAATGCTACTACTATTAGTAGAATTGATGCCACCTTTTCAGCTCGCGCCCCAAATGAAAATATAGCTAAACAGGTTATTGACCATTTGCGAAATGTATCTAATGGTCAAACTAAATCTACTCGTTCGCAGAATTGGGAATCAACTGTTATATGGAATGAAACTTCCAGACACCGTACTTTAGTTGCATATTTAAAACATGTTGAGCTACAGCATTATATTCAGCAATTAAGCTCTAAGCCATCCGCAAAAATGACCTCTTATCAAAAGGAGCAATTAAAGGTACTCTCTAATCCTGACCTGTTGGAGTTTGCTTCCGGTCTGGTTCGCTTTGAAGCTCGAATTAAAACGCGATATTTGAAGTCTTTCGGGCTTCCTCTTAATCTTTTTGATGCAATCCGCTTTGCTTCTGACTATAATAGTCAGGGTAAAGACCTGATTTTTGATTTATGGTCATTCTCGTTTTCTGAACTGTTTAAAGCATTTGAGGGGGATTCAATGAATATTTATGACGATTCCGCAGTATTGGACGCTATCCAGTCTAAACATTTTACTATTACCCCCTCTGGCAAAACTTCTTTTGCAAAAGCCTCTCGCTATTTTGGTTTTTATCGTCGTCTGGTAAACGAGGGTTATGATAGTGTTGCTCTTACTATGCCTCGTAATTCCTTTTGGCGTTATGTATCTGCATTAGTTGAATGTGGTATTCCTAAATCTCAACTGATGAATCTTTCTACCTGTAATAATGTTGTTCCGTTAGTTCGTTTTATTAACGTAGATTTTTCTTCCCAACGTCCTGACTGGTATAATGAGCCAGTTCTTAAAATCGCATAAGGTAATTCACAATGATTAAAGTTGAAATTAAACCATCTCAAGCCCAATTTACTACTCGTTCTGGTGTTTCTCGTCAGGGCAAGCCTTATTCACTGAATGAGCAGCTTTGTTACGTTGATTTGGGTAATGAATATCCGGTTCTTGTCAAGATTACTCTTGATGAAGGTCAGCCAGCCTATGCGCCTGGTCTGTACACCGTTCATCTGTCCTCTTTCAAAGTTGGTCAGTTCGGTTCCCTTATGATTGACCGTCTGCGCCTCGTTCCGGCTAAGTAACATGGAGCAGGTCGCGGATTTCGACACAATTTATCAGGCGATGATACAAATCTCCGTTGTACTTTGTTTCGCGCTTGGTATAATCGCTGGGGGTCAAAGATGAGTGTTTTAGTGTATTCTTTTGCCTCTTTCGTTTTAGGTTGGTGCCTTCGTAGTGGCATTACGTATTTTACCCGTTTAATGGAAACTTCCTCATGAAAAAGTCTTTAGTCCTCAAAGCCTCTGTAGCCGTTGCTACCCTCGTTCCGATGCTGTCTTTCGCTGCTGAGGGTGACGATCCCGCAAAAGCGGCCTTTAACTCCCTGCAAGCCTCAGCGACCGAATATATCGGTTATGCGTGGGCGATGGTTGTTGTCATTGTCGGCGCAACTATCGGTATCAAGCTGTTTAAGAAATTCACCTCGAAAGCAAGCTGATAAACCGATACAATTAAAGGCTCCTTTTGGAGCCTTTTTTTTGGAGATTTTCAACGTGAAAAAATTATTATTCGCAATTCCTTTAGTTGTTCCTTTCTATTCTCACTCCGCTGAAACTGTTGAAAGTTGTTTAGCAAAATCCCATACAGAAAATTCATTTACTAACGTCTGGAAAGACGACAAAACTTTAGATCGTTACGCTAACTATGAGGGCTGTCTGTGGAATGCTACAGGCGTTGTAGTTTGTACTGGTGACGAAACTCAGTGTTACGGTACATGGGTTCCTATTGGGCTTGCTATCCCTGAAAATGAGGGTGGTGGCTCTGAGGGTGGCGGTTCTGAGGGTGGCGGTTCTGAGGGTGGCGGTACTAAACCTCCTGAGTACGGTGATACACCTATTCCGGGCTATACTTATATCAACCCTCTCGACGGCACTTATCCGCCTGGTACTGAGCAAAACCCCGCTAATCCTAATCCTTCTCTTGAGGAGTCTCAGCCTCTTAATACTTTCATGTTTCAGAATAATAGGTTCCGAAATAGGCAGGGGGCATTAACTGTTTATACGGGCACTGTTACTCAAGGCACTGACCCCGTTAAAACTTATTACCAGTACACTCCTGTATCATCAAAAGCCATGTATGACGCTTACTGGAACGGTAAATTCAGAGACTGCGCTTTCCATTCTGGCTTTAATGAGGATTTATTTGTTTGTGAATATCAAGGCCAATCGTCTGACCTGCCTCAACCTCCTGTCAATGCTGGCGGCGGCTCTGGTGGTGGTTCTGGTGGCGGCTCTGAGGGTGGTGGCTCTGAGGGTGGCGGTTCTGAGGGTGGCGGCTCTGAGGGAGGCGGTTCCGGTGGTGGCTCTGGTTCCGGTGATTTTGATTATGAAAAGATGGCAAACGCTAATAAGGGGGCTATGACCGAAAATGCCGATGAAAACGCGCTACAGTCTGACGCTAAAGGCAAACTTGATTCTGTCGCTACTGATTACGGTGCTGCTATCGATGGTTTCATTGGTGACGTTTCCGGCCTTGCTAATGGTAATGGTGCTACTGGTGATTTTGCTGGCTCTAATTCCCAAATGGCTCAAGTCGGTGACGGTGATAATTCACCTTTAATGAATAATTTCCGTCAATATTTACCTTCCCTCCCTCAATCGGTTGAATGTCGCCCTTTTGTCTTTGGCGCTGGTAAACCATATGAATTTTCTATTGATTGTGACAAAATAAACTTATTCCGTGGTGTCTTTGCGTTTCTTTTATATGTTGCCACCTTTATGTATGTATTTTCTACGTTTGCTAACATACTGCGTAATAAGGAGTCTTAATCATGCCAGTTCTTTTGGGTATTCCGTTATTATTGCGTTTCCTCGGTTTCCTTCTGGTAACTTTGTTCGGCTATCTGCTTACTTTTCTTAAAAAGGGCTTCGGTAAGATAGCTATTGCTATTTCATTGTTTCTTGCTCTTATTATTGGGCTTAACTCAATTCTTGTGGGTTATCTCTCTGATATTAGCGCTCAATTACCCTCTGACTTTGTTCAGGGTGTTCAGTTAATTCTCCCGTCTAATGCGCTTCCCTGTTTTTATGTTATTCTCTCTGTAAAGGCTGCTATTTTCATTTTTGACGTTAAACAAAAAATCGTTTCTTATTTGGATTGGGATAAATAATATGGCTGTTTATTTTGTAACTGGCAAATTAGGCTCTGGAAAGACGCTCGTTAGCGTTGGTAAGATTCAGGATAAAATTGTAGCTGGGTGCAAAATAGCAACTAATCTTGATTTAAGGCTTCAAAACCTCCCGCAAGTCGGGAGGTTCGCTAAAACGCCTCGCGTTCTTAGAATACCGGATAAGCCTTCTATATCTGATTTGCTTGCTATTGGGCGCGGTAATGATTCCTACGATGAAAATAAAAACGGCTTGCTTGTTCTCGATGAGTGCGGTACTTGGTTTAATACCCGTTCTTGGAATGATAAGGAAAGACAGCCGATTATTGATTGGTTTCTACATGCTCGTAAATTAGGATGGGATATTATTTTTCTTGTTCAGGACTTATCTATTGTTGATAAACAGGCGCGTTCTGCATTAGCTGAACATGTTGTTTATTGTCGTCGTCTGGACAGAATTACTTTACCTTTTGTCGGTACTTTATATTCTCTTATTACTGGCTCGAAAATGCCTCTGCCTAAATTACATGTTGGCGTTGTTAAATATGGCGATTCTCAATTAAGCCCTACTGTTGAGCGTTGGCTTTATACTGGTAAGAATTTGTATAACGCATATGATACTAAACAGGCTTTTTCTAGTAATTATGATTCCGGTGTTTATTCTTATTTAACGCCTTATTTATCACACGGTCGGTATTTCAAACCATTAAATTTAGGTCAGAAGATGAAATTAACTAAAATATATTTGAAAAAGTTTTCTCGCGTTCTTTGTCTTGCGATTGGATTTGCATCAGCATTTACATATAGTTATATAACCCAACCTAAGCCGGAGGTTAAAAAGGTAGTCTCTCAGACCTATGATTTTGATAAATTCACTATTGACTCTTCTCAGCGTCTTAATCTAAGCTATCGCTATGTTTTCAAGGATTCTAAGGGAAAATTAATTAATAGCGACGATTTACAGAAGCAAGGTTATTCACTCACATATATTGATTTATGTACTGTTTCCATTAAAAAAGGTAATTCAAATGAAATTGTTAAATGTAATTAATTTTGTTTTCTTGATGTTTGTTTCATCATCTTCTTTTGCTCAGGTAATTGAAATGAATAATTCGCCTCTGCGCGATTTTGTAACTTGGTATTCAAAGCAATCAGGCGAATCCGTTATTGTTTCTCCCGATGTAAAAGGTACTGTTACTGTATATTCATCTGACGTTAAACCTGAAAATCTACGCAATTTCTTTATTTCTGTTTTACGTGCAAATAATTTTGATATGGTAGGTTCTAACCCTTCCATTATTCAGAAGTATAATCCAAACAATCAGGATTATATTGATGAATTGCCATCATCTGATAATCAGGAATATGATGATAATTCCGCTCCTTCTGGTGGTTTCTTTGTTCCGCAAAATGATAATGTTACTCAAACTTTTAAAATTAATAACGTTCGGGCAAAGGATTTAATACGAGTTGTCGAATTGTTTGTAAAGTCTAATACTTCTAAATCCTCAAATGTATTATCTATTGACGGCTCTAATCTATTAGTTGTTAGTGCTCCTAAAGATATTTTAGATAACCTTCCTCAATTCCTTTCAACTGTTGATTTGCCAACTGACCAGATATTGATTGAGGGTTTGATATTTGAGGTTCAGCAAGGTGATGCTTTAGATTTTTCATTTGCTGCTGGCTCTCAGCGTGGCACTGTTGCAGGCGGTGTTAATACTGACCGCCTCACCTCTGTTTTATCTTCTGCTGGTGGTTCGTTCGGTATTTTTAATGGCGATGTTTTAGGGCTATCAGTTCGCGCATTAAAGACTAATAGCCATTCAAAAATATTGTCTGTGCCACGTATTCTTACGCTTTCAGGTCAGAAGGGTTCTATCTCTGTTGGCCAGAATGTCCCTTTTATTACTGGTCGTGTGACTGGTGAATCTGCCAATGTAAATAATCCATTTCAGACGATTGAGCGTCAAAATGTAGGTATTTCCATGAGCGTTTTTCCTGTTGCAATGGCTGGCGGTAATATTGTTCTGGATATTACCAGCAAGGCCGATAGTTTGAGTTCTTCTACTCAGGCAAGTGATGTTATTACTAATCAAAGAAGTATTGCTACAACGGTTAATTTGCGTGATGGACAGACTCTTTTACTCGGTGGCCTCACTGATTATAAAAACACTTCTCAGGATTCTGGCGTACCGTTCCTGTCTAAAATCCCTTTAATCGGCCTCCTGTTTAGCTCCCGCTCTGATTCTAACGAGGAAAGCACGTTATACGTGCTCGTCAAAGCAACCATAGTACGCGCCCTGTAGCGGCGCATTAAGCGCGGCGGGTGTGGTGGTTACGCGCAGCGTGACCGCTACACTTGCCAGCGCCCTAGCGCCCGCTCCTTTCGCTTTCTTCCCTTCCTTTCTCGCCACGTTCGCCGGCTTTCCCCGTCAAGCTCTAAATCGGGGGCTCCCTTTAGGGTTCCGATTTAGTGCTTTACGGCACCTCGACCCCAAAAAACTTGATTTGGGTGATGGTTCACGTAGTGGGCCATCGCCC", + "is_scaffold": true, + "domains": [ + {"helix": 5, "forward": false, "start": 19, "end": 399}, + {"helix": 4, "forward": true, "start": 19, "end": 26}, + {"helix": 3, "forward": false, "start": 12, "end": 26}, + {"helix": 2, "forward": true, "start": 12, "end": 30}, + {"helix": 1, "forward": false, "start": 16, "end": 30}, + {"helix": 0, "forward": true, "start": 16, "end": 58}, + {"helix": 1, "forward": false, "start": 30, "end": 58}, + {"helix": 2, "forward": true, "start": 30, "end": 54}, + {"helix": 3, "forward": false, "start": 26, "end": 54}, + {"helix": 4, "forward": true, "start": 26, "end": 68}, + {"helix": 3, "forward": false, "start": 54, "end": 68}, + {"helix": 2, "forward": true, "start": 54, "end": 72}, + {"helix": 1, "forward": false, "start": 58, "end": 72}, + {"helix": 0, "forward": true, "start": 58, "end": 100}, + {"helix": 1, "forward": false, "start": 72, "end": 100}, + {"helix": 2, "forward": true, "start": 72, "end": 96}, + {"helix": 3, "forward": false, "start": 68, "end": 96}, + {"helix": 4, "forward": true, "start": 68, "end": 110}, + {"helix": 3, "forward": false, "start": 96, "end": 110}, + {"helix": 2, "forward": true, "start": 96, "end": 114}, + {"helix": 1, "forward": false, "start": 100, "end": 114}, + {"helix": 0, "forward": true, "start": 100, "end": 142}, + {"helix": 1, "forward": false, "start": 114, "end": 142}, + {"helix": 2, "forward": true, "start": 114, "end": 138}, + {"helix": 3, "forward": false, "start": 110, "end": 138}, + {"helix": 4, "forward": true, "start": 110, "end": 152}, + {"helix": 3, "forward": false, "start": 138, "end": 152}, + {"helix": 2, "forward": true, "start": 138, "end": 156}, + {"helix": 1, "forward": false, "start": 142, "end": 156}, + {"helix": 0, "forward": true, "start": 142, "end": 184}, + {"helix": 1, "forward": false, "start": 156, "end": 184}, + {"helix": 2, "forward": true, "start": 156, "end": 180}, + {"helix": 3, "forward": false, "start": 152, "end": 180}, + {"helix": 4, "forward": true, "start": 152, "end": 194}, + {"helix": 3, "forward": false, "start": 180, "end": 194}, + {"helix": 2, "forward": true, "start": 180, "end": 198}, + {"helix": 1, "forward": false, "start": 184, "end": 198}, + {"helix": 0, "forward": true, "start": 184, "end": 226}, + {"helix": 1, "forward": false, "start": 198, "end": 226}, + {"helix": 2, "forward": true, "start": 198, "end": 222}, + {"helix": 3, "forward": false, "start": 194, "end": 222}, + {"helix": 4, "forward": true, "start": 194, "end": 236}, + {"helix": 3, "forward": false, "start": 222, "end": 236}, + {"helix": 2, "forward": true, "start": 222, "end": 240}, + {"helix": 1, "forward": false, "start": 226, "end": 240}, + {"helix": 0, "forward": true, "start": 226, "end": 268}, + {"helix": 1, "forward": false, "start": 240, "end": 268}, + {"helix": 2, "forward": true, "start": 240, "end": 264}, + {"helix": 3, "forward": false, "start": 236, "end": 264}, + {"helix": 4, "forward": true, "start": 236, "end": 278}, + {"helix": 3, "forward": false, "start": 264, "end": 278}, + {"helix": 2, "forward": true, "start": 264, "end": 282}, + {"helix": 1, "forward": false, "start": 268, "end": 282}, + {"helix": 0, "forward": true, "start": 268, "end": 310}, + {"helix": 1, "forward": false, "start": 282, "end": 310}, + {"helix": 2, "forward": true, "start": 282, "end": 306}, + {"helix": 3, "forward": false, "start": 278, "end": 306}, + {"helix": 4, "forward": true, "start": 278, "end": 320}, + {"helix": 3, "forward": false, "start": 306, "end": 320}, + {"helix": 2, "forward": true, "start": 306, "end": 324}, + {"helix": 1, "forward": false, "start": 310, "end": 324}, + {"helix": 0, "forward": true, "start": 310, "end": 352}, + {"helix": 1, "forward": false, "start": 324, "end": 352}, + {"helix": 2, "forward": true, "start": 324, "end": 348}, + {"helix": 3, "forward": false, "start": 320, "end": 348}, + {"helix": 4, "forward": true, "start": 320, "end": 362}, + {"helix": 3, "forward": false, "start": 348, "end": 362}, + {"helix": 2, "forward": true, "start": 348, "end": 366}, + {"helix": 1, "forward": false, "start": 352, "end": 366}, + {"helix": 0, "forward": true, "start": 352, "end": 394}, + {"helix": 1, "forward": false, "start": 366, "end": 394}, + {"helix": 2, "forward": true, "start": 366, "end": 390}, + {"helix": 3, "forward": false, "start": 362, "end": 390}, + {"helix": 4, "forward": true, "start": 362, "end": 404}, + {"helix": 3, "forward": false, "start": 390, "end": 404}, + {"helix": 2, "forward": true, "start": 390, "end": 408}, + {"helix": 1, "forward": false, "start": 394, "end": 408}, + {"helix": 0, "forward": true, "start": 394, "end": 436}, + {"helix": 1, "forward": false, "start": 408, "end": 436}, + {"helix": 2, "forward": true, "start": 408, "end": 432}, + {"helix": 3, "forward": false, "start": 404, "end": 432}, + {"helix": 4, "forward": true, "start": 404, "end": 446}, + {"helix": 3, "forward": false, "start": 432, "end": 446}, + {"helix": 2, "forward": true, "start": 432, "end": 450}, + {"helix": 1, "forward": false, "start": 436, "end": 450}, + {"helix": 0, "forward": true, "start": 436, "end": 478}, + {"helix": 1, "forward": false, "start": 450, "end": 478}, + {"helix": 2, "forward": true, "start": 450, "end": 474}, + {"helix": 3, "forward": false, "start": 446, "end": 474}, + {"helix": 4, "forward": true, "start": 446, "end": 488}, + {"helix": 3, "forward": false, "start": 474, "end": 488}, + {"helix": 2, "forward": true, "start": 474, "end": 492}, + {"helix": 1, "forward": false, "start": 478, "end": 492}, + {"helix": 0, "forward": true, "start": 478, "end": 520}, + {"helix": 1, "forward": false, "start": 492, "end": 520}, + {"helix": 2, "forward": true, "start": 492, "end": 516}, + {"helix": 3, "forward": false, "start": 488, "end": 516}, + {"helix": 4, "forward": true, "start": 488, "end": 530}, + {"helix": 3, "forward": false, "start": 516, "end": 530}, + {"helix": 2, "forward": true, "start": 516, "end": 534}, + {"helix": 1, "forward": false, "start": 520, "end": 534}, + {"helix": 0, "forward": true, "start": 520, "end": 562}, + {"helix": 1, "forward": false, "start": 534, "end": 562}, + {"helix": 2, "forward": true, "start": 534, "end": 558}, + {"helix": 3, "forward": false, "start": 530, "end": 558}, + {"helix": 4, "forward": true, "start": 530, "end": 572}, + {"helix": 3, "forward": false, "start": 558, "end": 572}, + {"helix": 2, "forward": true, "start": 558, "end": 576}, + {"helix": 1, "forward": false, "start": 562, "end": 576}, + {"helix": 0, "forward": true, "start": 562, "end": 604}, + {"helix": 1, "forward": false, "start": 576, "end": 604}, + {"helix": 2, "forward": true, "start": 576, "end": 600}, + {"helix": 3, "forward": false, "start": 572, "end": 600}, + {"helix": 4, "forward": true, "start": 572, "end": 614}, + {"helix": 3, "forward": false, "start": 600, "end": 614}, + {"helix": 2, "forward": true, "start": 600, "end": 618}, + {"helix": 1, "forward": false, "start": 604, "end": 618}, + {"helix": 0, "forward": true, "start": 604, "end": 646}, + {"helix": 1, "forward": false, "start": 618, "end": 646}, + {"helix": 2, "forward": true, "start": 618, "end": 642}, + {"helix": 3, "forward": false, "start": 614, "end": 642}, + {"helix": 4, "forward": true, "start": 614, "end": 656}, + {"helix": 3, "forward": false, "start": 642, "end": 656}, + {"helix": 2, "forward": true, "start": 642, "end": 660}, + {"helix": 1, "forward": false, "start": 646, "end": 660}, + {"helix": 0, "forward": true, "start": 646, "end": 688}, + {"helix": 1, "forward": false, "start": 660, "end": 688}, + {"helix": 2, "forward": true, "start": 660, "end": 684}, + {"helix": 3, "forward": false, "start": 656, "end": 684}, + {"helix": 4, "forward": true, "start": 656, "end": 698}, + {"helix": 3, "forward": false, "start": 684, "end": 698}, + {"helix": 2, "forward": true, "start": 684, "end": 702}, + {"helix": 1, "forward": false, "start": 688, "end": 702}, + {"helix": 0, "forward": true, "start": 688, "end": 730}, + {"helix": 1, "forward": false, "start": 702, "end": 730}, + {"helix": 2, "forward": true, "start": 702, "end": 726}, + {"helix": 3, "forward": false, "start": 698, "end": 726}, + {"helix": 4, "forward": true, "start": 698, "end": 740}, + {"helix": 3, "forward": false, "start": 726, "end": 740}, + {"helix": 2, "forward": true, "start": 726, "end": 744}, + {"helix": 1, "forward": false, "start": 730, "end": 744}, + {"helix": 0, "forward": true, "start": 730, "end": 772}, + {"helix": 1, "forward": false, "start": 744, "end": 772}, + {"helix": 2, "forward": true, "start": 744, "end": 768}, + {"helix": 3, "forward": false, "start": 740, "end": 768}, + {"helix": 4, "forward": true, "start": 740, "end": 782}, + {"helix": 3, "forward": false, "start": 768, "end": 782}, + {"helix": 2, "forward": true, "start": 768, "end": 786}, + {"helix": 1, "forward": false, "start": 772, "end": 786}, + {"helix": 0, "forward": true, "start": 772, "end": 814}, + {"helix": 1, "forward": false, "start": 786, "end": 814}, + {"helix": 2, "forward": true, "start": 786, "end": 810}, + {"helix": 3, "forward": false, "start": 782, "end": 810}, + {"helix": 4, "forward": true, "start": 782, "end": 824}, + {"helix": 3, "forward": false, "start": 810, "end": 824}, + {"helix": 2, "forward": true, "start": 810, "end": 828}, + {"helix": 1, "forward": false, "start": 814, "end": 828}, + {"helix": 0, "forward": true, "start": 814, "end": 856}, + {"helix": 1, "forward": false, "start": 828, "end": 856}, + {"helix": 2, "forward": true, "start": 828, "end": 852}, + {"helix": 3, "forward": false, "start": 824, "end": 852}, + {"helix": 4, "forward": true, "start": 824, "end": 866}, + {"helix": 3, "forward": false, "start": 852, "end": 866}, + {"helix": 2, "forward": true, "start": 852, "end": 870}, + {"helix": 1, "forward": false, "start": 856, "end": 870}, + {"helix": 0, "forward": true, "start": 856, "end": 898}, + {"helix": 1, "forward": false, "start": 870, "end": 898}, + {"helix": 2, "forward": true, "start": 870, "end": 894}, + {"helix": 3, "forward": false, "start": 866, "end": 894}, + {"helix": 4, "forward": true, "start": 866, "end": 908}, + {"helix": 3, "forward": false, "start": 894, "end": 908}, + {"helix": 2, "forward": true, "start": 894, "end": 912}, + {"helix": 1, "forward": false, "start": 898, "end": 912}, + {"helix": 0, "forward": true, "start": 898, "end": 940}, + {"helix": 1, "forward": false, "start": 912, "end": 940}, + {"helix": 2, "forward": true, "start": 912, "end": 936}, + {"helix": 3, "forward": false, "start": 908, "end": 936}, + {"helix": 4, "forward": true, "start": 908, "end": 950}, + {"helix": 3, "forward": false, "start": 936, "end": 950}, + {"helix": 2, "forward": true, "start": 936, "end": 954}, + {"helix": 1, "forward": false, "start": 940, "end": 954}, + {"helix": 0, "forward": true, "start": 940, "end": 982}, + {"helix": 1, "forward": false, "start": 954, "end": 982}, + {"helix": 2, "forward": true, "start": 954, "end": 978}, + {"helix": 3, "forward": false, "start": 950, "end": 978}, + {"helix": 4, "forward": true, "start": 950, "end": 992}, + {"helix": 3, "forward": false, "start": 978, "end": 992}, + {"helix": 2, "forward": true, "start": 978, "end": 996}, + {"helix": 1, "forward": false, "start": 982, "end": 996}, + {"helix": 0, "forward": true, "start": 982, "end": 1024}, + {"helix": 1, "forward": false, "start": 996, "end": 1024}, + {"helix": 2, "forward": true, "start": 996, "end": 1020}, + {"helix": 3, "forward": false, "start": 992, "end": 1020}, + {"helix": 4, "forward": true, "start": 992, "end": 1034}, + {"helix": 3, "forward": false, "start": 1020, "end": 1034}, + {"helix": 2, "forward": true, "start": 1020, "end": 1038}, + {"helix": 1, "forward": false, "start": 1024, "end": 1038}, + {"helix": 0, "forward": true, "start": 1024, "end": 1066}, + {"helix": 1, "forward": false, "start": 1038, "end": 1066}, + {"helix": 2, "forward": true, "start": 1038, "end": 1062}, + {"helix": 3, "forward": false, "start": 1034, "end": 1062}, + {"helix": 4, "forward": true, "start": 1034, "end": 1076}, + {"helix": 3, "forward": false, "start": 1062, "end": 1076}, + {"helix": 2, "forward": true, "start": 1062, "end": 1080}, + {"helix": 1, "forward": false, "start": 1066, "end": 1080}, + {"helix": 0, "forward": true, "start": 1066, "end": 1108}, + {"helix": 1, "forward": false, "start": 1080, "end": 1108}, + {"helix": 2, "forward": true, "start": 1080, "end": 1104}, + {"helix": 3, "forward": false, "start": 1076, "end": 1104}, + {"helix": 4, "forward": true, "start": 1076, "end": 1118}, + {"helix": 3, "forward": false, "start": 1104, "end": 1118}, + {"helix": 2, "forward": true, "start": 1104, "end": 1122}, + {"helix": 1, "forward": false, "start": 1108, "end": 1122}, + {"helix": 0, "forward": true, "start": 1108, "end": 1150}, + {"helix": 1, "forward": false, "start": 1122, "end": 1150}, + {"helix": 2, "forward": true, "start": 1122, "end": 1146}, + {"helix": 3, "forward": false, "start": 1118, "end": 1146}, + {"helix": 4, "forward": true, "start": 1118, "end": 1160}, + {"helix": 3, "forward": false, "start": 1146, "end": 1160}, + {"helix": 2, "forward": true, "start": 1146, "end": 1164}, + {"helix": 1, "forward": false, "start": 1150, "end": 1164}, + {"helix": 0, "forward": true, "start": 1150, "end": 1192}, + {"helix": 1, "forward": false, "start": 1164, "end": 1192}, + {"helix": 2, "forward": true, "start": 1164, "end": 1188}, + {"helix": 3, "forward": false, "start": 1160, "end": 1188}, + {"helix": 4, "forward": true, "start": 1160, "end": 1202}, + {"helix": 3, "forward": false, "start": 1188, "end": 1202}, + {"helix": 2, "forward": true, "start": 1188, "end": 1206}, + {"helix": 1, "forward": false, "start": 1192, "end": 1206}, + {"helix": 0, "forward": true, "start": 1192, "end": 1234}, + {"helix": 1, "forward": false, "start": 1206, "end": 1234}, + {"helix": 2, "forward": true, "start": 1206, "end": 1230}, + {"helix": 3, "forward": false, "start": 1202, "end": 1230}, + {"helix": 4, "forward": true, "start": 1202, "end": 1244}, + {"helix": 3, "forward": false, "start": 1230, "end": 1244}, + {"helix": 2, "forward": true, "start": 1230, "end": 1248}, + {"helix": 1, "forward": false, "start": 1234, "end": 1248}, + {"helix": 0, "forward": true, "start": 1234, "end": 1276}, + {"helix": 1, "forward": false, "start": 1248, "end": 1276}, + {"helix": 2, "forward": true, "start": 1248, "end": 1272}, + {"helix": 3, "forward": false, "start": 1244, "end": 1272}, + {"helix": 4, "forward": true, "start": 1244, "end": 1279}, + {"helix": 5, "forward": false, "start": 399, "end": 1279} + ] + } + ] +} \ No newline at end of file From 3753e36022364d91fc9f4d6b55e739ee00f4234c Mon Sep 17 00:00:00 2001 From: Cosmo Date: Thu, 10 Sep 2020 13:53:19 +0100 Subject: [PATCH 15/17] Correct type annotation --- scadnano/scadnano.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 24632248..0e80c9fc 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -4133,7 +4133,7 @@ def _cadnano_v2_place_strand(self, strand: Strand, dct: dict, self._cadnano_v2_place_crossover(which_helix, next_helix, domain, next_domain, strand_type) - def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int, design_grid: Grid) -> dict: + def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int, design_grid: Grid) -> Dict[int, int]:: """Creates blank cadnanov2 helices in and initialized all their fields. """ helices_ids_reverse = {} From 627d45b993c9ebbbecd9aeaa493dafbadf2102ca Mon Sep 17 00:00:00 2001 From: Cosmo Date: Thu, 10 Sep 2020 16:18:35 +0100 Subject: [PATCH 16/17] Correcting syntax error --- scadnano/scadnano.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 0e80c9fc..9f5bfd1a 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -4133,7 +4133,7 @@ def _cadnano_v2_place_strand(self, strand: Strand, dct: dict, self._cadnano_v2_place_crossover(which_helix, next_helix, domain, next_domain, strand_type) - def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int, design_grid: Grid) -> Dict[int, int]:: + def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int, design_grid: Grid) -> Dict[int, int]: """Creates blank cadnanov2 helices in and initialized all their fields. """ helices_ids_reverse = {} From db82d47fd3fdb7f208afe78d978a30a11217a083 Mon Sep 17 00:00:00 2001 From: David Doty Date: Thu, 10 Sep 2020 15:26:45 -0700 Subject: [PATCH 17/17] bumped version --- scadnano/scadnano.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 9f5bfd1a..f3911efb 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -44,7 +44,7 @@ # commented out for now to support Py3.6, which does not support this feature # from __future__ import annotations -__version__ = "0.12.0" # version line; WARNING: do not remove or change this line or comment +__version__ = "0.12.1" # version line; WARNING: do not remove or change this line or comment import dataclasses from abc import abstractmethod, ABC