From 312e761f77007718e8dd0150906e012b30b9cd63 Mon Sep 17 00:00:00 2001 From: Benjamin Lee Date: Fri, 2 Apr 2021 13:14:32 -0700 Subject: [PATCH 01/18] Implemented oxdna export, but still need to add unit tests --- scadnano/scadnano.py | 387 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 385 insertions(+), 2 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index cd4cb996..f428af56 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -69,6 +69,9 @@ from collections import defaultdict, OrderedDict, Counter import sys import os.path +from math import sqrt, sin, cos, pi +from random import randint + default_scadnano_file_extension = 'sc' """Default filename extension when writing a scadnano file.""" @@ -5629,13 +5632,41 @@ def _write_plates_default(self, directory: str, filename: Optional[str], strands workbook.save(filename_plate) def to_oxdna_format(self) -> Tuple[str, str]: - raise NotImplementedError() + """ Exports to oxdna format. + + :return: two strings that are the contents of the *.conf and *.top file + suitable for reading by oxdna (https://sulcgroup.github.io/oxdna-viewer/) + """ + system = _oxdna_convert(self) + return system.ox_dna_output() + + def write_oxdna_files(self, directory: str = '.', filename_no_extension: Optional[str] = None): + """Write text file representing this :any:`Design`, + suitable for reading by oxdna (https://sulcgroup.github.io/oxdna-viewer/), + with the output files having the same name as the running script but with ``.py`` changed to + ``.conf`` and ``.top``, + unless `filename_no_extension` is explicitly specified. + For instance, if the script is named ``my_origami.py``, + then the design will be written to ``my_origami.conf`` and ``my_origami.top``. + + The strings written are those returned by :meth:`Design.to_oxdna_format`. + + :param directory: + directory in which to put file (default: current working directory) + :param filename_no_extension: + filename without extension (default: name of script without ``.py``). + """ + conf, top = self.to_oxdna_format() + + _write_file_same_name_as_running_python_script(conf, 'conf', directory, filename_no_extension) + _write_file_same_name_as_running_python_script(top, 'top', directory, filename_no_extension) + # @_docstring_parameter was used to substitute sc in for the filename extension, but it is # incompatible with .. code-block:: and caused a very strange and hard-to-determine error, # so I removed it. # @_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: Optional[str] = None, extension: Optional[str] = None, suppress_indent: bool = True) -> None: """Write text file representing this :any:`Design`, suitable for reading by the scadnano web interface, @@ -6332,3 +6363,355 @@ def _create_directory_and_set_filename(directory: str, filename: str) -> str: os.makedirs(directory) relative_filename = os.path.join(directory, filename) return relative_filename + +@dataclass(frozen=True) +class _OxdnaVector: + x: float = 0 + y: float = 0 + z: float = 0 + + def dot(self, other: "_OxdnaVector") -> float: + return self.x * other.x + self.y * other.y + self.z * other.z + + def cross(self, other: "_OxdnaVector") -> "_OxdnaVector": + x = self.y * other.z - self.z * other.y + y = self.z * other.x - self.x * other.z + z = self.x * other.y - self.y * other.x + return _OxdnaVector(x, y, z) + + def length(self) -> float: + return sqrt(self.x * self.x + self.y * self.y + self.z * self.z) + + def normalize(self) -> "_OxdnaVector": + length = self.length() + return _OxdnaVector(self.x / length, self.y / length, self.z / length) + + # returns a vector containing the min x, y, and z coords from both self and other + def coord_min(self, other: "_OxdnaVector") -> "_OxdnaVector": + return _OxdnaVector(min(self.x, other.x), min(self.y, other.y), min(self.z, other.z)) + + def coord_max(self, other: "_OxdnaVector") -> "_OxdnaVector": + return _OxdnaVector(max(self.x, other.x), max(self.y, other.y), max(self.z, other.z)) + + def __add__(self, other: "_OxdnaVector") -> "_OxdnaVector": + return _OxdnaVector(self.x + other.x, self.y + other.y, self.z + other.z) + + def __sub__(self, other: "_OxdnaVector") -> "_OxdnaVector": + return _OxdnaVector(self.x - other.x, self.y - other.y, self.z - other.z) + + def __mul__(self, scalar: float) -> "_OxdnaVector": + return _OxdnaVector(self.x * scalar, self.y * scalar, self.z * scalar) + + def __rmul__(self, scalar: float) -> "_OxdnaVector": + return _OxdnaVector(self.x * scalar, self.y * scalar, self.z * scalar) + + def __neg__(self) -> "_OxdnaVector": + return _OxdnaVector(-self.x, -self.y, -self.z) + + def __str__(self) -> str: + return '({}, {}, {})'.format(self.x, self.y, self.z) + + def __repr__(self) -> str: + return '_OxdnaVector({}, {}, {})'.format(self.x, self.y, self.z) + + # counterclockwise rotation around axis + def rotate(self, angle: float, axis: "_OxdnaVector") -> "_OxdnaVector": + u = axis.normalize() + c = cos(angle * pi / 180) + s = sin(angle * pi / 180) + + return u * self.dot(u) + (c * u.cross(self)).cross(u) - s * u.cross(self) + +# Constants related to oxdna export +_GROOVE_GAMMA = 20 +_BASE_DIST = 0.6 +_OXDNA_ORIGIN = _OxdnaVector(0, 0, 0) + +# r, b, and n represent the oxDNA conf file vectors that describe a nucleotide +# r is the position of the base +# b is the backbone-base vector +# n is the forward direction of the helix +@dataclass(frozen=True) +class _OxdnaNucleotide: + center: _OxdnaVector + normal: _OxdnaVector + forward: _OxdnaVector + base: str + + v: _OxdnaVector = field(init=False, default=_OXDNA_ORIGIN) # velocity for oxDNA conf file + L: _OxdnaVector = field(init=False, default=_OXDNA_ORIGIN) # angular velocity for oxDNA conf file + + @property + def b(self) -> _OxdnaVector: + return -self.normal.rotate(-_GROOVE_GAMMA, self.forward).normalize() + + @property + def r(self) -> _OxdnaVector: + return self.center - self.b * _BASE_DIST + + @property + def n(self) -> _OxdnaVector: + return self.forward + +@dataclass(frozen=True) +class _OxdnaStrand: + nucleotides: List[_OxdnaNucleotide] = field(default_factory=list) + + def join(self, other: "_OxdnaStrand") -> "_OxdnaStrand": + return _OxdnaStrand(self.nucleotides + other.nucleotides) + + +# represents an oxDNA system and contains a list of strands +# from its list of strands it can compute the oxDNA conf and top files +@dataclass(frozen=True) +class _OxdnaSystem: + strands: List[_OxdnaStrand] = field(default_factory=list) + + def compute_bounding_box(self) -> _OxdnaVector: + min_vec = None + max_vec = None + + for strand in self.strands: + for nuc in strand.nucleotides: + if min_vec is None: + min_vec = nuc.center + max_vec = nuc.center + else: + min_vec = min_vec.coord_min(nuc.center) + max_vec = max_vec.coord_max(nuc.center) + + if min_vec is not None: + return max_vec - min_vec + _OxdnaVector(5, 5, 5) + else: + return _OxdnaVector(1, 1, 1) + + def ox_dna_output(self) -> Tuple[str, str]: + + bbox = self.compute_bounding_box() + + conf = 't = {}\nb = {} {} {}\nE = {} {} {}\n'.format(0, bbox.x, bbox.y, bbox.z, 0, 0, 0) + topo = '' + + nuc_count = 0 + strand_count = 0 + + topo_list = [] + conf_list = [] + + for strand in self.strands: + strand_count += 1 + + nuc_index = 0 + for nuc in strand.nucleotides: + n5 = nuc_count - 1 + n3 = nuc_count + 1 + nuc_count += 1 + + if nuc_index == 0: + n5 = -1 + if nuc_index == len(strand.nucleotides) - 1: + n3 = -1 + nuc_index += 1 + + topo_list.append('{} {} {} {}'.format(strand_count, nuc.base, n3, n5)) + conf_list.append('{} {} {} '.format(nuc.r.x, nuc.r.y, nuc.r.z) + + '{} {} {} '.format(nuc.b.x, nuc.b.y, nuc.b.z) + + '{} {} {} '.format(nuc.n.x, nuc.n.y, nuc.n.z) + + '{} {} {} '.format(nuc.v.x, nuc.v.y, nuc.v.z) + + '{} {} {}'.format(nuc.L.x, nuc.L.y, nuc.L.z)) + + topo = '\n'.join(topo_list) + '\n' + conf = '\n'.join(conf_list) + '\n' + + topo = '{} {}\n'.format(nuc_count, strand_count) + topo + + return conf, topo + + +DIST_HEX = 2.55 # distance between helices on hex grid +DIST_SQUARE = 2.60 # distance between helices on square grid + + +# returns the origin, forward, and normal vectors of a helix + +def _oxdna_get_helix_vectors(design: Design, helix: Helix, grid: Grid) -> Tuple[_OxdnaVector, _OxdnaVector, _OxdnaVector]: + """ + TODO: document functions/methods with docstrings + :param helix: + :param grid: + :return: return tuple (origin, forward, normal) + origin -- the starting point of the center of a helix, assumed to be at offset 0 + forward -- the direction in which the helix propagates + normal -- a direction perpendicular to forward which represents the angle to the backbone at offset 0 + for the forward Domain on the Helix. + """ + forward = _OxdnaVector(0, 0, 1) + normal = _OxdnaVector(0, -1, 0) + + forward = forward.rotate(design.yaw_of_helix(helix), normal) + forward = forward.rotate(-design.pitch_of_helix(helix), _OxdnaVector(1, 0, 0)) + normal = normal.rotate(-design.pitch_of_helix(helix), _OxdnaVector(1, 0, 0)) + normal = normal.rotate(helix.roll, forward) + + x = 0 + y = 0 + z = 0 + if grid == Grid.none: + if helix.position is not None: + x = helix.position.x + y = helix.position.y + z = helix.position.z + else: + # see here: + # https://github.com/UC-Davis-molecular-computing/scadnano/blob/master/lib/src/util.dart#L799 + # https://github.com/UC-Davis-molecular-computing/scadnano/blob/master/lib/src/util.dart#L664 + # https://github.com/UC-Davis-molecular-computing/scadnano/blob/master/lib/src/util.dart#L706 + if grid == Grid.square: + h, v = helix.grid_position + x = h * DIST_SQUARE + y = v * DIST_SQUARE + if grid == Grid.hex: + h, v = helix.grid_position + x = (h + (v % 2) / 2) * DIST_HEX + y = v * sqrt(3) / 2 * DIST_HEX + if grid == Grid.honeycomb: + h, v = helix.grid_position + x = h * sqrt(3) / 2 * DIST_HEX + if h % 2 == 0: + y = (v * 3 + (v % 2)) / 2 * DIST_HEX + else: + y = (v * 3 - (v % 2) + 1) / 2 * DIST_HEX + + origin = _OxdnaVector(x, y, z) + + + return origin, forward, normal + +# if no sequence exists on a domain, generate one +def _oxdna_random_sequence(length: int) -> str: + bases = 'ACGT' + seq = '' + for i in range(length): + seq += bases[randint(0, 3)] + return seq + + +STEP_SIZE = 0.4 +STEP_ROT = -720 / 21 +MINOR_GROOVE = 150 + + +def _oxdna_convert(design: Design) -> _OxdnaSystem: + system = _OxdnaSystem() + + # each entry is the number of insertions - deletions since the start of a given helix + mod_map = {} + for idx, helix in design.helices.items(): + mod_map[idx] = [0] * (helix.max_offset - helix.min_offset) + + # insert each insertion / deletion as a postive / negative number + # TODO: report error if there is an insertion/deletion on one Domain and not the other + for strand in design.strands: + for domain in strand.domains: + if isinstance(domain, Domain): + helix = design.helices[domain.helix] + for pos, num, in domain.insertions: + mod_map[domain.helix][pos - helix.min_offset] = num + for pos in domain.deletions: + mod_map[domain.helix][pos - helix.min_offset] = -1 + + # propagate the modifier so it stays consistent accross domains + for idx, helix in design.helices.items(): + for offset in range(helix.min_offset + 1, helix.max_offset): + mod_map[idx][offset] += mod_map[idx][offset - 1] + + for strand in design.strands: + dom_strands: List[Tuple[_OxdnaStrand, bool]] = [] + for domain in strand.domains: + dom_strand = _OxdnaStrand() + # TODO: report error if DNA not assigned + seq = domain.dna_sequence() or _oxdna_random_sequence(domain.dna_length()) + + # handle normal domains + if isinstance(domain, Domain): + helix = design.helices[domain.helix] + origin, forward, normal = _oxdna_get_helix_vectors(design, helix, design.grid) + + if not domain.forward: + normal = normal.rotate(-MINOR_GROOVE, forward) + seq = seq[::-1] + + # dict / set for insertions / deletions to make lookup easier and cheaper when there are lots of them + insertions = {} + for pos, num in domain.insertions: + insertions[pos] = num + + deletions = set() + for pos in domain.deletions: + deletions.add(pos) + + # use Design.geometry field to figure out various distances + # https://github.com/UC-Davis-molecular-computing/scadnano/blob/master/lib/src/state/geometry.dart + + # index is used for finding the base in our sequence + # it doesn't increment on deletions so we don't skip bases + index = 0 + for offset in range(domain.start, domain.end): + if offset not in deletions: + mod = mod_map[domain.helix][offset - helix.min_offset] + + r = origin + forward * offset * STEP_SIZE + b = normal.rotate(STEP_ROT * (offset + mod), forward) + nuc = _OxdnaNucleotide(r, b, forward, seq[index]) + dom_strand.nucleotides.append(nuc) + index += 1 + + if offset in insertions: + num = insertions[offset] + for i in range(1, num + 1): + # offsets are positioned tightly in a single slice of the helix + r = origin + forward * (offset + i / (num + 1)) * STEP_SIZE + b = normal.rotate(STEP_ROT * (offset + mod + i), forward) + nuc = _OxdnaNucleotide(r, b, forward, seq[index]) + dom_strand.nucleotides.append(nuc) + index += 1 + # strands are stored from 5' to 3' end + if not domain.forward: + dom_strand.nucleotides.reverse() + dom_strands.append((dom_strand, False)) + # because we need to know the positions of nucleotides before and after the loopout + # we temporarily store domain strands with a boolean that is true if it's a loopout + # handle loopouts + else: + # we place the loopout nucleotides at temporary nonsense positions and orientations + # these will be updated later, for now we just need the base + for i in range(domain.length): + base = seq[i] + nuc = _OxdnaNucleotide(_OxdnaVector(), _OxdnaVector(0, -1, 0), _OxdnaVector(0, 0, 1), base) + dom_strand.nucleotides.append(nuc) + dom_strands.append((dom_strand, True)) + + sstrand = _OxdnaStrand() + # process loopouts and join strands + for i in range(len(dom_strands)): + dstrand, is_loopout = dom_strands[i] + if is_loopout: + prev_nuc = dom_strands[i - 1][0].nucleotides[-1] + next_nuc = dom_strands[i + 1][0].nucleotides[0] + + strand_length = len(dstrand.nucleotides) + + # now we position loopouts relative to the previous and next strand + # for now we use a linear interpolation + forward = next_nuc.center - prev_nuc.center + normal = (prev_nuc.forward + next_nuc.forward) * 0.5 + + for loopout_idx in range(strand_length): + pos = prev_nuc.center + forward * ((loopout_idx + 1) / (strand_length + 1)) + old_nuc = dstrand.nucleotides[loopout_idx] + new_nuc = _OxdnaNucleotide(pos, normal, forward.normalize(), old_nuc.base) + dstrand.nucleotides[loopout_idx] = new_nuc + + sstrand = sstrand.join(dstrand) + system.strands.append(sstrand) + return system \ No newline at end of file From 37d4795a30dc1150b00ffbdfd2d824c2f37efc8e Mon Sep 17 00:00:00 2001 From: David Doty Date: Thu, 20 May 2021 08:18:15 -0700 Subject: [PATCH 02/18] fixed bug where headers of conf file where not written (untested) --- scadnano/scadnano.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index f428af56..74301e87 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -5640,7 +5640,7 @@ def to_oxdna_format(self) -> Tuple[str, str]: system = _oxdna_convert(self) return system.ox_dna_output() - def write_oxdna_files(self, directory: str = '.', filename_no_extension: Optional[str] = None): + def write_oxdna_files(self, directory: str = '.', filename_no_extension: Optional[str] = None) -> None: """Write text file representing this :any:`Design`, suitable for reading by oxdna (https://sulcgroup.github.io/oxdna-viewer/), with the output files having the same name as the running script but with ``.py`` changed to @@ -6480,7 +6480,7 @@ def compute_bounding_box(self) -> _OxdnaVector: min_vec = min_vec.coord_min(nuc.center) max_vec = max_vec.coord_max(nuc.center) - if min_vec is not None: + if min_vec is not None and max_vec is not None: return max_vec - min_vec + _OxdnaVector(5, 5, 5) else: return _OxdnaVector(1, 1, 1) @@ -6489,15 +6489,12 @@ def ox_dna_output(self) -> Tuple[str, str]: bbox = self.compute_bounding_box() - conf = 't = {}\nb = {} {} {}\nE = {} {} {}\n'.format(0, bbox.x, bbox.y, bbox.z, 0, 0, 0) - topo = '' + conf_list = ['t = {}\nb = {} {} {}\nE = {} {} {}\n'.format(0, bbox.x, bbox.y, bbox.z, 0, 0, 0)] + topo_list = [''] nuc_count = 0 strand_count = 0 - topo_list = [] - conf_list = [] - for strand in self.strands: strand_count += 1 From a77e6952be6074c40a94d132f2abebd7be8c3997 Mon Sep 17 00:00:00 2001 From: Daniel Hader Date: Thu, 20 May 2021 13:00:26 -0500 Subject: [PATCH 03/18] Bug fix and cleanup of oxdna conversion functions and very basic unit test case --- scadnano/scadnano.py | 93 +++++++++++++++----------------- tests/scadnano_tests.py | 117 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 49 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 74301e87..a4fb06e3 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -6481,6 +6481,7 @@ def compute_bounding_box(self) -> _OxdnaVector: max_vec = max_vec.coord_max(nuc.center) if min_vec is not None and max_vec is not None: + # 5 is arbitrarily chosen so that the box has a bit of wiggle room return max_vec - min_vec + _OxdnaVector(5, 5, 5) else: return _OxdnaVector(1, 1, 1) @@ -6489,8 +6490,8 @@ def ox_dna_output(self) -> Tuple[str, str]: bbox = self.compute_bounding_box() - conf_list = ['t = {}\nb = {} {} {}\nE = {} {} {}\n'.format(0, bbox.x, bbox.y, bbox.z, 0, 0, 0)] - topo_list = [''] + dat_list = ['t = {}\nb = {} {} {}\nE = {} {} {}'.format(0, bbox.x, bbox.y, bbox.z, 0, 0, 0)] + top_list = [] nuc_count = 0 strand_count = 0 @@ -6510,28 +6511,24 @@ def ox_dna_output(self) -> Tuple[str, str]: n3 = -1 nuc_index += 1 - topo_list.append('{} {} {} {}'.format(strand_count, nuc.base, n3, n5)) - conf_list.append('{} {} {} '.format(nuc.r.x, nuc.r.y, nuc.r.z) + - '{} {} {} '.format(nuc.b.x, nuc.b.y, nuc.b.z) + - '{} {} {} '.format(nuc.n.x, nuc.n.y, nuc.n.z) + - '{} {} {} '.format(nuc.v.x, nuc.v.y, nuc.v.z) + - '{} {} {}'.format(nuc.L.x, nuc.L.y, nuc.L.z)) + top_list.append('{} {} {} {}'.format(strand_count, nuc.base, n3, n5)) + dat_list.append('{} {} {} '.format(nuc.r.x, nuc.r.y, nuc.r.z) + + '{} {} {} '.format(nuc.b.x, nuc.b.y, nuc.b.z) + + '{} {} {} '.format(nuc.n.x, nuc.n.y, nuc.n.z) + + '{} {} {} '.format(nuc.v.x, nuc.v.y, nuc.v.z) + + '{} {} {}'.format(nuc.L.x, nuc.L.y, nuc.L.z)) - topo = '\n'.join(topo_list) + '\n' - conf = '\n'.join(conf_list) + '\n' + top = '\n'.join(top_list) + '\n' + dat = '\n'.join(dat_list) + '\n' - topo = '{} {}\n'.format(nuc_count, strand_count) + topo + top = '{} {}\n'.format(nuc_count, strand_count) + top - return conf, topo - - -DIST_HEX = 2.55 # distance between helices on hex grid -DIST_SQUARE = 2.60 # distance between helices on square grid + return dat, top +NM_TO_OX_UNITS = 1.0 / 0.8518 # returns the origin, forward, and normal vectors of a helix - -def _oxdna_get_helix_vectors(design: Design, helix: Helix, grid: Grid) -> Tuple[_OxdnaVector, _OxdnaVector, _OxdnaVector]: +def _oxdna_get_helix_vectors(design: Design, helix: Helix) -> Tuple[_OxdnaVector, _OxdnaVector, _OxdnaVector]: """ TODO: document functions/methods with docstrings :param helix: @@ -6542,6 +6539,9 @@ def _oxdna_get_helix_vectors(design: Design, helix: Helix, grid: Grid) -> Tuple[ normal -- a direction perpendicular to forward which represents the angle to the backbone at offset 0 for the forward Domain on the Helix. """ + grid = design.grid + geometry = design.geometry + forward = _OxdnaVector(0, 0, 1) normal = _OxdnaVector(0, -1, 0) @@ -6565,23 +6565,21 @@ def _oxdna_get_helix_vectors(design: Design, helix: Helix, grid: Grid) -> Tuple[ # https://github.com/UC-Davis-molecular-computing/scadnano/blob/master/lib/src/util.dart#L706 if grid == Grid.square: h, v = helix.grid_position - x = h * DIST_SQUARE - y = v * DIST_SQUARE + x = h * geometry.distance_between_helices() + y = v * geometry.distance_between_helices() if grid == Grid.hex: h, v = helix.grid_position - x = (h + (v % 2) / 2) * DIST_HEX - y = v * sqrt(3) / 2 * DIST_HEX + x = (h + (v % 2) / 2) * geometry.distance_between_helices() + y = v * sqrt(3) / 2 * geometry.distance_between_helices() if grid == Grid.honeycomb: h, v = helix.grid_position - x = h * sqrt(3) / 2 * DIST_HEX + x = h * sqrt(3) / 2 * geometry.distance_between_helices() if h % 2 == 0: - y = (v * 3 + (v % 2)) / 2 * DIST_HEX + y = (v * 3 + (v % 2)) / 2 * geometry.distance_between_helices() else: - y = (v * 3 - (v % 2) + 1) / 2 * DIST_HEX - - origin = _OxdnaVector(x, y, z) - + y = (v * 3 - (v % 2) + 1) / 2 * geometry.distance_between_helices() + origin = _OxdnaVector(x, y, z) * NM_TO_OX_UNITS return origin, forward, normal # if no sequence exists on a domain, generate one @@ -6593,14 +6591,11 @@ def _oxdna_random_sequence(length: int) -> str: return seq -STEP_SIZE = 0.4 -STEP_ROT = -720 / 21 -MINOR_GROOVE = 150 - - def _oxdna_convert(design: Design) -> _OxdnaSystem: system = _OxdnaSystem() - + geometry = design.geometry + step_rot = -360 / geometry.bases_per_turn + # each entry is the number of insertions - deletions since the start of a given helix mod_map = {} for idx, helix in design.helices.items(): @@ -6632,10 +6627,10 @@ def _oxdna_convert(design: Design) -> _OxdnaSystem: # handle normal domains if isinstance(domain, Domain): helix = design.helices[domain.helix] - origin, forward, normal = _oxdna_get_helix_vectors(design, helix, design.grid) + origin, forward, normal = _oxdna_get_helix_vectors(design, helix) if not domain.forward: - normal = normal.rotate(-MINOR_GROOVE, forward) + normal = normal.rotate(-geometry.minor_groove_angle, forward) seq = seq[::-1] # dict / set for insertions / deletions to make lookup easier and cheaper when there are lots of them @@ -6651,27 +6646,27 @@ def _oxdna_convert(design: Design) -> _OxdnaSystem: # https://github.com/UC-Davis-molecular-computing/scadnano/blob/master/lib/src/state/geometry.dart # index is used for finding the base in our sequence - # it doesn't increment on deletions so we don't skip bases index = 0 for offset in range(domain.start, domain.end): if offset not in deletions: mod = mod_map[domain.helix][offset - helix.min_offset] - r = origin + forward * offset * STEP_SIZE - b = normal.rotate(STEP_ROT * (offset + mod), forward) - nuc = _OxdnaNucleotide(r, b, forward, seq[index]) - dom_strand.nucleotides.append(nuc) - index += 1 - + # we have to check insertions first because they affect the index if offset in insertions: num = insertions[offset] - for i in range(1, num + 1): - # offsets are positioned tightly in a single slice of the helix - r = origin + forward * (offset + i / (num + 1)) * STEP_SIZE - b = normal.rotate(STEP_ROT * (offset + mod + i), forward) - nuc = _OxdnaNucleotide(r, b, forward, seq[index]) + for i in range(num): + r = origin + forward * (offset + mod - num + i) * geometry.rise_per_base_pair * NM_TO_OX_UNITS + b = normal.rotate(step_rot * (offset + mod - num + i), forward) + nuc = Nucleotide(r, b, forward, seq[index]) dom_strand.nucleotides.append(nuc) index += 1 + + r = origin + forward * (offset + mod) * geometry.rise_per_base_pair * NM_TO_OX_UNITS + b = normal.rotate(step_rot * (offset + mod), forward) + nuc = _OxdnaNucleotide(r, b, forward, seq[index]) + dom_strand.nucleotides.append(nuc) + index += 1 + # strands are stored from 5' to 3' end if not domain.forward: dom_strand.nucleotides.reverse() @@ -6711,4 +6706,4 @@ def _oxdna_convert(design: Design) -> _OxdnaSystem: sstrand = sstrand.join(dstrand) system.strands.append(sstrand) - return system \ No newline at end of file + return system diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index f5810b5f..2e53d2bd 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -6,6 +6,7 @@ import re import json import io +import math from typing import Iterable, Union, Dict, Any import scadnano as sc @@ -5714,3 +5715,119 @@ def test_dna_sequence_in__right_then_left_deletions_and_insertions(self) -> None # self.assertEqual("TTTACG", ss1.dna_sequence_in(6, 10)) # self.assertEqual("TTTTACG", ss1.dna_sequence_in(4, 10)) # self.assertEqual("TTTACGTACGT", ss1.dna_sequence_in(2, 10)) + +class TestOxdnaExport(unittest.TestCase): + def setUp(self) -> None: + self.OX_UNITS_TO_NM = 0.8518 + self.NM_TO_OX_UNITS = 1.0 / self.OX_UNITS_TO_NM + self.OX_BASE_DIST = 0.6 + + def test_basic_design(self) -> None: + helices = [ sc.Helix(max_offset=7), sc.Helix(max_offset=7) ] + domain1 = sc.Domain(0, True, 0, 7) + domain2 = sc.Domain(1, False, 0, 7) + domain3 = sc.Domain(0, False, 0, 7) + domain4 = sc.Domain(1, True, 0, 7) + strand1 = sc.Strand([domain1, domain2], is_scaffold=True) + strand2 = sc.Strand([domain3, domain4], is_scaffold=False) + + design = sc.Design(helices=helices, strands=[strand1, strand2], grid=sc.square) + geometry = design.geometry + + helix_angle = math.pi * 2 / geometry.bases_per_turn + + # expected values for verification + expected_num_nucleotides = 7 * 4 + expected_strand_length = 7 * 2 + # expected distance squared between adjacent nucleotide centers of mass + expected_adj_nuc_cm_dist2 = (2 * self.OX_BASE_DIST * math.sin(helix_angle / 2))**2 + (geometry.rise_per_base_pair * self.NM_TO_OX_UNITS)**2 + + dat, top = design.to_oxdna_format() + dat = dat.strip().split('\n') + top = top.strip().split('\n') + + # check length of output files matches num nucleotides plus header size + self.assertEqual(len(dat), expected_num_nucleotides + 3) + self.assertEqual(len(top), expected_num_nucleotides + 1) + + #find relevant values for nucleotides + cm_poss = [] + nbrs_3p = [] + nbrs_5p = [] + + for line in dat[3:]: + data = line.strip().split() + # make sure there are 15 values per line + self.assertEqual(len(data), 15) + + cm_poss.append(tuple([float(x) for x in data[0:3]])) + bb_vec = tuple([float(x) for x in data[3:6]]) + nm_vec = tuple([float(x) for x in data[6:9]]) + + # make sure normal vectors and backbone vectors are unit length + self.assertAlmostEqual(sum([x**2 for x in bb_vec]), 1.0) + self.assertAlmostEqual(sum([x**2 for x in nm_vec]), 1.0) + + for value in data[9:]: + self.assertAlmostEqual(float(value), 0) + + strand1_idxs = [] + strand2_idxs = [] + for nuc_idx, line in enumerate(top[1:]): + data = line.strip().split() + # make sure there are 4 values per line + self.assertEqual(len(data), 4) + + # make sure there are only 2 strands + self.assertIn(int(data[0]), [1, 2]) + # make sure base is valid + self.assertIn(data[1], ['A', 'C', 'G', 'T']) + + nbrs_3p.append(int(data[2])) + nbrs_5p.append(int(data[3])) + + if int(data[3]) == -1: + if int(data[0]) == 1: + strand1_idxs.append(nuc_idx) + else: + strand2_idxs.append(nuc_idx) + + # reconstruct strands using indices from oxDNA files + next_idx = nbrs_3p[strand1_idxs[0]] + while next_idx >= 0: + strand1_idxs.append(next_idx) + next_idx = nbrs_3p[strand1_idxs[-1]] + + + next_idx = nbrs_3p[strand2_idxs[0]] + while next_idx >= 0: + strand2_idxs.append(next_idx) + next_idx = nbrs_3p[strand2_idxs[-1]] + + # assert that our strands are the correct size + self.assertEqual(len(strand1_idxs), expected_strand_length) + self.assertEqual(len(strand2_idxs), expected_strand_length) + + for i in range(expected_strand_length - 1): + # ignore nucleotides between domains + if i == 6: + continue + + strand1_nuc_idx1 = strand1_idxs[i] + strand1_nuc_idx2 = strand1_idxs[i+1] + strand2_nuc_idx1 = strand1_idxs[i] + strand2_nuc_idx2 = strand1_idxs[i+1] + + s1_cmp1 = cm_poss[strand1_nuc_idx1] + s1_cmp2 = cm_poss[strand1_nuc_idx2] + s2_cmp1 = cm_poss[strand2_nuc_idx1] + s2_cmp2 = cm_poss[strand2_nuc_idx2] + + diff1 = tuple([s1_cmp1[j]-s1_cmp2[j] for j in range(3)]) + diff2 = tuple([s2_cmp1[j]-s2_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x**2 for x in diff1]) + sqr_dist2 = sum([x**2 for x in diff2]) + + # verify squared distance between adjacent nucleotides in a domain + self.assertAlmostEqual(sqr_dist1, expected_adj_nuc_cm_dist2) + self.assertAlmostEqual(sqr_dist2, expected_adj_nuc_cm_dist2) From d59c22d1ae4ce21f1e051b27791e0500f6e8387e Mon Sep 17 00:00:00 2001 From: David Doty Date: Wed, 26 May 2021 09:36:09 -0700 Subject: [PATCH 04/18] added note to top of README that relative links won't work on PyPI site --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 12daf9f3..fa6cc7fc 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ If you find scadnano useful in a scientific project, please cite its associated DNA 2020: *Proceedings of the 26th International Conference on DNA Computing and Molecular Programming* [ [paper](https://doi.org/10.4230/LIPIcs.DNA.2020.9) | [BibTeX](https://web.cs.ucdavis.edu/~doty/papers/scadnano.bib) ] +*Note:* If you are reading this on the PyPI website, some of the links below won't work. Many are relative links intended to be read on the [GitHub README page](https://github.com/UC-Davis-molecular-computing/scadnano-python-package#readme). + ## Table of contents From 72137fd8c3bded6d9f9be5b2bd1ebc7ed2b332a3 Mon Sep 17 00:00:00 2001 From: Anelise Cho <25128798+anelisecho@users.noreply.github.com> Date: Mon, 31 May 2021 22:48:07 -0700 Subject: [PATCH 05/18] Added Oxdna unit tests and additional comments --- tests/_jb_unittest_runner.sc | 24 + tests/scadnano_tests.py | 352 ++- ...est_16_helix_origami_rectangle_no_twist.sc | 1878 +++++++++++++++++ ...pe_2_helix_origami_deletions_insertions.sc | 36 + ..._stape_2_helix_origami_extremely_simple.sc | 17 + ...tape_2_helix_origami_extremely_simple_2.sc | 18 + .../test_6_helix_origami_rectangle.sc | 282 +++ .../cadnano_v2_export/test_circular_strand.sc | 18 + .../test_export_design_with_helix_group.sc | 20 + ...t_design_with_helix_group_not_same_grid.sc | 20 + 10 files changed, 2622 insertions(+), 43 deletions(-) create mode 100644 tests/_jb_unittest_runner.sc create mode 100644 tests/tests_inputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.sc create mode 100644 tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_deletions_insertions.sc create mode 100644 tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple.sc create mode 100644 tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple_2.sc create mode 100644 tests/tests_inputs/cadnano_v2_export/test_6_helix_origami_rectangle.sc create mode 100644 tests/tests_inputs/cadnano_v2_export/test_circular_strand.sc create mode 100644 tests/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group.sc create mode 100644 tests/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group_not_same_grid.sc diff --git a/tests/_jb_unittest_runner.sc b/tests/_jb_unittest_runner.sc new file mode 100644 index 00000000..bb0ad37c --- /dev/null +++ b/tests/_jb_unittest_runner.sc @@ -0,0 +1,24 @@ +{ + "version": "0.16.0", + "grid": "square", + "helices": [ + {"grid_position": [0, 0]}, + {"grid_position": [0, 1]} + ], + "strands": [ + { + "color": "#f74308", + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 7}, + {"helix": 1, "forward": false, "start": 0, "end": 7} + ] + }, + { + "color": "#57bb00", + "domains": [ + {"helix": 0, "forward": false, "start": 0, "end": 7}, + {"helix": 1, "forward": true, "start": 0, "end": 7} + ] + } + ] +} \ No newline at end of file diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index 2e53d2bd..7a0b3106 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -5723,15 +5723,18 @@ def setUp(self) -> None: self.OX_BASE_DIST = 0.6 def test_basic_design(self) -> None: - helices = [ sc.Helix(max_offset=7), sc.Helix(max_offset=7) ] - domain1 = sc.Domain(0, True, 0, 7) - domain2 = sc.Domain(1, False, 0, 7) - domain3 = sc.Domain(0, False, 0, 7) - domain4 = sc.Domain(1, True, 0, 7) - strand1 = sc.Strand([domain1, domain2], is_scaffold=True) - strand2 = sc.Strand([domain3, domain4], is_scaffold=False) - - design = sc.Design(helices=helices, strands=[strand1, strand2], grid=sc.square) + """ 2 double strands of length 7 connected across helices. + 0 7 + helix 0 [------+ + +------] + | | + helix 1 <------+ + +------> + """ + helices = [sc.Helix(max_offset=7), sc.Helix(max_offset=7)] + design = sc.Design(helices=helices, grid=sc.square) + design.strand(0, 0).to(7).cross(1).move(-7) + design.strand(0, 7).move(-7).cross(1).move(7) geometry = design.geometry helix_angle = math.pi * 2 / geometry.bases_per_turn @@ -5739,74 +5742,82 @@ def test_basic_design(self) -> None: # expected values for verification expected_num_nucleotides = 7 * 4 expected_strand_length = 7 * 2 - # expected distance squared between adjacent nucleotide centers of mass + # square of expected distance between adjacent nucleotide centers of mass expected_adj_nuc_cm_dist2 = (2 * self.OX_BASE_DIST * math.sin(helix_angle / 2))**2 + (geometry.rise_per_base_pair * self.NM_TO_OX_UNITS)**2 dat, top = design.to_oxdna_format() dat = dat.strip().split('\n') top = top.strip().split('\n') - # check length of output files matches num nucleotides plus header size - self.assertEqual(len(dat), expected_num_nucleotides + 3) - self.assertEqual(len(top), expected_num_nucleotides + 1) + # check length of output files are as expected (matches # of nucleotides plus header size) + self.assertEqual(expected_num_nucleotides + 3, len(dat)) + self.assertEqual(expected_num_nucleotides + 1, len(top)) - #find relevant values for nucleotides - cm_poss = [] + # find relevant values for nucleotides + cm_poss = [] # center of mass position nbrs_3p = [] nbrs_5p = [] for line in dat[3:]: data = line.strip().split() - # make sure there are 15 values per line - self.assertEqual(len(data), 15) + # make sure there are 15 values per line (3 values per vector * 5 vectors per line) + # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity + self.assertEqual(15, len(data)) cm_poss.append(tuple([float(x) for x in data[0:3]])) - bb_vec = tuple([float(x) for x in data[3:6]]) - nm_vec = tuple([float(x) for x in data[6:9]]) + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector # make sure normal vectors and backbone vectors are unit length - self.assertAlmostEqual(sum([x**2 for x in bb_vec]), 1.0) - self.assertAlmostEqual(sum([x**2 for x in nm_vec]), 1.0) + sqr_bb_vec = sum([x**2 for x in bb_vec]) + sqr_nm_vec = sum([x**2 for x in nm_vec]) + self.assertAlmostEqual(1.0, sqr_bb_vec) + self.assertAlmostEqual(1.0, sqr_nm_vec) - for value in data[9:]: - self.assertAlmostEqual(float(value), 0) + for value in data[9:]: # values for velocity and angular velocity vectors are 0 + self.assertAlmostEqual(0, float(value)) strand1_idxs = [] strand2_idxs = [] for nuc_idx, line in enumerate(top[1:]): data = line.strip().split() - # make sure there are 4 values per line - self.assertEqual(len(data), 4) + # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor + self.assertEqual(4, len(data)) # make sure there are only 2 strands - self.assertIn(int(data[0]), [1, 2]) + strand_num = int(data[0]) + self.assertIn(strand_num, [1, 2]) # make sure base is valid - self.assertIn(data[1], ['A', 'C', 'G', 'T']) + base = data[1] + self.assertIn(base, ['A', 'C', 'G', 'T']) nbrs_3p.append(int(data[2])) nbrs_5p.append(int(data[3])) - if int(data[3]) == -1: - if int(data[0]) == 1: - strand1_idxs.append(nuc_idx) + # append start of strand (no 5' neighbor) to list of indexes for strand + neighbor_5 = int(data[3]) + if neighbor_5 == -1: + if strand_num == 1: + strand1_start = nuc_idx + strand1_idxs.append(strand1_start) else: - strand2_idxs.append(nuc_idx) + strand2_start = nuc_idx + strand2_idxs.append(strand2_start) # reconstruct strands using indices from oxDNA files - next_idx = nbrs_3p[strand1_idxs[0]] + next_idx = nbrs_3p[strand1_start] while next_idx >= 0: strand1_idxs.append(next_idx) next_idx = nbrs_3p[strand1_idxs[-1]] - - next_idx = nbrs_3p[strand2_idxs[0]] + next_idx = nbrs_3p[strand2_start] while next_idx >= 0: strand2_idxs.append(next_idx) next_idx = nbrs_3p[strand2_idxs[-1]] - - # assert that our strands are the correct size - self.assertEqual(len(strand1_idxs), expected_strand_length) - self.assertEqual(len(strand2_idxs), expected_strand_length) + + # assert that strands are the correct length + self.assertEqual(expected_strand_length, len(strand1_idxs)) + self.assertEqual(expected_strand_length, len(strand2_idxs)) for i in range(expected_strand_length - 1): # ignore nucleotides between domains @@ -5815,19 +5826,274 @@ def test_basic_design(self) -> None: strand1_nuc_idx1 = strand1_idxs[i] strand1_nuc_idx2 = strand1_idxs[i+1] - strand2_nuc_idx1 = strand1_idxs[i] - strand2_nuc_idx2 = strand1_idxs[i+1] + strand2_nuc_idx1 = strand2_idxs[i] + strand2_nuc_idx2 = strand2_idxs[i+1] + # find the center of mass for adjacent nucleotides s1_cmp1 = cm_poss[strand1_nuc_idx1] s1_cmp2 = cm_poss[strand1_nuc_idx2] s2_cmp1 = cm_poss[strand2_nuc_idx1] s2_cmp2 = cm_poss[strand2_nuc_idx2] + # calculate and verify squared distance between adjacent nucleotides in a domain diff1 = tuple([s1_cmp1[j]-s1_cmp2[j] for j in range(3)]) diff2 = tuple([s2_cmp1[j]-s2_cmp2[j] for j in range(3)]) sqr_dist1 = sum([x**2 for x in diff1]) sqr_dist2 = sum([x**2 for x in diff2]) - # verify squared distance between adjacent nucleotides in a domain - self.assertAlmostEqual(sqr_dist1, expected_adj_nuc_cm_dist2) - self.assertAlmostEqual(sqr_dist2, expected_adj_nuc_cm_dist2) + self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist1) + self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist2) + + def test_honeycomb_design(self) -> None: + """ A single strand on a honeycomb grid. + 0 8 + helix 0 [-------+ + | + helix 1 +-------- + | + helix 2 +-------> + """ + helices = [sc.Helix(grid_position=(1, 1), max_offset=8), sc.Helix(grid_position=(0, 1), max_offset=8), sc.Helix(grid_position=(0, 2), max_offset=8)] + design = sc.Design(helices=helices, grid=sc.honeycomb) + design.strand(0, 0).to(8).cross(1).move(-8).cross(2).to(8) + geometry = design.geometry + + helix_angle = math.pi * 2 / geometry.bases_per_turn + + # expected values for verification + expected_num_nucleotides = 8 * 3 + expected_strand_length = 8 * 3 + # square of expected distance between adjacent nucleotide centers of mass + expected_adj_nuc_cm_dist2 = (2 * self.OX_BASE_DIST * math.sin(helix_angle / 2)) ** 2 + (geometry.rise_per_base_pair * self.NM_TO_OX_UNITS) ** 2 + + dat, top = design.to_oxdna_format() + dat = dat.strip().split('\n') + top = top.strip().split('\n') + + # check length of output files are as expected (matches # of nucleotides plus header size) + self.assertEqual(expected_num_nucleotides + 3, len(dat)) + self.assertEqual(expected_num_nucleotides + 1, len(top)) + + # find relevant values for nucleotides + cm_poss = [] # center of mass position + nbrs_3p = [] + nbrs_5p = [] + + for line in dat[3:]: + data = line.strip().split() + # make sure there are 15 values per line (3 values per vector * 5 vectors per line) + # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity + self.assertEqual(15, len(data)) + + cm_poss.append(tuple([float(x) for x in data[0:3]])) + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + + # make sure normal vectors and backbone vectors are unit length + sqr_bb_vec = sum([x ** 2 for x in bb_vec]) + sqr_nm_vec = sum([x ** 2 for x in nm_vec]) + self.assertAlmostEqual(1.0, sqr_bb_vec) + self.assertAlmostEqual(1.0, sqr_nm_vec) + + for value in data[9:]: # values for velocity and angular velocity vectors are 0 + self.assertAlmostEqual(0, float(value)) + + strand1_idxs = [] + for nuc_idx, line in enumerate(top[1:]): + data = line.strip().split() + # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor + self.assertEqual(4, len(data)) + + # make sure there's only 1 strand + strand_num = int(data[0]) + self.assertEqual(1, strand_num) + # make sure base is valid + base = data[1] + self.assertIn(base, ['A', 'C', 'G', 'T']) + + nbrs_3p.append(int(data[2])) + nbrs_5p.append(int(data[3])) + + # append start of strand (no 5' neighbor) to list of indexes for strand + neighbor_5 = int(data[3]) + if neighbor_5 == -1: + strand1_start = nuc_idx + strand1_idxs.append(strand1_start) + + # reconstruct strand using indices from oxDNA files + next_idx = nbrs_3p[strand1_start] + while next_idx >= 0: + strand1_idxs.append(next_idx) + next_idx = nbrs_3p[strand1_idxs[-1]] + + # assert that strand is correct length + self.assertEqual(expected_strand_length, len(strand1_idxs)) + + for i in range(expected_strand_length - 1): + # ignore nucleotides between domains + if i == 7: + continue + + if i == 15: + continue + + strand1_nuc_idx1 = strand1_idxs[i] + strand1_nuc_idx2 = strand1_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s1_cmp1 = cm_poss[strand1_nuc_idx1] + s1_cmp2 = cm_poss[strand1_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x ** 2 for x in diff1]) + + self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist1) + + def test_loopout_design(self) -> None: + """ 3 strands, one with a loopout + 0 7 13 + ^ loopout at 4 + [------>[------> + <------] + """ + helix = [sc.Helix(max_offset=14)] + design = sc.Design(helices=helix, grid=sc.square) + design.strand(0, 0).to(4).loopout(0, 4).to(7) + design.strand(0, 7).move(-7) + design.strand(0, 7).to(14) + geometry = design.geometry + + helix_angle = math.pi * 2 / geometry.bases_per_turn + + # expected values for verification + expected_num_nucleotides = 7 * 3 + 4 + expected_strand_length = 7 * 1 + # square of expected distance between adjacent nucleotide centers of mass + expected_adj_nuc_cm_dist2 = (2 * self.OX_BASE_DIST * math.sin(helix_angle / 2))**2 + (geometry.rise_per_base_pair * self.NM_TO_OX_UNITS)**2 + + dat, top = design.to_oxdna_format() + dat = dat.strip().split('\n') + top = top.strip().split('\n') + + # check length of output files are as expected (matches # of nucleotides plus header size) + self.assertEqual(expected_num_nucleotides + 3, len(dat)) + self.assertEqual(expected_num_nucleotides + 1, len(top)) + + # find relevant values for nucleotides + cm_poss = [] # center of mass position + nbrs_3p = [] + nbrs_5p = [] + + for line in dat[3:]: + data = line.strip().split() + # make sure there are 15 values per line (3 values per vector * 5 vectors per line) + # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity + self.assertEqual(15, len(data)) + + cm_poss.append(tuple([float(x) for x in data[0:3]])) + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + + # make sure normal vectors and backbone vectors are unit length + sqr_bb_vec = sum([x**2 for x in bb_vec]) + sqr_nm_vec = sum([x**2 for x in nm_vec]) + self.assertAlmostEqual(1.0, sqr_bb_vec) + self.assertAlmostEqual(1.0, sqr_nm_vec) + + for value in data[9:]: # values for velocity and angular velocity vectors are 0 + self.assertAlmostEqual(0, float(value)) + + strand1_idxs = [] + strand2_idxs = [] + strand3_idxs = [] + for nuc_idx, line in enumerate(top[1:]): + data = line.strip().split() + # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor + self.assertEqual(4, len(data)) + + # make sure there are only 3 strands + strand_num = int(data[0]) + self.assertIn(strand_num, [1, 2, 3]) + # make sure base is valid + base = data[1] + self.assertIn(base, ['A', 'C', 'G', 'T']) + + nbrs_3p.append(int(data[2])) + nbrs_5p.append(int(data[3])) + + # append start of strand (no 5' neighbor) to list of indexes for strand + neighbor_5 = int(data[3]) + if neighbor_5 == -1: + if strand_num == 1: + strand1_start = nuc_idx + strand1_idxs.append(strand1_start) + elif strand_num == 2: + strand2_start = nuc_idx + strand2_idxs.append(strand2_start) + else: + strand3_start = nuc_idx + strand3_idxs.append(strand3_start) + + # reconstruct strands using indices from oxDNA files + next_idx = nbrs_3p[strand1_start] + while next_idx >= 0: + strand1_idxs.append(next_idx) + next_idx = nbrs_3p[strand1_idxs[-1]] + + next_idx = nbrs_3p[strand2_start] + while next_idx >= 0: + strand2_idxs.append(next_idx) + next_idx = nbrs_3p[strand2_idxs[-1]] + + next_idx = nbrs_3p[strand3_start] + while next_idx >= 0: + strand3_idxs.append(next_idx) + next_idx = nbrs_3p[strand3_idxs[-1]] + + # assert that strands are the correct length + self.assertEqual(expected_strand_length + 4, len(strand1_idxs)) #1st strand has loopout of 4 + self.assertEqual(expected_strand_length, len(strand2_idxs)) + self.assertEqual(expected_strand_length, len(strand3_idxs)) + + for i in range(expected_strand_length + 4 - 1): + # ignore nucleotides between domains + if i == 4: + continue + + if i == 8: + continue + + strand1_nuc_idx1 = strand1_idxs[i] + strand1_nuc_idx2 = strand1_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s1_cmp1 = cm_poss[strand1_nuc_idx1] + s1_cmp2 = cm_poss[strand1_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x ** 2 for x in diff1]) + + #self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist1) #TODO: how to check distances between nucleotides in loopout? + + for i in range(expected_strand_length - 1): + strand2_nuc_idx1 = strand2_idxs[i] + strand2_nuc_idx2 = strand2_idxs[i+1] + strand3_nuc_idx1 = strand3_idxs[i] + strand3_nuc_idx2 = strand3_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s2_cmp1 = cm_poss[strand2_nuc_idx1] + s2_cmp2 = cm_poss[strand2_nuc_idx2] + s3_cmp1 = cm_poss[strand3_nuc_idx1] + s3_cmp2 = cm_poss[strand3_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff2 = tuple([s2_cmp1[j]-s2_cmp2[j] for j in range(3)]) + diff3 = tuple([s3_cmp1[j] - s3_cmp2[j] for j in range(3)]) + sqr_dist2 = sum([x**2 for x in diff2]) + sqr_dist3 = sum([x ** 2 for x in diff3]) + + self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist2) + self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist3) \ No newline at end of file diff --git a/tests/tests_inputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.sc b/tests/tests_inputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.sc new file mode 100644 index 00000000..937aa5a7 --- /dev/null +++ b/tests/tests_inputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.sc @@ -0,0 +1,1878 @@ +{ + "version": "0.16.0", + "grid": "square", + "helices": [ + {"max_offset": 448, "grid_position": [0, 0]}, + {"max_offset": 448, "grid_position": [0, 1]}, + {"max_offset": 448, "grid_position": [0, 2]}, + {"max_offset": 448, "grid_position": [0, 3]}, + {"max_offset": 448, "grid_position": [0, 4]}, + {"max_offset": 448, "grid_position": [0, 5]}, + {"max_offset": 448, "grid_position": [0, 6]}, + {"max_offset": 448, "grid_position": [0, 7]}, + {"max_offset": 448, "grid_position": [0, 8]}, + {"max_offset": 448, "grid_position": [0, 9]}, + {"max_offset": 448, "grid_position": [0, 10]}, + {"max_offset": 448, "grid_position": [0, 11]}, + {"max_offset": 448, "grid_position": [0, 12]}, + {"max_offset": 448, "grid_position": [0, 13]}, + {"max_offset": 448, "grid_position": [0, 14]}, + {"max_offset": 448, "grid_position": [0, 15]} + ], + "strands": [ + { + "color": "#0066cc", + "sequence": "TTCCCTTCCTTTCTCGCCACGTTCGCCGGCTTTCCCCGTCAAGCTCTAAATCGGGGGCTCCCTTTAGGGTTCCGATTTAGTGCTTTACGGCACCTCGACCCCAAAAAACTTGATTTGGGTGATGGTTCACGTAGTGGGCCATCGCCCTGATAGACGGTTTTTCGCCCTTTGACGTTGGAGTCCACGTTCTTTAATAGTGGACTCTTGTTCCAAACTGGAACAACACTCAACCCTATCTCGGGCTATTCTTTTGATTTATAAGGGATTTTGCCGATTTCGGAACCACCATCAAACAGGATTTTCGCCTGCTGGGGCAAACCAGCGTGGACCGCTTGCTGCAACTCTCTCAGGGCCAGGCGGTGAAGGGCAATCAGCTGTTGCCCGTCTCACTGGTGAAAAGAAAAACCACCCTGGCGCCCAATACGCAAACCGCCTCTCCCCGCGCGTTGGCCGATTCATTAATGCAGCTGGCACGACAGGTTTCCCGACTGGAAAGCGGGCAGTGAGCGCAACGCAATTAATGTGAGTTAGCTCACTCATTAGGCACCCCAGGCTTTACACTTTATGCTTCCGGCTCGTATGTTGTGTGGAATTGTGAGCGGATAACAATTTCACACAGGAAACAGCTATGACCATGATTACGAATTCGAGCTCGGTACCCGGGGATCCTCTAGAGTCGACCTGCAGGCATGCAAGCTTGGCACTGGCCGTCGTTTTACAACGTCGTGACTGGGAAAACCCTGGCGTTACCCAACTTAATCGCCTTGCAGCACATCCCCCTTTCGCCAGCTGGCGTAATAGCGAAGAGGCCCGCACCGATCGCCCTTCCCAACAGTTGCGCAGCCTGAATGGCGAATGGCGCTTTGCCTGGTTTCCGGCACCAGAAGCGGTGCCGGAAAGCTGGCTGGAGTGCGATCTTCCTGAGGCCGATACTGTCGTCGTCCCCTCAAACTGGCAGATGCACGGTTACGATGCGCCCATCTACACCAACGTGACCTATCCCATTACGGTCAATCCGCCGTTTGTTCCCACGGAGAATCCGACGGGTTGTTACTCGCTCACATTTAATGTTGATGAAAGCTGGCTACAGGAAGGCCAGACGCGAATTATTTTTGATGGCGTTCCTATTGGTTAAAAAATGAGCTGATTTAACAAAAATTTAATGCGAATTTTAACAAAATATTAACGTTTACAATTTAAATATTTGCTTATACAATCTTCCTGTTTTTGGGGCTTTTCTGATTATCAACCGGGGTACATATGATTGACATGCTAGTTTTACGATTACCGTTCATCGATTCTCTTGTTTGCTCCAGACTCTCAGGCAATGACCTGATAGCCTTTGTAGATCTCTCAAAAATAGCTACCCTCTCCGGCATTAATTTATCAGCTAGAACGGTTGAATATCATATTGATGGTGATTTGACTGTCTCCGGCCTTTCTCACCCTTTTGAATCTTTACCTACACATTACTCAGGCATTGCATTTAAAATATATGAGGGTTCTAAAAATTTTTATCCTTGCGTTGAAATAAAGGCTTCTCCCGCAAAAGTATTACAGGGTCATAATGTTTTTGGTACAACCGATTTAGCTTTATGCTCTGAGGCTTTATTGCTTAATTTTGCTAATTCTTTGCCTTGCCTGTATGATTTATTGGATGTTAATGCTACTACTATTAGTAGAATTGATGCCACCTTTTCAGCTCGCGCCCCAAATGAAAATATAGCTAAACAGGTTATTGACCATTTGCGAAATGTATCTAATGGTCAAACTAAATCTACTCGTTCGCAGAATTGGGAATCAACTGTTATATGGAATGAAACTTCCAGACACCGTACTTTAGTTGCATATTTAAAACATGTTGAGCTACAGCATTATATTCAGCAATTAAGCTCTAAGCCATCCGCAAAAATGACCTCTTATCAAAAGGAGCAATTAAAGGTACTCTCTAATCCTGACCTGTTGGAGTTTGCTTCCGGTCTGGTTCGCTTTGAAGCTCGAATTAAAACGCGATATTTGAAGTCTTTCGGGCTTCCTCTTAATCTTTTTGATGCAATCCGCTTTGCTTCTGACTATAATAGTCAGGGTAAAGACCTGATTTTTGATTTATGGTCATTCTCGTTTTCTGAACTGTTTAAAGCATTTGAGGGGGATTCAATGAATATTTATGACGATTCCGCAGTATTGGACGCTATCCAGTCTAAACATTTTACTATTACCCCCTCTGGCAAAACTTCTTTTGCAAAAGCCTCTCGCTATTTTGGTTTTTATCGTCGTCTGGTAAACGAGGGTTATGATAGTGTTGCTCTTACTATGCCTCGTAATTCCTTTTGGCGTTATGTATCTGCATTAGTTGAATGTGGTATTCCTAAATCTCAACTGATGAATCTTTCTACCTGTAATAATGTTGTTCCGTTAGTTCGTTTTATTAACGTAGATTTTTCTTCCCAACGTCCTGACTGGTATAATGAGCCAGTTCTTAAAATCGCATAAGGTAATTCACAATGATTAAAGTTGAAATTAAACCATCTCAAGCCCAATTTACTACTCGTTCTGGTGTTTCTCGTCAGGGCAAGCCTTATTCACTGAATGAGCAGCTTTGTTACGTTGATTTGGGTAATGAATATCCGGTTCTTGTCAAGATTACTCTTGATGAAGGTCAGCCAGCCTATGCGCCTGGTCTGTACACCGTTCATCTGTCCTCTTTCAAAGTTGGTCAGTTCGGTTCCCTTATGATTGACCGTCTGCGCCTCGTTCCGGCTAAGTAACATGGAGCAGGTCGCGGATTTCGACACAATTTATCAGGCGATGATACAAATCTCCGTTGTACTTTGTTTCGCGCTTGGTATAATCGCTGGGGGTCAAAGATGAGTGTTTTAGTGTATTCTTTTGCCTCTTTCGTTTTAGGTTGGTGCCTTCGTAGTGGCATTACGTATTTTACCCGTTTAATGGAAACTTCCTCATGAAAAAGTCTTTAGTCCTCAAAGCCTCTGTAGCCGTTGCTACCCTCGTTCCGATGCTGTCTTTCGCTGCTGAGGGTGACGATCCCGCAAAAGCGGCCTTTAACTCCCTGCAAGCCTCAGCGACCGAATATATCGGTTATGCGTGGGCGATGGTTGTTGTCATTGTCGGCGCAACTATCGGTATCAAGCTGTTTAAGAAATTCACCTCGAAAGCAAGCTGATAAACCGATACAATTAAAGGCTCCTTTTGGAGCCTTTTTTTTGGAGATTTTCAACGTGAAAAAATTATTATTCGCAATTCCTTTAGTTGTTCCTTTCTATTCTCACTCCGCTGAAACTGTTGAAAGTTGTTTAGCAAAATCCCATACAGAAAATTCATTTACTAACGTCTGGAAAGACGACAAAACTTTAGATCGTTACGCTAACTATGAGGGCTGTCTGTGGAATGCTACAGGCGTTGTAGTTTGTACTGGTGACGAAACTCAGTGTTACGGTACATGGGTTCCTATTGGGCTTGCTATCCCTGAAAATGAGGGTGGTGGCTCTGAGGGTGGCGGTTCTGAGGGTGGCGGTTCTGAGGGTGGCGGTACTAAACCTCCTGAGTACGGTGATACACCTATTCCGGGCTATACTTATATCAACCCTCTCGACGGCACTTATCCGCCTGGTACTGAGCAAAACCCCGCTAATCCTAATCCTTCTCTTGAGGAGTCTCAGCCTCTTAATACTTTCATGTTTCAGAATAATAGGTTCCGAAATAGGCAGGGGGCATTAACTGTTTATACGGGCACTGTTACTCAAGGCACTGACCCCGTTAAAACTTATTACCAGTACACTCCTGTATCATCAAAAGCCATGTATGACGCTTACTGGAACGGTAAATTCAGAGACTGCGCTTTCCATTCTGGCTTTAATGAGGATTTATTTGTTTGTGAATATCAAGGCCAATCGTCTGACCTGCCTCAACCTCCTGTCAATGCTGGCGGCGGCTCTGGTGGTGGTTCTGGTGGCGGCTCTGAGGGTGGTGGCTCTGAGGGTGGCGGTTCTGAGGGTGGCGGCTCTGAGGGAGGCGGTTCCGGTGGTGGCTCTGGTTCCGGTGATTTTGATTATGAAAAGATGGCAAACGCTAATAAGGGGGCTATGACCGAAAATGCCGATGAAAACGCGCTACAGTCTGACGCTAAAGGCAAACTTGATTCTGTCGCTACTGATTACGGTGCTGCTATCGATGGTTTCATTGGTGACGTTTCCGGCCTTGCTAATGGTAATGGTGCTACTGGTGATTTTGCTGGCTCTAATTCCCAAATGGCTCAAGTCGGTGACGGTGATAATTCACCTTTAATGAATAATTTCCGTCAATATTTACCTTCCCTCCCTCAATCGGTTGAATGTCGCCCTTTTGTCTTTGGCGCTGGTAAACCATATGAATTTTCTATTGATTGTGACAAAATAAACTTATTCCGTGGTGTCTTTGCGTTTCTTTTATATGTTGCCACCTTTATGTATGTATTTTCTACGTTTGCTAACATACTGCGTAATAAGGAGTCTTAATCATGCCAGTTCTTTTGGGTATTCCGTTATTATTGCGTTTCCTCGGTTTCCTTCTGGTAACTTTGTTCGGCTATCTGCTTACTTTTCTTAAAAAGGGCTTCGGTAAGATAGCTATTGCTATTTCATTGTTTCTTGCTCTTATTATTGGGCTTAACTCAATTCTTGTGGGTTATCTCTCTGATATTAGCGCTCAATTACCCTCTGACTTTGTTCAGGGTGTTCAGTTAATTCTCCCGTCTAATGCGCTTCCCTGTTTTTATGTTATTCTCTCTGTAAAGGCTGCTATTTTCATTTTTGACGTTAAACAAAAAATCGTTTCTTATTTGGATTGGGATAAATAATATGGCTGTTTATTTTGTAACTGGCAAATTAGGCTCTGGAAAGACGCTCGTTAGCGTTGGTAAGATTCAGGATAAAATTGTAGCTGGGTGCAAAATAGCAACTAATCTTGATTTAAGGCTTCAAAACCTCCCGCAAGTCGGGAGGTTCGCTAAAACGCCTCGCGTTCTTAGAATACCGGATAAGCCTTCTATATCTGATTTGCTTGCTATTGGGCGCGGTAATGATTCCTACGATGAAAATAAAAACGGCTTGCTTGTTCTCGATGAGTGCGGTACTTGGTTTAATACCCGTTCTTGGAATGATAAGGAAAGACAGCCGATTATTGATTGGTTTCTACATGCTCGTAAATTAGGATGGGATATTATTTTTCTTGTTCAGGACTTATCTATTGTTGATAAACAGGCGCGTTCTGCATTAGCTGAACATGTTGTTTATTGTCGTCGTCTGGACAGAATTACTTTACCTTTTGTCGGTACTTTATATTCTCTTATTACTGGCTCGAAAATGCCTCTGCCTAAATTACATGTTGGCGTTGTTAAATATGGCGATTCTCAATTAAGCCCTACTGTTGAGCGTTGGCTTTATACTGGTAAGAATTTGTATAACGCATATGATACTAAACAGGCTTTTTCTAGTAATTATGATTCCGGTGTTTATTCTTATTTAACGCCTTATTTATCACACGGTCGGTATTTCAAACCATTAAATTTAGGTCAGAAGATGAAATTAACTAAAATATATTTGAAAAAGTTTTCTCGCGTTCTTTGTCTTGCGATTGGATTTGCATCAGCATTTACATATAGTTATATAACCCAACCTAAGCCGGAGGTTAAAAAGGTAGTCTCTCAGACCTATGATTTTGATAAATTCACTATTGACTCTTCTCAGCGTCTTAATCTAAGCTATCGCTATGTTTTCAAGGATTCTAAGGGAAAATTAATTAATAGCGACGATTTACAGAAGCAAGGTTATTCACTCACATATATTGATTTATGTACTGTTTCCATTAAAAAAGGTAATTCAAATGAAATTGTTAAATGTAATTAATTTTGTTTTCTTGATGTTTGTTTCATCATCTTCTTTTGCTCAGGTAATTGAAATGAATAATTCGCCTCTGCGCGATTTTGTAACTTGGTATTCAAAGCAATCAGGCGAATCCGTTATTGTTTCTCCCGATGTAAAAGGTACTGTTACTGTATATTCATCTGACGTTAAACCTGAAAATCTACGCAATTTCTTTATTTCTGTTTTACGTGCAAATAATTTTGATATGGTAGGTTCTAACCCTTCCATTATTCAGAAGTATAATCCAAACAATCAGGATTATATTGATGAATTGCCATCATCTGATAATCAGGAATATGATGATAATTCCGCTCCTTCTGGTGGTTTCTTTGTTCCGCAAAATGATAATGTTACTCAAACTTTTAAAATTAATAACGTTCGGGCAAAGGATTTAATACGAGTTGTCGAATTGTTTGTAAAGTCTAATACTTCTAAATCCTCAAATGTATTATCTATTGACGGCTCTAATCTATTAGTTGTTAGTGCTCCTAAAGATATTTTAGATAACCTTCCTCAATTCCTTTCAACTGTTGATTTGCCAACTGACCAGATATTGATTGAGGGTTTGATATTTGAGGTTCAGCAAGGTGA", + "domains": [ + {"helix": 15, "forward": false, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 14, "forward": true, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 13, "forward": false, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 12, "forward": true, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 11, "forward": false, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 10, "forward": true, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 9, "forward": false, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 8, "forward": true, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 7, "forward": false, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 6, "forward": true, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 5, "forward": false, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 4, "forward": true, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 3, "forward": false, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 2, "forward": true, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 1, "forward": false, "start": 16, "end": 224, "deletions": [33, 81, 129, 177]}, + {"helix": 0, "forward": true, "start": 16, "end": 432, "deletions": [33, 81, 129, 177, 225, 273, 321, 369, 417]}, + {"helix": 1, "forward": false, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 2, "forward": true, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 3, "forward": false, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 4, "forward": true, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 5, "forward": false, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 6, "forward": true, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 7, "forward": false, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 8, "forward": true, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 9, "forward": false, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 10, "forward": true, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 11, "forward": false, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 12, "forward": true, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 13, "forward": false, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 14, "forward": true, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]}, + {"helix": 15, "forward": false, "start": 224, "end": 432, "deletions": [225, 273, 321, 369, 417]} + ], + "is_scaffold": true + }, + { + "color": "#f74308", + "sequence": "GCCGCTTTTGCGGGATTTGCAGGGAGTTAAAG", + "domains": [ + {"helix": 1, "forward": true, "start": 16, "end": 32}, + {"helix": 0, "forward": false, "start": 16, "end": 32} + ] + }, + { + "color": "#57bb00", + "sequence": "CAAGAGTAATCTTGACGCTGGCTGACCTTCAT", + "domains": [ + {"helix": 3, "forward": true, "start": 16, "end": 32}, + {"helix": 2, "forward": false, "start": 16, "end": 32} + ] + }, + { + "color": "#888888", + "sequence": "TGCAAAAGAAGTTTTGAATAGCGAGAGGCTTT", + "domains": [ + {"helix": 5, "forward": true, "start": 16, "end": 32}, + {"helix": 4, "forward": false, "start": 16, "end": 32} + ] + }, + { + "color": "#32b86c", + "sequence": "ACGGTGTCTGGAAGTTAATATGCAACTAAAGT", + "domains": [ + {"helix": 7, "forward": true, "start": 16, "end": 32}, + {"helix": 6, "forward": false, "start": 16, "end": 32} + ] + }, + { + "color": "#333333", + "sequence": "AGTCAAATCACCATCAGAGAAAGGCCGGAGAC", + "domains": [ + {"helix": 9, "forward": true, "start": 16, "end": 32}, + {"helix": 8, "forward": false, "start": 16, "end": 32} + ] + }, + { + "color": "#320096", + "sequence": "GGCGGATTGACCGTAACTCCGTGGGAACAAAC", + "domains": [ + {"helix": 11, "forward": true, "start": 16, "end": 32}, + {"helix": 10, "forward": false, "start": 16, "end": 32} + ] + }, + { + "color": "#03b6a2", + "sequence": "AAATTGTTATCCGCTCAGCTGTTTCCTGTGTG", + "domains": [ + {"helix": 13, "forward": true, "start": 16, "end": 32}, + {"helix": 12, "forward": false, "start": 16, "end": 32} + ] + }, + { + "color": "#7300de", + "sequence": "GAGTCCACTATTAAAGTTCCAGTTTGGAACAA", + "domains": [ + {"helix": 15, "forward": true, "start": 16, "end": 32}, + {"helix": 14, "forward": false, "start": 16, "end": 32} + ] + }, + { + "color": "#aaaa00", + "sequence": "AACCCATGTACCGTAAGCAAGCCCAATAGG", + "domains": [ + {"helix": 0, "forward": false, "start": 416, "end": 432, "deletions": [417]}, + {"helix": 1, "forward": true, "start": 416, "end": 432, "deletions": [417]} + ] + }, + { + "color": "#b8056c", + "sequence": "AGCCAGAATGGAAAGATAAATCCTCATTAA", + "domains": [ + {"helix": 2, "forward": false, "start": 416, "end": 432, "deletions": [417]}, + {"helix": 3, "forward": true, "start": 416, "end": 432, "deletions": [417]} + ] + }, + { + "color": "#007200", + "sequence": "ACTTGAGCCATTTGGTTATCACCGTCACCG", + "domains": [ + {"helix": 4, "forward": false, "start": 416, "end": 432, "deletions": [417]}, + {"helix": 5, "forward": true, "start": 416, "end": 432, "deletions": [417]} + ] + }, + { + "color": "#cc0000", + "sequence": "AACCCACAAGAATTGTAATATCAGAGAGAT", + "domains": [ + {"helix": 6, "forward": false, "start": 416, "end": 432, "deletions": [417]}, + {"helix": 7, "forward": true, "start": 416, "end": 432, "deletions": [417]} + ] + }, + { + "color": "#f7931e", + "sequence": "CATCGTAGGAATCATAGCCGTTTTTATTTT", + "domains": [ + {"helix": 8, "forward": false, "start": 416, "end": 432, "deletions": [417]}, + {"helix": 9, "forward": true, "start": 416, "end": 432, "deletions": [417]} + ] + }, + { + "color": "#f74308", + "sequence": "TAATTACTAGAAAAATAAACACCGGAATCA", + "domains": [ + {"helix": 10, "forward": false, "start": 416, "end": 432, "deletions": [417]}, + {"helix": 11, "forward": true, "start": 416, "end": 432, "deletions": [417]} + ] + }, + { + "color": "#57bb00", + "sequence": "TTAATTACATTTAACATCAAGAAAACAAAA", + "domains": [ + {"helix": 12, "forward": false, "start": 416, "end": 432, "deletions": [417]}, + {"helix": 13, "forward": true, "start": 416, "end": 432, "deletions": [417]} + ] + }, + { + "color": "#888888", + "sequence": "CTTTGCCCGAACGTTAACTCGTATTAAATC", + "domains": [ + {"helix": 14, "forward": false, "start": 416, "end": 432, "deletions": [417]}, + {"helix": 15, "forward": true, "start": 416, "end": 432, "deletions": [417]} + ] + }, + { + "color": "#32b86c", + "sequence": "AGAATAGAAAGGAACAACTAAAGGAATTGCG", + "domains": [ + {"helix": 0, "forward": false, "start": 216, "end": 248, "deletions": [225]} + ] + }, + { + "color": "#333333", + "sequence": "TACCAAGCCTCATCTTTGACCCCCTCAAGAG", + "domains": [ + {"helix": 2, "forward": false, "start": 208, "end": 216}, + {"helix": 1, "forward": true, "start": 208, "end": 232, "deletions": [225]} + ] + }, + { + "color": "#320096", + "sequence": "AAGGATTAAGAGGCTGAGACTCCAGCGATTA", + "domains": [ + {"helix": 1, "forward": true, "start": 232, "end": 240}, + {"helix": 2, "forward": false, "start": 216, "end": 240, "deletions": [225]} + ] + }, + { + "color": "#03b6a2", + "sequence": "AATCTACGACCAGTCAGGACGTTGTTCATAA", + "domains": [ + {"helix": 4, "forward": false, "start": 208, "end": 216}, + {"helix": 3, "forward": true, "start": 208, "end": 232, "deletions": [225]} + ] + }, + { + "color": "#7300de", + "sequence": "TCAAAATCAGCGTTTGCCATCTTGGAAGAAA", + "domains": [ + {"helix": 3, "forward": true, "start": 232, "end": 240}, + {"helix": 4, "forward": false, "start": 216, "end": 240, "deletions": [225]} + ] + }, + { + "color": "#aaaa00", + "sequence": "GCCCGAAATTGCATCAAAAAGATTACGTAGA", + "domains": [ + {"helix": 6, "forward": false, "start": 208, "end": 216}, + {"helix": 5, "forward": true, "start": 208, "end": 232, "deletions": [225]} + ] + }, + { + "color": "#b8056c", + "sequence": "AAATACATGCAGTATGTTAGCAAAAGAGGAA", + "domains": [ + {"helix": 5, "forward": true, "start": 232, "end": 240}, + {"helix": 6, "forward": false, "start": 216, "end": 240, "deletions": [225]} + ] + }, + { + "color": "#007200", + "sequence": "CAAAATTACATACAGGCAAGGCAACCTAATT", + "domains": [ + {"helix": 8, "forward": false, "start": 208, "end": 216}, + {"helix": 7, "forward": true, "start": 208, "end": 232, "deletions": [225]} + ] + }, + { + "color": "#cc0000", + "sequence": "TGCCAGTTAGCGTCTTTCCAGAGAGAATTAG", + "domains": [ + {"helix": 7, "forward": true, "start": 232, "end": 240}, + {"helix": 8, "forward": false, "start": 216, "end": 240, "deletions": [225]} + ] + }, + { + "color": "#f7931e", + "sequence": "TTGTATAAAGAAAAGCCCCAAAAAACAATAA", + "domains": [ + {"helix": 10, "forward": false, "start": 208, "end": 216}, + {"helix": 9, "forward": true, "start": 208, "end": 232, "deletions": [225]} + ] + }, + { + "color": "#f74308", + "sequence": "ACAACATGTCTGTCCAGACGACGCAGGAAGA", + "domains": [ + {"helix": 9, "forward": true, "start": 232, "end": 240}, + {"helix": 10, "forward": false, "start": 216, "end": 240, "deletions": [225]} + ] + }, + { + "color": "#57bb00", + "sequence": "CTCTTCGCTTGGGAAGGGCGATCGAGACTAC", + "domains": [ + {"helix": 12, "forward": false, "start": 208, "end": 216}, + {"helix": 11, "forward": true, "start": 208, "end": 232, "deletions": [225]} + ] + }, + { + "color": "#888888", + "sequence": "CTTTTTAAAATCATAGGTCTGAGGTGCGGGC", + "domains": [ + {"helix": 11, "forward": true, "start": 232, "end": 240}, + {"helix": 12, "forward": false, "start": 216, "end": 240, "deletions": [225]} + ] + }, + { + "color": "#32b86c", + "sequence": "CTTTTCACGTATTGGGCGCCAGGGAAACAGA", + "domains": [ + {"helix": 14, "forward": false, "start": 208, "end": 216}, + {"helix": 13, "forward": true, "start": 208, "end": 232, "deletions": [225]} + ] + }, + { + "color": "#333333", + "sequence": "AATAAAGAAATTATTTGCACGTATGGTTTTT", + "domains": [ + {"helix": 13, "forward": true, "start": 232, "end": 240}, + {"helix": 14, "forward": false, "start": 216, "end": 240, "deletions": [225]} + ] + }, + { + "color": "#320096", + "sequence": "GAACGTGGCGAGAAAGGAAGGGAATCACCTT", + "domains": [ + {"helix": 15, "forward": true, "start": 200, "end": 232, "deletions": [225]} + ] + }, + { + "color": "#03b6a2", + "sequence": "CCGATATATTCGGTCGCTGAGGCCGTCACC", + "domains": [ + {"helix": 0, "forward": false, "start": 32, "end": 56, "deletions": [33]}, + {"helix": 1, "forward": true, "start": 32, "end": 40, "deletions": [33]} + ] + }, + { + "color": "#7300de", + "sequence": "CTCAGCAGAGACCAGGCGCATAGAAGAACC", + "domains": [ + {"helix": 1, "forward": true, "start": 40, "end": 48}, + {"helix": 2, "forward": false, "start": 32, "end": 48, "deletions": [33]}, + {"helix": 3, "forward": true, "start": 32, "end": 40, "deletions": [33]} + ] + }, + { + "color": "#aaaa00", + "sequence": "GGATATTCGACGATAAAAACCAACCAGAGG", + "domains": [ + {"helix": 3, "forward": true, "start": 40, "end": 48}, + {"helix": 4, "forward": false, "start": 32, "end": 48, "deletions": [33]}, + {"helix": 5, "forward": true, "start": 32, "end": 40, "deletions": [33]} + ] + }, + { + "color": "#b8056c", + "sequence": "GGGTAATAGCTCAACATGTTTTATCATTCC", + "domains": [ + {"helix": 5, "forward": true, "start": 40, "end": 48}, + {"helix": 6, "forward": false, "start": 32, "end": 48, "deletions": [33]}, + {"helix": 7, "forward": true, "start": 32, "end": 40, "deletions": [33]} + ] + }, + { + "color": "#007200", + "sequence": "ATATAACAAAGATTCAAAAGGGTATATGAT", + "domains": [ + {"helix": 7, "forward": true, "start": 40, "end": 48}, + {"helix": 8, "forward": false, "start": 32, "end": 48, "deletions": [33]}, + {"helix": 9, "forward": true, "start": 32, "end": 40, "deletions": [33]} + ] + }, + { + "color": "#cc0000", + "sequence": "ATTCAACCACAACCCGTCGGATTTGGGATA", + "domains": [ + {"helix": 9, "forward": true, "start": 40, "end": 48}, + {"helix": 10, "forward": false, "start": 32, "end": 48, "deletions": [33]}, + {"helix": 11, "forward": true, "start": 32, "end": 40, "deletions": [33]} + ] + }, + { + "color": "#f7931e", + "sequence": "GGTCACGTCGTAATCATGGTCATACAATTC", + "domains": [ + {"helix": 11, "forward": true, "start": 40, "end": 48}, + {"helix": 12, "forward": false, "start": 32, "end": 48, "deletions": [33]}, + {"helix": 13, "forward": true, "start": 32, "end": 40, "deletions": [33]} + ] + }, + { + "color": "#f74308", + "sequence": "CACACAACTAGGGTTGAGTGTTGAACGTGG", + "domains": [ + {"helix": 13, "forward": true, "start": 40, "end": 48}, + {"helix": 14, "forward": false, "start": 32, "end": 48, "deletions": [33]}, + {"helix": 15, "forward": true, "start": 32, "end": 40, "deletions": [33]} + ] + }, + { + "color": "#57bb00", + "sequence": "ACTCCAACGTCAAAGGGCGAAAAAAAAGAATA", + "domains": [ + {"helix": 15, "forward": true, "start": 40, "end": 64}, + {"helix": 14, "forward": false, "start": 56, "end": 64} + ] + }, + { + "color": "#888888", + "sequence": "CGGTGTACCGAAAGACAGCATCGGACGCATAA", + "domains": [ + {"helix": 2, "forward": false, "start": 48, "end": 56}, + {"helix": 1, "forward": true, "start": 48, "end": 64}, + {"helix": 0, "forward": false, "start": 56, "end": 64} + ] + }, + { + "color": "#32b86c", + "sequence": "TACCAGACATTACCCAAATCAACGCAGATGAA", + "domains": [ + {"helix": 4, "forward": false, "start": 48, "end": 56}, + {"helix": 3, "forward": true, "start": 48, "end": 64}, + {"helix": 2, "forward": false, "start": 56, "end": 64} + ] + }, + { + "color": "#333333", + "sequence": "ATGCTGTAGTAAAATGTTTAGACTCCCTCGTT", + "domains": [ + {"helix": 6, "forward": false, "start": 48, "end": 56}, + {"helix": 5, "forward": true, "start": 48, "end": 64}, + {"helix": 4, "forward": false, "start": 56, "end": 64} + ] + }, + { + "color": "#320096", + "sequence": "TGTAGGTAGTTGATTCCCAATTCTTGAATATA", + "domains": [ + {"helix": 8, "forward": false, "start": 48, "end": 56}, + {"helix": 7, "forward": true, "start": 48, "end": 64}, + {"helix": 6, "forward": false, "start": 56, "end": 64} + ] + }, + { + "color": "#03b6a2", + "sequence": "AGCGAGTAGTTCTAGCTGATAAATGAGTAATG", + "domains": [ + {"helix": 10, "forward": false, "start": 48, "end": 56}, + {"helix": 9, "forward": true, "start": 48, "end": 64}, + {"helix": 8, "forward": false, "start": 56, "end": 64} + ] + }, + { + "color": "#7300de", + "sequence": "CTCGAATTTGGTGTAGATGGGCGCTAAATGTG", + "domains": [ + {"helix": 12, "forward": false, "start": 48, "end": 56}, + {"helix": 11, "forward": true, "start": 48, "end": 64}, + {"helix": 10, "forward": false, "start": 56, "end": 64} + ] + }, + { + "color": "#aaaa00", + "sequence": "GCCCGAGAATACGAGCCGGAAGCAGTACCGAG", + "domains": [ + {"helix": 14, "forward": false, "start": 48, "end": 56}, + {"helix": 13, "forward": true, "start": 48, "end": 64}, + {"helix": 12, "forward": false, "start": 56, "end": 64} + ] + }, + { + "color": "#b8056c", + "sequence": "GACAATGACAACAACCATCGCCCAACGAGGG", + "domains": [ + {"helix": 0, "forward": false, "start": 64, "end": 88, "deletions": [81]}, + {"helix": 1, "forward": true, "start": 64, "end": 72} + ] + }, + { + "color": "#007200", + "sequence": "TAGCAACGCAACTTTGAAAGAGGATAACAAAG", + "domains": [ + {"helix": 1, "forward": true, "start": 72, "end": 80}, + {"helix": 2, "forward": false, "start": 64, "end": 80}, + {"helix": 3, "forward": true, "start": 64, "end": 72} + ] + }, + { + "color": "#cc0000", + "sequence": "CTGCTCATAGCAACACTATCATAAGGATAGCG", + "domains": [ + {"helix": 3, "forward": true, "start": 72, "end": 80}, + {"helix": 4, "forward": false, "start": 64, "end": 80}, + {"helix": 5, "forward": true, "start": 64, "end": 72} + ] + }, + { + "color": "#f7931e", + "sequence": "TCCAATACCTTAGAGCTTAATTGCGCGAACGA", + "domains": [ + {"helix": 5, "forward": true, "start": 72, "end": 80}, + {"helix": 6, "forward": false, "start": 64, "end": 80}, + {"helix": 7, "forward": true, "start": 64, "end": 72} + ] + }, + { + "color": "#f74308", + "sequence": "GTAGATTTTTTAAATGCAATGCCTTAATGCCG", + "domains": [ + {"helix": 7, "forward": true, "start": 72, "end": 80}, + {"helix": 8, "forward": false, "start": 64, "end": 80}, + {"helix": 9, "forward": true, "start": 64, "end": 72} + ] + }, + { + "color": "#57bb00", + "sequence": "GAGAGGGTCAGCTTTCATCAACATATCGTAAC", + "domains": [ + {"helix": 9, "forward": true, "start": 72, "end": 80}, + {"helix": 10, "forward": false, "start": 64, "end": 80}, + {"helix": 11, "forward": true, "start": 64, "end": 72} + ] + }, + { + "color": "#888888", + "sequence": "CGTGCATCTCTAGAGGATCCCCGGTAAAGTGT", + "domains": [ + {"helix": 11, "forward": true, "start": 72, "end": 80}, + {"helix": 12, "forward": false, "start": 64, "end": 80}, + {"helix": 13, "forward": true, "start": 64, "end": 72} + ] + }, + { + "color": "#32b86c", + "sequence": "AAAGCCTGAATCCCTTATAAATCACCGTCTAT", + "domains": [ + {"helix": 13, "forward": true, "start": 72, "end": 80}, + {"helix": 14, "forward": false, "start": 64, "end": 80}, + {"helix": 15, "forward": true, "start": 64, "end": 72} + ] + }, + { + "color": "#333333", + "sequence": "CAGGGCGATGGCCCACTACGTGATTCCGAAA", + "domains": [ + {"helix": 15, "forward": true, "start": 72, "end": 96, "deletions": [81]}, + {"helix": 14, "forward": false, "start": 88, "end": 96} + ] + }, + { + "color": "#320096", + "sequence": "AACTGACGCTACAGAGGCTTTGGTTGCGCC", + "domains": [ + {"helix": 2, "forward": false, "start": 80, "end": 88, "deletions": [81]}, + {"helix": 1, "forward": true, "start": 80, "end": 96, "deletions": [81]}, + {"helix": 0, "forward": false, "start": 88, "end": 96} + ] + }, + { + "color": "#03b6a2", + "sequence": "TAGTAAGTCAGTGAATAAGGCTGGGAACCG", + "domains": [ + {"helix": 4, "forward": false, "start": 80, "end": 88, "deletions": [81]}, + {"helix": 3, "forward": true, "start": 80, "end": 96, "deletions": [81]}, + {"helix": 2, "forward": false, "start": 88, "end": 96} + ] + }, + { + "color": "#7300de", + "sequence": "CGGATGGTGCGGAATCGTCATAACGAGGCA", + "domains": [ + {"helix": 6, "forward": false, "start": 80, "end": 88, "deletions": [81]}, + {"helix": 5, "forward": true, "start": 80, "end": 96, "deletions": [81]}, + {"helix": 4, "forward": false, "start": 88, "end": 96} + ] + }, + { + "color": "#aaaa00", + "sequence": "CATATATAGTTTGACCATTAGACATTTTTG", + "domains": [ + {"helix": 8, "forward": false, "start": 80, "end": 88, "deletions": [81]}, + {"helix": 7, "forward": true, "start": 80, "end": 96, "deletions": [81]}, + {"helix": 6, "forward": false, "start": 88, "end": 96} + ] + }, + { + "color": "#b8056c", + "sequence": "CTGTAGCAGCTATTTTTGAGAGAGAACCCT", + "domains": [ + {"helix": 10, "forward": false, "start": 80, "end": 88, "deletions": [81]}, + {"helix": 9, "forward": true, "start": 80, "end": 96, "deletions": [81]}, + {"helix": 8, "forward": false, "start": 88, "end": 96} + ] + }, + { + "color": "#007200", + "sequence": "GGTCGACTGCCAGTTTGAGGGGTGGCCTTC", + "domains": [ + {"helix": 12, "forward": false, "start": 80, "end": 88, "deletions": [81]}, + {"helix": 11, "forward": true, "start": 80, "end": 96, "deletions": [81]}, + {"helix": 10, "forward": false, "start": 88, "end": 96} + ] + }, + { + "color": "#cc0000", + "sequence": "TCGGCAAGGGTGCCTAATGAGTTGCCTGCA", + "domains": [ + {"helix": 14, "forward": false, "start": 80, "end": 88, "deletions": [81]}, + {"helix": 13, "forward": true, "start": 80, "end": 96, "deletions": [81]}, + {"helix": 12, "forward": false, "start": 88, "end": 96} + ] + }, + { + "color": "#f7931e", + "sequence": "TTCTTAAACAGCTTGATACCGATAAGGACTAA", + "domains": [ + {"helix": 0, "forward": false, "start": 96, "end": 120}, + {"helix": 1, "forward": true, "start": 96, "end": 104} + ] + }, + { + "color": "#f74308", + "sequence": "AGACTTTTAGACGGTCAATCATAATGCCCTGA", + "domains": [ + {"helix": 1, "forward": true, "start": 104, "end": 112}, + {"helix": 2, "forward": false, "start": 96, "end": 112}, + {"helix": 3, "forward": true, "start": 96, "end": 104} + ] + }, + { + "color": "#57bb00", + "sequence": "CGAGAAACAACGCCAAAAGGAATTAATATTCA", + "domains": [ + {"helix": 3, "forward": true, "start": 104, "end": 112}, + {"helix": 4, "forward": false, "start": 96, "end": 112}, + {"helix": 5, "forward": true, "start": 96, "end": 104} + ] + }, + { + "color": "#888888", + "sequence": "TTGAATCCCCTTTTGATAAGAGGTTACATTTC", + "domains": [ + {"helix": 5, "forward": true, "start": 104, "end": 112}, + {"helix": 6, "forward": false, "start": 96, "end": 112}, + {"helix": 7, "forward": true, "start": 96, "end": 104} + ] + }, + { + "color": "#32b86c", + "sequence": "GCAAATGGAAGGATAAAAATTTTTATCTACAA", + "domains": [ + {"helix": 7, "forward": true, "start": 104, "end": 112}, + {"helix": 8, "forward": false, "start": 96, "end": 112}, + {"helix": 9, "forward": true, "start": 96, "end": 104} + ] + }, + { + "color": "#333333", + "sequence": "AGGCTATCAAAAATAATTCGCGTCACGACGAC", + "domains": [ + {"helix": 9, "forward": true, "start": 104, "end": 112}, + {"helix": 10, "forward": false, "start": 96, "end": 112}, + {"helix": 11, "forward": true, "start": 96, "end": 104} + ] + }, + { + "color": "#320096", + "sequence": "AGTATCGGCAGTGCCAAGCTTGCAGAGCTAAC", + "domains": [ + {"helix": 11, "forward": true, "start": 104, "end": 112}, + {"helix": 12, "forward": false, "start": 96, "end": 112}, + {"helix": 13, "forward": true, "start": 96, "end": 104} + ] + }, + { + "color": "#03b6a2", + "sequence": "TCACATTATCCTGTTTGATGGTGGACCATCAC", + "domains": [ + {"helix": 13, "forward": true, "start": 104, "end": 112}, + {"helix": 14, "forward": false, "start": 96, "end": 112}, + {"helix": 15, "forward": true, "start": 96, "end": 104} + ] + }, + { + "color": "#7300de", + "sequence": "CCAAATCAAGTTTTTTGGGGTCGACCCCAGCA", + "domains": [ + {"helix": 15, "forward": true, "start": 104, "end": 128}, + {"helix": 14, "forward": false, "start": 120, "end": 128} + ] + }, + { + "color": "#aaaa00", + "sequence": "CGAGGCGCTCATGAGGAAGTTTCCAGGTGAAT", + "domains": [ + {"helix": 2, "forward": false, "start": 112, "end": 120}, + {"helix": 1, "forward": true, "start": 112, "end": 128}, + {"helix": 0, "forward": false, "start": 120, "end": 128} + ] + }, + { + "color": "#b8056c", + "sequence": "AGATACATACCAGAACGAGTAGTAAGCCGGAA", + "domains": [ + {"helix": 4, "forward": false, "start": 112, "end": 120}, + {"helix": 3, "forward": true, "start": 112, "end": 128}, + {"helix": 2, "forward": false, "start": 120, "end": 128} + ] + }, + { + "color": "#007200", + "sequence": "TAATTGCTCCCTCAAATGCTTTAAACTAATGC", + "domains": [ + {"helix": 6, "forward": false, "start": 112, "end": 120}, + {"helix": 5, "forward": true, "start": 112, "end": 128}, + {"helix": 4, "forward": false, "start": 120, "end": 128} + ] + }, + { + "color": "#cc0000", + "sequence": "TTCAACGCTCAATAACCTGTTTAGAGTACCTT", + "domains": [ + {"helix": 8, "forward": false, "start": 112, "end": 120}, + {"helix": 7, "forward": true, "start": 112, "end": 128}, + {"helix": 6, "forward": false, "start": 120, "end": 128} + ] + }, + { + "color": "#f7931e", + "sequence": "ACGCCATCAGGTCATTGCCTGAGAGCCTTTAT", + "domains": [ + {"helix": 10, "forward": false, "start": 112, "end": 120}, + {"helix": 9, "forward": true, "start": 112, "end": 128}, + {"helix": 8, "forward": false, "start": 120, "end": 128} + ] + }, + { + "color": "#f74308", + "sequence": "ACGACGGCCCTCAGGAAGATCGCACAATAGGA", + "domains": [ + {"helix": 12, "forward": false, "start": 112, "end": 120}, + {"helix": 11, "forward": true, "start": 112, "end": 128}, + {"helix": 10, "forward": false, "start": 120, "end": 128} + ] + }, + { + "color": "#57bb00", + "sequence": "GGCGAAAAATTGCGTTGCGCTCACGTTGTAAA", + "domains": [ + {"helix": 14, "forward": false, "start": 112, "end": 120}, + {"helix": 13, "forward": true, "start": 112, "end": 128}, + {"helix": 12, "forward": false, "start": 120, "end": 128} + ] + }, + { + "color": "#888888", + "sequence": "ATCGGTTTATCAGCTTGCTTTCGATTAAAC", + "domains": [ + {"helix": 0, "forward": false, "start": 128, "end": 152, "deletions": [129]}, + {"helix": 1, "forward": true, "start": 128, "end": 136, "deletions": [129]} + ] + }, + { + "color": "#32b86c", + "sequence": "GGGTAAAATGCTCCATGTTACTTAATTGGG", + "domains": [ + {"helix": 1, "forward": true, "start": 136, "end": 144}, + {"helix": 2, "forward": false, "start": 128, "end": 144, "deletions": [129]}, + {"helix": 3, "forward": true, "start": 128, "end": 136, "deletions": [129]} + ] + }, + { + "color": "#333333", + "sequence": "CTTGAGATGGAATACCACATTCAACAGTTC", + "domains": [ + {"helix": 3, "forward": true, "start": 136, "end": 144}, + {"helix": 4, "forward": false, "start": 128, "end": 144, "deletions": [129]}, + {"helix": 5, "forward": true, "start": 128, "end": 136, "deletions": [129]} + ] + }, + { + "color": "#320096", + "sequence": "AGAAAACGAGGTCAGGATTAGAGCTATATT", + "domains": [ + {"helix": 5, "forward": true, "start": 136, "end": 144}, + {"helix": 6, "forward": false, "start": 128, "end": 144, "deletions": [129]}, + {"helix": 7, "forward": true, "start": 128, "end": 136, "deletions": [129]} + ] + }, + { + "color": "#03b6a2", + "sequence": "TTCATTTGACTTTTGCGGGAGAAGTCTGGA", + "domains": [ + {"helix": 7, "forward": true, "start": 136, "end": 144}, + {"helix": 8, "forward": false, "start": 128, "end": 144, "deletions": [129]}, + {"helix": 9, "forward": true, "start": 128, "end": 136, "deletions": [129]} + ] + }, + { + "color": "#7300de", + "sequence": "GCAAACAAAGCTCATTTTTTAACCTCCAGC", + "domains": [ + {"helix": 9, "forward": true, "start": 136, "end": 144}, + {"helix": 10, "forward": false, "start": 128, "end": 144, "deletions": [129]}, + {"helix": 11, "forward": true, "start": 128, "end": 136, "deletions": [129]} + ] + }, + { + "color": "#aaaa00", + "sequence": "CAGCTTTCTTTCCCAGTCACGACTGCCCGC", + "domains": [ + {"helix": 11, "forward": true, "start": 136, "end": 144}, + {"helix": 12, "forward": false, "start": 128, "end": 144, "deletions": [129]}, + {"helix": 13, "forward": true, "start": 128, "end": 136, "deletions": [129]} + ] + }, + { + "color": "#b8056c", + "sequence": "TTTCCAGTGTCCACGCTGGTTTGGGTGCCG", + "domains": [ + {"helix": 13, "forward": true, "start": 136, "end": 144}, + {"helix": 14, "forward": false, "start": 128, "end": 144, "deletions": [129]}, + {"helix": 15, "forward": true, "start": 128, "end": 136, "deletions": [129]} + ] + }, + { + "color": "#007200", + "sequence": "TAAAGCACTAAATCGGAACCCTAAAGAGTTGC", + "domains": [ + {"helix": 15, "forward": true, "start": 136, "end": 160}, + {"helix": 14, "forward": false, "start": 152, "end": 160} + ] + }, + { + "color": "#cc0000", + "sequence": "CCGCGACCTACGTAATGCCACTACTTAATTGT", + "domains": [ + {"helix": 2, "forward": false, "start": 144, "end": 152}, + {"helix": 1, "forward": true, "start": 144, "end": 160}, + {"helix": 0, "forward": false, "start": 152, "end": 160} + ] + }, + { + "color": "#f7931e", + "sequence": "GAGATTTAGGTTTAATTTCAACTTGTCGAAAT", + "domains": [ + {"helix": 4, "forward": false, "start": 144, "end": 152}, + {"helix": 3, "forward": true, "start": 144, "end": 160}, + {"helix": 2, "forward": false, "start": 152, "end": 160} + ] + }, + { + "color": "#f74308", + "sequence": "ACTCCAACAGAATGACCATAAATCCATCAGTT", + "domains": [ + {"helix": 6, "forward": false, "start": 144, "end": 152}, + {"helix": 5, "forward": true, "start": 144, "end": 160}, + {"helix": 4, "forward": false, "start": 152, "end": 160} + ] + }, + { + "color": "#57bb00", + "sequence": "CCTGTAATGGGCGCGAGCTGAAAAGGAAGCAA", + "domains": [ + {"helix": 8, "forward": false, "start": 144, "end": 152}, + {"helix": 7, "forward": true, "start": 144, "end": 160}, + {"helix": 6, "forward": false, "start": 152, "end": 160} + ] + }, + { + "color": "#888888", + "sequence": "GTTAAATCGAGAATCGATGAACGGATTATGAC", + "domains": [ + {"helix": 10, "forward": false, "start": 144, "end": 152}, + {"helix": 9, "forward": true, "start": 144, "end": 160}, + {"helix": 8, "forward": false, "start": 152, "end": 160} + ] + }, + { + "color": "#32b86c", + "sequence": "GCCAGGGTCGGCACCGCTTCTGGTAAATTTTT", + "domains": [ + {"helix": 12, "forward": false, "start": 144, "end": 152}, + {"helix": 11, "forward": true, "start": 144, "end": 160}, + {"helix": 10, "forward": false, "start": 152, "end": 160} + ] + }, + { + "color": "#333333", + "sequence": "AGCAAGCGCGGGAAACCTGTCGTGTGGGTAAC", + "domains": [ + {"helix": 14, "forward": false, "start": 144, "end": 152}, + {"helix": 13, "forward": true, "start": 144, "end": 160}, + {"helix": 12, "forward": false, "start": 152, "end": 160} + ] + }, + { + "color": "#320096", + "sequence": "AAAAAAGGCTCCAAAAGGAGCCTGAAGGCAC", + "domains": [ + {"helix": 0, "forward": false, "start": 160, "end": 184, "deletions": [177]}, + {"helix": 1, "forward": true, "start": 160, "end": 168} + ] + }, + { + "color": "#03b6a2", + "sequence": "CAACCTAATCGCCTGATAAATTGTTAATCATT", + "domains": [ + {"helix": 1, "forward": true, "start": 168, "end": 176}, + {"helix": 2, "forward": false, "start": 160, "end": 176}, + {"helix": 3, "forward": true, "start": 160, "end": 168} + ] + }, + { + "color": "#7300de", + "sequence": "GTGAATTATACAGGTAGAAAGATTAAAAATCA", + "domains": [ + {"helix": 3, "forward": true, "start": 168, "end": 176}, + {"helix": 4, "forward": false, "start": 160, "end": 176}, + {"helix": 5, "forward": true, "start": 160, "end": 168} + ] + }, + { + "color": "#aaaa00", + "sequence": "GGTCTTTACAAAGCGAACCAGACCGGTGGCAT", + "domains": [ + {"helix": 5, "forward": true, "start": 168, "end": 176}, + {"helix": 6, "forward": false, "start": 160, "end": 176}, + {"helix": 7, "forward": true, "start": 160, "end": 168} + ] + }, + { + "color": "#b8056c", + "sequence": "CAATTCTACGGTTGTACCAAAAACTAATCGTA", + "domains": [ + {"helix": 7, "forward": true, "start": 168, "end": 176}, + {"helix": 8, "forward": false, "start": 160, "end": 176}, + {"helix": 9, "forward": true, "start": 160, "end": 168} + ] + }, + { + "color": "#007200", + "sequence": "AAACTAGCTGTTAAAATTCGCATTGCCGGAAA", + "domains": [ + {"helix": 9, "forward": true, "start": 168, "end": 176}, + {"helix": 10, "forward": false, "start": 160, "end": 176}, + {"helix": 11, "forward": true, "start": 160, "end": 168} + ] + }, + { + "color": "#cc0000", + "sequence": "CCAGGCAATGCAAGGCGATTAAGTCCAGCTGC", + "domains": [ + {"helix": 11, "forward": true, "start": 168, "end": 176}, + {"helix": 12, "forward": false, "start": 160, "end": 176}, + {"helix": 13, "forward": true, "start": 160, "end": 168} + ] + }, + { + "color": "#f7931e", + "sequence": "ATTAATGAACCGCCTGGCCCTGAGAGGGAGCC", + "domains": [ + {"helix": 13, "forward": true, "start": 168, "end": 176}, + {"helix": 14, "forward": false, "start": 160, "end": 176}, + {"helix": 15, "forward": true, "start": 160, "end": 168} + ] + }, + { + "color": "#f74308", + "sequence": "CCCGATTTAGAGCTTGACGGGGAAGCTGATT", + "domains": [ + {"helix": 15, "forward": true, "start": 168, "end": 192, "deletions": [177]}, + {"helix": 14, "forward": false, "start": 184, "end": 192} + ] + }, + { + "color": "#57bb00", + "sequence": "TGTATCAAACGAAAGAGGCAAAATCTCCAA", + "domains": [ + {"helix": 2, "forward": false, "start": 176, "end": 184, "deletions": [177]}, + {"helix": 1, "forward": true, "start": 176, "end": 192, "deletions": [177]}, + {"helix": 0, "forward": false, "start": 184, "end": 192} + ] + }, + { + "color": "#888888", + "sequence": "ACATTATCCTTATGCGATTTTACGGAGATT", + "domains": [ + {"helix": 4, "forward": false, "start": 176, "end": 184, "deletions": [177]}, + {"helix": 3, "forward": true, "start": 176, "end": 192, "deletions": [177]}, + {"helix": 2, "forward": false, "start": 184, "end": 192} + ] + }, + { + "color": "#32b86c", + "sequence": "CGAGCTTCCCTGACTATTATAGACGGAACA", + "domains": [ + {"helix": 6, "forward": false, "start": 176, "end": 184, "deletions": [177]}, + {"helix": 5, "forward": true, "start": 176, "end": 192, "deletions": [177]}, + {"helix": 4, "forward": false, "start": 184, "end": 192} + ] + }, + { + "color": "#333333", + "sequence": "GCTAAATCTAATAGTAGTAGCATTTTAATT", + "domains": [ + {"helix": 8, "forward": false, "start": 176, "end": 184, "deletions": [177]}, + {"helix": 7, "forward": true, "start": 176, "end": 192, "deletions": [177]}, + {"helix": 6, "forward": false, "start": 184, "end": 192} + ] + }, + { + "color": "#320096", + "sequence": "AATATTTATGTCAATCATATGTAGCATAAA", + "domains": [ + {"helix": 10, "forward": false, "start": 176, "end": 184, "deletions": [177]}, + {"helix": 9, "forward": true, "start": 176, "end": 192, "deletions": [177]}, + {"helix": 8, "forward": false, "start": 184, "end": 192} + ] + }, + { + "color": "#03b6a2", + "sequence": "GATGTGCAGCGCCATTCGCCATTAAACGTT", + "domains": [ + {"helix": 12, "forward": false, "start": 176, "end": 184, "deletions": [177]}, + {"helix": 11, "forward": true, "start": 176, "end": 192, "deletions": [177]}, + {"helix": 10, "forward": false, "start": 184, "end": 192} + ] + }, + { + "color": "#7300de", + "sequence": "GCCCTTCATCGGCCAACGCGCGGAAAGGGG", + "domains": [ + {"helix": 14, "forward": false, "start": 176, "end": 184, "deletions": [177]}, + {"helix": 13, "forward": true, "start": 176, "end": 192, "deletions": [177]}, + {"helix": 12, "forward": false, "start": 184, "end": 192} + ] + }, + { + "color": "#aaaa00", + "sequence": "AATAATAATTTTTTCACGTTGAAAAGAATACA", + "domains": [ + {"helix": 0, "forward": false, "start": 192, "end": 216}, + {"helix": 1, "forward": true, "start": 192, "end": 200} + ] + }, + { + "color": "#b8056c", + "sequence": "CTAAAACAGCGAAACAAAGTACAAAGAACTGG", + "domains": [ + {"helix": 1, "forward": true, "start": 200, "end": 208}, + {"helix": 2, "forward": false, "start": 192, "end": 208}, + {"helix": 3, "forward": true, "start": 192, "end": 200} + ] + }, + { + "color": "#007200", + "sequence": "CTCATTATTTAATAAAACGAACTATCAGAAGC", + "domains": [ + {"helix": 3, "forward": true, "start": 200, "end": 208}, + {"helix": 4, "forward": false, "start": 192, "end": 208}, + {"helix": 5, "forward": true, "start": 192, "end": 200} + ] + }, + { + "color": "#cc0000", + "sequence": "AAAGCGGAGACTTCAAATATCGCGTTAACATC", + "domains": [ + {"helix": 5, "forward": true, "start": 200, "end": 208}, + {"helix": 6, "forward": false, "start": 192, "end": 208}, + {"helix": 7, "forward": true, "start": 192, "end": 200} + ] + }, + { + "color": "#f7931e", + "sequence": "CAATAAATAGCAATAAAGCCTCAGACCCCGGT", + "domains": [ + {"helix": 7, "forward": true, "start": 200, "end": 208}, + {"helix": 8, "forward": false, "start": 192, "end": 208}, + {"helix": 9, "forward": true, "start": 192, "end": 200} + ] + }, + { + "color": "#f74308", + "sequence": "TGATAATCGCAAATATTTAAATTGTCAGGCTG", + "domains": [ + {"helix": 9, "forward": true, "start": 200, "end": 208}, + {"helix": 10, "forward": false, "start": 192, "end": 208}, + {"helix": 11, "forward": true, "start": 192, "end": 200} + ] + }, + { + "color": "#57bb00", + "sequence": "CGCAACTGTATTACGCCAGCTGGCGGGAGAGG", + "domains": [ + {"helix": 11, "forward": true, "start": 200, "end": 208}, + {"helix": 12, "forward": false, "start": 192, "end": 208}, + {"helix": 13, "forward": true, "start": 192, "end": 200} + ] + }, + { + "color": "#888888", + "sequence": "CGGTTTGCCAGTGAGACGGGCAACAAGCCGGC", + "domains": [ + {"helix": 13, "forward": true, "start": 200, "end": 208}, + {"helix": 14, "forward": false, "start": 192, "end": 208}, + {"helix": 15, "forward": true, "start": 192, "end": 200} + ] + }, + { + "color": "#32b86c", + "sequence": "GCTGAACCTCAAATATCAAACCCTGAACCTAC", + "domains": [ + {"helix": 15, "forward": true, "start": 232, "end": 256}, + {"helix": 14, "forward": false, "start": 248, "end": 256} + ] + }, + { + "color": "#333333", + "sequence": "AAGTATTAGGATTAGCGGGGTTTTGCGGAGTG", + "domains": [ + {"helix": 2, "forward": false, "start": 240, "end": 248}, + {"helix": 1, "forward": true, "start": 240, "end": 256}, + {"helix": 0, "forward": false, "start": 248, "end": 256} + ] + }, + { + "color": "#320096", + "sequence": "CCCTTATTACCGGAACCAGAGCCAAAACATGA", + "domains": [ + {"helix": 4, "forward": false, "start": 240, "end": 248}, + {"helix": 3, "forward": true, "start": 240, "end": 256}, + {"helix": 2, "forward": false, "start": 248, "end": 256} + ] + }, + { + "color": "#03b6a2", + "sequence": "CTTATTACACATAAAGGTGGCAACTCATAGCC", + "domains": [ + {"helix": 6, "forward": false, "start": 240, "end": 248}, + {"helix": 5, "forward": true, "start": 240, "end": 256}, + {"helix": 4, "forward": false, "start": 248, "end": 256} + ] + }, + { + "color": "#7300de", + "sequence": "CGCTAACGACAAAATAAACAGCCATAAGACTC", + "domains": [ + {"helix": 8, "forward": false, "start": 240, "end": 248}, + {"helix": 7, "forward": true, "start": 240, "end": 256}, + {"helix": 6, "forward": false, "start": 248, "end": 256} + ] + }, + { + "color": "#aaaa00", + "sequence": "AAAGTAATTTCAGCTAATGCAGAACTTACCAA", + "domains": [ + {"helix": 10, "forward": false, "start": 240, "end": 248}, + {"helix": 9, "forward": true, "start": 240, "end": 256}, + {"helix": 8, "forward": false, "start": 248, "end": 256} + ] + }, + { + "color": "#b8056c", + "sequence": "TTTATCAACCTCCGGCTTAGGTTGCAAAAGGT", + "domains": [ + {"helix": 12, "forward": false, "start": 240, "end": 248}, + {"helix": 11, "forward": true, "start": 240, "end": 256}, + {"helix": 10, "forward": false, "start": 248, "end": 256} + ] + }, + { + "color": "#007200", + "sequence": "CATATCAAAATTGCGTAGATTTTCATAGTGAA", + "domains": [ + {"helix": 14, "forward": false, "start": 240, "end": 248}, + {"helix": 13, "forward": true, "start": 240, "end": 256}, + {"helix": 12, "forward": false, "start": 248, "end": 256} + ] + }, + { + "color": "#cc0000", + "sequence": "CTAAACAACTTTCAACAGTTTCAGCTCAGTA", + "domains": [ + {"helix": 0, "forward": false, "start": 256, "end": 280, "deletions": [273]}, + {"helix": 1, "forward": true, "start": 256, "end": 264} + ] + }, + { + "color": "#f7931e", + "sequence": "CCAGGCGGGGAACCTATTATTCTGCCACCGGA", + "domains": [ + {"helix": 1, "forward": true, "start": 264, "end": 272}, + {"helix": 2, "forward": false, "start": 256, "end": 272}, + {"helix": 3, "forward": true, "start": 256, "end": 264} + ] + }, + { + "color": "#f74308", + "sequence": "ACCGCCTCTCATCGGCATTTTCGGATATAAAA", + "domains": [ + {"helix": 3, "forward": true, "start": 264, "end": 272}, + {"helix": 4, "forward": false, "start": 256, "end": 272}, + {"helix": 5, "forward": true, "start": 256, "end": 264} + ] + }, + { + "color": "#57bb00", + "sequence": "GAAACGCAAAAGAACTGGCATGATTATTATTT", + "domains": [ + {"helix": 5, "forward": true, "start": 264, "end": 272}, + {"helix": 6, "forward": false, "start": 256, "end": 272}, + {"helix": 7, "forward": true, "start": 256, "end": 264} + ] + }, + { + "color": "#888888", + "sequence": "ATCCCAATCAATTTTATCCTGAATCGCGCCTG", + "domains": [ + {"helix": 7, "forward": true, "start": 264, "end": 272}, + {"helix": 8, "forward": false, "start": 256, "end": 272}, + {"helix": 9, "forward": true, "start": 256, "end": 264} + ] + }, + { + "color": "#32b86c", + "sequence": "TTTATCAAGAATATAAAGTACCGAGGTTATAT", + "domains": [ + {"helix": 9, "forward": true, "start": 264, "end": 272}, + {"helix": 10, "forward": false, "start": 256, "end": 272}, + {"helix": 11, "forward": true, "start": 256, "end": 264} + ] + }, + { + "color": "#333333", + "sequence": "AACTATATACGCTGAGAAGAGTCAAGGTTTAA", + "domains": [ + {"helix": 11, "forward": true, "start": 264, "end": 272}, + {"helix": 12, "forward": false, "start": 256, "end": 272}, + {"helix": 13, "forward": true, "start": 256, "end": 264} + ] + }, + { + "color": "#320096", + "sequence": "CGTCAGATAATAATGGAAGGGTTACAATCAAT", + "domains": [ + {"helix": 13, "forward": true, "start": 264, "end": 272}, + {"helix": 14, "forward": false, "start": 256, "end": 272}, + {"helix": 15, "forward": true, "start": 256, "end": 264} + ] + }, + { + "color": "#03b6a2", + "sequence": "ATCTGGTCAGTTGGCAAATCAACTGGATTAT", + "domains": [ + {"helix": 15, "forward": true, "start": 264, "end": 288, "deletions": [273]}, + {"helix": 14, "forward": false, "start": 280, "end": 288} + ] + }, + { + "color": "#7300de", + "sequence": "CTATTTCATAAGTGCCGTCGAGGGATTTTG", + "domains": [ + {"helix": 2, "forward": false, "start": 272, "end": 280, "deletions": [273]}, + {"helix": 1, "forward": true, "start": 272, "end": 288, "deletions": [273]}, + {"helix": 0, "forward": false, "start": 280, "end": 288} + ] + }, + { + "color": "#aaaa00", + "sequence": "CGCGTTTCCTCAGAGCCGCCACCCCCCTGC", + "domains": [ + {"helix": 4, "forward": false, "start": 272, "end": 280, "deletions": [273]}, + {"helix": 3, "forward": true, "start": 272, "end": 288, "deletions": [273]}, + {"helix": 2, "forward": false, "start": 280, "end": 288} + ] + }, + { + "color": "#b8056c", + "sequence": "ATACCCAAAGACACCACGGAATGACTGTAG", + "domains": [ + {"helix": 6, "forward": false, "start": 272, "end": 280, "deletions": [273]}, + {"helix": 5, "forward": true, "start": 272, "end": 288, "deletions": [273]}, + {"helix": 4, "forward": false, "start": 280, "end": 288} + ] + }, + { + "color": "#007200", + "sequence": "CCAGCTACCAAATAAGAAACGAATAACGGA", + "domains": [ + {"helix": 8, "forward": false, "start": 272, "end": 280, "deletions": [273]}, + {"helix": 7, "forward": true, "start": 272, "end": 288, "deletions": [273]}, + {"helix": 6, "forward": false, "start": 280, "end": 288} + ] + }, + { + "color": "#cc0000", + "sequence": "AATAAGACAATAGATAAGTCCTTTTTGCAC", + "domains": [ + {"helix": 10, "forward": false, "start": 272, "end": 280, "deletions": [273]}, + {"helix": 9, "forward": true, "start": 272, "end": 288, "deletions": [273]}, + {"helix": 8, "forward": false, "start": 280, "end": 288} + ] + }, + { + "color": "#f7931e", + "sequence": "GATTAAGGTAAATGCTGATGCAGAGCCAGT", + "domains": [ + {"helix": 12, "forward": false, "start": 272, "end": 280, "deletions": [273]}, + {"helix": 11, "forward": true, "start": 272, "end": 288, "deletions": [273]}, + {"helix": 10, "forward": false, "start": 280, "end": 288} + ] + }, + { + "color": "#f74308", + "sequence": "ACTTCTGGAATATACAGTAACAATAGCTTA", + "domains": [ + {"helix": 14, "forward": false, "start": 272, "end": 280, "deletions": [273]}, + {"helix": 13, "forward": true, "start": 272, "end": 288, "deletions": [273]}, + {"helix": 12, "forward": false, "start": 280, "end": 288} + ] + }, + { + "color": "#57bb00", + "sequence": "GTTAGTAAATGAATTTTCTGTATGAGGGTTGA", + "domains": [ + {"helix": 0, "forward": false, "start": 288, "end": 312}, + {"helix": 1, "forward": true, "start": 288, "end": 296} + ] + }, + { + "color": "#888888", + "sequence": "TATAAGTAGTATAAACAGTTAATGCCTCAGAA", + "domains": [ + {"helix": 1, "forward": true, "start": 296, "end": 304}, + {"helix": 2, "forward": false, "start": 288, "end": 304}, + {"helix": 3, "forward": true, "start": 288, "end": 296} + ] + }, + { + "color": "#32b86c", + "sequence": "CCGCCACCTTTGCCTTTAGCGTCAAAGTTTAT", + "domains": [ + {"helix": 3, "forward": true, "start": 296, "end": 304}, + {"helix": 4, "forward": false, "start": 288, "end": 304}, + {"helix": 5, "forward": true, "start": 288, "end": 296} + ] + }, + { + "color": "#333333", + "sequence": "TTTGTCACCCGAGGAAACGCAATATTTTTTGT", + "domains": [ + {"helix": 5, "forward": true, "start": 296, "end": 304}, + {"helix": 6, "forward": false, "start": 288, "end": 304}, + {"helix": 7, "forward": true, "start": 288, "end": 296} + ] + }, + { + "color": "#320096", + "sequence": "TTAACGTCTCAAGATTAGTTGCTAGAACAAGA", + "domains": [ + {"helix": 7, "forward": true, "start": 296, "end": 304}, + {"helix": 8, "forward": false, "start": 288, "end": 304}, + {"helix": 9, "forward": true, "start": 288, "end": 296} + ] + }, + { + "color": "#03b6a2", + "sequence": "AAAATAATAGGCAGAGGCATTTTCAATCCAAT", + "domains": [ + {"helix": 9, "forward": true, "start": 296, "end": 304}, + {"helix": 10, "forward": false, "start": 288, "end": 304}, + {"helix": 11, "forward": true, "start": 288, "end": 296} + ] + }, + { + "color": "#7300de", + "sequence": "CGCAAGACCCTTGAAAACATAGCGGTACCTTT", + "domains": [ + {"helix": 11, "forward": true, "start": 296, "end": 304}, + {"helix": 12, "forward": false, "start": 288, "end": 304}, + {"helix": 13, "forward": true, "start": 288, "end": 296} + ] + }, + { + "color": "#aaaa00", + "sequence": "TACATCGGTATAATCCTGATTGTTAGTTGAAA", + "domains": [ + {"helix": 13, "forward": true, "start": 296, "end": 304}, + {"helix": 14, "forward": false, "start": 288, "end": 304}, + {"helix": 15, "forward": true, "start": 288, "end": 296} + ] + }, + { + "color": "#b8056c", + "sequence": "GGAATTGAGGAAGGTTATCTAAAAGATGGCAA", + "domains": [ + {"helix": 15, "forward": true, "start": 296, "end": 320}, + {"helix": 14, "forward": false, "start": 312, "end": 320} + ] + }, + { + "color": "#007200", + "sequence": "CAGTGCCCTAGCCCGGAATAGGTGTTCCAGAC", + "domains": [ + {"helix": 2, "forward": false, "start": 304, "end": 312}, + {"helix": 1, "forward": true, "start": 304, "end": 320}, + {"helix": 0, "forward": false, "start": 312, "end": 320} + ] + }, + { + "color": "#cc0000", + "sequence": "GAATCAAGCTCAGAGCCACCACCCTTGAGTAA", + "domains": [ + {"helix": 4, "forward": false, "start": 304, "end": 312}, + {"helix": 3, "forward": true, "start": 304, "end": 320}, + {"helix": 2, "forward": false, "start": 312, "end": 320} + ] + }, + { + "color": "#f7931e", + "sequence": "GAAGGAAAAATCAATAGAAAATTCTAGCGACA", + "domains": [ + {"helix": 6, "forward": false, "start": 304, "end": 312}, + {"helix": 5, "forward": true, "start": 304, "end": 320}, + {"helix": 4, "forward": false, "start": 312, "end": 320} + ] + }, + { + "color": "#f74308", + "sequence": "GCCTTAAAAAAAATGAAAATAGCAAGTTACCA", + "domains": [ + {"helix": 8, "forward": false, "start": 304, "end": 312}, + {"helix": 7, "forward": true, "start": 304, "end": 320}, + {"helix": 6, "forward": false, "start": 312, "end": 320} + ] + }, + { + "color": "#57bb00", + "sequence": "TGTAATTTATCCCATCCTAATTTAGTTTTGAA", + "domains": [ + {"helix": 10, "forward": false, "start": 304, "end": 312}, + {"helix": 9, "forward": true, "start": 304, "end": 320}, + {"helix": 8, "forward": false, "start": 312, "end": 320} + ] + }, + { + "color": "#888888", + "sequence": "CTTAGAATAAAGAACGCGAGAAAACGCCAACA", + "domains": [ + {"helix": 12, "forward": false, "start": 304, "end": 312}, + {"helix": 11, "forward": true, "start": 304, "end": 320}, + {"helix": 10, "forward": false, "start": 312, "end": 320} + ] + }, + { + "color": "#32b86c", + "sequence": "TTCATCAAGAGAAACAATAACGGAAATTTTCC", + "domains": [ + {"helix": 14, "forward": false, "start": 304, "end": 312}, + {"helix": 13, "forward": true, "start": 304, "end": 320}, + {"helix": 12, "forward": false, "start": 312, "end": 320} + ] + }, + { + "color": "#333333", + "sequence": "AACGATCTAAAGTTTTGTCGTCTTATCACC", + "domains": [ + {"helix": 0, "forward": false, "start": 320, "end": 344, "deletions": [321]}, + {"helix": 1, "forward": true, "start": 320, "end": 328, "deletions": [321]} + ] + }, + { + "color": "#320096", + "sequence": "GTACTCAGAACGGGGTCAGTGCCTCAGAGC", + "domains": [ + {"helix": 1, "forward": true, "start": 328, "end": 336}, + {"helix": 2, "forward": false, "start": 320, "end": 336, "deletions": [321]}, + {"helix": 3, "forward": true, "start": 320, "end": 328, "deletions": [321]} + ] + }, + { + "color": "#03b6a2", + "sequence": "CGCCACCACAGCACCGTAATCAGATATGGT", + "domains": [ + {"helix": 3, "forward": true, "start": 328, "end": 336}, + {"helix": 4, "forward": false, "start": 320, "end": 336, "deletions": [321]}, + {"helix": 5, "forward": true, "start": 320, "end": 328, "deletions": [321]} + ] + }, + { + "color": "#7300de", + "sequence": "TTACCAGCCAGATAGCCGAACAAGCCTTTA", + "domains": [ + {"helix": 5, "forward": true, "start": 328, "end": 336}, + {"helix": 6, "forward": false, "start": 320, "end": 336, "deletions": [321]}, + {"helix": 7, "forward": true, "start": 320, "end": 328, "deletions": [321]} + ] + }, + { + "color": "#aaaa00", + "sequence": "CAGAGAGACCCGACTTGCGGGAGCGAGCAT", + "domains": [ + {"helix": 7, "forward": true, "start": 328, "end": 336}, + {"helix": 8, "forward": false, "start": 320, "end": 336, "deletions": [321]}, + {"helix": 9, "forward": true, "start": 320, "end": 328, "deletions": [321]} + ] + }, + { + "color": "#b8056c", + "sequence": "GTAGAAACCGCCATATTTAACAACTTTTTC", + "domains": [ + {"helix": 9, "forward": true, "start": 328, "end": 336}, + {"helix": 10, "forward": false, "start": 320, "end": 336, "deletions": [321]}, + {"helix": 11, "forward": true, "start": 320, "end": 328, "deletions": [321]} + ] + }, + { + "color": "#007200", + "sequence": "AAATATATTCGTCGCTATTAATTTTCGCCT", + "domains": [ + {"helix": 11, "forward": true, "start": 328, "end": 336}, + {"helix": 12, "forward": false, "start": 320, "end": 336, "deletions": [321]}, + {"helix": 13, "forward": true, "start": 320, "end": 328, "deletions": [321]} + ] + }, + { + "color": "#cc0000", + "sequence": "GATTGCTTTCCTGATTATCAGATTATCTTT", + "domains": [ + {"helix": 13, "forward": true, "start": 328, "end": 336}, + {"helix": 14, "forward": false, "start": 320, "end": 336, "deletions": [321]}, + {"helix": 15, "forward": true, "start": 320, "end": 328, "deletions": [321]} + ] + }, + { + "color": "#f7931e", + "sequence": "AGGAGCACTAACAACTAATAGATTGGAATTAT", + "domains": [ + {"helix": 15, "forward": true, "start": 328, "end": 352}, + {"helix": 14, "forward": false, "start": 344, "end": 352} + ] + }, + { + "color": "#f74308", + "sequence": "TAAGTTTTGAGGTTTAGTACCGCCGTTAGCGT", + "domains": [ + {"helix": 2, "forward": false, "start": 336, "end": 344}, + {"helix": 1, "forward": true, "start": 336, "end": 352}, + {"helix": 0, "forward": false, "start": 344, "end": 352} + ] + }, + { + "color": "#57bb00", + "sequence": "ATCGATAGGAACCACCACCAGAGCACTGGTAA", + "domains": [ + {"helix": 4, "forward": false, "start": 336, "end": 344}, + {"helix": 3, "forward": true, "start": 336, "end": 352}, + {"helix": 2, "forward": false, "start": 344, "end": 352} + ] + }, + { + "color": "#888888", + "sequence": "AAAGTAAGGCCAAAGACAAAAGGGATGAAACC", + "domains": [ + {"helix": 6, "forward": false, "start": 336, "end": 344}, + {"helix": 5, "forward": true, "start": 336, "end": 352}, + {"helix": 4, "forward": false, "start": 344, "end": 352} + ] + }, + { + "color": "#32b86c", + "sequence": "GCGAACCTATAACATAAAAACAGGTTTTAAGA", + "domains": [ + {"helix": 8, "forward": false, "start": 336, "end": 344}, + {"helix": 7, "forward": true, "start": 336, "end": 352}, + {"helix": 6, "forward": false, "start": 344, "end": 352} + ] + }, + { + "color": "#333333", + "sequence": "TTGAGAATCAATCAATAATCGGCTGCGTTTTA", + "domains": [ + {"helix": 10, "forward": false, "start": 336, "end": 344}, + {"helix": 9, "forward": true, "start": 336, "end": 352}, + {"helix": 8, "forward": false, "start": 344, "end": 352} + ] + }, + { + "color": "#320096", + "sequence": "TCTGTAAATTTAGTTAATTTCATCGGGCTTAA", + "domains": [ + {"helix": 12, "forward": false, "start": 336, "end": 344}, + {"helix": 11, "forward": true, "start": 336, "end": 352}, + {"helix": 10, "forward": false, "start": 344, "end": 352} + ] + }, + { + "color": "#03b6a2", + "sequence": "CATCATATTGAATACCAAGTTACAACCTTGCT", + "domains": [ + {"helix": 14, "forward": false, "start": 336, "end": 344}, + {"helix": 13, "forward": true, "start": 336, "end": 352}, + {"helix": 12, "forward": false, "start": 344, "end": 352} + ] + }, + { + "color": "#7300de", + "sequence": "GCATTCCACAGACAGCCCTCATAACCCTCAG", + "domains": [ + {"helix": 0, "forward": false, "start": 352, "end": 376, "deletions": [369]}, + {"helix": 1, "forward": true, "start": 352, "end": 360} + ] + }, + { + "color": "#aaaa00", + "sequence": "AACCGCCAGATGATACAGGAGTGTCGCCGCCA", + "domains": [ + {"helix": 1, "forward": true, "start": 360, "end": 368}, + {"helix": 2, "forward": false, "start": 352, "end": 368}, + {"helix": 3, "forward": true, "start": 352, "end": 360} + ] + }, + { + "color": "#b8056c", + "sequence": "GCATTGACGCCGGAAACGTCACCACGACATTC", + "domains": [ + {"helix": 3, "forward": true, "start": 360, "end": 368}, + {"helix": 4, "forward": false, "start": 352, "end": 368}, + {"helix": 5, "forward": true, "start": 352, "end": 360} + ] + }, + { + "color": "#007200", + "sequence": "AACCGATTATCTTACCGAAGCCCTGAAGCGCA", + "domains": [ + {"helix": 5, "forward": true, "start": 360, "end": 368}, + {"helix": 6, "forward": false, "start": 352, "end": 368}, + {"helix": 7, "forward": true, "start": 352, "end": 360} + ] + }, + { + "color": "#cc0000", + "sequence": "TTAGACGGATTCTAAGAACGCGAGGTCTTTCC", + "domains": [ + {"helix": 7, "forward": true, "start": 360, "end": 368}, + {"helix": 8, "forward": false, "start": 352, "end": 368}, + {"helix": 9, "forward": true, "start": 352, "end": 360} + ] + }, + { + "color": "#f7931e", + "sequence": "TTATCATTCCAACGCTCAACAGTATTCTGACC", + "domains": [ + {"helix": 9, "forward": true, "start": 360, "end": 368}, + {"helix": 10, "forward": false, "start": 352, "end": 368}, + {"helix": 11, "forward": true, "start": 352, "end": 360} + ] + }, + { + "color": "#f74308", + "sequence": "TAAATTTATATATGTGAGTGAATAAAATCGCG", + "domains": [ + {"helix": 11, "forward": true, "start": 360, "end": 368}, + {"helix": 12, "forward": false, "start": 352, "end": 368}, + {"helix": 13, "forward": true, "start": 352, "end": 360} + ] + }, + { + "color": "#57bb00", + "sequence": "CAGAGGCGAACCACCAGAAGGAGCAGAGCCGT", + "domains": [ + {"helix": 13, "forward": true, "start": 360, "end": 368}, + {"helix": 14, "forward": false, "start": 352, "end": 368}, + {"helix": 15, "forward": true, "start": 352, "end": 360} + ] + }, + { + "color": "#888888", + "sequence": "CAATAGATAATACATTTGAGGATTTTGCGGA", + "domains": [ + {"helix": 15, "forward": true, "start": 360, "end": 384, "deletions": [369]}, + {"helix": 14, "forward": false, "start": 376, "end": 384} + ] + }, + { + "color": "#32b86c", + "sequence": "GGCTTTTCCCTCAGAACCGCCACGCCTGTA", + "domains": [ + {"helix": 2, "forward": false, "start": 368, "end": 376, "deletions": [369]}, + {"helix": 1, "forward": true, "start": 368, "end": 384, "deletions": [369]}, + {"helix": 0, "forward": false, "start": 376, "end": 384} + ] + }, + { + "color": "#333333", + "sequence": "TAGCAAGAGGAGGTTGAGGCAGTCATACAT", + "domains": [ + {"helix": 4, "forward": false, "start": 368, "end": 376, "deletions": [369]}, + {"helix": 3, "forward": true, "start": 368, "end": 384, "deletions": [369]}, + {"helix": 2, "forward": false, "start": 376, "end": 384} + ] + }, + { + "color": "#320096", + "sequence": "AATAGCTGAGGGAGGGAAGGTAATTACCAT", + "domains": [ + {"helix": 6, "forward": false, "start": 368, "end": 376, "deletions": [369]}, + {"helix": 5, "forward": true, "start": 368, "end": 384, "deletions": [369]}, + {"helix": 4, "forward": false, "start": 376, "end": 384} + ] + }, + { + "color": "#03b6a2", + "sequence": "ATCCGGTGAGAATTAACTGAACGAAATAGC", + "domains": [ + {"helix": 8, "forward": false, "start": 368, "end": 376, "deletions": [369]}, + {"helix": 7, "forward": true, "start": 368, "end": 384, "deletions": [369]}, + {"helix": 6, "forward": false, "start": 376, "end": 384} + ] + }, + { + "color": "#7300de", + "sequence": "TATAAAGCCAAGAACGGGTATTGAAGGCTT", + "domains": [ + {"helix": 10, "forward": false, "start": 368, "end": 376, "deletions": [369]}, + {"helix": 9, "forward": true, "start": 368, "end": 384, "deletions": [369]}, + {"helix": 8, "forward": false, "start": 376, "end": 384} + ] + }, + { + "color": "#aaaa00", + "sequence": "AAATCAAATGGTTTGAAATACCCTTACCAG", + "domains": [ + {"helix": 12, "forward": false, "start": 368, "end": 376, "deletions": [369]}, + {"helix": 11, "forward": true, "start": 368, "end": 384, "deletions": [369]}, + {"helix": 10, "forward": false, "start": 376, "end": 384} + ] + }, + { + "color": "#b8056c", + "sequence": "ACAAAGAAATTATTCATTTCAACAGTACAT", + "domains": [ + {"helix": 14, "forward": false, "start": 368, "end": 376, "deletions": [369]}, + {"helix": 13, "forward": true, "start": 368, "end": 384, "deletions": [369]}, + {"helix": 12, "forward": false, "start": 376, "end": 384} + ] + }, + { + "color": "#007200", + "sequence": "TTTCGTCACCAGTACAAACTACAACCCTCAGA", + "domains": [ + {"helix": 0, "forward": false, "start": 384, "end": 408}, + {"helix": 1, "forward": true, "start": 384, "end": 392} + ] + }, + { + "color": "#cc0000", + "sequence": "GCCACCACACCGTTCCAGTAAGCGGTCAGACG", + "domains": [ + {"helix": 1, "forward": true, "start": 392, "end": 400}, + {"helix": 2, "forward": false, "start": 384, "end": 400}, + {"helix": 3, "forward": true, "start": 384, "end": 392} + ] + }, + { + "color": "#f7931e", + "sequence": "ATTGGCCTAATCACCAGTAGCACCAATATTGA", + "domains": [ + {"helix": 3, "forward": true, "start": 392, "end": 400}, + {"helix": 4, "forward": false, "start": 384, "end": 400}, + {"helix": 5, "forward": true, "start": 384, "end": 392} + ] + }, + { + "color": "#f74308", + "sequence": "CGGAAATTAAGAGCAAGAAACAATACCCTGAA", + "domains": [ + {"helix": 5, "forward": true, "start": 392, "end": 400}, + {"helix": 6, "forward": false, "start": 384, "end": 400}, + {"helix": 7, "forward": true, "start": 384, "end": 392} + ] + }, + { + "color": "#57bb00", + "sequence": "CAAAGTCAAAGCAAATCAGATATAAAACCAAG", + "domains": [ + {"helix": 7, "forward": true, "start": 392, "end": 400}, + {"helix": 8, "forward": false, "start": 384, "end": 400}, + {"helix": 9, "forward": true, "start": 384, "end": 392} + ] + }, + { + "color": "#888888", + "sequence": "TACCGCACATGCGTTATACAAATTGACCGTGT", + "domains": [ + {"helix": 9, "forward": true, "start": 392, "end": 400}, + {"helix": 10, "forward": false, "start": 384, "end": 400}, + {"helix": 11, "forward": true, "start": 384, "end": 392} + ] + }, + { + "color": "#32b86c", + "sequence": "GATAAATACCTTTTTTAATGGAAATTACCTGA", + "domains": [ + {"helix": 11, "forward": true, "start": 392, "end": 400}, + {"helix": 12, "forward": false, "start": 384, "end": 400}, + {"helix": 13, "forward": true, "start": 384, "end": 392} + ] + }, + { + "color": "#333333", + "sequence": "GCAAAAGATGAGTAACATTATCATTTAGAAGT", + "domains": [ + {"helix": 13, "forward": true, "start": 392, "end": 400}, + {"helix": 14, "forward": false, "start": 384, "end": 400}, + {"helix": 15, "forward": true, "start": 384, "end": 392} + ] + }, + { + "color": "#320096", + "sequence": "ATTAGACTTTACAAACAATTCGACATTAATTT", + "domains": [ + {"helix": 15, "forward": true, "start": 392, "end": 416}, + {"helix": 14, "forward": false, "start": 408, "end": 416} + ] + }, + { + "color": "#03b6a2", + "sequence": "CTGAATTTCCTCATTTTCAGGGATACACTGAG", + "domains": [ + {"helix": 2, "forward": false, "start": 400, "end": 408}, + {"helix": 1, "forward": true, "start": 400, "end": 416}, + {"helix": 0, "forward": false, "start": 408, "end": 416} + ] + }, + { + "color": "#7300de", + "sequence": "GCCAGCAATGATATTCACAAACAACGCAGTCT", + "domains": [ + {"helix": 4, "forward": false, "start": 400, "end": 408}, + {"helix": 3, "forward": true, "start": 400, "end": 416}, + {"helix": 2, "forward": false, "start": 408, "end": 416} + ] + }, + { + "color": "#aaaa00", + "sequence": "CCAATAATATTCATTAAAGGTGAAGAATTAGA", + "domains": [ + {"helix": 6, "forward": false, "start": 400, "end": 408}, + {"helix": 5, "forward": true, "start": 400, "end": 416}, + {"helix": 4, "forward": false, "start": 408, "end": 416} + ] + }, + { + "color": "#b8056c", + "sequence": "CCAATAGCGAGGGTAATTGAGCGCAGTTAAGC", + "domains": [ + {"helix": 8, "forward": false, "start": 400, "end": 408}, + {"helix": 7, "forward": true, "start": 400, "end": 416}, + {"helix": 6, "forward": false, "start": 408, "end": 416} + ] + }, + { + "color": "#007200", + "sequence": "AGTATCATTCATCGAGAACAAGCATACCGCGC", + "domains": [ + {"helix": 10, "forward": false, "start": 400, "end": 408}, + {"helix": 9, "forward": true, "start": 400, "end": 416}, + {"helix": 8, "forward": false, "start": 408, "end": 416} + ] + }, + { + "color": "#cc0000", + "sequence": "TTGAATTAAGGCGTTAAATAAGAAGCCTGTTT", + "domains": [ + {"helix": 12, "forward": false, "start": 400, "end": 408}, + {"helix": 11, "forward": true, "start": 400, "end": 416}, + {"helix": 10, "forward": false, "start": 408, "end": 416} + ] + }, + { + "color": "#f7931e", + "sequence": "TAAAAGTTAGATGATGAAACAAACAATTTCAT", + "domains": [ + {"helix": 14, "forward": false, "start": 400, "end": 408}, + {"helix": 13, "forward": true, "start": 400, "end": 416}, + {"helix": 12, "forward": false, "start": 408, "end": 416} + ] + } + ] +} \ No newline at end of file diff --git a/tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_deletions_insertions.sc b/tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_deletions_insertions.sc new file mode 100644 index 00000000..cfc25b74 --- /dev/null +++ b/tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_deletions_insertions.sc @@ -0,0 +1,36 @@ +{ + "version": "0.16.0", + "grid": "square", + "helices": [ + {"grid_position": [0, 0]}, + {"grid_position": [0, 1]} + ], + "strands": [ + { + "color": "#0066cc", + "sequence": "AACGTAACGTAACGTAACGTAACGTAACGTAACGTAACGTAACGTAACGTAACGTAACGTAACGTAACG", + "domains": [ + {"helix": 1, "forward": false, "start": 0, "end": 16, "deletions": [12], "insertions": [[6, 3]]}, + {"helix": 0, "forward": true, "start": 0, "end": 32, "deletions": [11, 12, 24], "insertions": [[6, 1], [18, 2]]}, + {"helix": 1, "forward": false, "start": 16, "end": 32, "deletions": [24], "insertions": [[18, 4]]} + ], + "is_scaffold": true + }, + { + "color": "#f74308", + "sequence": "GTTACGTTACGTTACGTTGTTACGTTACGTTAC", + "domains": [ + {"helix": 1, "forward": true, "start": 0, "end": 16, "deletions": [12], "insertions": [[6, 3]]}, + {"helix": 0, "forward": false, "start": 0, "end": 16, "deletions": [11, 12], "insertions": [[6, 1]]} + ] + }, + { + "color": "#57bb00", + "sequence": "ACGTTACGTTACGTTACCGTTACGTTACGTTACGTT", + "domains": [ + {"helix": 0, "forward": false, "start": 16, "end": 32, "deletions": [24], "insertions": [[18, 2]]}, + {"helix": 1, "forward": true, "start": 16, "end": 32, "deletions": [24], "insertions": [[18, 4]]} + ] + } + ] +} \ No newline at end of file diff --git a/tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple.sc b/tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple.sc new file mode 100644 index 00000000..130d6941 --- /dev/null +++ b/tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple.sc @@ -0,0 +1,17 @@ +{ + "version": "0.16.0", + "grid": "square", + "helices": [ + {"grid_position": [0, 0]}, + {"grid_position": [0, 1]} + ], + "strands": [ + { + "color": "#0066cc", + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 32} + ], + "is_scaffold": true + } + ] +} \ No newline at end of file diff --git a/tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple_2.sc b/tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple_2.sc new file mode 100644 index 00000000..46003fe4 --- /dev/null +++ b/tests/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple_2.sc @@ -0,0 +1,18 @@ +{ + "version": "0.16.0", + "grid": "square", + "helices": [ + {"grid_position": [0, 0]}, + {"grid_position": [0, 1]} + ], + "strands": [ + { + "color": "#0066cc", + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 32}, + {"helix": 1, "forward": false, "start": 0, "end": 32} + ], + "is_scaffold": true + } + ] +} \ No newline at end of file diff --git a/tests/tests_inputs/cadnano_v2_export/test_6_helix_origami_rectangle.sc b/tests/tests_inputs/cadnano_v2_export/test_6_helix_origami_rectangle.sc new file mode 100644 index 00000000..aa030c0a --- /dev/null +++ b/tests/tests_inputs/cadnano_v2_export/test_6_helix_origami_rectangle.sc @@ -0,0 +1,282 @@ +{ + "version": "0.16.0", + "grid": "square", + "helices": [ + {"max_offset": 192, "grid_position": [0, 0]}, + {"max_offset": 192, "grid_position": [0, 1]}, + {"max_offset": 192, "grid_position": [0, 2]}, + {"max_offset": 192, "grid_position": [0, 3]}, + {"max_offset": 192, "grid_position": [0, 4]}, + {"max_offset": 192, "grid_position": [0, 5]} + ], + "strands": [ + { + "color": "#0066cc", + "sequence": "TTCCCTTCCTTTCTCGCCACGTTCGCCGGCTTTCCCCGTCAAGCTCTAAATCGGGGGCTCCCTTTAGGGTTCCGATTTAGTGCTTTACGGCACCTCGACCCCAAAAAACTTGATTTGGGTGATGGTTCACGTAGTGGGCCATCGCCCTGATAGACGGTTTTTCGCCCTTTGACGTTGGAGTCCACGTTCTTTAATAGTGGACTCTTGTTCCAAACTGGAACAACACTCAACCCTATCTCGGGCTATTCTTTTGATTTATAAGGGATTTTGCCGATTTCGGAACCACCATCAAACAGGATTTTCGCCTGCTGGGGCAAACCAGCGTGGACCGCTTGCTGCAACTCTCTCAGGGCCAGGCGGTGAAGGGCAATCAGCTGTTGCCCGTCTCACTGGTGAAAAGAAAAACCACCCTGGCGCCCAATACGCAAACCGCCTCTCCCCGCGCGTTGGCCGATTCATTAATGCAGCTGGCACGACAGGTTTCCCGACTGGAAAGCGGGCAGTGAGCGCAACGCAATTAATGTGAGTTAGCTCACTCATTAGGCACCCCAGGCTTTACACTTTATGCTTCCGGCTCGTATGTTGTGTGGAATTGTGAGCGGATAACAATTTCACACAGGAAACAGCTATGACCATGATTACGAATTCGAGCTCGGTACCCGGGGATCCTCTAGAGTCGACCTGCAGGCATGCAAGCTTGGCACTGGCCGTCGTTTTACAACGTCGTGACTGGGAAAACCCTGGCGTTACCCAACTTAATCGCCTTGCAGCACATCCCCCTTTCGCCAGCTGGCGTAATAGCGAAGAGGCCCGCACCGATCGCCCTTCCCAACAGTTGCGCAGCCTGAATGGCGAATGGCGCTTTGCCTGGTTTCCGGCACCAGAAGCGGTGCCGGAAAGCTGGCTGGAGTGCGATCTTCCTGAGGCCGATACTGTCGTCGT", + "domains": [ + {"helix": 5, "forward": false, "start": 16, "end": 96, "deletions": [33, 81]}, + {"helix": 4, "forward": true, "start": 16, "end": 96, "deletions": [33, 81]}, + {"helix": 3, "forward": false, "start": 16, "end": 96, "deletions": [33, 81]}, + {"helix": 2, "forward": true, "start": 16, "end": 96, "deletions": [33, 81]}, + {"helix": 1, "forward": false, "start": 16, "end": 96, "deletions": [33, 81]}, + {"helix": 0, "forward": true, "start": 16, "end": 176, "deletions": [33, 81, 129]}, + {"helix": 1, "forward": false, "start": 96, "end": 176, "deletions": [129]}, + {"helix": 2, "forward": true, "start": 96, "end": 176, "deletions": [129]}, + {"helix": 3, "forward": false, "start": 96, "end": 176, "deletions": [129]}, + {"helix": 4, "forward": true, "start": 96, "end": 176, "deletions": [129]}, + {"helix": 5, "forward": false, "start": 96, "end": 176, "deletions": [129]} + ], + "is_scaffold": true + }, + { + "color": "#f74308", + "sequence": "GTGAGACGGGCAACAGGTTTTTCTTTTCACCA", + "domains": [ + {"helix": 1, "forward": true, "start": 16, "end": 32}, + {"helix": 0, "forward": false, "start": 16, "end": 32} + ] + }, + { + "color": "#57bb00", + "sequence": "AGGGTTGAGTGTTGTTAAGAATAGCCCGAGAT", + "domains": [ + {"helix": 3, "forward": true, "start": 16, "end": 32}, + {"helix": 2, "forward": false, "start": 16, "end": 32} + ] + }, + { + "color": "#888888", + "sequence": "AAATCGGAACCCTAAAGGTGCCGTAAAGCACT", + "domains": [ + {"helix": 5, "forward": true, "start": 16, "end": 32}, + {"helix": 4, "forward": false, "start": 16, "end": 32} + ] + }, + { + "color": "#32b86c", + "sequence": "GTGCCTAATGAGTGAGAAGTGTAAAGCCTGGG", + "domains": [ + {"helix": 0, "forward": false, "start": 160, "end": 176}, + {"helix": 1, "forward": true, "start": 160, "end": 176} + ] + }, + { + "color": "#333333", + "sequence": "AGTGCCAAGCTTGCATTTGTAAAACGACGGCC", + "domains": [ + {"helix": 2, "forward": false, "start": 160, "end": 176}, + {"helix": 3, "forward": true, "start": 160, "end": 176} + ] + }, + { + "color": "#320096", + "sequence": "AGCGCCATTCGCCATTGCCGGAAACCAGGCAA", + "domains": [ + {"helix": 4, "forward": false, "start": 160, "end": 176}, + {"helix": 5, "forward": true, "start": 160, "end": 176} + ] + }, + { + "color": "#03b6a2", + "sequence": "CCAGTCGGGAAACCTGTCGTGCCAGCTGCATT", + "domains": [ + {"helix": 0, "forward": false, "start": 88, "end": 120} + ] + }, + { + "color": "#7300de", + "sequence": "CGAAAATCCACGCTGGTTTGCCCTGTTTCC", + "domains": [ + {"helix": 2, "forward": false, "start": 80, "end": 88, "deletions": [81]}, + {"helix": 1, "forward": true, "start": 80, "end": 104, "deletions": [81]} + ] + }, + { + "color": "#aaaa00", + "sequence": "TGTGTGAAGTAATCATGGTCATAGCCAGCAGG", + "domains": [ + {"helix": 1, "forward": true, "start": 104, "end": 112}, + {"helix": 2, "forward": false, "start": 88, "end": 112} + ] + }, + { + "color": "#b8056c", + "sequence": "AGGGCGACAAAGGGCGAAAAACGAAAGGGG", + "domains": [ + {"helix": 4, "forward": false, "start": 80, "end": 88, "deletions": [81]}, + {"helix": 3, "forward": true, "start": 80, "end": 104, "deletions": [81]} + ] + }, + { + "color": "#007200", + "sequence": "GATGTGCTTATTACGCCAGCTGGCCGTCTATC", + "domains": [ + {"helix": 3, "forward": true, "start": 104, "end": 112}, + {"helix": 4, "forward": false, "start": 88, "end": 112} + ] + }, + { + "color": "#cc0000", + "sequence": "AACGTGGCGAGAAAGGAAGGGAAACGACGAC", + "domains": [ + {"helix": 5, "forward": true, "start": 72, "end": 104, "deletions": [81]} + ] + }, + { + "color": "#f7931e", + "sequence": "TTTGCGTATTGGGCGCCAGGGTGCTGATTG", + "domains": [ + {"helix": 0, "forward": false, "start": 32, "end": 56, "deletions": [33]}, + {"helix": 1, "forward": true, "start": 32, "end": 40, "deletions": [33]} + ] + }, + { + "color": "#f74308", + "sequence": "CCCTTCACTCCCTTATAAATCAACCAGTTT", + "domains": [ + {"helix": 1, "forward": true, "start": 40, "end": 48}, + {"helix": 2, "forward": false, "start": 32, "end": 48, "deletions": [33]}, + {"helix": 3, "forward": true, "start": 32, "end": 40, "deletions": [33]} + ] + }, + { + "color": "#57bb00", + "sequence": "GGAACAAGGTTTTTTGGGGTCGAGGGAGCC", + "domains": [ + {"helix": 3, "forward": true, "start": 40, "end": 48}, + {"helix": 4, "forward": false, "start": 32, "end": 48, "deletions": [33]}, + {"helix": 5, "forward": true, "start": 32, "end": 40, "deletions": [33]} + ] + }, + { + "color": "#888888", + "sequence": "CCCGATTTAGAGCTTGACGGGGAACCATCACC", + "domains": [ + {"helix": 5, "forward": true, "start": 40, "end": 64}, + {"helix": 4, "forward": false, "start": 56, "end": 64} + ] + }, + { + "color": "#32b86c", + "sequence": "CGGCAAAACGCCTGGCCCTGAGAGAGAGGCGG", + "domains": [ + {"helix": 2, "forward": false, "start": 48, "end": 56}, + {"helix": 1, "forward": true, "start": 48, "end": 64}, + {"helix": 0, "forward": false, "start": 56, "end": 64} + ] + }, + { + "color": "#333333", + "sequence": "CAAATCAAAGTCCACTATTAAAGATCCGAAAT", + "domains": [ + {"helix": 4, "forward": false, "start": 48, "end": 56}, + {"helix": 3, "forward": true, "start": 48, "end": 64}, + {"helix": 2, "forward": false, "start": 56, "end": 64} + ] + }, + { + "color": "#320096", + "sequence": "AATGAATCGGCCAACGCGCGGGGAGTTGCAG", + "domains": [ + {"helix": 0, "forward": false, "start": 64, "end": 88, "deletions": [81]}, + {"helix": 1, "forward": true, "start": 64, "end": 72} + ] + }, + { + "color": "#03b6a2", + "sequence": "CAAGCGGTCCTGTTTGATGGTGGTACGTGGAC", + "domains": [ + {"helix": 1, "forward": true, "start": 72, "end": 80}, + {"helix": 2, "forward": false, "start": 64, "end": 80}, + {"helix": 3, "forward": true, "start": 64, "end": 72} + ] + }, + { + "color": "#7300de", + "sequence": "TCCAACGTTGGCCCACTACGTGAAAGCCGGCG", + "domains": [ + {"helix": 3, "forward": true, "start": 72, "end": 80}, + {"helix": 4, "forward": false, "start": 64, "end": 80}, + {"helix": 5, "forward": true, "start": 64, "end": 72} + ] + }, + { + "color": "#aaaa00", + "sequence": "AGTATCGGCCTCAGGAAGATCGCAGTGCGGGC", + "domains": [ + {"helix": 5, "forward": true, "start": 104, "end": 128}, + {"helix": 4, "forward": false, "start": 120, "end": 128} + ] + }, + { + "color": "#b8056c", + "sequence": "TCGAATTCATTGTTATCCGCTCACCCCGCTTT", + "domains": [ + {"helix": 2, "forward": false, "start": 112, "end": 120}, + {"helix": 1, "forward": true, "start": 112, "end": 128}, + {"helix": 0, "forward": false, "start": 120, "end": 128} + ] + }, + { + "color": "#007200", + "sequence": "CTCTTCGCGCAAGGCGATTAAGTTTACCGAGC", + "domains": [ + {"helix": 4, "forward": false, "start": 112, "end": 120}, + {"helix": 3, "forward": true, "start": 112, "end": 128}, + {"helix": 2, "forward": false, "start": 120, "end": 128} + ] + }, + { + "color": "#cc0000", + "sequence": "CATTAATTGCGTTGCGCTCACTGAATTCCA", + "domains": [ + {"helix": 0, "forward": false, "start": 128, "end": 152, "deletions": [129]}, + {"helix": 1, "forward": true, "start": 128, "end": 136, "deletions": [129]} + ] + }, + { + "color": "#f7931e", + "sequence": "CACAACATTAGAGGATCCCCGGGGGGTAAC", + "domains": [ + {"helix": 1, "forward": true, "start": 136, "end": 144}, + {"helix": 2, "forward": false, "start": 128, "end": 144, "deletions": [129]}, + {"helix": 3, "forward": true, "start": 128, "end": 136, "deletions": [129]} + ] + }, + { + "color": "#f74308", + "sequence": "GCCAGGGTTGGGAAGGGCGATCGCTCCAGC", + "domains": [ + {"helix": 3, "forward": true, "start": 136, "end": 144}, + {"helix": 4, "forward": false, "start": 128, "end": 144, "deletions": [129]}, + {"helix": 5, "forward": true, "start": 128, "end": 136, "deletions": [129]} + ] + }, + { + "color": "#57bb00", + "sequence": "CAGCTTTCCGGCACCGCTTCTGGTCAGGCTGC", + "domains": [ + {"helix": 5, "forward": true, "start": 136, "end": 160}, + {"helix": 4, "forward": false, "start": 152, "end": 160} + ] + }, + { + "color": "#888888", + "sequence": "GTCGACTCACGAGCCGGAAGCATACTAACTCA", + "domains": [ + {"helix": 2, "forward": false, "start": 144, "end": 152}, + {"helix": 1, "forward": true, "start": 144, "end": 160}, + {"helix": 0, "forward": false, "start": 152, "end": 160} + ] + }, + { + "color": "#32b86c", + "sequence": "GCAACTGTTTTCCCAGTCACGACGGCCTGCAG", + "domains": [ + {"helix": 4, "forward": false, "start": 144, "end": 152}, + {"helix": 3, "forward": true, "start": 144, "end": 160}, + {"helix": 2, "forward": false, "start": 152, "end": 160} + ] + } + ] +} \ No newline at end of file diff --git a/tests/tests_inputs/cadnano_v2_export/test_circular_strand.sc b/tests/tests_inputs/cadnano_v2_export/test_circular_strand.sc new file mode 100644 index 00000000..4c3f5705 --- /dev/null +++ b/tests/tests_inputs/cadnano_v2_export/test_circular_strand.sc @@ -0,0 +1,18 @@ +{ + "version": "0.16.0", + "grid": "square", + "helices": [ + {"max_offset": 24, "grid_position": [0, 0]}, + {"max_offset": 24, "grid_position": [0, 1]} + ], + "strands": [ + { + "circular": true, + "color": "#f74308", + "domains": [ + {"helix": 1, "forward": true, "start": 0, "end": 8}, + {"helix": 0, "forward": false, "start": 0, "end": 8} + ] + } + ] +} \ No newline at end of file diff --git a/tests/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group.sc b/tests/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group.sc new file mode 100644 index 00000000..477ff07a --- /dev/null +++ b/tests/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group.sc @@ -0,0 +1,20 @@ +{ + "version": "0.16.0", + "groups": { + "east": { + "position": {"x": 10, "y": 0, "z": 0}, + "grid": "square" + }, + "south": { + "position": {"x": 0, "y": 10, "z": 0}, + "grid": "square" + } + }, + "helices": [ + {"group": "south", "max_offset": 24, "grid_position": [0, 0]}, + {"group": "south", "max_offset": 25, "grid_position": [0, 1]}, + {"group": "east", "max_offset": 22, "grid_position": [0, 0]}, + {"group": "east", "max_offset": 23, "grid_position": [0, 1]} + ], + "strands": [] +} \ No newline at end of file diff --git a/tests/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group_not_same_grid.sc b/tests/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group_not_same_grid.sc new file mode 100644 index 00000000..12f5d39e --- /dev/null +++ b/tests/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group_not_same_grid.sc @@ -0,0 +1,20 @@ +{ + "version": "0.16.0", + "groups": { + "east": { + "position": {"x": 10, "y": 0, "z": 0}, + "grid": "honeycomb" + }, + "south": { + "position": {"x": 0, "y": 10, "z": 0}, + "grid": "square" + } + }, + "helices": [ + {"group": "south", "max_offset": 24, "grid_position": [0, 0]}, + {"group": "south", "max_offset": 25, "grid_position": [0, 1]}, + {"group": "east", "max_offset": 22, "grid_position": [0, 0]}, + {"group": "east", "max_offset": 23, "grid_position": [0, 1]} + ], + "strands": [] +} \ No newline at end of file From 4cb98a2517482a7a810208252d240370b935da69 Mon Sep 17 00:00:00 2001 From: Anelise Cho <25128798+anelisecho@users.noreply.github.com> Date: Thu, 10 Jun 2021 11:32:30 -0700 Subject: [PATCH 06/18] Fixed OxDNA export issue where insertions would throw error, finished OxDNA export loopout unit test --- scadnano/scadnano.py | 6 +- tests/scadnano_tests.py | 187 +++++++++++++++++++++++++++------------- 2 files changed, 131 insertions(+), 62 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index a4fb06e3..ffe557da 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -6429,7 +6429,7 @@ def rotate(self, angle: float, axis: "_OxdnaVector") -> "_OxdnaVector": # r, b, and n represent the oxDNA conf file vectors that describe a nucleotide # r is the position of the base -# b is the backbone-base vector +# b is the backbone-base vector (in documentation as versor: more info on versors here https://eater.net/quaternions) # n is the forward direction of the helix @dataclass(frozen=True) class _OxdnaNucleotide: @@ -6482,7 +6482,7 @@ def compute_bounding_box(self) -> _OxdnaVector: if min_vec is not None and max_vec is not None: # 5 is arbitrarily chosen so that the box has a bit of wiggle room - return max_vec - min_vec + _OxdnaVector(5, 5, 5) + return 1.5 * (max_vec - min_vec + _OxdnaVector(5, 5, 5)) # changed else: return _OxdnaVector(1, 1, 1) @@ -6657,7 +6657,7 @@ def _oxdna_convert(design: Design) -> _OxdnaSystem: for i in range(num): r = origin + forward * (offset + mod - num + i) * geometry.rise_per_base_pair * NM_TO_OX_UNITS b = normal.rotate(step_rot * (offset + mod - num + i), forward) - nuc = Nucleotide(r, b, forward, seq[index]) + nuc = _OxdnaNucleotide(r, b, forward, seq[index]) dom_strand.nucleotides.append(nuc) index += 1 diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index 7a0b3106..f644e17c 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -5721,6 +5721,11 @@ def setUp(self) -> None: self.OX_UNITS_TO_NM = 0.8518 self.NM_TO_OX_UNITS = 1.0 / self.OX_UNITS_TO_NM self.OX_BASE_DIST = 0.6 + self.BASES_PER_TURN = 10.5 + self.HELIX_ANGLE = math.pi * 2 / self.BASES_PER_TURN + self.RISE_PER_BASE_PAIR = 0.332 + # square of expected distance between adjacent nucleotide centers of mass + self.EXPECTED_ADJ_NUC_CM_DIST2 = (2 * self.OX_BASE_DIST * math.sin(self.HELIX_ANGLE / 2)) ** 2 + (self.RISE_PER_BASE_PAIR * self.NM_TO_OX_UNITS) ** 2 def test_basic_design(self) -> None: """ 2 double strands of length 7 connected across helices. @@ -5735,15 +5740,10 @@ def test_basic_design(self) -> None: design = sc.Design(helices=helices, grid=sc.square) design.strand(0, 0).to(7).cross(1).move(-7) design.strand(0, 7).move(-7).cross(1).move(7) - geometry = design.geometry - - helix_angle = math.pi * 2 / geometry.bases_per_turn # expected values for verification expected_num_nucleotides = 7 * 4 expected_strand_length = 7 * 2 - # square of expected distance between adjacent nucleotide centers of mass - expected_adj_nuc_cm_dist2 = (2 * self.OX_BASE_DIST * math.sin(helix_angle / 2))**2 + (geometry.rise_per_base_pair * self.NM_TO_OX_UNITS)**2 dat, top = design.to_oxdna_format() dat = dat.strip().split('\n') @@ -5761,7 +5761,7 @@ def test_basic_design(self) -> None: for line in dat[3:]: data = line.strip().split() # make sure there are 15 values per line (3 values per vector * 5 vectors per line) - # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity + # order of vectors: center of mass position, backbone base versor, normal versor, velocity, angular velocity (more info on versors: https://eater.net/quaternions) self.assertEqual(15, len(data)) cm_poss.append(tuple([float(x) for x in data[0:3]])) @@ -5820,7 +5820,7 @@ def test_basic_design(self) -> None: self.assertEqual(expected_strand_length, len(strand2_idxs)) for i in range(expected_strand_length - 1): - # ignore nucleotides between domains + # ignore nucleotide distance between domains (on crossover) if i == 6: continue @@ -5841,8 +5841,8 @@ def test_basic_design(self) -> None: sqr_dist1 = sum([x**2 for x in diff1]) sqr_dist2 = sum([x**2 for x in diff2]) - self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist1) - self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist2) + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist2) def test_honeycomb_design(self) -> None: """ A single strand on a honeycomb grid. @@ -5856,15 +5856,10 @@ def test_honeycomb_design(self) -> None: helices = [sc.Helix(grid_position=(1, 1), max_offset=8), sc.Helix(grid_position=(0, 1), max_offset=8), sc.Helix(grid_position=(0, 2), max_offset=8)] design = sc.Design(helices=helices, grid=sc.honeycomb) design.strand(0, 0).to(8).cross(1).move(-8).cross(2).to(8) - geometry = design.geometry - - helix_angle = math.pi * 2 / geometry.bases_per_turn # expected values for verification expected_num_nucleotides = 8 * 3 expected_strand_length = 8 * 3 - # square of expected distance between adjacent nucleotide centers of mass - expected_adj_nuc_cm_dist2 = (2 * self.OX_BASE_DIST * math.sin(helix_angle / 2)) ** 2 + (geometry.rise_per_base_pair * self.NM_TO_OX_UNITS) ** 2 dat, top = design.to_oxdna_format() dat = dat.strip().split('\n') @@ -5930,7 +5925,7 @@ def test_honeycomb_design(self) -> None: self.assertEqual(expected_strand_length, len(strand1_idxs)) for i in range(expected_strand_length - 1): - # ignore nucleotides between domains + # ignore nucleotide distances between domains (on crossovers) if i == 7: continue @@ -5948,29 +5943,115 @@ def test_honeycomb_design(self) -> None: diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) sqr_dist1 = sum([x ** 2 for x in diff1]) - self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist1) + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) + + def test_deletion_design(self) -> None: + """ + 0 7 + [---X--> + """ + helix = [sc.Helix(max_offset=7)] + design = sc.Design(helices=helix, grid=sc.square) + design.strand(0, 0).to(7) + design.add_deletion(helix=0, offset=4) + + # expected values for verification + expected_num_nucleotides = 6 + expected_strand_length = 6 + + dat, top = design.to_oxdna_format() + dat = dat.strip().split('\n') + top = top.strip().split('\n') + + # check length of output files are as expected (matches # of nucleotides plus header size) + self.assertEqual(expected_num_nucleotides + 3, len(dat)) + self.assertEqual(expected_num_nucleotides + 1, len(top)) + + # find relevant values for nucleotides + cm_poss = [] # center of mass position + nbrs_3p = [] + nbrs_5p = [] + + for line in dat[3:]: + data = line.strip().split() + # make sure there are 15 values per line (3 values per vector * 5 vectors per line) + # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity + self.assertEqual(15, len(data)) + + cm_poss.append(tuple([float(x) for x in data[0:3]])) + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + + # make sure normal vectors and backbone vectors are unit length + sqr_bb_vec = sum([x ** 2 for x in bb_vec]) + sqr_nm_vec = sum([x ** 2 for x in nm_vec]) + self.assertAlmostEqual(1.0, sqr_bb_vec) + self.assertAlmostEqual(1.0, sqr_nm_vec) + + for value in data[9:]: # values for velocity and angular velocity vectors are 0 + self.assertAlmostEqual(0, float(value)) + + strand1_idxs = [] + for nuc_idx, line in enumerate(top[1:]): + data = line.strip().split() + # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor + self.assertEqual(4, len(data)) + + # make sure there's only 1 strand + strand_num = int(data[0]) + self.assertEqual(1, strand_num) + # make sure base is valid + base = data[1] + self.assertIn(base, ['A', 'C', 'G', 'T']) + + nbrs_3p.append(int(data[2])) + nbrs_5p.append(int(data[3])) + + # append start of strand (no 5' neighbor) to list of indexes for strand + neighbor_5 = int(data[3]) + if neighbor_5 == -1: + strand1_start = nuc_idx + strand1_idxs.append(strand1_start) + + # reconstruct strand using indices from oxDNA files + next_idx = nbrs_3p[strand1_start] + while next_idx >= 0: + strand1_idxs.append(next_idx) + next_idx = nbrs_3p[strand1_idxs[-1]] + + # assert that strand is correct length + self.assertEqual(expected_strand_length, len(strand1_idxs)) + + for i in range(expected_strand_length - 1): + strand1_nuc_idx1 = strand1_idxs[i] + strand1_nuc_idx2 = strand1_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s1_cmp1 = cm_poss[strand1_nuc_idx1] + s1_cmp2 = cm_poss[strand1_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x ** 2 for x in diff1]) + + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) def test_loopout_design(self) -> None: - """ 3 strands, one with a loopout - 0 7 13 - ^ loopout at 4 - [------>[------> + """ 2 strands, one with a loopout + 0 7 + ^ loopout at 4 of length = 4 bases + [------> <------] """ helix = [sc.Helix(max_offset=14)] design = sc.Design(helices=helix, grid=sc.square) design.strand(0, 0).to(4).loopout(0, 4).to(7) - design.strand(0, 7).move(-7) - design.strand(0, 7).to(14) - geometry = design.geometry - - helix_angle = math.pi * 2 / geometry.bases_per_turn + design.strand(0, 7).to(0) # expected values for verification - expected_num_nucleotides = 7 * 3 + 4 - expected_strand_length = 7 * 1 - # square of expected distance between adjacent nucleotide centers of mass - expected_adj_nuc_cm_dist2 = (2 * self.OX_BASE_DIST * math.sin(helix_angle / 2))**2 + (geometry.rise_per_base_pair * self.NM_TO_OX_UNITS)**2 + expected_num_nucleotides = 7 * 2 + 4 + expected_strand_1_length = 7 + 4 # strand 1 has loopout of 4 + expected_strand_2_length = 7 dat, top = design.to_oxdna_format() dat = dat.strip().split('\n') @@ -6006,15 +6087,14 @@ def test_loopout_design(self) -> None: strand1_idxs = [] strand2_idxs = [] - strand3_idxs = [] for nuc_idx, line in enumerate(top[1:]): data = line.strip().split() # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor self.assertEqual(4, len(data)) - # make sure there are only 3 strands + # make sure there are only 2 strands strand_num = int(data[0]) - self.assertIn(strand_num, [1, 2, 3]) + self.assertIn(strand_num, [1, 2]) # make sure base is valid base = data[1] self.assertIn(base, ['A', 'C', 'G', 'T']) @@ -6028,12 +6108,9 @@ def test_loopout_design(self) -> None: if strand_num == 1: strand1_start = nuc_idx strand1_idxs.append(strand1_start) - elif strand_num == 2: + else: strand2_start = nuc_idx strand2_idxs.append(strand2_start) - else: - strand3_start = nuc_idx - strand3_idxs.append(strand3_start) # reconstruct strands using indices from oxDNA files next_idx = nbrs_3p[strand1_start] @@ -6046,22 +6123,21 @@ def test_loopout_design(self) -> None: strand2_idxs.append(next_idx) next_idx = nbrs_3p[strand2_idxs[-1]] - next_idx = nbrs_3p[strand3_start] - while next_idx >= 0: - strand3_idxs.append(next_idx) - next_idx = nbrs_3p[strand3_idxs[-1]] - # assert that strands are the correct length - self.assertEqual(expected_strand_length + 4, len(strand1_idxs)) #1st strand has loopout of 4 - self.assertEqual(expected_strand_length, len(strand2_idxs)) - self.assertEqual(expected_strand_length, len(strand3_idxs)) + self.assertEqual(expected_strand_1_length, len(strand1_idxs)) + self.assertEqual(expected_strand_2_length, len(strand2_idxs)) - for i in range(expected_strand_length + 4 - 1): - # ignore nucleotides between domains - if i == 4: - continue + # calculate distance between nucleotides preceding and following loopout and confirm that it matches expected distance between nucleotides. + # loopout here is nucleotides 4 to 7, so check distance between nucleotides 3 to 8 + cm_poss_pre_loopout = cm_poss[strand1_idxs[3]] + cm_poss_post_loopout = cm_poss[strand1_idxs[8]] + diff = tuple([cm_poss_pre_loopout[j] - cm_poss_post_loopout[j] for j in range(3)]) + sqr_dist = sum([x ** 2 for x in diff]) + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist) + + for i in range(expected_strand_1_length - 1): - if i == 8: + if i in [3, 4, 5, 6, 7]: #skip nucleotide distances having to do with loopout, as these won't have regular distance between nucleotides (i = 3 denotes distance from 3 to 4, which includes loopout) continue strand1_nuc_idx1 = strand1_idxs[i] @@ -6075,25 +6151,18 @@ def test_loopout_design(self) -> None: diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) sqr_dist1 = sum([x ** 2 for x in diff1]) - #self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist1) #TODO: how to check distances between nucleotides in loopout? + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) - for i in range(expected_strand_length - 1): + for i in range(expected_strand_2_length - 1): # check adjacent nucleotide distances for second strand strand2_nuc_idx1 = strand2_idxs[i] strand2_nuc_idx2 = strand2_idxs[i+1] - strand3_nuc_idx1 = strand3_idxs[i] - strand3_nuc_idx2 = strand3_idxs[i + 1] # find the center of mass for adjacent nucleotides s2_cmp1 = cm_poss[strand2_nuc_idx1] s2_cmp2 = cm_poss[strand2_nuc_idx2] - s3_cmp1 = cm_poss[strand3_nuc_idx1] - s3_cmp2 = cm_poss[strand3_nuc_idx2] # calculate and verify squared distance between adjacent nucleotides in a domain diff2 = tuple([s2_cmp1[j]-s2_cmp2[j] for j in range(3)]) - diff3 = tuple([s3_cmp1[j] - s3_cmp2[j] for j in range(3)]) sqr_dist2 = sum([x**2 for x in diff2]) - sqr_dist3 = sum([x ** 2 for x in diff3]) - self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist2) - self.assertAlmostEqual(expected_adj_nuc_cm_dist2, sqr_dist3) \ No newline at end of file + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist2) \ No newline at end of file From 3d62c9296e56d16369d94205d7c6e84751a9d6d4 Mon Sep 17 00:00:00 2001 From: Anelise Cho <25128798+anelisecho@users.noreply.github.com> Date: Fri, 11 Jun 2021 11:49:47 -0700 Subject: [PATCH 07/18] Update scadnano_tests.py added insertion unit test --- tests/scadnano_tests.py | 92 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index f644e17c..49c3e02d 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -6036,6 +6036,98 @@ def test_deletion_design(self) -> None: self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) + def test_insertion_design(self) -> None: + """ + 0 7 + ^ insertion of length 1 + [------> + """ + helix = [sc.Helix(max_offset=10)] + design = sc.Design(helices=helix, grid=sc.square) + design.strand(0, 0).to(7) + design.add_insertion(helix=0, offset=4, length=1) + + # expected values for verification + expected_num_nucleotides = 8 + expected_strand_length = 8 + + dat, top = design.to_oxdna_format() + dat = dat.strip().split('\n') + top = top.strip().split('\n') + + # check length of output files are as expected (matches # of nucleotides plus header size) + self.assertEqual(expected_num_nucleotides + 3, len(dat)) + self.assertEqual(expected_num_nucleotides + 1, len(top)) + + # find relevant values for nucleotides + cm_poss = [] # center of mass position + nbrs_3p = [] + nbrs_5p = [] + + for line in dat[3:]: + data = line.strip().split() + # make sure there are 15 values per line (3 values per vector * 5 vectors per line) + # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity + self.assertEqual(15, len(data)) + + cm_poss.append(tuple([float(x) for x in data[0:3]])) + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + + # make sure normal vectors and backbone vectors are unit length + sqr_bb_vec = sum([x ** 2 for x in bb_vec]) + sqr_nm_vec = sum([x ** 2 for x in nm_vec]) + self.assertAlmostEqual(1.0, sqr_bb_vec) + self.assertAlmostEqual(1.0, sqr_nm_vec) + + for value in data[9:]: # values for velocity and angular velocity vectors are 0 + self.assertAlmostEqual(0, float(value)) + + strand1_idxs = [] + for nuc_idx, line in enumerate(top[1:]): + data = line.strip().split() + # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor + self.assertEqual(4, len(data)) + + # make sure there's only 1 strand + strand_num = int(data[0]) + self.assertEqual(1, strand_num) + # make sure base is valid + base = data[1] + self.assertIn(base, ['A', 'C', 'G', 'T']) + + nbrs_3p.append(int(data[2])) + nbrs_5p.append(int(data[3])) + + # append start of strand (no 5' neighbor) to list of indexes for strand + neighbor_5 = int(data[3]) + if neighbor_5 == -1: + strand1_start = nuc_idx + strand1_idxs.append(strand1_start) + + # reconstruct strand using indices from oxDNA files + next_idx = nbrs_3p[strand1_start] + while next_idx >= 0: + strand1_idxs.append(next_idx) + next_idx = nbrs_3p[strand1_idxs[-1]] + + # assert that strand is correct length + self.assertEqual(expected_strand_length, len(strand1_idxs)) + + for i in range(expected_strand_length - 1): + strand1_nuc_idx1 = strand1_idxs[i] + strand1_nuc_idx2 = strand1_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s1_cmp1 = cm_poss[strand1_nuc_idx1] + s1_cmp2 = cm_poss[strand1_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x ** 2 for x in diff1]) + + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) + def test_loopout_design(self) -> None: """ 2 strands, one with a loopout 0 7 From 49a259ece37a732c65a98386023126710b056eda Mon Sep 17 00:00:00 2001 From: David Doty Date: Fri, 11 Jun 2021 15:08:57 -0700 Subject: [PATCH 08/18] fixed Sphinx docstring error due to *'s being interpreted as emphasis --- scadnano/scadnano.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index ffe557da..c51a3b47 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -5632,10 +5632,11 @@ def _write_plates_default(self, directory: str, filename: Optional[str], strands workbook.save(filename_plate) def to_oxdna_format(self) -> Tuple[str, str]: - """ Exports to oxdna format. + """Exports to oxdna format. - :return: two strings that are the contents of the *.conf and *.top file - suitable for reading by oxdna (https://sulcgroup.github.io/oxdna-viewer/) + :return: + two strings that are the contents of the .conf and .top file + suitable for reading by oxdna (https://sulcgroup.github.io/oxdna-viewer/) """ system = _oxdna_convert(self) return system.ox_dna_output() From e6a9c8d95e84c40c76a0dfaee11e4b0be6aa96d8 Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 28 Jun 2021 08:57:17 -0700 Subject: [PATCH 09/18] updated variable names in oxDNA export code --- .gitignore | 1 + scadnano/scadnano.py | 73 +++++++++++++++++++++-------------------- tests/scadnano_tests.py | 12 +++---- tests_inputs/.gitignore | 1 + 4 files changed, 46 insertions(+), 41 deletions(-) create mode 100644 tests_inputs/.gitignore diff --git a/.gitignore b/.gitignore index d1839490..080f7fe0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pyache__/* *.egg-info /scadnano.zip .pypirc +tests/tests_outputs/cadnano_v2_export/* tests/tests_outputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.dna tests/tests_outputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.json tests/tests_outputs/cadnano_v2_export/test_2_stape_2_helix_origami_deletions_insertions.dna diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index c51a3b47..f656d6b5 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -72,7 +72,6 @@ from math import sqrt, sin, cos, pi from random import randint - default_scadnano_file_extension = 'sc' """Default filename extension when writing a scadnano file.""" @@ -5638,7 +5637,7 @@ def to_oxdna_format(self) -> Tuple[str, str]: two strings that are the contents of the .conf and .top file suitable for reading by oxdna (https://sulcgroup.github.io/oxdna-viewer/) """ - system = _oxdna_convert(self) + system = _convert_design_to_oxdna_system(self) return system.ox_dna_output() def write_oxdna_files(self, directory: str = '.', filename_no_extension: Optional[str] = None) -> None: @@ -5662,12 +5661,12 @@ def write_oxdna_files(self, directory: str = '.', filename_no_extension: Optiona _write_file_same_name_as_running_python_script(conf, 'conf', directory, filename_no_extension) _write_file_same_name_as_running_python_script(top, 'top', directory, filename_no_extension) - # @_docstring_parameter was used to substitute sc in for the filename extension, but it is # incompatible with .. code-block:: and caused a very strange and hard-to-determine error, # so I removed it. # @_docstring_parameter(default_extension=default_scadnano_file_extension) - def write_scadnano_file(self, directory: str = '.', filename: Optional[str] = None, extension: Optional[str] = None, + def write_scadnano_file(self, directory: str = '.', filename: Optional[str] = None, + extension: Optional[str] = None, suppress_indent: bool = True) -> None: """Write text file representing this :any:`Design`, suitable for reading by the scadnano web interface, @@ -6365,6 +6364,7 @@ def _create_directory_and_set_filename(directory: str, filename: str) -> str: relative_filename = os.path.join(directory, filename) return relative_filename + @dataclass(frozen=True) class _OxdnaVector: x: float = 0 @@ -6423,11 +6423,13 @@ def rotate(self, angle: float, axis: "_OxdnaVector") -> "_OxdnaVector": return u * self.dot(u) + (c * u.cross(self)).cross(u) - s * u.cross(self) + # Constants related to oxdna export _GROOVE_GAMMA = 20 _BASE_DIST = 0.6 _OXDNA_ORIGIN = _OxdnaVector(0, 0, 0) + # r, b, and n represent the oxDNA conf file vectors that describe a nucleotide # r is the position of the base # b is the backbone-base vector (in documentation as versor: more info on versors here https://eater.net/quaternions) @@ -6439,8 +6441,8 @@ class _OxdnaNucleotide: forward: _OxdnaVector base: str - v: _OxdnaVector = field(init=False, default=_OXDNA_ORIGIN) # velocity for oxDNA conf file - L: _OxdnaVector = field(init=False, default=_OXDNA_ORIGIN) # angular velocity for oxDNA conf file + v: _OxdnaVector = field(init=False, default=_OXDNA_ORIGIN) # velocity for oxDNA conf file + L: _OxdnaVector = field(init=False, default=_OXDNA_ORIGIN) # angular velocity for oxDNA conf file @property def b(self) -> _OxdnaVector: @@ -6454,6 +6456,7 @@ def r(self) -> _OxdnaVector: def n(self) -> _OxdnaVector: return self.forward + @dataclass(frozen=True) class _OxdnaStrand: nucleotides: List[_OxdnaNucleotide] = field(default_factory=list) @@ -6483,7 +6486,8 @@ def compute_bounding_box(self) -> _OxdnaVector: if min_vec is not None and max_vec is not None: # 5 is arbitrarily chosen so that the box has a bit of wiggle room - return 1.5 * (max_vec - min_vec + _OxdnaVector(5, 5, 5)) # changed + # 1.5 multiplier is to make all crossovers appear (advice from Oxdna authors) + return 1.5 * (max_vec - min_vec + _OxdnaVector(5, 5, 5)) # changed else: return _OxdnaVector(1, 1, 1) @@ -6491,7 +6495,7 @@ def ox_dna_output(self) -> Tuple[str, str]: bbox = self.compute_bounding_box() - dat_list = ['t = {}\nb = {} {} {}\nE = {} {} {}'.format(0, bbox.x, bbox.y, bbox.z, 0, 0, 0)] + dat_list = [f't = 0\nb = {bbox.x} {bbox.y} {bbox.z}\nE = 0 0 0'] top_list = [] nuc_count = 0 @@ -6512,22 +6516,24 @@ def ox_dna_output(self) -> Tuple[str, str]: n3 = -1 nuc_index += 1 - top_list.append('{} {} {} {}'.format(strand_count, nuc.base, n3, n5)) - dat_list.append('{} {} {} '.format(nuc.r.x, nuc.r.y, nuc.r.z) + - '{} {} {} '.format(nuc.b.x, nuc.b.y, nuc.b.z) + - '{} {} {} '.format(nuc.n.x, nuc.n.y, nuc.n.z) + - '{} {} {} '.format(nuc.v.x, nuc.v.y, nuc.v.z) + - '{} {} {}'.format(nuc.L.x, nuc.L.y, nuc.L.z)) + top_list.append(f'{strand_count} {nuc.base} {n3} {n5}') + dat_list.append(f'{nuc.r.x} {nuc.r.y} {nuc.r.z} ' + + f'{nuc.b.x} {nuc.b.y} {nuc.b.z} ' + + f'{nuc.n.x} {nuc.n.y} {nuc.n.z} ' + + f'{nuc.v.x} {nuc.v.y} {nuc.v.z} ' + + f'{nuc.L.x} {nuc.L.y} {nuc.L.z}') top = '\n'.join(top_list) + '\n' dat = '\n'.join(dat_list) + '\n' - top = '{} {}\n'.format(nuc_count, strand_count) + top + top = f'{nuc_count} {strand_count}\n' + top return dat, top + NM_TO_OX_UNITS = 1.0 / 0.8518 + # returns the origin, forward, and normal vectors of a helix def _oxdna_get_helix_vectors(design: Design, helix: Helix) -> Tuple[_OxdnaVector, _OxdnaVector, _OxdnaVector]: """ @@ -6583,6 +6589,7 @@ def _oxdna_get_helix_vectors(design: Design, helix: Helix) -> Tuple[_OxdnaVector origin = _OxdnaVector(x, y, z) * NM_TO_OX_UNITS return origin, forward, normal + # if no sequence exists on a domain, generate one def _oxdna_random_sequence(length: int) -> str: bases = 'ACGT' @@ -6592,11 +6599,11 @@ def _oxdna_random_sequence(length: int) -> str: return seq -def _oxdna_convert(design: Design) -> _OxdnaSystem: +def _convert_design_to_oxdna_system(design: Design) -> _OxdnaSystem: system = _OxdnaSystem() geometry = design.geometry step_rot = -360 / geometry.bases_per_turn - + # each entry is the number of insertions - deletions since the start of a given helix mod_map = {} for idx, helix in design.helices.items(): @@ -6608,10 +6615,10 @@ def _oxdna_convert(design: Design) -> _OxdnaSystem: for domain in strand.domains: if isinstance(domain, Domain): helix = design.helices[domain.helix] - for pos, num, in domain.insertions: - mod_map[domain.helix][pos - helix.min_offset] = num - for pos in domain.deletions: - mod_map[domain.helix][pos - helix.min_offset] = -1 + for ins_offset, ins_length in domain.insertions: + mod_map[domain.helix][ins_offset - helix.min_offset] = ins_length + for deletion in domain.deletions: + mod_map[domain.helix][deletion - helix.min_offset] = -1 # propagate the modifier so it stays consistent accross domains for idx, helix in design.helices.items(): @@ -6622,8 +6629,7 @@ def _oxdna_convert(design: Design) -> _OxdnaSystem: dom_strands: List[Tuple[_OxdnaStrand, bool]] = [] for domain in strand.domains: dom_strand = _OxdnaStrand() - # TODO: report error if DNA not assigned - seq = domain.dna_sequence() or _oxdna_random_sequence(domain.dna_length()) + seq = domain.dna_sequence() or 'T' * domain.dna_length() # handle normal domains if isinstance(domain, Domain): @@ -6632,16 +6638,11 @@ def _oxdna_convert(design: Design) -> _OxdnaSystem: if not domain.forward: normal = normal.rotate(-geometry.minor_groove_angle, forward) - seq = seq[::-1] - - # dict / set for insertions / deletions to make lookup easier and cheaper when there are lots of them - insertions = {} - for pos, num in domain.insertions: - insertions[pos] = num + seq = seq[::-1] # reverse DNA sequence - deletions = set() - for pos in domain.deletions: - deletions.add(pos) + # dict / set for insertions / deletions to make lookup cheaper when there are lots of them + deletions = set(domain.deletions) + insertions = dict(domain.insertions) # use Design.geometry field to figure out various distances # https://github.com/UC-Davis-molecular-computing/scadnano/blob/master/lib/src/state/geometry.dart @@ -6656,12 +6657,13 @@ def _oxdna_convert(design: Design) -> _OxdnaSystem: if offset in insertions: num = insertions[offset] for i in range(num): - r = origin + forward * (offset + mod - num + i) * geometry.rise_per_base_pair * NM_TO_OX_UNITS + r = origin + forward * ( + offset + mod - num + i) * geometry.rise_per_base_pair * NM_TO_OX_UNITS b = normal.rotate(step_rot * (offset + mod - num + i), forward) nuc = _OxdnaNucleotide(r, b, forward, seq[index]) dom_strand.nucleotides.append(nuc) index += 1 - + r = origin + forward * (offset + mod) * geometry.rise_per_base_pair * NM_TO_OX_UNITS b = normal.rotate(step_rot * (offset + mod), forward) nuc = _OxdnaNucleotide(r, b, forward, seq[index]) @@ -6680,7 +6682,8 @@ def _oxdna_convert(design: Design) -> _OxdnaSystem: # these will be updated later, for now we just need the base for i in range(domain.length): base = seq[i] - nuc = _OxdnaNucleotide(_OxdnaVector(), _OxdnaVector(0, -1, 0), _OxdnaVector(0, 0, 1), base) + nuc = _OxdnaNucleotide(_OxdnaVector(), _OxdnaVector(0, -1, 0), _OxdnaVector(0, 0, 1), + base) dom_strand.nucleotides.append(nuc) dom_strands.append((dom_strand, True)) diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index 49c3e02d..42a6f317 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -5746,19 +5746,19 @@ def test_basic_design(self) -> None: expected_strand_length = 7 * 2 dat, top = design.to_oxdna_format() - dat = dat.strip().split('\n') - top = top.strip().split('\n') + dat_lines = dat.strip().split('\n') + top_lines = top.strip().split('\n') # check length of output files are as expected (matches # of nucleotides plus header size) - self.assertEqual(expected_num_nucleotides + 3, len(dat)) - self.assertEqual(expected_num_nucleotides + 1, len(top)) + self.assertEqual(expected_num_nucleotides + 3, len(dat_lines)) + self.assertEqual(expected_num_nucleotides + 1, len(top_lines)) # find relevant values for nucleotides cm_poss = [] # center of mass position nbrs_3p = [] nbrs_5p = [] - for line in dat[3:]: + for line in dat_lines[3:]: data = line.strip().split() # make sure there are 15 values per line (3 values per vector * 5 vectors per line) # order of vectors: center of mass position, backbone base versor, normal versor, velocity, angular velocity (more info on versors: https://eater.net/quaternions) @@ -5779,7 +5779,7 @@ def test_basic_design(self) -> None: strand1_idxs = [] strand2_idxs = [] - for nuc_idx, line in enumerate(top[1:]): + for nuc_idx, line in enumerate(top_lines[1:]): data = line.strip().split() # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor self.assertEqual(4, len(data)) diff --git a/tests_inputs/.gitignore b/tests_inputs/.gitignore new file mode 100644 index 00000000..8eeea6a3 --- /dev/null +++ b/tests_inputs/.gitignore @@ -0,0 +1 @@ +cadnano_v2_export/ \ No newline at end of file From 1937acd76d07498d8dc6e7acc7bba64c5d106eb4 Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 28 Jun 2021 11:06:04 -0700 Subject: [PATCH 10/18] removed some PEP and mypy warnings and bumped version --- scadnano/scadnano.py | 54 ++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index f656d6b5..fa7fa81a 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -54,7 +54,7 @@ # commented out for now to support Py3.6, which does not support this feature # from __future__ import annotations -__version__ = "0.16.0" # version line; WARNING: do not remove or change this line or comment +__version__ = "0.16.1" # version line; WARNING: do not remove or change this line or comment import dataclasses from abc import abstractmethod, ABC, ABCMeta @@ -421,8 +421,8 @@ class M13Variant(enum.Enum): https://www.neb.com/products/n4040-m13mp18-single-stranded-dna - http://www.bayoubiolabs.com/biochemicat/vectors/pUCM13/ - """ + http://www.bayoubiolabs.com/biochemicat/vectors/pUCM13/ + """ # noqa p7560 = "p7560" """Variant of M13mp18 that is 7560 bases long. Available from, for example @@ -468,7 +468,7 @@ def m13(rotation: int = 5587, variant: M13Variant = M13Variant.p7249) -> str: :param rotation: rotation of circular strand. Valid values are 0 through length-1. :param variant: variant of M13 strand to use :return: M13 strand sequence - """ # noqa (suppress PEP warning) + """ # noqa seq = _m13_variants[variant] return _rotate_string(seq, rotation) @@ -3504,7 +3504,8 @@ def mandatory_field(ret_type: Type, json_map: Dict[str, Any], main_key: str, *, def optional_field(default_value: Any, json_map: Dict[str, Any], main_key: str, *, - legacy_keys: Sequence[str] = (), transformer=None) -> Any: + legacy_keys: Iterable[str] = (), + transformer: Optional[Callable[[Any], Any]] = None) -> Any: # like dict.get, except that it checks for multiple keys, and it can transform the value if it is the # wrong type by specifying transformer keys = (main_key,) + tuple(legacy_keys) @@ -6537,9 +6538,7 @@ def ox_dna_output(self) -> Tuple[str, str]: # returns the origin, forward, and normal vectors of a helix def _oxdna_get_helix_vectors(design: Design, helix: Helix) -> Tuple[_OxdnaVector, _OxdnaVector, _OxdnaVector]: """ - TODO: document functions/methods with docstrings - :param helix: - :param grid: + :param helix: Helix whose vectors we are getting :return: return tuple (origin, forward, normal) origin -- the starting point of the center of a helix, assumed to be at offset 0 forward -- the direction in which the helix propagates @@ -6557,9 +6556,9 @@ def _oxdna_get_helix_vectors(design: Design, helix: Helix) -> Tuple[_OxdnaVector normal = normal.rotate(-design.pitch_of_helix(helix), _OxdnaVector(1, 0, 0)) normal = normal.rotate(helix.roll, forward) - x = 0 - y = 0 - z = 0 + x: float = 0.0 + y: float = 0.0 + z: float = 0.0 if grid == Grid.none: if helix.position is not None: x = helix.position.x @@ -6570,24 +6569,24 @@ def _oxdna_get_helix_vectors(design: Design, helix: Helix) -> Tuple[_OxdnaVector # https://github.com/UC-Davis-molecular-computing/scadnano/blob/master/lib/src/util.dart#L799 # https://github.com/UC-Davis-molecular-computing/scadnano/blob/master/lib/src/util.dart#L664 # https://github.com/UC-Davis-molecular-computing/scadnano/blob/master/lib/src/util.dart#L706 + if helix.grid_position is None: + raise AssertionError('helix.grid_position should be assigned if grid is not Grid.none') + h, v = helix.grid_position if grid == Grid.square: - h, v = helix.grid_position x = h * geometry.distance_between_helices() y = v * geometry.distance_between_helices() if grid == Grid.hex: - h, v = helix.grid_position x = (h + (v % 2) / 2) * geometry.distance_between_helices() y = v * sqrt(3) / 2 * geometry.distance_between_helices() if grid == Grid.honeycomb: - h, v = helix.grid_position x = h * sqrt(3) / 2 * geometry.distance_between_helices() if h % 2 == 0: y = (v * 3 + (v % 2)) / 2 * geometry.distance_between_helices() else: y = (v * 3 - (v % 2) + 1) / 2 * geometry.distance_between_helices() - origin = _OxdnaVector(x, y, z) * NM_TO_OX_UNITS - return origin, forward, normal + origin_ = _OxdnaVector(x, y, z) * NM_TO_OX_UNITS + return origin_, forward, normal # if no sequence exists on a domain, generate one @@ -6607,10 +6606,12 @@ def _convert_design_to_oxdna_system(design: Design) -> _OxdnaSystem: # each entry is the number of insertions - deletions since the start of a given helix mod_map = {} for idx, helix in design.helices.items(): - mod_map[idx] = [0] * (helix.max_offset - helix.min_offset) + if helix.max_offset is not None: + mod_map[idx] = [0] * (helix.max_offset - helix.min_offset) + else: + raise AssertionError('helix.max_offset should be non-None') # insert each insertion / deletion as a postive / negative number - # TODO: report error if there is an insertion/deletion on one Domain and not the other for strand in design.strands: for domain in strand.domains: if isinstance(domain, Domain): @@ -6622,8 +6623,11 @@ def _convert_design_to_oxdna_system(design: Design) -> _OxdnaSystem: # propagate the modifier so it stays consistent accross domains for idx, helix in design.helices.items(): - for offset in range(helix.min_offset + 1, helix.max_offset): - mod_map[idx][offset] += mod_map[idx][offset - 1] + if helix.max_offset is not None: + for offset in range(helix.min_offset + 1, helix.max_offset): + mod_map[idx][offset] += mod_map[idx][offset - 1] + else: + raise AssertionError('helix.max_offset should be non-None') for strand in design.strands: dom_strands: List[Tuple[_OxdnaStrand, bool]] = [] @@ -6634,11 +6638,11 @@ def _convert_design_to_oxdna_system(design: Design) -> _OxdnaSystem: # handle normal domains if isinstance(domain, Domain): helix = design.helices[domain.helix] - origin, forward, normal = _oxdna_get_helix_vectors(design, helix) + origin_, forward, normal = _oxdna_get_helix_vectors(design, helix) if not domain.forward: normal = normal.rotate(-geometry.minor_groove_angle, forward) - seq = seq[::-1] # reverse DNA sequence + seq = seq[::-1] # reverse DNA sequence # dict / set for insertions / deletions to make lookup cheaper when there are lots of them deletions = set(domain.deletions) @@ -6657,14 +6661,14 @@ def _convert_design_to_oxdna_system(design: Design) -> _OxdnaSystem: if offset in insertions: num = insertions[offset] for i in range(num): - r = origin + forward * ( - offset + mod - num + i) * geometry.rise_per_base_pair * NM_TO_OX_UNITS + r = origin_ + forward * ( + offset + mod - num + i) * geometry.rise_per_base_pair * NM_TO_OX_UNITS b = normal.rotate(step_rot * (offset + mod - num + i), forward) nuc = _OxdnaNucleotide(r, b, forward, seq[index]) dom_strand.nucleotides.append(nuc) index += 1 - r = origin + forward * (offset + mod) * geometry.rise_per_base_pair * NM_TO_OX_UNITS + r = origin_ + forward * (offset + mod) * geometry.rise_per_base_pair * NM_TO_OX_UNITS b = normal.rotate(step_rot * (offset + mod), forward) nuc = _OxdnaNucleotide(r, b, forward, seq[index]) dom_strand.nucleotides.append(nuc) From 52a2a5e22f8e3febdfb4c196d496307d5ee3be62 Mon Sep 17 00:00:00 2001 From: Constantine Evans Date: Tue, 29 Jun 2021 16:29:10 +0100 Subject: [PATCH 11/18] fix purification/scale typo in to_idt_bulk_input_format --- scadnano/scadnano.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index fa7fa81a..e04ebc07 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -5312,7 +5312,7 @@ def to_idt_bulk_input_format(self, f'this strand should have been filtered out by _idt_strands_to_export') if strand.idt is not None: scale = strand.idt.scale - purification = strand.idt.scale + purification = strand.idt.purification else: scale = default_idt_scale purification = default_idt_purification From c9401f70e1569f2d6929f5397943f57508f5368c Mon Sep 17 00:00:00 2001 From: David Doty Date: Wed, 30 Jun 2021 08:12:12 -0700 Subject: [PATCH 12/18] closes #177; export to IDT plates rebalances strands among last two plates if the last plate has too few strands --- .github/workflows/run_unit_tests.yml | 4 +- .gitignore | 1 + scadnano/scadnano.py | 64 ++++++++- tests/scadnano_tests.py | 122 +++++++++++------- ...est_16_helix_origami_rectangle_no_twist.sc | 2 +- ...pe_2_helix_origami_deletions_insertions.sc | 2 +- ..._stape_2_helix_origami_extremely_simple.sc | 2 +- ...tape_2_helix_origami_extremely_simple_2.sc | 2 +- .../test_6_helix_origami_rectangle.sc | 2 +- .../cadnano_v2_export/test_circular_strand.sc | 2 +- .../test_export_design_with_helix_group.sc | 2 +- ...t_design_with_helix_group_not_same_grid.sc | 2 +- 12 files changed, 144 insertions(+), 63 deletions(-) diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index c3f08deb..e38dfddc 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -20,8 +20,8 @@ jobs: uses: s-weigand/setup-conda@v1 with: activate-conda: true - - name: Install xlwt with conda - run: conda install xlwt=1.3.0 + - name: Install xlwt and xlrd with conda + run: conda install xlwt=1.3.0 xlrd=2.0.1 - name: Install docutils with conda run: conda install docutils=0.16 - name: Test with unittest diff --git a/.gitignore b/.gitignore index 080f7fe0..c1a53056 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pyache__/* *.egg-info /scadnano.zip .pypirc +tests/tests_outputs/ tests/tests_outputs/cadnano_v2_export/* tests/tests_outputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.dna tests/tests_outputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.json diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index fa7fa81a..3387e987 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -3435,6 +3435,30 @@ def rows(self) -> List[str]: def cols(self) -> List[int]: return _96WELL_PLATE_COLS if self is PlateType.wells96 else _384WELL_PLATE_COLS + def num_wells_per_plate(self) -> int: + """ + :return: + number of wells in this plate type + """ + if self is PlateType.wells96: + return 96 + elif self is PlateType.wells384: + return 384 + else: + raise AssertionError('unreachable') + + def min_wells_per_plate(self) -> int: + """ + :return: + minimum number of wells in this plate type to avoid extra charge by IDT + """ + if self is PlateType.wells96: + return 24 + elif self is PlateType.wells384: + return 96 + else: + raise AssertionError('unreachable') + class _PlateCoordinate: @@ -3465,6 +3489,11 @@ def col(self) -> int: def well(self) -> str: return f'{self.row()}{self.col()}' + def advance_to_next_plate(self): + self._row_idx = 0 + self._col_idx = 0 + self._plate += 1 + 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) @@ -5543,7 +5572,7 @@ def write_idt_plate_excel_file(self, *, directory: str = '.', filename: str = No self._write_plates_assuming_explicit_plates_in_each_strand(directory, filename, strands_to_export) else: self._write_plates_default(directory=directory, filename=filename, - strands_to_export=strands_to_export, + strands=strands_to_export, plate_type=plate_type, warn_using_default_plates=warn_using_default_plates) @@ -5596,7 +5625,7 @@ def _setup_excel_file(directory: str, filename: Optional[str]) -> Tuple[str, Any workbook = xlwt.Workbook() return filename_plate, workbook - def _write_plates_default(self, directory: str, filename: Optional[str], strands_to_export: List[Strand], + def _write_plates_default(self, directory: str, filename: Optional[str], strands: List[Strand], plate_type: PlateType = PlateType.wells96, warn_using_default_plates: bool = True) -> None: plate_coord = _PlateCoordinate(plate_type=plate_type) @@ -5605,7 +5634,22 @@ def _write_plates_default(self, directory: str, filename: Optional[str], strands filename_plate, workbook = self._setup_excel_file(directory, filename) worksheet = self._add_new_excel_plate_sheet(f'plate{plate}', workbook) - for strand in strands_to_export: + + num_strands_per_plate = plate_type.num_wells_per_plate() + num_plates_needed = len(strands) // num_strands_per_plate + if len(strands) % num_strands_per_plate != 0: + num_plates_needed += 1 + + min_strands_per_plate = plate_type.min_wells_per_plate() + + num_strands_plates_except_final = max(0, (num_plates_needed - 1) * num_strands_per_plate) + num_strands_final_plate = len(strands) - num_strands_plates_except_final + final_plate_less_than_min_required = num_strands_final_plate < min_strands_per_plate + + num_strands_remaining = len(strands) + on_final_plate = num_plates_needed == 1 + + for strand in strands: if strand.idt is not None: if warn_using_default_plates and strand.idt.plate is not None: print( @@ -5620,7 +5664,19 @@ def _write_plates_default(self, directory: str, filename: Optional[str], strands worksheet.write(excel_row, 0, well) worksheet.write(excel_row, 1, strand.idt_export_name()) worksheet.write(excel_row, 2, strand.idt_dna_sequence()) - plate_coord.increment() + num_strands_remaining -= 1 + + # IDT charges extra for a plate with < 24 strands for 96-well plate + # or < 96 strands for 384-well plate. + # So if we would have fewer than that many on the last plate, + # shift some from the penultimate plate. + if not on_final_plate and \ + final_plate_less_than_min_required and \ + num_strands_remaining == min_strands_per_plate: + plate_coord.advance_to_next_plate() + else: + plate_coord.increment() + if plate != plate_coord.plate(): workbook.save(filename_plate) plate = plate_coord.plate() diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index 42a6f317..d615f737 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -1,14 +1,12 @@ import os -import sys -# sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import unittest -import unittest.mock as mock import re import json -import io import math from typing import Iterable, Union, Dict, Any +import xlrd + import scadnano as sc import scadnano.origami_rectangle as rect import scadnano.modifications as mod @@ -833,6 +831,36 @@ def test_to_idt_bulk_input_format__duplicate_names_different_purifications(self) # self.assertIn('two strands with same IDT name', printed) # self.assertIn('s1_r', printed) + def test_write_idt_plate_excel_file(self) -> None: + strand_len = 10 + + # add 10 strands in excess of 3 plates + for plate_type in [sc.PlateType.wells96, sc.PlateType.wells384]: + filename = f'test_excel_export_{plate_type.num_wells_per_plate()}.xls' + helices = [sc.Helix(max_offset=100) for _ in range(1)] + design = sc.Design(helices=helices, strands=[], grid=sc.square) + for strand_idx in range(3 * plate_type.num_wells_per_plate() + 10): + design.strand(0, strand_len * strand_idx).move(strand_len).with_name(f's{strand_idx}') + design.strands[-1].set_dna_sequence('T' * strand_len) + + design.write_idt_plate_excel_file(filename=filename, plate_type=plate_type) + + book = xlrd.open_workbook(filename) + self.assertEqual(4, book.nsheets) + for plate in range(4): + sheet = book.sheet_by_index(plate) + self.assertEqual(3, sheet.ncols) + + if plate == 2: # penultimate plate + expected_wells = plate_type.num_wells_per_plate() - plate_type.min_wells_per_plate() + 10 + elif plate == 3: # last plate + expected_wells = plate_type.min_wells_per_plate() + else: + expected_wells = plate_type.num_wells_per_plate() + + self.assertEqual(expected_wells + 1, sheet.nrows) + + os.remove(filename) class TestExportCadnanoV2(unittest.TestCase): @@ -988,7 +1016,7 @@ def test_circular_strand(self) -> None: helices = [sc.Helix(max_offset=24) for _ in range(2)] design = sc.Design(helices=helices, grid=sc.square) - design.strand(1,0).move(8).cross(0).move(-8).as_circular() + design.strand(1, 0).move(8).cross(0).move(-8).as_circular() design.write_scadnano_file(directory=self.input_path, filename=f'test_circular_strand.{self.ext}') design.export_cadnano_v2(directory=self.output_path, @@ -2758,23 +2786,21 @@ def test_set_helix_idx(self) -> None: class TestDesignPitchYawRollOfHelix(unittest.TestCase): def setUp(self) -> None: n = 'north' - helix = sc.Helix(max_offset=12, group=n, grid_position=(1,2), roll=4) + helix = sc.Helix(max_offset=12, group=n, grid_position=(1, 2), roll=4) - group_north = sc.HelixGroup(position=sc.Position3D(x=0, y=-200, z=0), grid=sc.square, pitch=12, yaw=40, roll=32) + group_north = sc.HelixGroup(position=sc.Position3D(x=0, y=-200, z=0), grid=sc.square, pitch=12, + yaw=40, roll=32) self.design = sc.Design(helices=[helix], groups={n: group_north}, strands=[]) self.helix = helix - def test_design_pitch_of_helix(self) -> None: - self.assertEqual(12, self.design.pitch_of_helix(self.helix)) - + self.assertEqual(12, self.design.pitch_of_helix(self.helix)) def test_design_yaw_of_helix(self) -> None: - self.assertEqual(40, self.design.yaw_of_helix(self.helix)) - + self.assertEqual(40, self.design.yaw_of_helix(self.helix)) def test_design_roll_of_helix(self) -> None: - self.assertEqual(36, self.design.roll_of_helix(self.helix)) + self.assertEqual(36, self.design.roll_of_helix(self.helix)) class TestHelixGroups(unittest.TestCase): @@ -2895,7 +2921,6 @@ def test_helix_groups_to_from_JSON(self) -> None: 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) -> None: helices = [ sc.Helix(max_offset=20, group="north"), @@ -3149,7 +3174,6 @@ def test_grid_design_level_converted_to_enum_from_string(self) -> None: self.assertTrue(type(grid) is sc.Grid) self.assertEqual(sc.Grid.square, grid) - def test_grid_helix_group_level_converted_to_enum_from_string(self) -> None: # reproduces an error where the grid was stored as a string instead of the Grid enum type json_str = ''' @@ -3735,7 +3759,6 @@ def test_only_individual_helices_specify_pitch_and_yaw(self) -> None: self.assertEqual(5, helix0.roll) self.assertEqual(pitch_25_yaw_19_group_name, helix0.group) - # Helix 1 should have been moved to a new helix group pitch_21_yaw_13_group_name = f'pitch_21.0_yaw_13.0' pitch_21_yaw_13_group = d.groups[pitch_21_yaw_13_group_name] @@ -3747,7 +3770,6 @@ def test_only_individual_helices_specify_pitch_and_yaw(self) -> None: self.assertEqual(3, len(d.groups)) - def test_only_helix_groups_specify_pitch_and_yaw(self) -> None: json_str = """ { @@ -3789,7 +3811,7 @@ def test_only_helix_groups_specify_pitch_and_yaw(self) -> None: helix0 = d.helices[0] helix1 = d.helices[1] - north_str= 'north' + north_str = 'north' south_str = 'south' north_group = d.groups[north_str] south_group = d.groups[south_str] @@ -3807,7 +3829,6 @@ def test_only_helix_groups_specify_pitch_and_yaw(self) -> None: self.assertEqual(98, south_group.yaw) self.assertEqual(south_str, helix1.group) - def test_both_helix_groups_and_helices_do_not_specify_pitch_nor_yaw(self) -> None: json_str = """ { @@ -3845,7 +3866,7 @@ def test_both_helix_groups_and_helices_do_not_specify_pitch_nor_yaw(self) -> Non helix0 = d.helices[0] helix1 = d.helices[1] - north_str= 'north' + north_str = 'north' south_str = 'south' north_group = d.groups[north_str] south_group = d.groups[south_str] @@ -3863,7 +3884,6 @@ def test_both_helix_groups_and_helices_do_not_specify_pitch_nor_yaw(self) -> Non self.assertEqual(0, south_group.yaw) self.assertEqual(south_str, helix1.group) - def test_multiple_helix_groups_helices_specify_pitch_and_yaw(self) -> None: json_str = """ { @@ -5716,6 +5736,7 @@ def test_dna_sequence_in__right_then_left_deletions_and_insertions(self) -> None # self.assertEqual("TTTTACG", ss1.dna_sequence_in(4, 10)) # self.assertEqual("TTTACGTACGT", ss1.dna_sequence_in(2, 10)) + class TestOxdnaExport(unittest.TestCase): def setUp(self) -> None: self.OX_UNITS_TO_NM = 0.8518 @@ -5725,8 +5746,9 @@ def setUp(self) -> None: self.HELIX_ANGLE = math.pi * 2 / self.BASES_PER_TURN self.RISE_PER_BASE_PAIR = 0.332 # square of expected distance between adjacent nucleotide centers of mass - self.EXPECTED_ADJ_NUC_CM_DIST2 = (2 * self.OX_BASE_DIST * math.sin(self.HELIX_ANGLE / 2)) ** 2 + (self.RISE_PER_BASE_PAIR * self.NM_TO_OX_UNITS) ** 2 - + self.EXPECTED_ADJ_NUC_CM_DIST2 = (2 * self.OX_BASE_DIST * math.sin(self.HELIX_ANGLE / 2)) ** 2 + ( + self.RISE_PER_BASE_PAIR * self.NM_TO_OX_UNITS) ** 2 + def test_basic_design(self) -> None: """ 2 double strands of length 7 connected across helices. 0 7 @@ -5754,7 +5776,7 @@ def test_basic_design(self) -> None: self.assertEqual(expected_num_nucleotides + 1, len(top_lines)) # find relevant values for nucleotides - cm_poss = [] # center of mass position + cm_poss = [] # center of mass position nbrs_3p = [] nbrs_5p = [] @@ -5765,16 +5787,16 @@ def test_basic_design(self) -> None: self.assertEqual(15, len(data)) cm_poss.append(tuple([float(x) for x in data[0:3]])) - bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector - nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector # make sure normal vectors and backbone vectors are unit length - sqr_bb_vec = sum([x**2 for x in bb_vec]) - sqr_nm_vec = sum([x**2 for x in nm_vec]) + sqr_bb_vec = sum([x ** 2 for x in bb_vec]) + sqr_nm_vec = sum([x ** 2 for x in nm_vec]) self.assertAlmostEqual(1.0, sqr_bb_vec) self.assertAlmostEqual(1.0, sqr_nm_vec) - for value in data[9:]: # values for velocity and angular velocity vectors are 0 + for value in data[9:]: # values for velocity and angular velocity vectors are 0 self.assertAlmostEqual(0, float(value)) strand1_idxs = [] @@ -5825,9 +5847,9 @@ def test_basic_design(self) -> None: continue strand1_nuc_idx1 = strand1_idxs[i] - strand1_nuc_idx2 = strand1_idxs[i+1] + strand1_nuc_idx2 = strand1_idxs[i + 1] strand2_nuc_idx1 = strand2_idxs[i] - strand2_nuc_idx2 = strand2_idxs[i+1] + strand2_nuc_idx2 = strand2_idxs[i + 1] # find the center of mass for adjacent nucleotides s1_cmp1 = cm_poss[strand1_nuc_idx1] @@ -5836,10 +5858,10 @@ def test_basic_design(self) -> None: s2_cmp2 = cm_poss[strand2_nuc_idx2] # calculate and verify squared distance between adjacent nucleotides in a domain - diff1 = tuple([s1_cmp1[j]-s1_cmp2[j] for j in range(3)]) - diff2 = tuple([s2_cmp1[j]-s2_cmp2[j] for j in range(3)]) - sqr_dist1 = sum([x**2 for x in diff1]) - sqr_dist2 = sum([x**2 for x in diff2]) + diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) + diff2 = tuple([s2_cmp1[j] - s2_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x ** 2 for x in diff1]) + sqr_dist2 = sum([x ** 2 for x in diff2]) self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist2) @@ -5853,7 +5875,8 @@ def test_honeycomb_design(self) -> None: | helix 2 +-------> """ - helices = [sc.Helix(grid_position=(1, 1), max_offset=8), sc.Helix(grid_position=(0, 1), max_offset=8), sc.Helix(grid_position=(0, 2), max_offset=8)] + helices = [sc.Helix(grid_position=(1, 1), max_offset=8), sc.Helix(grid_position=(0, 1), max_offset=8), + sc.Helix(grid_position=(0, 2), max_offset=8)] design = sc.Design(helices=helices, grid=sc.honeycomb) design.strand(0, 0).to(8).cross(1).move(-8).cross(2).to(8) @@ -6142,7 +6165,7 @@ def test_loopout_design(self) -> None: # expected values for verification expected_num_nucleotides = 7 * 2 + 4 - expected_strand_1_length = 7 + 4 # strand 1 has loopout of 4 + expected_strand_1_length = 7 + 4 # strand 1 has loopout of 4 expected_strand_2_length = 7 dat, top = design.to_oxdna_format() @@ -6154,7 +6177,7 @@ def test_loopout_design(self) -> None: self.assertEqual(expected_num_nucleotides + 1, len(top)) # find relevant values for nucleotides - cm_poss = [] # center of mass position + cm_poss = [] # center of mass position nbrs_3p = [] nbrs_5p = [] @@ -6165,16 +6188,16 @@ def test_loopout_design(self) -> None: self.assertEqual(15, len(data)) cm_poss.append(tuple([float(x) for x in data[0:3]])) - bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector - nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector # make sure normal vectors and backbone vectors are unit length - sqr_bb_vec = sum([x**2 for x in bb_vec]) - sqr_nm_vec = sum([x**2 for x in nm_vec]) + sqr_bb_vec = sum([x ** 2 for x in bb_vec]) + sqr_nm_vec = sum([x ** 2 for x in nm_vec]) self.assertAlmostEqual(1.0, sqr_bb_vec) self.assertAlmostEqual(1.0, sqr_nm_vec) - for value in data[9:]: # values for velocity and angular velocity vectors are 0 + for value in data[9:]: # values for velocity and angular velocity vectors are 0 self.assertAlmostEqual(0, float(value)) strand1_idxs = [] @@ -6229,7 +6252,8 @@ def test_loopout_design(self) -> None: for i in range(expected_strand_1_length - 1): - if i in [3, 4, 5, 6, 7]: #skip nucleotide distances having to do with loopout, as these won't have regular distance between nucleotides (i = 3 denotes distance from 3 to 4, which includes loopout) + if i in [3, 4, 5, 6, + 7]: # skip nucleotide distances having to do with loopout, as these won't have regular distance between nucleotides (i = 3 denotes distance from 3 to 4, which includes loopout) continue strand1_nuc_idx1 = strand1_idxs[i] @@ -6245,16 +6269,16 @@ def test_loopout_design(self) -> None: self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) - for i in range(expected_strand_2_length - 1): # check adjacent nucleotide distances for second strand + for i in range(expected_strand_2_length - 1): # check adjacent nucleotide distances for second strand strand2_nuc_idx1 = strand2_idxs[i] - strand2_nuc_idx2 = strand2_idxs[i+1] + strand2_nuc_idx2 = strand2_idxs[i + 1] # find the center of mass for adjacent nucleotides s2_cmp1 = cm_poss[strand2_nuc_idx1] s2_cmp2 = cm_poss[strand2_nuc_idx2] # calculate and verify squared distance between adjacent nucleotides in a domain - diff2 = tuple([s2_cmp1[j]-s2_cmp2[j] for j in range(3)]) - sqr_dist2 = sum([x**2 for x in diff2]) + diff2 = tuple([s2_cmp1[j] - s2_cmp2[j] for j in range(3)]) + sqr_dist2 = sum([x ** 2 for x in diff2]) - self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist2) \ No newline at end of file + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist2) diff --git a/tests_inputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.sc b/tests_inputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.sc index 937aa5a7..eb393316 100644 --- a/tests_inputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.sc +++ b/tests_inputs/cadnano_v2_export/test_16_helix_origami_rectangle_no_twist.sc @@ -1,5 +1,5 @@ { - "version": "0.16.0", + "version": "0.16.1", "grid": "square", "helices": [ {"max_offset": 448, "grid_position": [0, 0]}, diff --git a/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_deletions_insertions.sc b/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_deletions_insertions.sc index cfc25b74..1676952a 100644 --- a/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_deletions_insertions.sc +++ b/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_deletions_insertions.sc @@ -1,5 +1,5 @@ { - "version": "0.16.0", + "version": "0.16.1", "grid": "square", "helices": [ {"grid_position": [0, 0]}, diff --git a/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple.sc b/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple.sc index 130d6941..44b512e2 100644 --- a/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple.sc +++ b/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple.sc @@ -1,5 +1,5 @@ { - "version": "0.16.0", + "version": "0.16.1", "grid": "square", "helices": [ {"grid_position": [0, 0]}, diff --git a/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple_2.sc b/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple_2.sc index 46003fe4..e503d730 100644 --- a/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple_2.sc +++ b/tests_inputs/cadnano_v2_export/test_2_stape_2_helix_origami_extremely_simple_2.sc @@ -1,5 +1,5 @@ { - "version": "0.16.0", + "version": "0.16.1", "grid": "square", "helices": [ {"grid_position": [0, 0]}, diff --git a/tests_inputs/cadnano_v2_export/test_6_helix_origami_rectangle.sc b/tests_inputs/cadnano_v2_export/test_6_helix_origami_rectangle.sc index aa030c0a..3e7de816 100644 --- a/tests_inputs/cadnano_v2_export/test_6_helix_origami_rectangle.sc +++ b/tests_inputs/cadnano_v2_export/test_6_helix_origami_rectangle.sc @@ -1,5 +1,5 @@ { - "version": "0.16.0", + "version": "0.16.1", "grid": "square", "helices": [ {"max_offset": 192, "grid_position": [0, 0]}, diff --git a/tests_inputs/cadnano_v2_export/test_circular_strand.sc b/tests_inputs/cadnano_v2_export/test_circular_strand.sc index 4c3f5705..b2c24422 100644 --- a/tests_inputs/cadnano_v2_export/test_circular_strand.sc +++ b/tests_inputs/cadnano_v2_export/test_circular_strand.sc @@ -1,5 +1,5 @@ { - "version": "0.16.0", + "version": "0.16.1", "grid": "square", "helices": [ {"max_offset": 24, "grid_position": [0, 0]}, diff --git a/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group.sc b/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group.sc index 477ff07a..f1fe48e2 100644 --- a/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group.sc +++ b/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group.sc @@ -1,5 +1,5 @@ { - "version": "0.16.0", + "version": "0.16.1", "groups": { "east": { "position": {"x": 10, "y": 0, "z": 0}, diff --git a/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group_not_same_grid.sc b/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group_not_same_grid.sc index 12f5d39e..c487b708 100644 --- a/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group_not_same_grid.sc +++ b/tests_inputs/cadnano_v2_export/test_export_design_with_helix_group_not_same_grid.sc @@ -1,5 +1,5 @@ { - "version": "0.16.0", + "version": "0.16.1", "groups": { "east": { "position": {"x": 10, "y": 0, "z": 0}, From 92699a7398323b6d306bfcf8388bf932d2ccc906 Mon Sep 17 00:00:00 2001 From: David Doty Date: Wed, 30 Jun 2021 08:16:07 -0700 Subject: [PATCH 13/18] updated IDT Excel export API documentation to discuss rebalancing among last two plates to ensure minimum strand count --- scadnano/scadnano.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 3387e987..453a88d1 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -5515,6 +5515,11 @@ def write_idt_plate_excel_file(self, *, directory: str = '.', filename: str = No For instance, if the script is named ``my_origami.py``, then the sequences will be written to ``my_origami.xls``. + If the last plate as fewer than 24 strands for a 96-well plate, or fewer than 96 strands for a + 384-well plate, then the last two plates are rebalanced to ensure that each plate has at least + that number of strands, because IDT charges extra for a plate with too few strands: + https://www.idtdna.com/pages/products/custom-dna-rna/dna-oligos/custom-dna-oligos + :param directory: specifies a directory in which to place the file, either absolute or relative to the current working directory. Default is the current working directory. From 1f0d2c5de9266d0b3bf37894277964e0436fd94c Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 5 Jul 2021 12:36:10 -0700 Subject: [PATCH 14/18] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4674e0f..04c6cb75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,7 +152,7 @@ So the steps for committing to the master branch are: 3. Ensure all unit tests pass. -4. In the Python repo, ensure that the documentation is generated without errors. From the subfolder `doc`, run the command `make html`, ensure there are no errors, and inspect the documentation it generates in the folder `build`. +4. In the Python repo, ensure that the documentation is generated without errors. First, run `pip install sphinx sphinx_rtd_theme`. Then, from within the subfolder `doc`, run the command `make html` (or `make.bat html` on Windows), ensure there are no errors, and inspect the documentation it generates in the folder `build`. 5. Create a PR to merge changes from dev into master. From 7f8f336239ffe774b6597096d5cc79e6db3d46a1 Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 5 Jul 2021 13:04:43 -0700 Subject: [PATCH 15/18] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04c6cb75..32a5012c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,7 +152,7 @@ So the steps for committing to the master branch are: 3. Ensure all unit tests pass. -4. In the Python repo, ensure that the documentation is generated without errors. First, run `pip install sphinx sphinx_rtd_theme`. Then, from within the subfolder `doc`, run the command `make html` (or `make.bat html` on Windows), ensure there are no errors, and inspect the documentation it generates in the folder `build`. +4. In the Python repo, ensure that the documentation is generated without errors. First, run `pip install sphinx sphinx_rtd_theme`. This installs [Sphinx](https://www.sphinx-doc.org/en/master/), which is the most well-supported documentation generator for Python. (It's not very friendly, the syntax for things like links in docstrings is awkward, but it's well supported, so we use it.) Then, from within the subfolder `doc`, run the command `make html` (or `make.bat html` on Windows), ensure there are no errors, and inspect the documentation it generates in the folder `_build`. 5. Create a PR to merge changes from dev into master. From c6dcd00f74db4d733d77ace8b9cf41427ebf86ff Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 5 Jul 2021 13:13:23 -0700 Subject: [PATCH 16/18] Update CONTRIBUTING.md --- CONTRIBUTING.md | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32a5012c..eaf93c06 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,13 @@ or post questions as issues on the [issues page](https://github.com/UC-Davis-mol +## Table of contents +* [What should I know before I get started?](#what-should-i-know-before-i-get-started) +* [Cloning](#cloning) +* [Pushing to the repository dev branch and documenting changes (done on all updates)](#pushing-to-the-repository-dev-branch-and-documenting-changes-done-on-all-updates) +* [Pushing to the repository main branch and documenting changes (done less frequently)](#pushing-to-the-repository-main-branch-and-documenting-changes-done-less-frequently) +* [Styleguide](#styleguide) @@ -54,10 +60,8 @@ We use [git](https://git-scm.com/docs/gittutorial) and [GitHub](https://guides.g -## Making Contributions - -### Cloning +## Cloning The first step is cloning the repository so you have it available locally. @@ -82,7 +86,7 @@ git checkout dev -### Pushing to the repository dev branch and documenting changes (done on all updates) +## Pushing to the repository dev branch and documenting changes (done on all updates) Minor changes, such as updating README, adding example files, etc., can be committed directly to the `dev` branch. (Note: currently this option is only available to administrators; other contributors should follow the instructions below.) @@ -90,7 +94,7 @@ For any more significant change that is made (e.g., closing an issue, adding a n 1. If there is not already a GitHub issue describing the desired change, make one. Make sure that its title is a self-contained description, and that it describes the change we would like to make to the software. For example, *"problem with importing gridless design"* is a bad title. A better title is *"fix problem where importing gridless design with negative x coordinates throws exception"*. -2. Make a new branch specifically for the issue. Base this branch off of `dev` (**WARNING**: in GitHub desktop, the default is to base it off of `master`, so switch that). The title of the issue (with appropriate hyphenation) is a good name for the branch. (In GitHub Desktop, if you paste the title of the issue, it automatically adds the hyphens.) +2. Make a new branch specifically for the issue. Base this branch off of `dev` (**WARNING**: in GitHub desktop, the default is to base it off of `main`, so switch that). The title of the issue (with appropriate hyphenation) is a good name for the branch. (In GitHub Desktop, if you paste the title of the issue, it automatically adds the hyphens.) 3. If it is about fixing a bug, *first* add tests to reproduce the bug before working on fixing it. (This is so-called [test-driven development](https://www.google.com/search?q=test-driven+development)) @@ -102,7 +106,7 @@ For any more significant change that is made (e.g., closing an issue, adding a n 7. Commit the changes. In the commit message, reference the issue using the phrase "fixes #123" or "closes #123" (see [here](https://docs.github.com/en/enterprise/2.16/user/github/managing-your-work-on-github/closing-issues-using-keywords)). Also, in the commit message, describe the issue that was fixed (one easy way is to copy the title of the issue); this message will show up in automatically generated release notes, so this is part of the official documentation of what changed. -8. Create a pull request (PR). **WARNING:** by default, it will want to merge into the `master` branch. Change the destination branch to `dev`. +8. Create a pull request (PR). **WARNING:** by default, it will want to merge into the `main` branch. Change the destination branch to `dev`. 9. Wait for all checks to complete (see next section), and then merge the changes from the new branch into `dev`. This will typically require someone else to review the code first and possibly request changes. @@ -118,13 +122,13 @@ For any more significant change that is made (e.g., closing an issue, adding a n -### Pushing to the repository master branch and documenting changes (done less frequently) +## Pushing to the repository main branch and documenting changes (done less frequently) -Less frequently, pull requests (abbreviated PR) can be made from `dev` to `master`, but make sure that `dev` is working before merging to `master` as all changes to `master` are automatically built and deployed to [PyPI](https://pypi.org/project/scadnano/), which is the site hosting the pip installation package, and [readthedocs](https://scadnano-python-package.readthedocs.io/en/latest/), which is the site hosting the API documentation. That is, changes to master immediately affect users installing via pip or reading online documention, so it is critical that these work. +Less frequently, pull requests (abbreviated PR) can be made from `dev` to `main`, but make sure that `dev` is working before merging to `main` as all changes to `main` are automatically built and deployed to [PyPI](https://pypi.org/project/scadnano/), which is the site hosting the pip installation package, and [readthedocs](https://scadnano-python-package.readthedocs.io/en/latest/), which is the site hosting the API documentation. That is, changes to main immediately affect users installing via pip or reading online documention, so it is critical that these work. **WARNING:** Always wait for the checks to complete. This is important to ensure that unit tests pass. -We have an automated release system (through a GitHub action) that automatically creates release notes when changes are merged into the master branch. +We have an automated release system (through a GitHub action) that automatically creates release notes when changes are merged into the main branch. Although the GitHub web interface abbreviates long commit messages, the full commit message is included for each commit in a PR. @@ -140,27 +144,27 @@ Breaking changes should be announced explicitly, perhaps in the commit message, See here for an example: https://github.com/UC-Davis-molecular-computing/scadnano-python-package/releases/tag/v0.10.0 -So the steps for committing to the master branch are: +So the steps for committing to the main branch are: 1. If necessary, follow the instructions above to merge changes from a temporary branch to the `dev` branch. There will typically be several of these. Despite GitHub's suggestions to keep commit messages short and put longer text in descriptions, because only the commit message is included in the release notes, it's okay to put more detail in the message (but very long stuff should go in the description, or possibly documentation such as the README.md file). One of the changes committed should change the version number. We follow [semantic versioning](https://semver.org/). This is a string of the form `"MAJOR.MINOR.PATCH"`, e.g., `"0.9.3"` - - For the web interface repo scadnano, this is located at the top of the file https://github.com/UC-Davis-molecular-computing/scadnano/blob/master/lib/src/constants.dart - - For the Python library repo scadnano-python-package, this is located in two places: the bottom of the file https://github.com/UC-Davis-molecular-computing/scadnano-python-package/blob/master/scadnano/_version.py (as `__version__ = "0.9.3"` or something similar) and the near the top of the file https://github.com/UC-Davis-molecular-computing/scadnano-python-package/blob/master/scadnano/scadnano.py (as `__version__ = "0.9.3"` or something similar). This latter one is only there for users who do not install from PyPI, and who simply download the file scadnano.py to put it in a directory with their script). + - For the web interface repo scadnano, this is located at the top of the file https://github.com/UC-Davis-molecular-computing/scadnano/blob/main/lib/src/constants.dart + - For the Python library repo scadnano-python-package, this is located in two places: the bottom of the file https://github.com/UC-Davis-molecular-computing/scadnano-python-package/blob/main/scadnano/_version.py (as `__version__ = "0.9.3"` or something similar) and the near the top of the file https://github.com/UC-Davis-molecular-computing/scadnano-python-package/blob/main/scadnano/scadnano.py (as `__version__ = "0.9.3"` or something similar). This latter one is only there for users who do not install from PyPI, and who simply download the file scadnano.py to put it in a directory with their script). The PATCH version numbers are not always synced between the two repos, but, they should stay synced on MAJOR and MINOR versions. **Note:** right now this isn't quite true since MINOR versions deal with backwards-compatible feature additions, and some features are supported on one but not the other; e.g., modifications can be made in the Python package but not the web interface, and calculating helix rolls/positions from crossovers can be done in the web interface but not the Python package. But post-version-1.0.0, the major and minor versions of the should be enforced. 3. Ensure all unit tests pass. -4. In the Python repo, ensure that the documentation is generated without errors. First, run `pip install sphinx sphinx_rtd_theme`. This installs [Sphinx](https://www.sphinx-doc.org/en/master/), which is the most well-supported documentation generator for Python. (It's not very friendly, the syntax for things like links in docstrings is awkward, but it's well supported, so we use it.) Then, from within the subfolder `doc`, run the command `make html` (or `make.bat html` on Windows), ensure there are no errors, and inspect the documentation it generates in the folder `_build`. +4. In the Python repo, ensure that the documentation is generated without errors. First, run `pip install sphinx sphinx_rtd_theme`. This installs [Sphinx](https://www.sphinx-doc.org/en/main/), which is the most well-supported documentation generator for Python. (It's not very friendly, the syntax for things like links in docstrings is awkward, but it's well supported, so we use it.) Then, from within the subfolder `doc`, run the command `make html` (or `make.bat html` on Windows), ensure there are no errors, and inspect the documentation it generates in the folder `_build`. -5. Create a PR to merge changes from dev into master. +5. Create a PR to merge changes from dev into main. 6. One the PR is reviewed and approved, do the merge. 7. Once the PR changes are merged, a release will be automatically created here: https://github.com/UC-Davis-molecular-computing/scadnano/releases or https://github.com/UC-Davis-molecular-computing/scadnano-python-package/releases. It will have a title that is a placerholder, which is a reminder to change its title and tag. Each commit will be documented, with the commit message (but not description) included in the release notes. -8. Change *both* the title *and* tag to the version number with a `v` prepended, e.g., `v0.9.3`. It is imperative to change the tag before the next merge into master, or else the release (which defaults to the tag `latest`) will be overwritten. +8. Change *both* the title *and* tag to the version number with a `v` prepended, e.g., `v0.9.3`. It is imperative to change the tag before the next merge into main, or else the release (which defaults to the tag `latest`) will be overwritten. From 4c8b0e025a89b5632beb740b2e66b177cbf8e887 Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 5 Jul 2021 13:38:35 -0700 Subject: [PATCH 17/18] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eaf93c06..4a7f34c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -124,7 +124,7 @@ For any more significant change that is made (e.g., closing an issue, adding a n ## Pushing to the repository main branch and documenting changes (done less frequently) -Less frequently, pull requests (abbreviated PR) can be made from `dev` to `main`, but make sure that `dev` is working before merging to `main` as all changes to `main` are automatically built and deployed to [PyPI](https://pypi.org/project/scadnano/), which is the site hosting the pip installation package, and [readthedocs](https://scadnano-python-package.readthedocs.io/en/latest/), which is the site hosting the API documentation. That is, changes to main immediately affect users installing via pip or reading online documention, so it is critical that these work. +Less frequently, pull requests (abbreviated PR) can be made from `dev` to `main`, but make sure that `dev` is working before merging to `main` as all changes to `main` are automatically built and deployed to [PyPI](https://pypi.org/project/scadnano/), which is the site hosting the pip installation package, and [readthedocs](https://scadnano-python-package.readthedocs.io/en/latest/), which is the site hosting the API documentation. That is, changes to main immediately affect users installing via pip or reading online documentation, so it is critical that these work. **WARNING:** Always wait for the checks to complete. This is important to ensure that unit tests pass. From f9968340605bc401507545d14d485c8dfeeb6bfe Mon Sep 17 00:00:00 2001 From: David Doty Date: Sun, 18 Jul 2021 10:45:28 -0700 Subject: [PATCH 18/18] closes #139 (not this commit actually, but I want this to show up in the release notes) --- scadnano/scadnano.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 74699af9..d2961a64 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -82,7 +82,7 @@ def _pairwise(iterable: Iterable) -> Iterable: """s -> (s0,s1), (s1,s2), (s2, s3), ...""" a, b = itertools.tee(iterable) next(b, None) - return zip(a, b) + return zip(a, b) # for putting evaluated expressions in docstrings