From ed0ea3bebf7ea1ae3d6af62e0872977c69c0fc5d Mon Sep 17 00:00:00 2001 From: David Doty Date: Tue, 25 Aug 2020 07:10:18 -0700 Subject: [PATCH 1/3] removed example; made StrandBuilder._strand private and added getter that raiases exception if Strand has not been created yet --- examples/flatish_sst_ribbon.py | 162 --------- examples/output_designs/flatish_sst_ribbon.sc | 321 ------------------ scadnano/scadnano.py | 46 +-- 3 files changed, 27 insertions(+), 502 deletions(-) delete mode 100644 examples/flatish_sst_ribbon.py delete mode 100644 examples/output_designs/flatish_sst_ribbon.sc diff --git a/examples/flatish_sst_ribbon.py b/examples/flatish_sst_ribbon.py deleted file mode 100644 index 18aaec16..00000000 --- a/examples/flatish_sst_ribbon.py +++ /dev/null @@ -1,162 +0,0 @@ -import argparse - -import scadnano as sc - - -def main() -> None: - parser = argparse.ArgumentParser( - description='Create a scadnano design for a flat(ish) SST zig-zag ribbon of a specified width.') - upper_limit_width = 50 - parser.add_argument('-w', '--width', required=True, type=int, - help='width of ribbon (number of tiles in one zig or one zag); ' - f'must be even number between 4 and {upper_limit_width}', - choices=range(4, upper_limit_width + 1, 2)) - parser.add_argument('-d', '--duple', action='store_true', - help='whether to use duples (double-tiles) to turn direction; ' - 'otherwise 1.5-tiles are used') - args = parser.parse_args() - width: int = args.width - duple: bool = args.duple - design = create_design(width=width, duple=duple) - print(design.to_json()) - design.write_scadnano_file(directory='output_designs') - - -def create_design(width: int, duple: bool) -> sc.Design: - helices = [sc.Helix() for _ in range(width + 4)] - design = sc.Design(helices=helices, grid=sc.Grid.none) - - add_tick_marks(design) - add_zig_tiles(design, width) - add_zag_tiles(design, width) - add_zig_seed_tiles(design, width) - add_duples(design, width, duple) - add_seed(design, width) - - return design - - -def add_tick_marks(design: sc.Design) -> None: - num_helices = len(design.helices) - for idx, helix in design.helices.items(): - tick_start = num_helices - idx - 2 - if tick_start % 2 == 1: - tick_start += 1 - helix.major_tick_start = tick_start - helix.major_tick_periodic_distances = [12, 9] if idx % 2 == 0 else [10, 11] - - -def add_zig_tiles(design: sc.Design, width: int) -> None: - green = sc.Color(hex_string='#009900') - low_helix_idx = 4 - high_helix_idx = low_helix_idx + width - 3 - offset_5p_even = design.helices[low_helix_idx].major_tick_start + 12 + (width // 2 + 1) * 21 - offset_5p_odd = offset_5p_even - 12 - for idx in range(low_helix_idx, high_helix_idx + 1, 2): - # 12-9-11-10 strand ("even" strand) - design.strand(idx, offset_5p_even).move(-12).move(-9) \ - .cross(idx - 1).move(11).move(10).with_color(green) - offset_5p_even -= 23 - - # 11-10-12-9 strand ("odd" strand) - design.strand(idx + 1, offset_5p_odd).move(-11).move(-10) \ - .cross(idx).move(12).move(9).with_color(green) - offset_5p_odd -= 23 - - -def add_zag_tiles(design: sc.Design, width: int) -> None: - red = sc.Color(hex_string='#ff0000') - low_helix_idx = 3 - high_helix_idx = low_helix_idx + width - 3 - offset_5p_odd = design.helices[low_helix_idx].major_tick_start + (width // 2 + 1) * 21 - offset_5p_even = offset_5p_odd - 11 - for idx in range(low_helix_idx, high_helix_idx + 1, 2): - # 11-10-12-9 strand ("odd" strand) - design.strand(idx, offset_5p_odd).move(-11).move(-10) \ - .cross(idx - 1).move(12).move(9).with_color(red) - offset_5p_odd -= 23 - - # 12-9-11-10 strand ("even" strand) - design.strand(idx + 1, offset_5p_even).move(-12).move(-9) \ - .cross(idx).move(11).move(10).with_color(red) - offset_5p_even -= 23 - - -def add_zig_seed_tiles(design: sc.Design, width: int) -> None: - dark_green = sc.Color(hex_string='#006600') - low_helix_idx = 1 - high_helix_idx = low_helix_idx + width - 2 - offset_5p_odd = design.helices[low_helix_idx].major_tick_start + (width // 2 + 1) * 21 - offset_5p_even = offset_5p_odd - 11 - - for idx in range(low_helix_idx, high_helix_idx + 1, 2): - # 11-10-12-9 strand ("odd" strand) - design.strand(idx, offset_5p_odd).move(-11).move(-10) \ - .cross(idx - 1).move(12).move(9).with_color(dark_green) - offset_5p_odd -= 23 - - if idx < high_helix_idx: - # 12-9-11-10 strand ("even" strand) - design.strand(idx + 1, offset_5p_even).move(-12).move(-9) \ - .cross(idx).move(11).move(10).with_color(dark_green) - offset_5p_even -= 23 - - -def duple_at(design: sc.Design, color: sc.Color, duple: bool, start_offset: int, top_helix: int) -> None: - mid_helix = top_helix - 1 - bot_helix = mid_helix - 1 - if duple: - design.strand(top_helix, start_offset) \ - .move(-11).move(-10) \ - .cross(mid_helix) \ - .move(4).loopout(mid_helix, 4).move(-4) \ - .move(-9) \ - .cross(bot_helix) \ - .move(11).move(10) \ - .cross(mid_helix) \ - .move(-4).loopout(mid_helix, 4).move(4) \ - .move(9) \ - .with_color(color) - else: - raise NotImplementedError() - - -def add_seed_duple(design: sc.Design, width: int, duple: bool, color: sc.Color) -> None: - top_helix = width + 1 - start_offset = 2 + 21 * 2 - duple_at(design, color, duple, start_offset, top_helix) - - -def add_zig_to_zag_duple(design: sc.Design, width: int, duple: bool, color: sc.Color) -> None: - top_helix = width + 3 - start_offset = 21 * 3 - duple_at(design, color, duple, start_offset, top_helix) - - -def add_zag_to_zig_duple(design: sc.Design, width: int, duple: bool, color: sc.Color) -> None: - top_helix = 3 - start_offset = design.helices[top_helix].major_tick_start + (width // 2 + 2) * 21 - duple_at(design, color, duple, start_offset, top_helix) - - -def add_duples(design: sc.Design, width: int, duple: bool) -> None: - black = sc.Color(hex_string='#000000') - add_seed_duple(design, width, duple, black) - add_zig_to_zag_duple(design, width, duple, black) - add_zag_to_zig_duple(design, width, duple, black) - - -def add_seed(design: sc.Design, width: int) -> None: - blue = sc.Color(hex_string='#0066cc') - start_helix = 0 - start_offset = design.helices[start_helix].major_tick_start + (width // 2 + 1) * 21 - strand_builder = design.strand(start_helix, start_offset).move(-9) - for helix in range(1, width + 1, 2): - strand_builder.move(-12).loopout(helix, 8).move(-11) - if helix < width: - strand_builder.loopout(helix + 1, 8) - strand_builder.with_color(blue) - - -if __name__ == '__main__': - main() diff --git a/examples/output_designs/flatish_sst_ribbon.sc b/examples/output_designs/flatish_sst_ribbon.sc deleted file mode 100644 index 7e0712c9..00000000 --- a/examples/output_designs/flatish_sst_ribbon.sc +++ /dev/null @@ -1,321 +0,0 @@ -{ - "version": "0.11.0", - "grid": "none", - "helices": [ - { - "major_tick_start": 10, - "max_offset": null, - "position": {"x": 0, "y": 0.0, "z": 0}, - "major_tick_periodic_distances": [12, 9] - }, - { - "major_tick_start": 10, - "max_offset": null, - "position": {"x": 0, "y": 3.0, "z": 0}, - "major_tick_periodic_distances": [10, 11] - }, - { - "major_tick_start": 8, - "max_offset": null, - "position": {"x": 0, "y": 6.0, "z": 0}, - "major_tick_periodic_distances": [12, 9] - }, - { - "major_tick_start": 8, - "max_offset": null, - "position": {"x": 0, "y": 9.0, "z": 0}, - "major_tick_periodic_distances": [10, 11] - }, - { - "major_tick_start": 6, - "max_offset": null, - "position": {"x": 0, "y": 12.0, "z": 0}, - "major_tick_periodic_distances": [12, 9] - }, - { - "major_tick_start": 6, - "max_offset": null, - "position": {"x": 0, "y": 15.0, "z": 0}, - "major_tick_periodic_distances": [10, 11] - }, - { - "major_tick_start": 4, - "max_offset": null, - "position": {"x": 0, "y": 18.0, "z": 0}, - "major_tick_periodic_distances": [12, 9] - }, - { - "major_tick_start": 4, - "max_offset": null, - "position": {"x": 0, "y": 21.0, "z": 0}, - "major_tick_periodic_distances": [10, 11] - }, - { - "major_tick_start": 2, - "max_offset": null, - "position": {"x": 0, "y": 24.0, "z": 0}, - "major_tick_periodic_distances": [12, 9] - }, - { - "major_tick_start": 2, - "max_offset": null, - "position": {"x": 0, "y": 27.0, "z": 0}, - "major_tick_periodic_distances": [10, 11] - }, - { - "max_offset": null, - "position": {"x": 0, "y": 30.0, "z": 0}, - "major_tick_periodic_distances": [12, 9] - }, - { - "max_offset": null, - "position": {"x": 0, "y": 33.0, "z": 0}, - "major_tick_periodic_distances": [10, 11] - } - ], - "strands": [ - { - "color": "#009900", - "domains": [ - {"helix": 4, "forward": false, "start": 111, "end": 123}, - {"helix": 4, "forward": false, "start": 102, "end": 111}, - {"helix": 3, "forward": true, "start": 102, "end": 113}, - {"helix": 3, "forward": true, "start": 113, "end": 123} - ] - }, - { - "color": "#009900", - "domains": [ - {"helix": 5, "forward": false, "start": 100, "end": 111}, - {"helix": 5, "forward": false, "start": 90, "end": 100}, - {"helix": 4, "forward": true, "start": 90, "end": 102}, - {"helix": 4, "forward": true, "start": 102, "end": 111} - ] - }, - { - "color": "#009900", - "domains": [ - {"helix": 6, "forward": false, "start": 88, "end": 100}, - {"helix": 6, "forward": false, "start": 79, "end": 88}, - {"helix": 5, "forward": true, "start": 79, "end": 90}, - {"helix": 5, "forward": true, "start": 90, "end": 100} - ] - }, - { - "color": "#009900", - "domains": [ - {"helix": 7, "forward": false, "start": 77, "end": 88}, - {"helix": 7, "forward": false, "start": 67, "end": 77}, - {"helix": 6, "forward": true, "start": 67, "end": 79}, - {"helix": 6, "forward": true, "start": 79, "end": 88} - ] - }, - { - "color": "#009900", - "domains": [ - {"helix": 8, "forward": false, "start": 65, "end": 77}, - {"helix": 8, "forward": false, "start": 56, "end": 65}, - {"helix": 7, "forward": true, "start": 56, "end": 67}, - {"helix": 7, "forward": true, "start": 67, "end": 77} - ] - }, - { - "color": "#009900", - "domains": [ - {"helix": 9, "forward": false, "start": 54, "end": 65}, - {"helix": 9, "forward": false, "start": 44, "end": 54}, - {"helix": 8, "forward": true, "start": 44, "end": 56}, - {"helix": 8, "forward": true, "start": 56, "end": 65} - ] - }, - { - "color": "#ff0000", - "domains": [ - {"helix": 3, "forward": false, "start": 102, "end": 113}, - {"helix": 3, "forward": false, "start": 92, "end": 102}, - {"helix": 2, "forward": true, "start": 92, "end": 104}, - {"helix": 2, "forward": true, "start": 104, "end": 113} - ] - }, - { - "color": "#ff0000", - "domains": [ - {"helix": 4, "forward": false, "start": 90, "end": 102}, - {"helix": 4, "forward": false, "start": 81, "end": 90}, - {"helix": 3, "forward": true, "start": 81, "end": 92}, - {"helix": 3, "forward": true, "start": 92, "end": 102} - ] - }, - { - "color": "#ff0000", - "domains": [ - {"helix": 5, "forward": false, "start": 79, "end": 90}, - {"helix": 5, "forward": false, "start": 69, "end": 79}, - {"helix": 4, "forward": true, "start": 69, "end": 81}, - {"helix": 4, "forward": true, "start": 81, "end": 90} - ] - }, - { - "color": "#ff0000", - "domains": [ - {"helix": 6, "forward": false, "start": 67, "end": 79}, - {"helix": 6, "forward": false, "start": 58, "end": 67}, - {"helix": 5, "forward": true, "start": 58, "end": 69}, - {"helix": 5, "forward": true, "start": 69, "end": 79} - ] - }, - { - "color": "#ff0000", - "domains": [ - {"helix": 7, "forward": false, "start": 56, "end": 67}, - {"helix": 7, "forward": false, "start": 46, "end": 56}, - {"helix": 6, "forward": true, "start": 46, "end": 58}, - {"helix": 6, "forward": true, "start": 58, "end": 67} - ] - }, - { - "color": "#ff0000", - "domains": [ - {"helix": 8, "forward": false, "start": 44, "end": 56}, - {"helix": 8, "forward": false, "start": 35, "end": 44}, - {"helix": 7, "forward": true, "start": 35, "end": 46}, - {"helix": 7, "forward": true, "start": 46, "end": 56} - ] - }, - { - "color": "#006600", - "domains": [ - {"helix": 1, "forward": false, "start": 104, "end": 115}, - {"helix": 1, "forward": false, "start": 94, "end": 104}, - {"helix": 0, "forward": true, "start": 94, "end": 106}, - {"helix": 0, "forward": true, "start": 106, "end": 115} - ] - }, - { - "color": "#006600", - "domains": [ - {"helix": 2, "forward": false, "start": 92, "end": 104}, - {"helix": 2, "forward": false, "start": 83, "end": 92}, - {"helix": 1, "forward": true, "start": 83, "end": 94}, - {"helix": 1, "forward": true, "start": 94, "end": 104} - ] - }, - { - "color": "#006600", - "domains": [ - {"helix": 3, "forward": false, "start": 81, "end": 92}, - {"helix": 3, "forward": false, "start": 71, "end": 81}, - {"helix": 2, "forward": true, "start": 71, "end": 83}, - {"helix": 2, "forward": true, "start": 83, "end": 92} - ] - }, - { - "color": "#006600", - "domains": [ - {"helix": 4, "forward": false, "start": 69, "end": 81}, - {"helix": 4, "forward": false, "start": 60, "end": 69}, - {"helix": 3, "forward": true, "start": 60, "end": 71}, - {"helix": 3, "forward": true, "start": 71, "end": 81} - ] - }, - { - "color": "#006600", - "domains": [ - {"helix": 5, "forward": false, "start": 58, "end": 69}, - {"helix": 5, "forward": false, "start": 48, "end": 58}, - {"helix": 4, "forward": true, "start": 48, "end": 60}, - {"helix": 4, "forward": true, "start": 60, "end": 69} - ] - }, - { - "color": "#006600", - "domains": [ - {"helix": 6, "forward": false, "start": 46, "end": 58}, - {"helix": 6, "forward": false, "start": 37, "end": 46}, - {"helix": 5, "forward": true, "start": 37, "end": 48}, - {"helix": 5, "forward": true, "start": 48, "end": 58} - ] - }, - { - "color": "#006600", - "domains": [ - {"helix": 7, "forward": false, "start": 35, "end": 46}, - {"helix": 7, "forward": false, "start": 25, "end": 35}, - {"helix": 6, "forward": true, "start": 25, "end": 37}, - {"helix": 6, "forward": true, "start": 37, "end": 46} - ] - }, - { - "color": "#000000", - "domains": [ - {"helix": 9, "forward": false, "start": 33, "end": 44}, - {"helix": 9, "forward": false, "start": 23, "end": 33}, - {"helix": 8, "forward": true, "start": 23, "end": 27}, - {"loopout": 4}, - {"helix": 8, "forward": false, "start": 23, "end": 27}, - {"helix": 8, "forward": false, "start": 14, "end": 23}, - {"helix": 7, "forward": true, "start": 14, "end": 25}, - {"helix": 7, "forward": true, "start": 25, "end": 35}, - {"helix": 8, "forward": false, "start": 31, "end": 35}, - {"loopout": 4}, - {"helix": 8, "forward": true, "start": 31, "end": 35}, - {"helix": 8, "forward": true, "start": 35, "end": 44} - ] - }, - { - "color": "#000000", - "domains": [ - {"helix": 11, "forward": false, "start": 52, "end": 63}, - {"helix": 11, "forward": false, "start": 42, "end": 52}, - {"helix": 10, "forward": true, "start": 42, "end": 46}, - {"loopout": 4}, - {"helix": 10, "forward": false, "start": 42, "end": 46}, - {"helix": 10, "forward": false, "start": 33, "end": 42}, - {"helix": 9, "forward": true, "start": 33, "end": 44}, - {"helix": 9, "forward": true, "start": 44, "end": 54}, - {"helix": 10, "forward": false, "start": 50, "end": 54}, - {"loopout": 4}, - {"helix": 10, "forward": true, "start": 50, "end": 54}, - {"helix": 10, "forward": true, "start": 54, "end": 63} - ] - }, - { - "color": "#000000", - "domains": [ - {"helix": 3, "forward": false, "start": 123, "end": 134}, - {"helix": 3, "forward": false, "start": 113, "end": 123}, - {"helix": 2, "forward": true, "start": 113, "end": 117}, - {"loopout": 4}, - {"helix": 2, "forward": false, "start": 113, "end": 117}, - {"helix": 2, "forward": false, "start": 104, "end": 113}, - {"helix": 1, "forward": true, "start": 104, "end": 115}, - {"helix": 1, "forward": true, "start": 115, "end": 125}, - {"helix": 2, "forward": false, "start": 121, "end": 125}, - {"loopout": 4}, - {"helix": 2, "forward": true, "start": 121, "end": 125}, - {"helix": 2, "forward": true, "start": 125, "end": 134} - ] - }, - { - "color": "#0066cc", - "domains": [ - {"helix": 0, "forward": false, "start": 106, "end": 115}, - {"helix": 0, "forward": false, "start": 94, "end": 106}, - {"loopout": 8}, - {"helix": 1, "forward": false, "start": 83, "end": 94}, - {"loopout": 8}, - {"helix": 2, "forward": false, "start": 71, "end": 83}, - {"loopout": 8}, - {"helix": 3, "forward": false, "start": 60, "end": 71}, - {"loopout": 8}, - {"helix": 4, "forward": false, "start": 48, "end": 60}, - {"loopout": 8}, - {"helix": 5, "forward": false, "start": 37, "end": 48}, - {"loopout": 8}, - {"helix": 6, "forward": false, "start": 25, "end": 37}, - {"loopout": 8}, - {"helix": 7, "forward": false, "start": 14, "end": 25} - ] - } - ] -} \ No newline at end of file diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 65b109ba..c63a7396 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -1207,7 +1207,8 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs) -> dict: # if we have major ticks or position, it's harder to read Helix on one line, # so don't wrap it in NoIndent, but still wrap longer sub-objects in them - use_no_indent_helix: bool = not (self.major_ticks is not None or self.position is not None) + # use_no_indent_helix: bool = not (self.major_ticks is not None or self.position is not None) + use_no_indent_helix: bool = not (self.major_ticks is not None) if self.group != default_group_name: dct[group_key] = self.group @@ -1925,10 +1926,16 @@ def __init__(self, design: 'Design', helix: int, offset: int): self.current_helix: int = helix self.current_offset: int = offset self.loopout_length: Optional[int] = None - self.strand: Optional[Strand] = None + self._strand: Optional[Strand] = None self.just_moved_to_helix: bool = True self.last_domain: Optional[Domain] = None + @property + def strand(self) -> 'Strand': + if self._strand is None: + raise ValueError('no strand has been created yet') + return self._strand + # remove quotes when Python 3.6 support dropped def cross(self, helix: int, offset: Optional[int] = None, move: Optional[int] = None) -> 'StrandBuilder': """ @@ -2039,14 +2046,14 @@ def to(self, offset: int) -> 'StrandBuilder': # remove quotes when Python 3.6 s domain = Domain(helix=self.current_helix, forward=forward, start=start, end=end) self.last_domain = domain - if self.strand: + if self._strand is not None: if self.loopout_length is not None: - self.design.append_domain(self.strand, Loopout(self.loopout_length)) - self.design.append_domain(self.strand, domain) + self.design.append_domain(self._strand, Loopout(self.loopout_length)) + self.design.append_domain(self._strand, domain) self.loopout_length = None else: - self.strand = Strand(domains=[domain]) - self.design.add_strand(self.strand) + self._strand = Strand(domains=[domain]) + self.design.add_strand(self._strand) self.current_offset = offset @@ -2090,7 +2097,7 @@ def as_scaffold(self) -> 'StrandBuilder': # remove quotes when Python 3.6 suppo :return: self """ - self.strand.set_scaffold(True) + self._strand.set_scaffold(True) return self # remove quotes when Python 3.6 support dropped @@ -2101,7 +2108,7 @@ def with_modification_5p(self, mod: Modification5Prime) -> 'StrandBuilder': :param mod: 5' modification :return: self """ - self.strand.set_modification_5p(mod) + self._strand.set_modification_5p(mod) return self # remove quotes when Python 3.6 support dropped @@ -2112,7 +2119,7 @@ def with_modification_3p(self, mod: Modification3Prime) -> 'StrandBuilder': :param mod: 3' modification :return: self """ - self.strand.set_modification_3p(mod) + self._strand.set_modification_3p(mod) return self # remove quotes when Python 3.6 support dropped @@ -2126,7 +2133,7 @@ def with_modification_internal(self, idx: int, mod: ModificationInternal, :param warn_on_no_dna: whether to print warning to screen if DNA has not been assigned :return: self """ - self.strand.set_modification_internal(idx, mod, warn_on_no_dna) + self._strand.set_modification_internal(idx, mod, warn_on_no_dna) return self # remove quotes when Python 3.6 support dropped @@ -2137,7 +2144,7 @@ def with_color(self, color: Color) -> 'StrandBuilder': :param color: color to set for Strand :return: self """ - self.strand.set_color(color) + self._strand.set_color(color) return self # remove quotes when Python 3.6 support dropped @@ -2156,7 +2163,7 @@ def with_sequence(self, sequence: str, assign_complement: bool = True) -> 'Stran :py:meth:`Design.assign_dna`. :return: self """ - self.design.assign_dna(strand=self.strand, sequence=sequence, assign_complement=assign_complement) + self.design.assign_dna(strand=self._strand, sequence=sequence, assign_complement=assign_complement) return self # remove quotes when Python 3.6 support dropped @@ -2183,8 +2190,8 @@ def with_domain_sequence(self, sequence: str, assign_complement: bool = True) -> :py:meth:`Design.assign_dna`. :return: self """ - last_domain = self.strand.domains[-1] - self.design.assign_dna(strand=self.strand, sequence=sequence, domain=last_domain, + last_domain = self._strand.domains[-1] + self.design.assign_dna(strand=self._strand, sequence=sequence, domain=last_domain, assign_complement=assign_complement) return self @@ -2200,7 +2207,7 @@ def with_label(self, label) -> 'StrandBuilder': :param label: label to assign to the :any:`Strand` :return: self """ - self.strand.set_label(label) + self._strand.set_label(label) return self # remove quotes when Python 3.6 support dropped @@ -2224,7 +2231,7 @@ def with_domain_label(self, label) -> 'StrandBuilder': :param label: label to assign to the :any:`Domain` :return: self """ - last_domain = self.strand.domains[-1] + last_domain = self._strand.domains[-1] last_domain.set_label(label) return self @@ -3479,8 +3486,9 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs): # max_offset still needs to be checked here since it requires global knowledge of Strands # if 0 == helix_json[min_offset_key]: # del helix_json[min_offset_key] - max_offset = max((domain.end for domain in helix.domains), default=-1) - if max_offset == helix_json[max_offset_key]: + max_offset = max((domain.end for strand in self.strands for domain in strand.bound_domains()), + default=-1) + if max_offset == helix_json[max_offset_key] or helix_json[max_offset_key] is None: del helix_json[max_offset_key] return dct From 23d5a1c3e302f2e81e62461b3e3493d3a8e72585 Mon Sep 17 00:00:00 2001 From: David Doty Date: Tue, 25 Aug 2020 07:37:39 -0700 Subject: [PATCH 2/3] fixed unit test after changing indenting JSON behavior if Helix.position is specified --- tests/scadnano_tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index 2c077e23..332de0ab 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -2608,14 +2608,14 @@ def test_nondefault_geometry_some_default(self): self.assertAlmostEqual(geometry_expected.minor_groove_angle, geometry_actual.minor_groove_angle) self.assertAlmostEqual(geometry_expected.inter_helix_gap, geometry_actual.inter_helix_gap) - def test_lack_of_NoIndent_on_helix_if_position_or_major_ticks_present(self): + def test_lack_of_NoIndent_on_helix_if_position_or_major_ticks_present(self) -> None: helices = [sc.Helix(position=sc.Position3D(0, 0, 0))] strands = [] design = sc.Design(helices=helices, strands=strands) json_map = design.to_json_serializable(suppress_indent=True) helix_json = json_map[sc.helices_key][0] - self.assertFalse(isinstance(helix_json, sc.NoIndent)) - self.assertTrue(isinstance(helix_json[sc.position_key], sc.NoIndent)) + self.assertTrue(isinstance(helix_json, sc.NoIndent)) + # self.assertTrue(isinstance(helix_json[sc.position_key], sc.NoIndent)) def test_NoIndent_on_helix_without_position_or_major_ticks_present(self) -> None: helices = [sc.Helix()] From 9a969ce3a538f342339d824d5198c5c0ddd06a58 Mon Sep 17 00:00:00 2001 From: David Doty Date: Tue, 25 Aug 2020 16:00:41 -0700 Subject: [PATCH 3/3] added loopout labels; bumped version --- scadnano/scadnano.py | 52 +++++++++++++++++++++++++-------------- tests/scadnano_tests.py | 54 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 18 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index c63a7396..80f364c0 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -44,7 +44,7 @@ # commented out for now to support Python 3.6, which does not support this feature # from __future__ import annotations -__version__ = "0.11.1" # version line; WARNING: do not remove or change this line or comment +__version__ = "0.11.2" # version line; WARNING: do not remove or change this line or comment import dataclasses from abc import abstractmethod, ABC @@ -1412,7 +1412,8 @@ class Domain(_JSONSerializable): The total number of bases at this offset is num_insertions+1.""" label: Any = None - """Generic "label" object to associate to this :any:`Domain`. + """ + Generic "label" object to associate to this :any:`Domain`. Useful for associating extra information with the :any:`Domain` that will be serialized, for example, for DNA sequence design. It must be an object (e.g., a dict or primitive type such as str or int) @@ -1427,7 +1428,7 @@ class Domain(_JSONSerializable): def __post_init__(self): self._check_start_end() - def __repr__(self): + def __repr__(self) -> str: rep = (f'Domain(helix={self.helix}' f', forward={self.forward}' f', start={self.start}' @@ -1437,17 +1438,17 @@ def __repr__(self): ')' return rep - def __str__(self): + def __str__(self) -> str: return repr(self) def strand(self) -> 'Strand': # remove quotes when Python 3.6 support dropped return self._parent_strand - def set_label(self, label): + def set_label(self, label) -> None: """Sets label of this :any:`Domain`.""" self.label = label - def _check_start_end(self): + def _check_start_end(self) -> None: if self.start >= self.end: raise StrandError(self._parent_strand, f'start = {self.start} must be less than end = {self.end}') @@ -1761,20 +1762,36 @@ class Loopout(_JSONSerializable): length: int """Length (in DNA bases) of this Loopout.""" + label: Any = None + """ + Generic "label" object to associate to this :any:`Loopout`. + + Useful for associating extra information with the :any:`Loopout` that will be serialized, for example, + for DNA sequence design. It must be an object (e.g., a dict or primitive type such as str or int) + that is naturally JSON serializable. (Calling ``json.dumps`` on the object should succeed without + having to specify a custom encoder.) + """ + # not serialized; for efficiency # remove quotes when Python 3.6 support dropped _parent_strand: 'Strand' = field(init=False, repr=False, compare=False, default=None) - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: dict) -> Union[dict, NoIndent]: dct = {loopout_key: self.length} - return NoIndent(dct) + if self.label is not None: + dct[domain_label_key] = self.label + return NoIndent(dct) if suppress_indent else dct - def __repr__(self): + def __repr__(self) -> str: return f'Loopout({self.length})' - def __str__(self): + def __str__(self) -> str: return repr(self) + def set_label(self, label: Any) -> None: + """Sets label of this :any:`Loopout`.""" + self.label = label + @staticmethod def is_loopout() -> bool: """Indicates if this is a :any:`Loopout` (always true). @@ -1819,12 +1836,13 @@ def get_seq_start_idx(self) -> int: return self_seq_idx_start @staticmethod - def from_json(json_map) -> 'Loopout': # remove quotes when Python 3.6 support dropped + def from_json(json_map: dict) -> 'Loopout': # remove quotes when Python 3.6 support dropped # XXX: this should never fail since we detect whether to call this from_json by the presence # of a length key in json_map length_str = mandatory_field(Loopout, json_map, loopout_key) length = int(length_str) - return Loopout(length=length) + label = json_map.get(domain_label_key) + return Loopout(length=length, label=label) _wctable = str.maketrans('ACGTacgt', 'TGCAtgca') @@ -1925,7 +1943,7 @@ def __init__(self, design: 'Design', helix: int, offset: int): self.design: Design = design self.current_helix: int = helix self.current_offset: int = offset - self.loopout_length: Optional[int] = None + # self.loopout_length: Optional[int] = None self._strand: Optional[Strand] = None self.just_moved_to_helix: bool = True self.last_domain: Optional[Domain] = None @@ -1980,8 +1998,9 @@ def loopout(self, helix: int, length: int, offset: Optional[int] = None, move: O Mutually excusive with `offset`. :return: self """ - self.loopout_length = length - return self.cross(helix, offset=offset, move=move) + self.cross(helix, offset=offset, move=move) + self.design.append_domain(self._strand, Loopout(length)) + return self def move(self, delta: int) -> 'StrandBuilder': # remove quotes when Python 3.6 support dropped """ @@ -2047,10 +2066,7 @@ def to(self, offset: int) -> 'StrandBuilder': # remove quotes when Python 3.6 s domain = Domain(helix=self.current_helix, forward=forward, start=start, end=end) self.last_domain = domain if self._strand is not None: - if self.loopout_length is not None: - self.design.append_domain(self._strand, Loopout(self.loopout_length)) self.design.append_domain(self._strand, domain) - self.loopout_length = None else: self._strand = Strand(domains=[domain]) self.design.add_strand(self._strand) diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index 332de0ab..92f3222d 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -7,6 +7,8 @@ import json from typing import Iterable +from docutils.nodes import label + import scadnano as sc import scadnano.origami_rectangle as rect import scadnano.modifications as mod @@ -36,6 +38,58 @@ def setUp(self): helices = [sc.Helix(max_offset=100) for _ in range(6)] self.design_6helix = sc.Design(helices=helices, strands=[], grid=sc.square) + def test_strand__loopouts_with_labels(self): + design = self.design_6helix + sb = design.strand(0, 0) + sb.to(10) + sb.loopout(1, 8) + sb.with_domain_label('loop0') + sb.to(5) + sb.with_domain_label('dom1') + sb.cross(2) + sb.to(10) + sb.with_domain_label('dom2') + sb.loopout(3, 12) + sb.with_domain_label('loop1') + sb.to(5) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Loopout(8, label='loop0'), + sc.Domain(1, False, 5, 10, label='dom1'), + sc.Domain(2, True, 5, 10, label='dom2'), + sc.Loopout(12, label='loop1'), + sc.Domain(3, False, 5, 10), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__loopouts_with_labels_to_json(self): + design = self.design_6helix + sb = design.strand(0, 0) + sb.to(10) + sb.loopout(1, 8) + sb.with_domain_label('loop0') + sb.to(5) + sb.with_domain_label('dom1') + sb.cross(2) + sb.to(10) + sb.with_domain_label('dom2') + sb.loopout(3, 12) + sb.with_domain_label('loop1') + sb.to(5) + design_json_map = design.to_json_serializable(suppress_indent=False) + design_from_json = sc.Design.from_scadnano_json_map(design_json_map) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Loopout(8, label='loop0'), + sc.Domain(1, False, 5, 10, label='dom1'), + sc.Domain(2, True, 5, 10, label='dom2'), + sc.Loopout(12, label='loop1'), + sc.Domain(3, False, 5, 10), + ]) + self.assertEqual(1, len(design_from_json.strands)) + self.assertEqual(expected_strand, design_from_json.strands[0]) + def test_strand__0_0_to_10_cross_1_to_5(self): design = self.design_6helix sb = design.strand(0, 0)