summaryrefslogtreecommitdiff
path: root/cad/src/dna/model/DnaMarker.py
blob: a993907f7c5bd6a779897dd8024f04bbb18050d0 (plain)
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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
# Copyright 2007-2008 Nanorex, Inc.  See LICENSE file for details.
"""
DnaMarker.py - marked positions on atom chains, moving to live atoms as needed

Used internally for base indexing in strands and segments; perhaps used in the
future to mark subsequence endpoints for relations or display styles.

@author: Bruce
@version: $Id$
@copyright: 2007-2008 Nanorex, Inc.  See LICENSE file for details.

TODO: complete the marker-moving code; don't kill markers instead of moving them.
"""

from dna.model.ChainAtomMarker import ChainAtomMarker

from dna.model.DnaStrand import DnaStrand
from dna.model.DnaSegment import DnaSegment

# for parent_node_of_class:
from dna.model.DnaGroup import DnaGroup
from dna.model.DnaStrandOrSegment import DnaStrandOrSegment

from dna.model.DnaGroup import find_or_make_DnaGroup_for_homeless_object

from dna.updater.dna_updater_prefs import pref_draw_internal_markers

from utilities import debug_flags

from graphics.drawing.drawers import drawwirecube

from utilities.constants import orange

from files.mmp.files_mmp_registration import MMP_RecordParser
from files.mmp.files_mmp_registration import register_MMP_RecordParser

# ==

# constants

_CONTROLLING_IS_UNKNOWN = True # represents an unknown value of self.controlling
    # note: this value of True is not strictly correct, but simplifies the code
    #e rename?

# ==

# globals and their accessors [TODO -- move these to updater globals or so]

_marker_newness_counter = 1

_homeless_dna_markers = {}

def _f_get_homeless_dna_markers(): # MISNAMED, see is_homeless docstring in superclass. Maybe rename to
        # something that ends in "markers_that_need_update"?
    """
    Return the homeless dna markers,
    and clear the list so they won't be returned again
    (unless they are newly made homeless).

    [Friend function for dna updater. Other calls
     would cause it to miss newly homeless markers,
     causing serious bugs.]
    """
    res = _homeless_dna_markers.values()
    _homeless_dna_markers.clear()
    res = filter( lambda marker: marker.is_homeless(), res ) # review: is filtering needed?
    return res

def _f_are_there_any_homeless_dna_markers(): # MISNAMED, see _f_get_homeless_dna_markers docstring
    """
    [friend function for dna updater]

    Are there any DnaMarkers needing action by the dna updater?

    @see: _f_get_homeless_dna_markers

    @note: this function does not filter the markers that have been
           recorded as possibly needing update. _f_get_homeless_dna_markers
           does filter them. So this might return True even if that would
           return an empty list. This is ok, but callers should be aware of it.
    """
    return not not _homeless_dna_markers

# ==

#e refile these utilities

def reversed_list(list1):
    """
    Return a reversed copy of list1 (of the same class as list1),
    which can be a Python list, or any object obeying the same API.
    """
    list1 = list1[:]
    list1.reverse()
    return list1

# ==

_superclass = ChainAtomMarker

