Skip to content

Commit

Permalink
added with_deletions and with_insertions methods to `StrandBuilde…
Browse files Browse the repository at this point in the history
…r` and used them in unit tests
  • Loading branch information
dave-doty committed Apr 12, 2022
1 parent d7b70a2 commit f25a28f
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 65 deletions.
108 changes: 98 additions & 10 deletions scadnano/scadnano.py
Original file line number Diff line number Diff line change
Expand Up @@ -2529,10 +2529,11 @@ def with_domain_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel
Assigns `name` as of the most recently created :any:`Domain` or :any:`Loopout` in
the :any:`Strand` being built. This should be called immediately after a :any:`Domain` is created
via a call to
:py:meth:`StrandBuilder.to`,
:py:meth:`StrandBuilder.update_to`,
:meth:`StrandBuilder.to`,
:meth:`StrandBuilder.move`,
:meth:`StrandBuilder.update_to`,
or
:py:meth:`StrandBuilder.loopout`, e.g.,
:meth:`StrandBuilder.loopout`, e.g.,
.. code-block:: Python
Expand All @@ -2551,17 +2552,20 @@ def with_domain_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel
def with_domain_label(self, label: DomainLabel) -> 'StrandBuilder[StrandLabel, DomainLabel]':
"""
Assigns `label` as label of the most recently created :any:`Domain` or :any:`Loopout` in
the :any:`Strand` being built. This should be called immediately after a :any:`Domain` is created
via a call to
:py:meth:`StrandBuilder.to`,
:py:meth:`StrandBuilder.update_to`,
the :any:`Strand` being built. This should be called immediately after
a :any:`Domain` or :any:`Loopout` is created via a call to
:meth:`StrandBuilder.to`,
:meth:`StrandBuilder.move`,
:meth:`StrandBuilder.update_to`,
or
:py:meth:`StrandBuilder.loopout`, e.g.,
:meth:`StrandBuilder.loopout`, e.g.,
.. code-block:: Python
design.draw_strand(0, 5).to(8).with_domain_label('domain 1')\\
.cross(1).to(5).with_domain_label('domain 2')\\
design.draw_strand(0, 5)\\
.to(8).with_domain_label('domain 1')\\
.cross(1)\\
.to(5).with_domain_label('domain 2')\\
.loopout(2, 4).with_domain_label('domain 3')\\
.to(10).with_domain_label('domain 4')
Expand All @@ -2574,6 +2578,90 @@ def with_domain_label(self, label: DomainLabel) -> 'StrandBuilder[StrandLabel, D
last_domain.set_label(label)
return self

def with_deletions(self,
deletions: Union[int, Iterable[int]]) -> 'StrandBuilder[StrandLabel, DomainLabel]':
"""
Assigns `deletions` as the deletion(s) of the most recently created
:any:`Domain` the :any:`Strand` being built. This should be called immediately after
a :any:`Domain` is created via a call to
:meth:`StrandBuilder.to`,
:meth:`StrandBuilder.move`,
:meth:`StrandBuilder.update_to`, e.g.,
.. code-block:: Python
design.draw_strand(0, 0)\\
.move(8).with_deletions(4)\\
.cross(1)\\
.move(-8).with_deletions([2, 3])
:param deletions:
a single int, or an Iterable of ints, indicating the offset at which to put the deletion(s)
:return: self
"""
if self._strand is None:
raise ValueError('no Strand created yet; make at least one domain first')
last_domain = self._strand.domains[-1]

if not isinstance(last_domain, Domain):
raise ValueError(f'can only create a deletion on a bound Domain, not a {type(last_domain)};\n'
f'be sure only to call with_deletions immediately after a call to '
f'to, move, or update_to')
if not isinstance(deletions, int) and not hasattr(deletions, '__iter__'):
raise ValueError(f'deletions must be a single int or an iterable of ints, '
f'but it is {type(deletions)}')
if isinstance(deletions, int):
last_domain.deletions = [deletions]
else:
last_domain.deletions = list(deletions)

return self

def with_insertions(self, insertions: Union[Tuple[int, int], Iterable[Tuple[int, int]]]) \
-> 'StrandBuilder[StrandLabel, DomainLabel]':
"""
Assigns `insertions` as the insertion(s) of the most recently created
:any:`Domain` the :any:`Strand` being built. This should be called immediately after
a :any:`Domain` is created via a call to
:meth:`StrandBuilder.to`,
:meth:`StrandBuilder.move`,
:meth:`StrandBuilder.update_to`, e.g.,
.. code-block:: Python
design.draw_strand(0, 0)\\
.move(8).with_insertions((4, 2))\\
.cross(1)\\
.move(-8).with_insertions([(2, 3), (3, 3)])
:param insertions:
a single pair of ints (tuple), or an Iterable of pairs of ints (tuples)
indicating the offset at which to put the insertion(s)
:return: self
"""
if self._strand is None:
raise ValueError('no Strand created yet; make at least one domain first')
last_domain = self._strand.domains[-1]
if not isinstance(last_domain, Domain):
raise ValueError(f'can only create an insertion on a bound Domain, not a {type(last_domain)};\n'
f'be sure only to call with_insertions immediately after a call to '
f'to, move, or update_to')

type_msg = (f'insertions must be a single pair of ints or an iterable of pairs of ints, '
f'but it is {type(insertions)}')

if not hasattr(insertions, '__iter__'):
raise ValueError(type_msg)
if isinstance(insertions, tuple) and len(insertions) > 0 and isinstance(insertions[0], int):
last_domain.insertions = [insertions]
else:
for ins in insertions:
if not (isinstance(ins, tuple) and len(ins) > 0 and isinstance(ins[0], int)):
raise ValueError(type_msg)
last_domain.insertions = list(insertions)

return self


@dataclass
class Strand(_JSONSerializable, Generic[StrandLabel, DomainLabel]):
Expand Down
102 changes: 47 additions & 55 deletions tests/scadnano_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1710,6 +1710,26 @@ class TestInlineInsDel(unittest.TestCase):
Tests inlining of insertions/deletions.
"""

