Skip to content

Commit c274ec0

Browse files
authored
Merge pull request #275 from UC-Davis-molecular-computing/dev
Dev
2 parents 186a488 + 5189e8d commit c274ec0

File tree

7 files changed

+515
-102
lines changed

7 files changed

+515
-102
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import scadnano as sc
2+
import modifications as mod
3+
import dataclasses
4+
5+
6+
def create_design() -> sc.Design:
7+
'''
8+
0 1 2
9+
012345678901234567890
10+
0 <--+c+--]
11+
'''
12+
helices = [sc.Helix(max_offset=20) for _ in range(1)]
13+
design = sc.Design(helices=helices, grid=sc.square)
14+
sb = design.draw_strand(0, 16)
15+
sb.move(-4)
16+
sb.cross(0, 11)
17+
sb.move(-4)
18+
19+
for helix in design.helices.values():
20+
helix.major_ticks = [0, 5, 10, 15, 20]
21+
22+
design.relax_helix_rolls()
23+
24+
return design
25+
26+
27+
if __name__ == '__main__':
28+
d = create_design()
29+
d.write_scadnano_file(directory='output_designs')
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"version": "0.18.2",
3+
"grid": "square",
4+
"helices": [
5+
{
6+
"max_offset": 20,
7+
"grid_position": [0, 0],
8+
"roll": 282.857142857,
9+
"major_ticks": [0, 5, 10, 15, 20]
10+
}
11+
],
12+
"strands": [
13+
{
14+
"color": "#f74308",
15+
"domains": [
16+
{"helix": 0, "forward": false, "start": 12, "end": 16},
17+
{"helix": 0, "forward": false, "start": 7, "end": 11}
18+
]
19+
}
20+
]
21+
}