class DnaMarker( ChainAtomMarker):
    """
    A ChainAtomMarker specialized for DNA axis or strand atoms
    (base atoms only, not Pl atoms).

    Abstract class; see subclasses DnaSegmentMarker and
    DnaStrandMarker.

    Description of how this object stays updated in various situations: ### UPDATE after details are coded @@@

    When read from an mmp file, or copied, it is valid but has no
    cached chain, etc... these will be added when the dna updater
    creates a new chain and ladder and one of these "takes over
    this marker". Part of that "takeover" is for self to record info
    so next_atom is not needed for understanding direction, or anything
    else about the chain self is presently on, since that chain might
    not be valid when self later needs to move along it.
    (This info consists of self's index (which rail and where) and direction
     in a DnaLadder.)

    After copy, in self.fixup_after_copy(), ... ### NIM; spelling? (check atom order then, and record info so next_atom not needed) @@@

    After Undo, our _undo_update method will arrange for equivalent
    checks or updates to be done... ### REVIEW, IMPLEM (also check atom order then, and record info so next_atom not needed) @@@

    If our marker atom dies, self.remove_atom() records this so the
    dna updater will move self to a new marker atom, and make sure
    the new (or preexisting) chain/ladder there takes over self. ### DOIT for preexisting -- can happen! @@@
    (We have a wholechain which can't be preexisting after we're moved,
     but our ladder and its ladder rail chain can be.)

    If our next_atom dies or becomes unbonded from our marker_atom,
    but our marker_atom remains alive, we can probably stay put but
    we might need a new direction indicator in place of next_atom.
    The dna updater handles this too, as a special case of "moving us".
        ### REVIEW, does it work? what call makes this happen? has to come out of changed_atoms, we might not be "homeless" @@@

    After any of these moves or other updates, our marker_atom and next_atom
    are updated to be sufficient when taken alone to specify our
    position and direction on a chain of atoms. This is so those attrs
    are the only ones needing declaration for undo/copy/save for that purpose.

    We also maintain internal "cached" attrs like our index within a ladder,
    for efficiency and to help us move when those atoms become killed or
    get changed in other ways.
    """
    ### REVIEW: what if neither atom is killed, but their bonding changes?
    # do we need to look for marker jigs on changed atoms
    # and treat them as perhaps needing to be moved? YES. DOIT. #### @@@@
    # [same issue as in comment in docstring "we might not be homeless"]


    # Jig or Node API class constants:

    sym = "DnaMarker" # ?? maybe used only on subclasses for SegmentMarker & StrandMarker? maybe never visible?
        # TODO: not sure if this gets into mmp file, but i suppose it does... and it gets copied, and has undoable properties...
        # and in theory it could appear in MT, though in practice, probably only in some PM listwidget, but still,
        # doing the same things a "visible node" can do is good for that.

    # we'll define mmp_record_name differently in each subclass
    ## mmp_record_name = "DnaMarker" # @@@@ not yet read; also the mmp format might be verbose (has color)

    # icon_names = ("missing", "missing-hidden")
    icon_names = ('modeltree/DnaMarker.png', 'modeltree/DnaMarker-hide.png') # stubs; not normally seen for now

    copyable_attrs = _superclass.copyable_attrs + ()
        # todo: add some more -- namely the user settings about how it moves, whether it lives, etc


    # future user settings, which for now are per-subclass constants:

    control_priority = 1

    _wants_to_be_controlling = True # class constant; false variant is nim


    # default values of instance variables:

    wholechain = None

    _position_holder = None # mutable object which records our position
        # in self.wholechain, should be updated when we move
        # [080307, replacing _rail and _baseindex from 080306]

    controlling = _CONTROLLING_IS_UNKNOWN

    _inside_its_own_strand_or_segment = False # 080222 ### REVIEW: behavior when copied? undo? (copy this attr along with .dad?) @@@@
        # whether self is inside the right DnaStrandOrSegment in the model tree
        # (guess: matters only for controlling markers)

##    _advise_new_chain_direction = 0 ###k needed??? @@@
##        # temporarily set to 0 or -1 or 1 for use when moving self and setting a new chain

    _newness = None

    _info_for_step2 = None # rename -- just for asserts - says whether more action is needed to make it ok [revised 080311]

