Skip to content

Commit

Permalink
closes #14; add support for circular Strands
Browse files Browse the repository at this point in the history
  • Loading branch information
dave-doty committed Dec 13, 2020
1 parent 9d1ac96 commit 4b13ef2
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 4 deletions.
18 changes: 18 additions & 0 deletions examples/circular.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import scadnano as sc
import modifications as mod
import dataclasses

def create_design():
helices = [sc.Helix(max_offset=30) for _ in range(2)]
design = sc.Design(helices=helices, grid=sc.square)

design.strand(0,0).move(10).cross(1).move(-10).as_circular()
design.strand(0,10).move(10).loopout(1, 5).move(-10).as_circular()
design.strand(0,20).move(10).cross(1).move(-10)

return design


if __name__ == '__main__':
design = create_design()
design.write_scadnano_file(directory='output_designs')
34 changes: 34 additions & 0 deletions examples/output_designs/circular.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"version": "0.13.0",
"grid": "square",
"helices": [
{"grid_position": [0, 0]},
{"grid_position": [0, 1]}
],
"strands": [
{
"circular": true,
"color": "#f74308",
"domains": [
{"helix": 0, "forward": true, "start": 0, "end": 10},
{"helix": 1, "forward": false, "start": 0, "end": 10}
]
},
{
"circular": true,
"color": "#57bb00",
"domains": [
{"helix": 0, "forward": true, "start": 10, "end": 20},
{"loopout": 5},
{"helix": 1, "forward": false, "start": 10, "end": 20}
]
},
{
"color": "#888888",
"domains": [
{"helix": 0, "forward": true, "start": 20, "end": 30},
{"helix": 1, "forward": false, "start": 20, "end": 30}
]
}
]
}
61 changes: 57 additions & 4 deletions scadnano/scadnano.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,7 @@ def m13(rotation: int = 5587, variant: M13Variant = M13Variant.p7249) -> str:

# Strand keys
strand_name_key = 'name'
circular_key = 'circular'
color_key = 'color'
dna_sequence_key = 'sequence'
legacy_dna_sequence_keys = ['dna_sequence'] # support legacy names for these ideas
Expand Down Expand Up @@ -2201,6 +2202,17 @@ def update_to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]':

return self

def as_circular(self) -> 'StrandBuilder[StrandLabel, DomainLabel]':
"""
Makes :any:`Strand` being built circular.
:return: self
"""
if self._strand is None:
raise ValueError('no Strand created yet; make at least one domain first')
self._strand.set_circular()
return self

# remove quotes when Py3.6 support dropped
def as_scaffold(self) -> 'StrandBuilder[StrandLabel, DomainLabel]':
"""
Expand Down Expand Up @@ -2477,6 +2489,13 @@ class Strand(_JSONSerializable, Generic[StrandLabel, DomainLabel]):
and could be either single-stranded or double-stranded,
whereas each :any:`Loopout` is single-stranded and has no associated :any:`Helix`."""

circular: bool = False
"""If True, this :any:`Strand` is circular and has no 5' or 3' end. Although there is still a
first and last :any:`Domain`, we interpret there to be a crossover from the 3' end of the last domain
to the 5' end of the first domain, and any circular permutation of :py:data:`Strand.domains`
should result in a functionally equivalent :any:`Strand`. It is illegal to have a
:any:`Modification5Prime` or :any:`Modification3Prime` on a circular :any:`Strand`."""

dna_sequence: Optional[str] = None
"""Do not assign directly to this field. Always use :any:`Design.assign_dna`
(for complementarity checking) or :any:`Strand.set_dna_sequence`
Expand Down Expand Up @@ -2509,10 +2528,14 @@ class Strand(_JSONSerializable, Generic[StrandLabel, DomainLabel]):
:any:`Design` is a scaffold, then the design is considered a DNA origami design."""

modification_5p: Optional[Modification5Prime] = None
"""5' modification; None if there is no 5' modification."""
"""
5' modification; None if there is no 5' modification. Illegal to have if :py:`Strand.circular` is True.
"""

modification_3p: Optional[Modification3Prime] = None
"""3' modification; None if there is no 5' modification."""
"""
3' modification; None if there is no 3' modification. Illegal to have if :py:`Strand.circular` is True.
"""