examples/output_designs/relax_helix_rolls.sc

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
{
2-
"version": "0.18.1",
2+
"version": "0.18.2",
33
"grid": "square",
44
"helices": [
55
{
6-
"max_offset": 60,
76
"grid_position": [0, 0],
8-
"roll": 40.71428571400003,
9-
"major_ticks": [0, 5, 13]
7+
"roll": 120.0,
8+
"major_ticks": [0, 5, 11]
109
},
1110
{
12-
"max_offset": 60,
1311
"grid_position": [0, 1],
14-
"roll": 72.85714285699999,
15-
"major_ticks": [0, 5, 13]
16-
},
17-
{
18-
"max_offset": 60,
19-
"grid_position": [1, 0],
20-
"roll": 68.57142857100001,
21-
"major_ticks": [0, 5, 13]
12+
"roll": 150.0,
13+
"major_ticks": [0, 5, 11]
2214
}
2315
],
2416
"strands": [
@@ -32,8 +24,8 @@
3224
{
3325
"color": "#57bb00",
3426
"domains": [
35-
{"helix": 0, "forward": true, "start": 5, "end": 13},
36-
{"helix": 2, "forward": false, "start": 5, "end": 13}
27+
{"helix": 0, "forward": true, "start": 5, "end": 11},
28+
{"helix": 1, "forward": false, "start": 5, "end": 11}
3729
]
3830
}
3931
]

examples/relax_helix_rolls.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,20 @@ def create_design() -> sc.Design:
7070

7171
design3h2.relax_helix_rolls()
7272

73-
return design3h2
73+
# return design3h2
74+
75+
helices = [sc.Helix(max_offset=11) for _ in range(2)]
76+
design2 = sc.Design(helices=helices, grid=sc.square)
77+
design2.draw_strand(0, 0).move(5).cross(1).move(-5)
78+
design2.draw_strand(0, 5).move(6).cross(1).move(-6)
79+
80+
for helix in design2.helices.values():
81+
helix.major_ticks = [0, 5, 11]
82+
83+
design2.relax_helix_rolls()
84+
# design2.relax_helix_rolls()
85+
86+
return design2
7487

7588

7689
if __name__ == '__main__':

scadnano/scadnano.py

Lines changed: 103 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
# needed to use forward annotations: https://docs.python.org/3/whatsnew/3.7.html#whatsnew37-pep563
5454
from __future__ import annotations
5555

56-
__version__ = "0.18.1" # version line; WARNING: do not remove or change this line or comment
56+
__version__ = "0.18.2" # version line; WARNING: do not remove or change this line or comment
5757

5858
import collections
5959
import dataclasses
@@ -485,11 +485,12 @@ def m13(rotation: int = 5587, variant: M13Variant = M13Variant.p7249) -> str:
485485
so assigning this sequence to the scaffold :any:`Strand` in a :any:`Design`
486486
means that the "5' end" of the scaffold :any:`Strand`
487487
(which is a fiction since the actual circular DNA strand has no endpoint)
488-
will have the sequence starting at position 5587 starting at the displayed 5' in scadnano,
489-
assigned until the displayed 3' end.
488+
will have the sequence starting at position 5587 (if another value for `rotation` is not specified)
489+
starting at the displayed 5' in scadnano, assigned until the displayed 3' end.
490490
Assuming the displayed scaffold :any:`Strand` has length :math:`n < 7249`, then a loopout of length
491491
:math:`7249 - n` consisting of the undisplayed bases will be present in the actual DNA structure.
492-
For a more detailed discussion of why this particular rotation of M13 is chosen,
492+
493+
For a more detailed discussion of why this particular rotation of M13 is chosen as the default,
493494
see
494495
`Supplementary Note S8 <http://www.dna.caltech.edu/Papers/DNAorigami-supp1.linux.pdf>`_
495496
in
@@ -1763,56 +1764,92 @@ def backbone_angle_at_offset(self, offset: int, forward: bool, geometry: Geometr
17631764
angle %= 360
17641765
return angle
17651766

1766-
def crossovers(self) -> List[Tuple[int, int, bool]]:
1767+
def crossover_addresses(self,
1768+
helices: Dict[int, Helix],
1769+
allow_intrahelix: bool = True,
1770+
allow_intergroup: bool = True) -> List[Tuple[int, int, bool]]:
17671771
"""
1772+
:param helices:
1773+
The dict of helices in which this :any:`Helix` is contained, that contains other helices
1774+
to which it might be connected by crossovers.
1775+
:param allow_intrahelix:
1776+
if ``False``, then do not return crossovers to the same :any:`Helix` as this :any:`Helix`
1777+
:param allow_intergroup:
1778+
if ``False``, then do not return crossovers to a :any:`Helix` in a different helix group
1779+
as this :any:`Helix`
17681780
:return:
1769-
list of triples (`offset`, `helix_idx`, `forward`) of all crossovers incident to this
1781+
list of triples (`helix_idx`, `offset`, `forward`) of all crossovers incident to this
17701782
:any:`Helix`, where `offset` is the offset of the crossover and `helix_idx` is the
17711783
:data:`Helix.idx` of the other :any:`Helix` incident to the crossover.
17721784
"""
1773-
crossovers: List[Tuple[int, int, bool]] = []
1785+
1786+
def allow_crossover_to(helix2: Helix) -> bool:
1787+
if not allow_intrahelix and helix2.idx == self.idx:
1788+
return False
1789+
if not allow_intergroup and helix2.group != self.group:
1790+
return False
1791+
return True
1792+
1793+
addresses: List[Tuple[int, int, bool]] = []
17741794
for domain in self.domains:
17751795
strand = domain.strand()
1776-
domains = strand.bound_domains()
1777-
num_domains = len(domains)
1778-
domain_idx = domains.index(domain)
1796+
domains_on_strand = strand.bound_domains()
1797+
num_domains = len(domains_on_strand)
1798+
domain_idx = domains_on_strand.index(domain)
1799+
domain_idx_in_substrands = strand.domains.index(domain)
17791800

17801801
# if not first domain, then there is a crossover to the previous domain
17811802
if domain_idx > 0:
1782-
offset = domain.offset_5p()
1783-
other_domain = domains[domain_idx - 1]
1784-
other_helix_idx = other_domain.helix
1785-
crossovers.append((offset, other_helix_idx, domain.forward))
1803+
# ... unless there's a loopout between them
1804+
previous_substrand = strand.domains[domain_idx_in_substrands - 1]
1805+
if isinstance(previous_substrand, Domain):
1806+
offset = domain.offset_5p()
1807+
other_domain = domains_on_strand[domain_idx - 1]
1808+
assert previous_substrand == other_domain
1809+
other_helix_idx = other_domain.helix
1810+
other_helix = helices[other_helix_idx]
1811+
if allow_crossover_to(other_helix):
1812+
addresses.append((other_helix_idx, offset, domain.forward))
17861813

17871814
# if not last domain, then there is a crossover to the next domain
17881815
if domain_idx < num_domains - 1:
1789-
offset = domain.offset_3p()
1790-
other_domain = domains[domain_idx + 1]
1791-
other_helix_idx = other_domain.helix
1792-
crossovers.append((offset, other_helix_idx, domain.forward))
1793-
1794-
return crossovers
1816+
# ... unless there's a loopout between them
1817+
next_substrand = strand.domains[domain_idx_in_substrands + 1]
1818+
if isinstance(next_substrand, Domain):
1819+
offset = domain.offset_3p()
1820+
other_domain = domains_on_strand[domain_idx + 1]
1821+
assert next_substrand == other_domain
1822+
other_helix_idx = other_domain.helix
1823+
other_helix = helices[other_helix_idx]
1824+
if allow_crossover_to(other_helix):
1825+
addresses.append((other_helix_idx, offset, domain.forward))
1826+
1827+
return addresses
17951828

17961829
def relax_roll(self, helices: Dict[int, Helix], grid: Grid, geometry: Geometry) -> None:
17971830
"""
17981831
Like :meth:`Design.relax_helix_rolls`, but only for this :any:`Helix`.
17991832
"""
1800-
angle = self.compute_relaxed_roll(helices, grid, geometry)
1801-
self.roll = angle
1833+
roll_delta = self.compute_relaxed_roll_delta(helices, grid, geometry)
1834+
self.roll += roll_delta
18021835

1803-
def compute_relaxed_roll(self, helices: Dict[int, Helix], grid: Grid, geometry: Geometry) -> float:
1836+
def compute_relaxed_roll_delta(self, helices: Dict[int, Helix], grid: Grid, geometry: Geometry) -> float:
18041837
"""
1805-
Like :meth:`Helix.relax_roll`, but just returns the new roll without altering this :any:`Helix`,
1806-
rather than changing the field :data:`Helix.roll`.
1838+
Like :meth:`Helix.relax_roll`, but just returns the amount by which to rotate the current roll,
1839+
without actually altering the field :data:`Helix.roll`.
18071840
"""
18081841
angles = []
1809-
for offset, helix_idx, forward in self.crossovers():
1842+
addresses = self.crossover_addresses(helices, allow_intrahelix=False, allow_intergroup=False)
1843+
for helix_idx, offset, forward in addresses:
18101844
other_helix = helices[helix_idx]
18111845
angle_of_other_helix = angle_from_helix_to_helix(self, other_helix, grid, geometry)
18121846
crossover_angle = self.backbone_angle_at_offset(offset, forward, geometry)
18131847
relative_angle = (crossover_angle, angle_of_other_helix)
18141848
angles.append(relative_angle)
1815-
angle = minimum_strain_angle(angles)
1849+
if len(angles) == 0:
1850+
angle = 0.0
1851+
else:
1852+
angle = minimum_strain_angle(angles)
18161853
return angle
18171854

18181855

@@ -1875,6 +1912,8 @@ def minimum_strain_angle(relative_angles: List[Tuple[float, float]]) -> float:
18751912
(but not changing any "zero angle" :math:`\mu_i`)
18761913
such that :math:`\sum_i [(\theta + \theta_i) - \mu_i]^2` is minimized.
18771914
"""
1915+
if len(relative_angles) == 0:
1916+
raise ValueError('cannot find minimum strain angle unless relative_angles is nonempty')
18781917
adjusted_angles = [angle - zero_angle for angle, zero_angle in relative_angles]
18791918
ave_angle = average_angle(adjusted_angles)
18801919
min_strain_angle = -ave_angle
@@ -1917,6 +1956,8 @@ def average_angle(angles: List[float]) -> float:
19171956
average angle of the list of angles, normalized to be between 0 and 360.
19181957
"""
19191958
num_angles = len(angles)
1959+
if num_angles == 0:
1960+
raise ValueError('cannot take average of empty list of angles')
19201961
mean_angle = sum(angles) / num_angles
19211962
min_dist = float('inf')
19221963
optimal_angle = 0
@@ -2775,8 +2816,8 @@ def strand(self) -> Strand:
27752816
def cross(self, helix: int, offset: Optional[int] = None, move: Optional[int] = None) \
27762817
-> StrandBuilder:
27772818
"""
2778-
Add crossover. To have any effect, must be followed by call to :py:meth:`StrandBuilder.to`
2779-
or :py:meth:`StrandBuilder.move`.
2819+
Add crossover. To have any effect, must be followed by call to :meth:`StrandBuilder.to`
2820+
or :meth:`StrandBuilder.move`.
27802821
27812822
:param helix: :any:`Helix` to crossover to
27822823
:param offset: new offset on `helix`. If not specified, defaults to current offset.
@@ -2788,8 +2829,13 @@ def cross(self, helix: int, offset: Optional[int] = None, move: Optional[int] =
27882829
Mutually excusive with `offset`.
27892830
:return: self
27902831
"""
2832+
if helix not in self.design.helices:
2833+
helix_idxs_str = ", ".join(str(idx) for idx in self.design.helices.keys())
2834+
raise IllegalDesignError(f'cannot cross to helix {helix} since it does not exist;\n'
2835+
f'valid helix indices: {helix_idxs_str}')
27912836
if self._strand is None:
2792-
raise ValueError('no Strand created yet; make at least one domain first')
2837+
raise ValueError('cannot cross because no Strand created yet; make at least one domain first '
2838+
'using move() or to()')
27932839
if self._most_recently_added_substrand_is_extension():
27942840
raise IllegalDesignError('Cannot cross after an extension.')
27952841
if move is not None and offset is not None:
@@ -2826,8 +2872,14 @@ def loopout(self, helix: int, length: int, offset: Optional[int] = None, move: O
28262872
Mutually excusive with `offset`.
28272873
:return: self
28282874
"""
2875+
if helix not in self.design.helices:
2876+
helix_idxs_str = ", ".join(str(idx) for idx in self.design.helices.keys())
2877+
raise IllegalDesignError(f'cannot loopout to helix {helix} since it does not exist;\n'
2878+
f'valid helix indices: {helix_idxs_str}')
2879+
28292880
if self._strand is None:
2830-
raise ValueError('no Strand created yet; make at least one domain first')
2881+
raise ValueError('cannot loopout because no Strand created yet; make at least one domain first '
2882+
'using move() or to()')
28312883
self.cross(helix, offset=offset, move=move)
28322884
self.design.append_domain(self._strand, Loopout(length))
28332885
return self
@@ -2906,6 +2958,12 @@ def move(self, delta: int) -> StrandBuilder:
29062958
"relative move", whereas :py:meth:`StrandBuilder.to` and :py:meth:`StrandBuilder.update_to`
29072959
are "absolute moves".
29082960
2961+
**NOTE**: The parameter `delta` does not indicate how much we move from the current offset.
2962+
It indicates the total length of the domain after the move. For instance, if we are currently
2963+
on offset 10, and we call ``move(5)``, this will create a domain starting at offset 10 and ending
2964+
at offset 14, for a total length of 5, occuping 5 offsets: 10, 11, 12, 13, 14. (But if we imagine
2965+
moving from offset 10, we've only moved by 4 offsets to arrive at 14, not 5 offsets.)
2966+
29092967
This updates the underlying :any:`Design` with a new :any:`Domain`,
29102968
and if :py:meth:`StrandBuilder.loopout` was last called on this :any:`StrandBuilder`,
29112969
also a new :any:`Loopout`.
@@ -3514,7 +3572,7 @@ def dna_sequence(self) -> Optional[str]:
35143572
nums = json.loads(strand.label) # nums is now the list [1, 2, 3]
35153573
"""
35163574

3517-
# not serialized; efficient way to see a list of all domains on a given helix
3575+
# not serialized; efficient way to see a list of all domains on a given helix on this strand
35183576
_helix_idx_domain_map: Dict[int, List[Domain]] = field(
35193577
init=False, repr=False, compare=False, default_factory=dict)
35203578

@@ -3539,14 +3597,14 @@ def __init__(self,
35393597
self.modifications_int = modifications_int if modifications_int is not None else dict()
35403598
self.name = name
35413599
self.label = label
3542-
self._helix_idx_domain_map = _helix_idx_domain_map if _helix_idx_domain_map is not None else dict()
3600+
self._helix_idx_domain_map = _helix_idx_domain_map if _helix_idx_domain_map is not None \
3601+
else defaultdict(list)
35433602
if dna_sequence is not None:
35443603
self.set_dna_sequence(dna_sequence)
35453604
self.__post_init__()
35463605

35473606
def __post_init__(self) -> None:
35483607
self._ensure_domains_not_none()
3549-
self._helix_idx_domain_map = defaultdict(list)
35503608

35513609
self.set_domains(self.domains)
35523610

@@ -6885,7 +6943,17 @@ def insert_domain(self, strand: Strand, order: int, domain: Union[Domain, Loopou
68856943
self._check_strand_references_legal_helices(strand)
68866944
self._check_loopouts_not_consecutive_or_singletons_or_zero_length()
68876945
if isinstance(domain, Domain):
6888-
self.helices[domain.helix].domains.append(domain)
6946+
domains_on_helix = self.helices[domain.helix].domains
6947+
if len(domains_on_helix) == 0:
6948+
domains_on_helix.append(domain)
6949+
else:
6950+
i = 0
6951+
while (i < len(domains_on_helix) and
6952+
(domains_on_helix[i].start, domains_on_helix[i].forward) <
6953+
(domain.start, domain.forward)):
6954+
i += 1
6955+
domains_on_helix.insert(i, domain)
6956+
# self.helices[domain.helix].domains.append(domain)
68896957
self._check_strands_overlap_legally(domain_to_check=domain)
68906958

68916959
def remove_domain(self, strand: Strand, domain: Union[Domain, Loopout]) -> None:

0 commit comments

Comments
 (0)