# not needed I think [080311]:
##    # guess: also needs a ladder, and indices into the ladder (which rail, rail/chain posn, rail/chain direction)@@@@

    # == Jig or Node API methods (overridden or extended from ChainAtomMarker == _superclass):

    def __init__(self, assy, atomlist):
        """
        """
        _superclass.__init__(self, assy, atomlist) # see also super's _length_1_chain attr
        global _marker_newness_counter
        _marker_newness_counter += 1
        self._newness = _marker_newness_counter
        if debug_flags.DEBUG_DNA_UPDATER_VERBOSE: # made verbose on 080201
            print "dna updater: made %r" % (self,)
        return

    def kill(self):
        """
        Extend Node method, for debugging and for notifying self.wholechain
        """
        if debug_flags.DEBUG_DNA_UPDATER_VERBOSE:
            msg = "dna updater: killing %r" % (self,)
            print msg
        if self.wholechain:
            self.wholechain._f_marker_killed(self)
            self._clear_wholechain()
        self._inside_its_own_strand_or_segment = False # guess
        _superclass.kill(self)
        return

    def _draw_jig(self, glpane, color, highlighted = False):
        """
        [overrides superclass method]
        """
        if self._should_draw(): # this test is not present in superclass
            for a in self.atoms:
                chunk = a.molecule
                dispdef = chunk.get_dispdef(glpane)
                disp, rad = a.howdraw(dispdef)
                if self.picked:
                    rad *= 1.01
                drawwirecube(color, a.posn(), rad)
                # draw next_atom in a different color than marked_atom
                color = orange # this is the other thing that differs from superclass
        return

    def _should_draw(self):
        return pref_draw_internal_markers()

    def is_glpane_content_itself(self): #bruce 080319
        """
        @see: For documentation, see Node method docstring.

        @rtype: boolean

        [overrides Jig method, but as of 080319 has same implem]
        """
        # Note: we want this return value even if self *is* shown
        # (due to debug prefs), provided it's an internal marker.
        # It might even be desirable for other kinds of markers
        # which are not internal. See comment in Jig method,
        # for the effect of this being False for things visible in GLPane.
        # [bruce 080319]
        return False

    def gl_update_node(self): # not presently used
        """
        Cause whatever graphics areas show self to update themselves.

        [Should be made a Node API method, but revised to do the right thing
         for nodes drawn into display lists.]
        """
        self.assy.glpane.gl_update()

    # ==

    def get_oldness(self):
        """
        Older markers might compete better for controlling their wholechain;
        return our "oldness" (arbitrary scale, comparable with Python standard
        ordering).
        """
        return (- self._newness)

    def wants_to_be_controlling(self):
        return self._wants_to_be_controlling

    def set_wholechain(self, wholechain, position_holder, controlling = _CONTROLLING_IS_UNKNOWN):
        """
        [to be called by dna updater]

        @param wholechain: a new WholeChain which now owns us (not None)
        """
        # revised 080306/080307
        assert wholechain
        self.wholechain = wholechain
        self._position_holder = position_holder
        self.set_whether_controlling(controlling) # might kill self

    def _clear_wholechain(self): # 080306
        self.wholechain = None
        self._position_holder = None
        self.controlling = _CONTROLLING_IS_UNKNOWN # 080306; never kills self [review: is that ok?]
        return

    def forget_wholechain(self, wholechain):
        """
        Remove any references we have to wholechain.

        @param wholechain: a WholeChain which refs us and is being destroyed
        """
        assert wholechain
        if self.wholechain is wholechain:
            self._clear_wholechain()
        return

    def _undo_update(self): # in class DnaMarker
        """
        """
        ## print "note: undo_update is partly nim in %r" % self
        _homeless_dna_markers[id(self)] = self # ok?? sufficient?? [080118]
        # guess, 080227: this should be enough, since our own attrs (in superclass)
        # are declared as undoable state... but just to be safe, do these as well:
        self._clear_wholechain()
            # a new one will shortly be made by the dna updater and take us over
        self._inside_its_own_strand_or_segment = False
        self._info_for_step2 = None # precaution
        _superclass._undo_update(self)
        return

    def remove_atom(self, atom, **opts):
        """
        [extends superclass method]
        """
        _homeless_dna_markers[id(self)] = self # TODO: also do this when copied? not sure it's needed.
        _superclass.remove_atom(self, atom, **opts)
        return

    def changed_structure(self, atom): # 080118
        """
        [extends superclass method]
        """
        # (we extend this method so that if bonds change so that marked_atom and
        #  next_atom are no longer next to one another, we'll get updated as
        #  needed.)
        _homeless_dna_markers[id(self)] = self
        _superclass.changed_structure(self, atom)
        return

    #e also add a method [nim in api] for influencing how we draw the atom?
    # guess: no -- might be sufficient (and is better, in case old markers lie around)
    # to just use the Jig.draw method, and draw the marker explicitly. Then it can draw
    # other graphics too, be passed style info from its DnaGroup/Strand/Segment, etc.

    # == ChainAtomMarker API methods (overridden or extended):

    # == other methods

    def getDnaGroup(self):
        """
        Return the DnaGroup we are contained in, or None if we're not
        inside one.

        @note: if we are inside a DnaStrand or DnaSegment, then our
        DnaGroup will be the same as its DnaGroup. But we can be outside
        one (if this is permitted) and still have a DnaGroup.

        @note: returning None should never happen
        if we have survived a run of the dna updater.

        @see: get_DnaStrand, get_DnaSegment
        """
        return self.parent_node_of_class( DnaGroup)

    def _get_DnaStrandOrSegment(self):
        """
        [private]
        Non-friend callers should instead use the appropriate
        method out of get_DnaStrand or get_DnaSegment.
        """
        return self.parent_node_of_class( DnaStrandOrSegment)

    def _f_get_owning_strand_or_segment(self, make = False):
        """
        [friend method for dna updater]
        Return the DnaStrand or DnaSegment which owns this marker,
        or None if there isn't one,
        even if the internal model tree
        has not yet been updated to reflect this properly
        (which may only happen when self is newly made or
         perhaps homeless).

        @param make: if True, never return None, but instead make a new
                     owning DnaStrandOrSegment and move self into it,
                     or decide that one we happen to be in can own us.

        Note: non-friend callers (outside the dna updater, and not running
        just after creating this before the updater has a chance to run
        or gets called explicitly) should instead use the appropriate
        method out of get_DnaStrand or get_DnaSegment.
        """
        # fyi - used in WholeChain.find_or_make_strand_or_segment
        # revised, and make added, 080222
        if self._inside_its_own_strand_or_segment:
            return self._get_DnaStrandOrSegment()
        if make:
            # if we're in a good enough one already, just make it official
            in_this_already = self._already_in_suitable_DnaStrandOrSegment()
            if in_this_already:
                self._inside_its_own_strand_or_segment = True
                return in_this_already # optim of return self._get_DnaStrandOrSegment()
            # otherwise make a new one and move into it (and return it)
            return self._make_strand_or_segment()
        return None

    def _already_in_suitable_DnaStrandOrSegment(self):
        """
        [private helper for _f_get_owning_strand_or_segment]
        We're some wholechain's controlling marker,
        but we have no owning strand or segment.
        Determine whether the one we happen to be inside (if any)
        can own us. If it can, return it (but don't make it actually
         own us). Otherwise return None.
        """
        candidate = self._get_DnaStrandOrSegment() # looks above us in model tree
        if not candidate:
            return None
        if candidate is not self.dad:
            return None
        if not isinstance( candidate, self._DnaStrandOrSegment_class):
            return None
        # does it own anyone else already?
        for member in candidate.members:
            if isinstance(member, self.__class__):
                if member._inside_its_own_strand_or_segment:
                    return None
        return candidate

    def _make_strand_or_segment(self):
        """
        [private helper for _f_get_owning_strand_or_segment]
        We're some wholechain's controlling marker,
        and we need a new owning strand or segment.
        Make one and move into it (in the model tree).
        """
        chunk = self.marked_atom.molecule # 080120 7pm untested [when written, in class WholeChain]
        # find the right DnaGroup (or make one if there is none).
        # ASSUME the chunk was created inside the right DnaGroup
        # if there is one. There is no way to check this here -- user
        # operations relying on dna updater have to put new PAM atoms
        # inside an existing DnaGroup if they want to use it.
        # We'll leave them there (or put them all into an arbitrary
        # one if different atoms in a wholechain are in different
        # DnaGroups -- which is an error by the user op).
        dnaGroup = chunk.parent_node_of_class(DnaGroup)
        if dnaGroup is None:
            # If it was not in a DnaGroup, it should not be in a DnaStrand or
            # DnaSegment either (since we should never make those without
            # immediately putting them into a valid DnaGroup or making them
            # inside one). But until we implement the group cleanup of just-
            # read mmp files, we can't assume it's not in one here!
            # However, we can ignore it if it is (discarding it and making
            # our own new one to hold our chunks and markers),
            # and just print a warning here, trusting that in the future
            # the group cleanup of just-read mmp files will fix things better.
            if chunk.parent_node_of_class(DnaStrandOrSegment):
                print "dna updater: " \
                      "will discard preexisting %r which was not in a DnaGroup" \
                      % (chunk.parent_node_of_class(DnaStrandOrSegment),), \
                      "(bug or unfixed mmp file)"
            dnaGroup = find_or_make_DnaGroup_for_homeless_object(chunk)
                # Note: all our chunks will eventually get moved from
                # whereever they are now into this new DnaGroup.
                # If some are old and have group structure around them,
                # it will be discarded. To avoid this, newly read old mmp files
                # should get preprocessed separately (as discussed also in
                # update_DNA_groups).
        # now make a strand or segment in that DnaGroup (review: in a certain subGroup?)
        strand_or_segment = dnaGroup.make_DnaStrandOrSegment_for_marker(self)
        strand_or_segment.move_into_your_members(self)
            # this is necessary for the following assignment to make sense
            # (review: is it ok that we ignore the return value here?? @@@@)
        assert self._get_DnaStrandOrSegment() is strand_or_segment
        self._inside_its_own_strand_or_segment = True
        return strand_or_segment

    # ==

    def set_whether_controlling(self, controlling):
        """
        our new chain tells us whether we control its atom indexing, etc, or not
        [depending on our settings, we might die or behave differently if we're not]
        """
        self.controlling = controlling
        if not controlling and self.wants_to_be_controlling():
            self.kill() # in future, might depend more on user-settable properties and/or on subclass
        return

    # ==

    # marker move methods. These are called by dna updater and/or our new WholeChain
    # in this order: [### REVIEW, is this correct? @@@@@@]
    # - _f_move_to_live_atompair_step1, for markers that need to move; might kill self
    # - f_kill_during_move, if wholechain scan finds problem with a marker on one of its atoms; will kill self
    # - xxx_own_marker if a wholechain takes over self ### WRONG, called later
    # - _f_move_to_live_atompair_step2, for markers for which step1 returned true (even if they were killed by f_kill_during_move)
    #   - this prints a bug warning if neither of f_kill_during_move or xx_own_marker was called since _step1 (or if _step1 not called?)

    def _f_move_to_live_atompair_step1(self): # rewritten 080311 ### RENAME, no step1 @@@
        """
        [friend method, called from dna_updater]

        One of our atoms died or changed structure (e.g. got rebonded).
        We still know our old wholechain and our position along it
        (since this is early during a dna updater run),
        so use that info to move a new location on our old wholechain
        so that we are on two live atoms which are adjacent on it.
        Track base position change as we do this (partly nim, since not used).

        If this works, return True; later updater steps must call one of XXX ###doc
        to verify our atoms are still adjacent on the same new wholechain,
        and record it and our position on it. (Also record any info they need
        to run, but for now, this is only used by assertions or debug prints,
        since just knowing our two atoms and total base index motion should
        be sufficient.)

        If this fails, die and return False.

        @return: whether this marker is still alive after this method runs.
        @rtype: boolean
        """
        # REVIEW: should we not return anything and just make callers check marker.killed()?

        if self._info_for_step2 is not None:
            print "bug? _info_for_step2 is not None as _f_move_to_live_atompair_step1 starts in %r" % self

        self._info_for_step2 = None

        # Algorithm -- just find the nearest live atom in the right direction.
        # Do this by scanning old atom lists in chain objects known to markers,
        # so no need for per-atom stored info.
        # [Possible alternative (rejected): sort new atoms for best new home -- bad,
        #  since requires storing that info on atoms, or lots of lookup.]

        if not self.is_homeless():
            # was an assert, but now I worry that some code plops us onto that list
            # for other reasons so this might not always be true, so make it safe
            # and find out (ie debug print) [bruce 080306]
            print "bug or ok?? not %r.is_homeless() in _f_move_to_live_atompair_step1" % self # might be common after undo??

        old_wholechain = self.wholechain
        if not old_wholechain:
            if debug_flags.DEBUG_DNA_UPDATER:
                print "fyi: no wholechain in %r._f_move_to_live_atompair_step1()" % self
                # I think this might be common after mmp read. If so, perhaps remove debug print.
                # In any case, tolerate it. Don't move, but do require new wholechain
                # to find us. Note: this seems to happen a lot for 1-atom chains, not sure why. @@@ DIAGNOSE
            self._info_for_step2 = True
            return not self.killed() #bruce 080318 bugfix, was return True
                # note: this can return False. I didn't verify it can return True,
                # but probably it can (e.g. after mmp read).
            # don't do:
            ## self.kill()
            ## return False

        old_atom1 = self.marked_atom
        old_atom2 = self.next_atom # might be at next higher or next lower index, or be the same

        if not old_atom1.killed() and not old_atom2.killed():
            # No need to move (note, these atoms might be the same)
            # but (if they are not the same) we don't know if they are still
            # bonded so as to be adjacent in the new wholechain. I don't yet know
            # if that will be checked in this method or a later one.
            # Also, if we return True, we may need to record what self needs to do in a later method.
            # And we may want all returns to have a uniform debug print.
            # So -- call a common submethod to do whatever needs doing when we find the
            # atom pair we might move to.
            return self._move_to(old_atom1, old_atom2)
                # note: no pos arg means we're reusing old pos

        # We need to move (or die). Find where to move to if we can.

        if old_atom1 is old_atom2:
            # We don't know which way to scan, and more importantly,
            # either this means the wholechain has length 1 or there's a bug.
            # So don't try to move in this case.
            if len(old_wholechain) != 1:
                print "bug? marker %r has only one atom but its wholechain %r is length %d (not 1)" % \
                      (self, len(old_wholechain))
            assert old_atom1.killed() # checked by prior code
            if debug_flags.DEBUG_DNA_UPDATER:
                print "kill %r since we can't move a 1-atom marker" % self
            self.kill()
            return False

        # Slide this pair of atoms along the wholechain
        # (using the pair to define a direction of sliding and the relative
        #  index of the amount of slide) until we find two adjacent unkilled
        # atoms (if such exist). Try first to right, then (if self was not a
        # ring) to the left.
        # (Detect a ring by whether the initial position gets returned at the
        #  end (which is detected by the generator and makes it stop after
        #  returning that), or by old_wholechain.ringQ (easier, do that).)

        for direction_of_slide in (1, -1):
            if old_wholechain.ringQ and direction_of_slide == -1:
                break
            pos_holder = self._position_holder
            pos_generator = pos_holder.yield_rail_index_direction_counter(
                                relative_direction = direction_of_slide,
                                counter = 0, countby = direction_of_slide,
                             )
                # todo: when markers track their current base index,
                # pass its initial value to counter
                # and our direction of motion to countby.
                # (Maybe we're already passing the right thing to countby,
                #  if base indexing always goes up in the direction
                #  from marked_atom to next_atom, but maybe it doesn't
                #  for some reason.)

            if direction_of_slide == 1:
                _check_atoms = [old_atom1, old_atom2]
                    # for assertions only -- make sure we hit these atoms first,
                    # in this order
            else:
                _check_atoms = [old_atom1] # if we knew pos of atom2 we could
                    # start there, and we could save it from when we go to the
                    # right, but there's no need.

            unkilled_atoms_posns = [] # the adjacent unkilled atoms and their posns, at current loop point

            NEED_N_ATOMS = 2 # todo: could optimize, knowing that this is 2

            for pos in pos_generator:
                rail, index, direction, counter = pos
                atom = rail.baseatoms[index]
                if _check_atoms:
                    popped = _check_atoms.pop(0)
                    ## assert atom is popped - fails when i delete a few duplex atoms, then make bare axis atom
                    if not (atom is popped):
                        print "\n*** BUG: not (atom %r is _check_atoms.pop(0) %r), remaining _check_atoms %r, other data %r" % \
                              (atom, popped, _check_atoms, (unkilled_atoms_posns, self, self._position_holder))
                if not atom.killed():
                    unkilled_atoms_posns.append( (atom, pos) )
                else:
                    unkilled_atoms_posns = []
                if len(unkilled_atoms_posns) >= NEED_N_ATOMS:
                    # we found enough atoms to be our new position.
                    # (this is the only new position we'll consider --
                    #  if anything is wrong with it, we'll die.)
                    if direction_of_slide == -1:
                        unkilled_atoms_posns.reverse()
                    atom1, pos1 = unkilled_atoms_posns[-2]
                    atom2, pos2 = unkilled_atoms_posns[-1]
                    del pos2
                    return self._move_to(atom1, atom2, pos1)
                continue # next pos from pos_generator
            continue # next direction_of_slide

        # didn't find a good position to the right or left.

        if debug_flags.DEBUG_DNA_UPDATER_VERBOSE:
            print "kill %r since we can't find a place to move it to" % self
        self.kill()
        return False

    def _move_to(self, atom1, atom2, atom1pos = None): # revised 080311
        """
        [private helper for _move_step1; does its side effects
         for an actual or possible move, and returns its return value]

        Consider moving self to atom1 and atom2 at atom1pos.
        If all requirements for this being ok are met, do it
        (including telling wholechain and/or self where we moved to,
         in self.position_holder)
        (this might be done partly in _move_step2, in which case
         we must record enough info here for it to do that).
        Otherwise die.
        Return whether we're still alive after the step1 part of this.
        """
        assert not atom1.killed() and not atom2.killed()

        if atom1pos is not None:
            # moving to a new position
            rail, index, direction, counter = atom1pos
            del counter # in future, use this to update our baseindex
            assert atom1 is rail.baseatoms[index]
        else:
            # moving to same position we're at now (could optimize)
            rail, index, direction = self._position_holder.pos
            atom1, atom2 = self.marked_atom, self.next_atom
            assert atom1 is rail.baseatoms[index]

        ok_for_step1 = True

        # semi-obs comment:
        # The atom pair we might move to is (atom1, atom2), at the relative
        # index relindex(?) from the index of self.marked_atom (in the direction
        # from that to self.next_atom). (This is in terms of
        # the implicit index direction in which atom2 comes after atom1, regardless
        # of internal wholechain indices, if those even exist. If we had passed
        # norepeat = False and old_wholechain was a ring, this might be
        # larger than its length as we wrapped around the ring indefinitely.) ## move some to docstring of method used in caller

        # We'll move to this new atom pair if they are adjacent in a new chain
        # found later (and consistent in bond direction if applicable? not sure).
        #
        # We can probably figure out whether they're adjacent right now, but for
        # now let's try waiting for the new chain and deciding then if we're
        # still ok. (Maybe we won't even have to -- maybe we could do it lazily
        # in set_whether_controlling, only checking this if we want to stay alive
        # once we know whether we're controlling. [review])

        if ok_for_step1:
            self.setAtoms([atom1, atom2]) # maybe needed even in "move to same pos" case
            self._position_holder.set_position(rail, index, direction)
                # review: probably not needed, since if we actually moved,
                # a new wholechain will take us over, so we could probably
                # set this holder as invalid instead -- not 100% sure I'm right
            self._info_for_step2 = True # this means, we do need to check atom adjacency in a later method
        else:
            # in future, whether to die here will depend on settings in self.
            # review: if we don't die here, do we return False or True?
            self.kill()
        return ok_for_step1

    def _f_kill_during_move(self, new_wholechain, why): # 080311
        """
        [friend method, called from WholeChain.__init__ during dna_updater run]
        """
        if debug_flags.DEBUG_DNA_UPDATER: # SOON: _VERBOSE
            print "_f_kill_during_move(%r, %r %r)" % (self, new_wholechain, why)
        # I think we're still on an old wholechain, so we can safely
        # die in the usual way (which calls a friend method on it).
        assert self.wholechain is not new_wholechain
        # not sure we can assert self.wholechain is not None, but find out...
        # actually this will be normal for mmp read, or other ways external code
        # can create markers, so remove when seen: @@@@
        if self.wholechain is None:
            print "\nfyi: remove when seen: self.wholechain is None during _f_kill_during_move(%r, %r, %r)" % \
                  ( self, new_wholechain, why)
        self.kill()
        self._info_for_step2 = None # for debug, record no need to do more to move self
        return

    def _f_new_position_for(self, new_wholechain, (atom1info, atom2info)): # 080311
        """
        Assuming self.marked_atom has the position info atom1info
        (a pair of rail and baseindex) and self.next_atom has atom2info
        (also checking this assumption by asserts),
        both in new_wholechain,
        figure out new position and direction for self in new_wholechain
        if we can. It's ok to rely on rail.neighbor_baseatoms
        when doing this. We need to work properly (and return proper
        direction) if one or both atoms are on a length-1 rail,
        and fail gracefully if atoms are the same and/or new_wholechain
        has only one atom.

        If we can, return new pos as a tuple (rail, baseindex, direction),
        where rail and baseindex are for atom1 (revise API if we ever
        need to swap our atoms too). Caller will make those into a
        PositionInWholeChain object for new_wholechain.

        If we can't, return None. This happens
        if our atoms are not
        adjacent in new_wholechain (too far away, or the same atom).

        Never have side effects.
        """
        rail1, baseindex1 = atom1info
        rail2, baseindex2 = atom2info

        assert self.marked_atom is rail1.baseatoms[baseindex1]
        assert self.next_atom is rail2.baseatoms[baseindex2]

        if self.marked_atom is self.next_atom:
            return None

        # Note: now we know our atoms differ, which rules out length 1
        # new_wholechain, which means it will yield at least two positions
        # below each time we scan its atoms, which simplifies following code.

        # Simplest way to work for different rails, either or both
        # of length 1, is to try scanning in both directions.

        try_these = [
            (rail1, baseindex1, 1),
            (rail1, baseindex1, -1),
         ]
        for rail, baseindex, direction in try_these:
            pos = rail, baseindex, direction
            generator = new_wholechain.yield_rail_index_direction_counter( pos )

            # I thought this should work, but it failed -- I now think it was
            # just a logic bug (since we might be at the wholechain end and
            # therefore get only one position from the generator),
            # but to remove sources of doubt, do it the more primitive
            # way below.
            ## pos_counter_A = generator.next()
            ## pos_counter_B = generator.next() # can get exceptions.StopIteration - bug?

            iter = 0
            pos_counter_A = pos_counter_B = None
            for pos_counter in generator:
                iter += 1
                if iter == 1:
                    # should always happen
                    pos_counter_A = pos_counter
                elif iter == 2:
                    # won't happen if we start at the end we scan towards
                    pos_counter_B = pos_counter
                    break
                continue
            del generator
            assert iter in (1, 2)
            railA, indexA, directionA, counter_junk = pos_counter_A
            assert (railA, indexA, directionA) == (rail, baseindex, direction)
            if iter < 2:
                ## print "fyi: only one position yielded from %r.yield_rail_index_direction_counter( %r )" % \
                ##    ( new_wholechain, pos )
                ##      # remove when debugged, this is probably normal, see comment above
                # this direction doesn't work -- no atomB!
                pass
            else:
                # this direction works iff we found the right atom
                railB, indexB, directionB, counter_junk = pos_counter_B
                atomB = railB.baseatoms[indexB]
                if atomB is self.next_atom:
                    return rail, baseindex, direction
                pass
            continue # try next direction in try_these

        return None

    def _f_new_pos_ok_during_move(self, new_wholechain): # 080311
        """
        [friend method, called from WholeChain.__init__ during dna_updater run]
        """
        del new_wholechain
        self._info_for_step2 = None # for debug, record no need to do more to move self
        return

    def _f_should_be_done_with_move(self): # 080311
        """
        [friend method, called from dna_updater]
        """
        if self.killed():
            return
        if self._info_for_step2: # TODO: fix in _undo_update @@@@@
            print "bug? marker %r was not killed or fixed after move" % self
        # Note about why we can probably assert this: Any marker
        # that moves either finds new atoms whose chain has a change
        # somewhere that leads us to find it (otherwise that chain would
        # be unchanged and could have no new connection to whatever other
        # chain lost atoms which the marker was on), or finds none, and
        # can't move, and either dies or is of no concern to us.
        self._info_for_step2 = None # don't report the same bug again for self
        return

    def DnaStrandOrSegment_class(self):
        return self._DnaStrandOrSegment_class

    pass # end of class DnaMarker