modifications_int: Dict[int, ModificationInternal] = field(default_factory=dict)
""":any:`Modification`'s to the DNA sequence (e.g., biotin, Cy3/Cy5 fluorphores). Maps offset to
Expand Down Expand Up @@ -2574,6 +2597,8 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D
dct: Dict[str, Any] = OrderedDict()
if self.name is not None:
dct[strand_name_key] = self.name
if self.circular:
dct[circular_key] = self.circular
if self.color is not None:
dct[color_key] = self.color.to_json_serializable(suppress_indent)
if self.dna_sequence is not None:
Expand Down Expand Up @@ -2617,6 +2642,7 @@ def from_json(json_map: dict) -> 'Strand': # remove quotes when Py3.6 support d
raise IllegalDesignError('Loopout at end of Strand not supported')

is_scaffold = json_map.get(is_scaffold_key, False)
circular = json_map.get(circular_key, False)

dna_sequence = optional_field(None, json_map, dna_sequence_key, *legacy_dna_sequence_keys)

Expand All @@ -2638,6 +2664,7 @@ def decimal_int_to_hex(d: int) -> str:
return Strand(
domains=domains,
dna_sequence=dna_sequence,
circular=circular,
color=color,
idt=idt,
is_scaffold=is_scaffold,
Expand Down Expand Up @@ -2699,6 +2726,28 @@ def set_color(self, color: Color) -> None:
"""Sets color of this :any:`Strand`."""
self.color = color

def set_circular(self, circular: bool = True) -> None:
"""
Sets this to be a circular :any:`Strand` (or non-circular if optional parameter is False.
:param circular:
whether to make this :any:`Strand` circular (True) or linear (False)
:raises StrandError:
if this :any:`Strand` has a 5' or 3' modification
"""
if circular and self.modification_5p is not None:
raise StrandError(self, "cannot have a 5' modification on a circular strand")
if circular and self.modification_3p is not None:
raise StrandError(self, "cannot have a 3' modification on a circular strand")
self.circular = circular

def set_linear(self) -> None:
"""
Makes this a linear (non-circular) :any:`Strand`. Equivalent to calling
`self.set_circular(False)`.
"""
self.set_circular(False)

def set_default_idt(self, use_default_idt: bool = True, skip_scaffold: bool = True,
unique_names: bool = False) -> None:
"""
Expand Down Expand Up @@ -2744,11 +2793,15 @@ def set_default_idt(self, use_default_idt: bool = True, skip_scaffold: bool = Tr
self.idt = None

def set_modification_5p(self, mod: Modification5Prime = None) -> None:
"""Sets 5' modification to be `mod`."""
"""Sets 5' modification to be `mod`. `mod` cannot be non-None if :any:`Strand.circular` is True."""
if self.circular and mod is not None:
raise StrandError(self, "cannot have a 5' modification on a circular strand")
self.modification_5p = mod

def set_modification_3p(self, mod: Modification3Prime = None) -> None:
"""Sets 3' modification to be `mod`."""
"""Sets 3' modification to be `mod`. `mod` cannot be non-None if :any:`Strand.circular` is True."""
if self.circular and mod is not None:
raise StrandError(self, "cannot have a 3' modification on a circular strand")
self.modification_3p = mod

def remove_modification_5p(self) -> None:
Expand Down
40 changes: 40 additions & 0 deletions tests/scadnano_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3601,6 +3601,46 @@ def set_colors_black(*strands) -> None:
strand.set_color(sc.Color(r=0, g=0, b=0))


class TestCircularStrands(unittest.TestCase):

def setUp(self) -> None:
helices = [sc.Helix(max_offset=10) for _ in range(2)]
self.design = sc.Design(helices=helices, strands=[])
self.design.strand(0, 0).move(10).cross(1).move(-10)
self.strand = self.design.strands[0]
r'''
0 [--------\
|
1 <--------/
'''

def test_can_add_internal_mod_to_circular_strand(self) -> None:
self.strand.set_circular()
self.assertEqual(True, self.strand.circular)
self.strand.set_modification_internal(2, mod.biotin_int)
self.assertEqual(1, len(self.strand.modifications_int))

def test_cannot_make_strand_circular_if_5p_mod(self) -> None:
self.strand.set_modification_5p(mod.biotin_5p)
with self.assertRaises(sc.StrandError):
self.strand.set_circular(True)

def test_cannot_make_strand_circular_if_3p_mod(self) -> None:
self.strand.set_modification_3p(mod.biotin_3p)
with self.assertRaises(sc.StrandError):
self.strand.set_circular(True)

def test_add_5p_mod_to_circular_strand(self) -> None:
self.strand.set_circular(True)
with self.assertRaises(sc.StrandError):
self.strand.set_modification_5p(mod.biotin_5p)

def test_add_3p_mod_to_circular_strand(self) -> None:
self.strand.set_circular(True)
with self.assertRaises(sc.StrandError):
self.strand.set_modification_3p(mod.biotin_3p)


class TestAddStrand(unittest.TestCase):

def test_add_strand__with_loopout(self) -> None:
Expand Down

0 comments on commit 4b13ef2

Please sign in to comment.