1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
|
# Copyright 2007-2008 Nanorex, Inc. See LICENSE file for details.
"""
dna_updater_follow_strand.py - helper function for dna_updater_ladders.
@author: Bruce
@version: $Id$
@copyright: 2007-2008 Nanorex, Inc. See LICENSE file for details.
"""
from utilities.constants import noop
_FAKE_AXIS_FOR_BARE_STRANDS = [1]
# arbitrary non-false object comparable using 'is'
# (it might be enough for it to be not None, even if false)
# (the code could be simplified to let it just be None,
# but it's not worth the trouble right now)
# [bruce 080117]
def dna_updater_follow_strand(phase, strand, strand_axis_info, break_axis, break_strand, axis_break_between_Q = None):
"""
If phase is 1:
Loop over strand's base atoms, watching the strand join and leave
axis chains and move along them; break both axis and strand
whenever it joins, leaves, or moves discontinuously
along axis (treating lack of Ax as moving continuously on an
imaginary Ax chain different from all real ones)
(for details, see the code comments).
The breaks must be done only virtually, by calling the provided
functions with the 3 args (chain, index, whichside) -- the API
for those functions should be the same as for the method break_chain_later
of the helper class chains_to_break in the calling code.
If phase is 2:
Same, but the only breaks we make are strand_breaks at the same
places at which axis_break_between_Q says the axis has a break
(for its API doc see chains_to_break.break_between_Q in caller).
@return: None
"""
# For phase 1:
#
# Loop over strand's base atoms, watching the strand join and leave
# axis chains and move along them. If it jumps (moves more than one base)
# or stays on the same atom (implying that it switched ladder sides --
# not physically realistic, but we can't depend on it never happening),
# pretend that it left the axis and rejoined it. Leaving the axis
# (pretend or really) breaks the strand there (in terms of which ladder
# rail it will end up in) and breaks the axis after the leaving point
# (or on both sides of that axis atom, if we didn't yet know the
# direction of travel of the strand along the axis, since we just
# entered the axis). Joining an axis breaks that axis before that
# point, relative to the direction we'll travel in, so record this
# and break the axis as we start to travel.
#
# The only other case is if we just change directions, i.e. we hit axis
# atoms A, B, A. (This is not physically realistic, if only due to the
# implied base stacking, but we can't depend on it not happening.)
# This implies we changed sides of the axis (from one ladder rail to
# the other), but we don't know if that was before or after B, so we
# pretend we left and rejoined both before and after B. (In fact,
# since either leaving or joining requires a break, maybe both these
# breaks are always required. If not, extra breaks at this stage are ok,
# since they'll be fixed later when we merge ladders.)
#
# In more detail: in case we switched after B in that A B A sequence,
# do whatever a jump after B would do (no special case required).
# In case we switched sides before B, we wish we'd done a jump at the
# prior step, but we know enough now (in the step when we detect this)
# to do all the same things that would do, so do them now.
#
# About rings -- we could be aware of ring index circular order,
# but we want rings in ladders to be "aligned" in terms of indexes
# in those ladders (one index for all 3 atoms on one ladder rung),
# so the easiest way to ensure that is just to pretend rings are
# chains at this stage (for both strand and axis rings), and break
# strands whereever they go around the ring (jumping from end to
# start). This means we should just ignore ring index circularity.
#
# (We may or may not patch this later by noticing which ladders are
# whole rings... since moebius rings are possible, or partial rings
# (axis rail is ring, strand rail is not), it's probably best to
# ignore this on ladders and just follow the individual chains as
# needed.)
# To get started, we're in the state of having left some axis
# (all done with it) and about to enter a new one. The loop body
# can't know that, so record an impossible value of which axis
# we're in. In general, the loop variables need to know which
# axis we were in, where we were, and which way we were going
# or that we just entered it, with a special case for having
# been in no axis or having been all done with the prior axis.
# (For phase 2, just modify this slightly.)
if strand.baselength() == 0:
# This was not supposed to be possible, but it can happen in the MMKit
# for the "bare Pl atom". Maybe the strand should refuse to be made then,
# but this special case seems to fix the assertion error we otherwise
# got below, from "assert prior_axis is not None # since at least one s_atom",
# so for now, permit it (with debug print) and see if it causes any other trouble.
print "fyi: dna_updater_follow_strand bailing on 0-length strand %r" % (strand,)
return
assert phase in (1,2)
if phase == 2:
break_strand_phase2 = break_strand
break_strand = noop # optimization
break_axis = noop # optimization
assert callable( axis_break_between_Q)
assert callable( break_strand_phase2)
else:
assert axis_break_between_Q is None
break_strand_phase2 = None # not used, but mollify pylint
assert callable(break_axis)
assert callable(break_strand)
prior_axis = None
prior_index = None
prior_direction = None # normally, -1 or 1, or 0 for "just entered"
for s_atom, s_index in list(strand.baseatom_index_pairs()) + [(None, None)]:
if s_atom is None:
# last iteration, just to make strand seem to jump off prior_axis
# at the end
assert prior_axis is not None # since at least one s_atom
(axis, index) = None, None
else:
try:
(axis, index) = strand_axis_info[s_atom.key]
# Note: this fails for a bare strand atom (permittable
# by debug_prefs), e.g. in the mmkit -- and more
# importantly is now permitted "officially" for
# representing single strands,
# at least until we implement some 'unpaired-base' atoms.
# So, pretend all bare strand atoms live on a special
# fake axis and move along it continuously.
except KeyError:
axis = _FAKE_AXIS_FOR_BARE_STRANDS
index = s_index # always acts like continuous motion
pass
# set the variables jumped, new_direction, changed_direction;
# these are their values unless changed explicitly just below:
jumped = False
new_direction = 0 # unknown
changed_direction = False
if axis is prior_axis: # 'is' is ok since these are objects, both current, so disjoint in atoms
delta = index - prior_index
# ok to ignore ring index circularity (see above for why)
if delta not in (-1,1):
# see long comment above for why even 0 means we jumped
jumped = True
else:
new_direction = delta # only used if we don't jump
if phase == 2:
if (axis is not _FAKE_AXIS_FOR_BARE_STRANDS) and \
axis_break_between_Q(axis, index, prior_index):
# break the strand to match the axis break we just passed over
break_strand_phase2(strand, s_index, -1)
if prior_direction and new_direction != prior_direction:
jumped = True
new_direction = 0 # required when we pretend to jump
changed_direction = True
del delta
else:
jumped = True
# (after this, jumped, new_direction, changed_direction are final)
# We need helper routines to do a jump, since there are two
# different jumps we might do. We need them in pieces since
# we can't always call them all at once for one jump.
# (This would be true even if we recorded all jumps and
# did them all later, since we don't find out what to record
# except in these pieces, during different steps.)
def do_jump_from_axis(from_axis, from_index, from_direction):
# break from_axis after the leaving point,
# or on both sides of that if we had just entered it
# (in which case "after" is an undefined direction).
if from_direction == 1:
break_axis( from_axis, from_index, 1)
elif from_direction == -1:
break_axis( from_axis, from_index, -1)
else:
break_axis( from_axis, from_index, 1)
break_axis( from_axis, from_index, -1)
return
def do_jump_of_strand( strand, new_s_index):
# break strand at the jump
break_strand( strand, new_s_index, -1)
return
def do_jump_to_axis_once_new_direction_known( to_axis, entry_index, new_direction):
# break backwards, before entry point
break_axis( to_axis, entry_index, - new_direction)
return
if jumped:
assert new_direction == 0
if prior_axis is not None and prior_axis is not _FAKE_AXIS_FOR_BARE_STRANDS:
do_jump_from_axis( prior_axis, prior_index, prior_direction)
if s_index is not None:
do_jump_of_strand( strand, s_index)
# set up to break new axis before the entry point --
# this is done by recording the state as described above,
# so the next loop iteration will do it (by calling
# do_jump_to_axis_once_new_direction_known), once it knows
# the direction of travel. (If we leave immediately and
# never know the direction, do_jump_from_axis breaks
# the axis in both directions and takes care of it.)
if changed_direction:
# (rare, not physically realistic; see long comment above)
# The after-jump is taken care of, but we have to do the
# before-jump too (see long comment above for details).
# We know the axis atom path was A B A (with B being prior
# and A being current), so we know all necessary indices,
# since we can compute arbitrarily old strand indices.
# The jump to do right here is A1 -> B A2 where A2 is
# current step (and steps A1 and A2 had same axis atom)...
# uh oh, we don't know what the prior direction was at
# that time -- just use the worst case of 0, which breaks
# axis on both sides of A.
A1_axis = axis
A1_index = index
X_to_A1_direction = 0 # worst-case guess
do_jump_from_axis( A1_axis, A1_index, X_to_A1_direction )
B_strand = strand
B_s_index = s_index - 1
do_jump_of_strand( B_strand, B_s_index)
B_axis = axis
B_index = prior_index
A1_to_B_direction = prior_direction
B_to_A2_direction = - A1_to_B_direction # (same axis)
# note: not new_direction, since we zeroed that artificially!
assert B_to_A2_direction in (-1, 1)
do_jump_to_axis_once_new_direction_known( B_axis, B_index, B_to_A2_direction )
pass
else:
# we didn't jump
assert new_direction in (-1,1)
assert not changed_direction
assert prior_axis is axis
if not prior_direction:
# we just entered this axis in the prior step -
# break it before entry point
if axis is not _FAKE_AXIS_FOR_BARE_STRANDS:
do_jump_to_axis_once_new_direction_known( axis, prior_index, new_direction )
pass
# record state for the next iteration:
prior_axis = axis
prior_index = index
prior_direction = new_direction
continue # for s_atom, s_index in strand.baseatom_index_pairs()
return # from dna_updater_follow_strand
# end
|