# ==

class DnaSegmentMarker(DnaMarker): #e rename to DnaAxisMarker? guess: no...
    """
    A kind of DnaMarker for marking an axis atom of a DnaSegment
    (and, therefore, a base-pair position within the segment).
    """
    _DnaStrandOrSegment_class = DnaSegment
    featurename = "Dna Segment Marker" # might never be visible
    mmp_record_name = "DnaSegmentMarker"
    def get_DnaSegment(self):
        """
        Return the DnaSegment that owns us, or None if none does.
        [It's not yet decided whether the latter case can occur
        normally if we have survived a run of the dna updater.]

        @see: self.controlling, for whether we control base indexing
        (and perhaps other aspects) of the DnaSegment that owns us.

        @see: getDnaGroup
        """
        res = self._get_DnaStrandOrSegment()
        assert isinstance(res, DnaSegment)
        return res
    pass

# ==

class DnaStrandMarker(DnaMarker):
    """
    A kind of DnaMarker for marking an atom of a DnaStrand
    (and, therefore, a base position along the strand).
    """
    _DnaStrandOrSegment_class = DnaStrand
    featurename = "Dna Strand Marker" # might never be visible
    mmp_record_name = "DnaStrandMarker"
    def get_DnaStrand(self):
        """
        Return the DnaStrand that owns us, or None if none does.
        [It's not yet decided whether the latter case can occur
        normally if we have survived a run of the dna updater.]

        @see: self.controlling, for whether we control base indexing
        (and perhaps other aspects) of the DnaStrand that owns us.

        @see: getDnaGroup
        """
        res = self._get_DnaStrandOrSegment()
        assert isinstance(res, DnaStrand)
        return res
    pass