def setUp(self) -> None:
self.design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[],
grid=sc.square)


def test_no_deletion_after_loopout(self) -> None:
# not really a test of inlining, but I added the with_deletions and with_insertions to help these
# tests, so easier just to test this behavior here
with self.assertRaises(ValueError):
self.design.draw_strand(0, 0).move(8).loopout(0, 5, 10).with_deletions(4)

def test_no_insertion_after_loopout(self) -> None:
# not really a test of inlining, but I added the with_deletions and with_insertions to help these
# tests, so easier just to test this behavior here
with self.assertRaises(ValueError):
self.design.draw_strand(0, 0).move(8).loopout(0, 5, 10).with_insertions((4, 2))


def test_inline_deletions_insertions__one_deletion(self) -> None:
"""
before
Expand All @@ -1722,10 +1742,8 @@ def test_inline_deletions_insertions__one_deletion(self) -> None:
| | | |
0 [----->
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 8, deletions=[4])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(8).with_deletions(4)
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=23, major_ticks=[0, 7, 15, 23], start=0, end=7)

Expand Down Expand Up @@ -1753,10 +1771,8 @@ def test_inline_deletions_insertions__two_deletions(self) -> None:
| | | |
0 [---->
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 8, deletions=[2, 4])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(8).with_deletions([2, 4])
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=22, major_ticks=[0, 6, 14, 22], start=0, end=6)

Expand All @@ -1772,10 +1788,8 @@ def test_inline_deletions_insertions__one_insertion(self) -> None:
| | | |
0 [------->
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 8, insertions=[(4, 1)])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(8).with_insertions((4, 1))
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=25, major_ticks=[0, 9, 17, 25], start=0, end=9)

Expand All @@ -1791,10 +1805,8 @@ def test_inline_deletions_insertions__two_insertions(self) -> None:
| | | |
0 [---------->
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 8, insertions=[(2, 3), (4, 1)])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(8).with_insertions([(2, 3), (4, 1)])
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=28, major_ticks=[0, 12, 20, 28], start=0, end=12)

Expand All @@ -1810,10 +1822,8 @@ def test_inline_deletions_insertions__one_deletion_one_insertion(self) -> None:
| | | |
0 [-------->
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 8, deletions=[4], insertions=[(2, 3)])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(8).with_deletions(4).with_insertions((2, 3))
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=26, major_ticks=[0, 10, 18, 26], start=0, end=10)

Expand All @@ -1829,10 +1839,8 @@ def test_inline_deletions_insertions__one_deletion_right_of_major_tick(self) ->
| | | |
0 [--------->
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 12, deletions=[9])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(12).with_deletions(9)
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=23, major_ticks=[0, 8, 15, 23], start=0, end=11)

Expand All @@ -1849,10 +1857,8 @@ def test_inline_deletions_insertions__one_deletion_on_major_tick(self) -> None:
| . . . . . . . | . . . . . . | . . . . . . . |
[ - - - - - - - - - >
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 12, deletions=[8])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(12).with_deletions(8)
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=23, major_ticks=[0, 8, 15, 23], start=0, end=11)

Expand All @@ -1868,10 +1874,8 @@ def test_inline_deletions_insertions__one_deletion_left_of_major_tick(self) -> N
| | | |
0 [--------->
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 12, deletions=[7])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(12).with_deletions(7)
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=23, major_ticks=[0, 7, 15, 23], start=0, end=11)

Expand All @@ -1887,10 +1891,8 @@ def test_inline_deletions_insertions__one_insertion_right_of_major_tick(self) ->
| | | |
0 [----------->
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 12, insertions=[(9, 1)])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(12).with_insertions((9, 1))
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=25, major_ticks=[0, 8, 17, 25], start=0, end=13)

Expand All @@ -1906,10 +1908,8 @@ def test_inline_deletions_insertions__one_insertion_on_major_tick(self) -> None:
| | | |
0 [----------->
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 12, insertions=[(8, 1)])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(12).with_insertions((8, 1))
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=25, major_ticks=[0, 8, 17, 25], start=0, end=13)

Expand All @@ -1925,10 +1925,8 @@ def test_inline_deletions_insertions__one_insertion_left_of_major_tick(self) ->
| | | |
0 [----------->
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 12, insertions=[(7, 1)])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(12).with_insertions((7, 1))
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=25, major_ticks=[0, 9, 17, 25], start=0, end=13)

Expand All @@ -1944,10 +1942,8 @@ def test_inline_deletions_insertions__deletions_insertions_in_multiple_domains(s
| | | |
0 [------------------------->
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[sc.Strand([sc.Domain(0, True, 0, 24, deletions=[19], insertions=[(5, 2), (11, 1)])])],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(24).with_deletions(19).with_insertions([(5, 2), (11, 1)])
design.inline_deletions_insertions()
self.helix0_strand0_inlined_test(design, max_offset=26, major_ticks=[0, 10, 19, 26], start=0, end=26)

Expand All @@ -1964,13 +1960,9 @@ def test_inline_deletions_insertions__deletions_insertions_in_multiple_domains_t
| . . . . . . . . | . . . . . . . . | . . . . . . |
[ - - - - - - - - - - - - - - > [ - - - - - - - >
"""
design = sc.Design(
helices=[sc.Helix(max_offset=24, major_tick_distance=8)],
strands=[
sc.Strand([sc.Domain(0, True, 0, 14, deletions=[2], insertions=[(5, 2), (10, 1)])]),
sc.Strand([sc.Domain(0, True, 14, 24, deletions=[19])]),
],
grid=sc.square)
design = self.design
design.draw_strand(0, 0).move(14).with_deletions(2).with_insertions([(5, 2), (10, 1)])
design.draw_strand(0, 14).to(24).with_deletions(19)
design.inline_deletions_insertions()
self.assertEqual(1, len(design.helices))
self.assertEqual(2, len(design.strands))
Expand Down

0 comments on commit f25a28f

Please sign in to comment.