# ==

class _MMP_RecordParser_for_DnaSegmentMarker( MMP_RecordParser): #bruce 080227
    """
    Read the MMP record for a DnaSegmentMarker Jig.
    """
    def read_record(self, card):
        constructor = DnaSegmentMarker
        jig = self.read_new_jig(card, constructor)
            # note: for markers with marked_atom == next_atom,
            # the mmp record will have exactly one atom;
            # current reading code trusts this and sets self._length_1_chain
            # in __init__, but it would be better to second-guess it and verify
            # that the chain is length 1, and mark self as invalid if not.
            # [bruce 080227 comment]
            #
            # note: this includes a call of self.addmember
            # to add the new jig into the model
        return
    pass

class _MMP_RecordParser_for_DnaStrandMarker( MMP_RecordParser): #bruce 080227
    """
    Read the MMP record for a DnaStrandMarker Jig.
    """
    def read_record(self, card):
        constructor = DnaStrandMarker
        jig = self.read_new_jig(card, constructor)
            # see comments in _MMP_RecordParser_for_DnaSegmentMarker
        return
    pass

def register_MMP_RecordParser_for_DnaMarkers(): #bruce 080227
    """
    [call this during init, before reading any mmp files]
    """
    register_MMP_RecordParser( DnaSegmentMarker.mmp_record_name,
                               _MMP_RecordParser_for_DnaSegmentMarker )
    register_MMP_RecordParser( DnaStrandMarker.mmp_record_name,
                               _MMP_RecordParser_for_DnaStrandMarker )
    return

# end