summaryrefslogtreecommitdiff
path: root/cad/src/simulation/movie.py
blob: e24f54b44ef8ae67cc26cea9b768d0b5fe75c028 (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
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
# Copyright 2005-2009 Nanorex, Inc.  See LICENSE file for details.
"""
movie.py -- class Movie, used for simulation parameters and open movie files

@author: Mark
@version: $Id$
@copyright: 2005-2009 Nanorex, Inc.  See LICENSE file for details.

History:

Initially by Mark.

Some parts rewritten by Bruce circa 050427.
"""

import os, sys
from struct import unpack
from PyQt4.Qt import Qt, qApp, QApplication, QCursor, SIGNAL
from utilities.Log import redmsg, orangemsg, greenmsg
from geometry.VQT import A
from foundation.state_utils import IdentityCopyMixin
from operations.move_atoms_and_normalize_bondpoints import move_atoms_and_normalize_bondpoints
from utilities import debug_flags
from platform_dependent.PlatformDependent import fix_plurals
from utilities.debug import print_compact_stack, print_compact_traceback
from files.dpb_trajectory.moviefile import MovieFile #e might be renamed, creation API revised, etc

import foundation.env as env

ADD = True
SUBTRACT = False
FWD = 1
REV = -1
_DEBUG0 = 0
_DEBUG1 = 0 # DO NOT COMMIT WITH 1
_DEBUG_DUMP = 0 # DO NOT COMMIT WITH 1

playDirection = { FWD : "Forward", REV : "Reverse" }

# ==

class Movie(IdentityCopyMixin): #bruce 080321 bugfix: added IdentityCopyMixin
    """
    Movie object.

    Multiple purposes (which ought to be split into separate objects more than
    they have been so far):
    - Holds state of one playable or playing movie,
      and provides methods for playing it,
      and has moviefile name and metainfo;
    - also (before its moviefile is made) holds parameters needed
      for setting up a new simulator run
      (even for Minimize, though it might never make a moviefile);
    - those parameters might be used as defaults (by external code) for setting
      up another sim run.

    Warnings:
    - methods related to playing are intimately tied to movieMode.py's
      Property Manager;
    - so far, only supports playing one movie at a time;
    - so far, provisions/checks for changing Parts during movie playing are
      limited.

    Movie lifecycle
    [bruce 050427 intention -- some details are obs or need review #####@@@@@]:

    - If we make the movie in this session (or someday, if we read a movie
      node from an mmp file), we give it an alist, and we should never change
      that alist after that, but we'll need to check it sometimes, in case of
      atoms changing Parts or being killed.

      If we make the movie as a way of playing the trajectory in an existing
      file, then when we do that (or when needed) we come up with an alist,
      and likewise never change the alist after that.

      (In the future, if there's a "play on selection" option, this only
      affects which atoms move when we play the movie -- it doesn't alter the
      alist, which is needed in its original form for interpreting frames in
      the moviefile.)

      Maybe the Movie is not meant to be ever played (e.g. it's just for
      holding sim params, perhaps for Minimize), but if it is, then external
      code optionally queries might_be_playable() to decide whether to try
      playing it (e.g. when entering "Play Movie"), calls cueMovie() before
      actually starting to play it (this verifies it has a valid alist (or that
      one can be constructed) which is playable now, and perhaps changes the
      current Part so the playing movie will be visible), then calls other
      methods to control the playing, then calls _close when the playing is done
      (which snuggles singlets and disables some movieMode PM controls).

      But even between _close and the next cueMovie(), the alist is maintained
      -- switching Parts is not enough to try reloading the movie for
      playing on different atoms. If changing it to play on different
      atoms is ever needed, we'll add specific support for that.
      Not only is alist maintained, so is valuable info about the moviefile,
      like cached frames. The file might be closed (to save on open files
      for when we have multiple loaded movies, and to help us detect
      whether the file gets overwritten with new data); if closed,
      it's reopened on the next cueMovie(), and it's always rechecked on
      cueMovie() for being overwritten. #####@@@@@ doit or testit

    State variables involved in all this (incomplete list, there's also
    currentFrame and a few others in the playing-state):

    - isOpen() tells whether we're between cueMovie() and _close. (It has no
      guaranteed relation to whether any file object is open, though in
      practice it might coincide with that for the moviefile.)

    - alist is None or a valid list of atoms (this might be replaced by
      an object for holding that list)

    - the first time cueMovie() is called, the movie file header is parsed,
      and an alist is assigned if possible and not already known, and an
      "alist_and_moviefile" object to hold both of them and keep them in
      correspondence is created, and if this works the file is never again
      fully reparsed, though it might be rechecked later to ensure it hasn't
      been overwritten.
      #####@@@@@ is it ok to do this for each existing call of cueMovie()?

    - might_be_playable() returns True if this object *might* be playable,
      provided cueMovie() has not yet been called and succeeded
      (i.e. if we don't yet have an alist_and_moviefile object);
      but after cueMovie() has once succeeded, it returns True iff the alist
      is currently ok to try to play from the file (according to our
      alist_and_moviefile).
      (This might always be True, depending on our policy for atoms moved
      to other parts or killed, but it might trigger history warnings in
      some cases -- not yet decided #####@@@@@).
      It won't actually recheck the file (to detect overwrites) until
      cueMovie() is called. (The goal is for might_be_playable to be fast
      enough to use in e.g. updating a node-icon in the MT, in the future.)
    """
    #bruce 050324 comment: note that this class is misnamed --
    # it's really a SimRunnerAndResultsUser... which might
    # make and then use .xyz or .dpb results; if .dpb, it's able
    # to play the movie; if .xyz, it just makes it and uses it once
    # and presently doesn't even do it in methods, but in external
    # code (nonetheless it's used). Probably should split it into subclasses
    # and have one for .xyz and one for .dpb, and put that ext code
    # into one of them as methods. ###@@@
    #bruce 050329 comment: this file is mostly about the movie-playable DPB file;
    # probably it should turn into a subclass of SimRun, so the objects for other
    # kinds of sim runs (eg minimize) can be different. The superclass would
    # have most of the "writemovie" function (write sim input and run sim)
    # as a method, with subclasses customizing it.

    duration = 0.0 # seconds it took to create moviefile (if one was created and after that's finished) [bruce 060112]
    ref_frame = None # None or (frame_number, sim_posn_array) for a reference frame for use in playing the movie
        # from purely differential data in old-format moviefiles (to finish fixing bug 1297; someday can help improve fix to 1273)
        # (see comments in get_sim_posns for relevant info and caveats) [bruce 060112]

    ignore_slider_and_spinbox = False # (in case needed before init done)
    minimize_flag = False # whether we're doing some form of Minimize [bruce 060112]
    def __init__(self, assy, name=None):
        """
        ###doc; note that this Movie might be made to hold params for a sim run,
        and then be told its filename, or to read a previously saved file;
        pre-050326 code always stored filename from outside and didn't tell
        this object how it was becoming valid, etc...
        """
        self.assy = assy
        self.win = self.assy.w
        self.glpane = self.assy.o ##e if in future there's more than one glpane, recompute this whenever starting to play the movie

        # for future use: name of the movie that appears in the modelTree.
        self.name = name or "" # assumed to be a string by some code
        # the name of the movie file
        self.filename = "" #bruce 050326 comment: so far this is only set by external code; i'll change that someday
##        # movie "file object"
##        self.fileobj = None
        # the total number of frames actually in our moviefile [might differ from number requested]
        self.totalFramesActual = 0
            # bruce 050324 split uses of self.totalFrames into totalFramesActual and totalFramesRequested
            # to help fix some bugs, especially when these numbers differ
        # currentFrame is the most recent frame number of this movie that was played (i.e. used to set model atom positions)
        # by either movieMode or real-time viewing of movie as it's being created
        # [bruce 060108 added realtime update of this attr, and revised some related code, re bug 1273]
        self.currentFrame = 0
        # the most recent frame number of this movie that was played during its creation by realtime dynamics, or 0 [bruce 060108]
        self.realtime_played_framenumber = 0
        # the starting (current) frame number when we last entered MOVIE mode   ###k
        self.startFrame = 0
        # a flag that indicates whether this Movie has been cueMovie() since the last _close
        # [*not* whether moviefile is open or closed, like it indicated before bruce 050427]
        self.isOpen = False
        # a flag that indicates the current direction the movie is playing
        self.playDirection = FWD
        #e someday [mark, unknown date]: a flag that indicates if the movie and the part are synchronized
        ## self.isValid = False # [bruce 050427 comment: should be renamed to avoid confusion with QColor.isValid]
        # the number of atoms in each frame of the movie.
        self.natoms = 0
        # show each frame when _playToFrame is called
        self.showEachFrame = False
        # a flag that indicates the movie is paused
        self.isPaused = True
        # 'movie_is_playing' is a flag that indicates a movie is playing. It is
        # used by other code to speed up rendering times by optionally disabling
        # the (re)building of display lists for each frame of the movie.
        # [Mark 051209.]
        self.win.movie_is_playing = False
        # moveToEnd: a flag that indicates the movie is currently fast-forwarding to the end.
        # [bruce 050428 comment: in present code, self.moveToEnd might not be properly maintained
        #  (it's never set back to False except by _pause; I don't know if _pause is
        #   always called, but if it needs to be, this is not documented),
        #   and it's also not used. I suggest replacing it with a logical combination
        #   of other flags, if it's ever needed.]
        self.moveToEnd = False
        # a flag that indicates if the wait (hourglass) cursor is displayed.
        self.waitCursor = False
        # a flag to tell whether we should add energy information to the tracefile during dynamics runs
        self.print_energy = False

        # simulator parameters to be used when creating this movie,
        # or that were used when it was created;
        # these should be stored in the dpb file header so they
        # can be retrieved later. These will be the default values used by
        # the simsetup dialog, or were the values entered by the user.
        # If the sim parameters change, they might need to be updated in all places marked "SIMPARAMS" in the code.
        self.totalFramesRequested = 900
            # bruce 050325 added totalFramesRequested, changed some uses of totalFrames to this
        self.temp = 300
        self.stepsper = 10
##        self.watch_motion = False # whether to show atom motion in realtime [changed by Mark, 060424]
##            # (note: this default value affects Dynamics, but not Minimize, which uses its own user pref for this,
##            #  but never changes this value to match that [as of 060424; note added by Bruce])
        self._update_data = None

        self.update_cond = None # as of 060705 this is also used to derive self.watch_motion, in __getattr__
        self.timestep = 10
            # Note [bruce 050325]: varying the timestep is not yet supported,
            # and this attribute is not presently used in the cad code.
        # support for new options for Alpha7 [bruce 060108]
        self.create_movie_file = True # whether to store movie file
            # [nim (see NFR/bug 1286), treated as always T -- current code uses growing moviefile length to measure progress;
            #  to implem this, use framebuffer callback instead, but only when this option is asked for.]
        # bruce 050324 added these:
        self.alist = None # list of atoms for which this movie was made, if this has yet been defined
        self.alist_and_moviefile = None #bruce 050427: hold checked correspondence between alist and moviefile, if we have one
        self.debug_dump("end of init")
        return

    ##bruce 050428 removing 1-hour-old why_not_playable feature,
    ## since ill-conceived, incomplete, and what's there doesn't work (for unknown reasons).
##    why_not_playable = ""
##        # reason why we're not playable (when we're not), if known (always a string; a phrase; never used if truly playable)
    def might_be_playable(self): #bruce 050427
        """
        Is it reasonable to try to play this movie?
        This does NOT check whether it's still valid for its atoms or the current part;
        if the caller then tries to play it, we'll check that and complain.
        BUT if it has been previously checked and found invalid, this should return False or perhaps redo the check.
        For more info see docstring of class Movie.
        """
        if self.file_trashed:
            #bruce 050428: some new sim in this process has trashed our file (even if it didn't complete properly)
            # (btw, this doesn't yet help if it was some other process which did the trashing)
            #e (it would be nicer if new sims would store into a temp file until they completed successfully;
            #   even then they need to set this (via method below)
            #   and they might want to ask before doing that, or use a new filename)
            return False #e history message??
        filename_ok = self.filename and self.filename.endswith('.dpb') and os.path.exists(self.filename)
        if not self.alist_and_moviefile:
##            if not filename_ok and not self.why_not_playable:
##                self.why_not_playable = "need filename of existing .dpb file" # can this ever be seen??
            return filename_ok # ok to try playing it, though we don't yet know for sure whether this will work.
        else:
            res = self.alist_and_moviefile.might_be_playable()
##            self.why_not_playable = self.alist_and_moviefile.why_not_playable
##                #e (is this a sign the whynot should be part of the retval?)
            return res

    file_trashed = False
    def fyi_reusing_your_moviefile( self, moviefile):
        """
        If moviefile happens to be the name of our own moviefile,
        know that it's being trashed and we'll never again be playable
        (unless we have not yet read any data from it,
         in which case we're no worse off than before, I suppose).
        """
        if self.filename and self.filename == moviefile and self.alist_and_moviefile:
            self.warning( "overwriting moviefile (previously open movie will no longer be playable)." )
                # note, this wording is from point of view of caller -- *we* are the previously open movie.
                # I'm assuming it's overwritten, not only removed, since no reason to remove it except to overwrite it.
            self.file_trashed = True
##            self.why_not_playable = "moviefile overwritten by a more recent simulation" #e or get more detail from an arg?
        return

    def __getattr__(self, attr): # in class Movie
        if attr == 'history':
            #bruce 050913 revised this; I suspect it's not needed and could be removed
            print_compact_stack("deprecated code warning: something accessed Movie.history attribute: ") #bruce 060705 -> _stack
            return env.history
        elif attr == 'watch_motion': #bruce 060705
##            if env.debug():
##                print ("debug: fyi: Movie.watch_motion attribute computed from update_cond: ")
            try:
                return not not self.update_cond
            except:
                return False
        raise AttributeError, attr

    def destroy(self): #bruce 050325
        # so far this is only meant to be called before the file has been made
        # (eg it doesn't destroy our big fancy subobjects);
        # it should be revised to work either way and _close if necessary.
        # for now, just break cycles.
        self.win = self.assy = self.part = self.alist = self.fileobj = None
        del self.fileobj # obs attrname
        del self.part

    # == methods for letting this object (just after __init__) represent a previously saved movie file

    def represent_this_moviefile( self, mfile, part = None): #bruce 050326
        """
        Try to start representing the given moviefile (which must end
        with '.dpb');

        Return true iff this succeeds; if it fails DON'T emit error message.
        if part is supplied, also [NIM] make sure mfile is valid for current
        state of that part.
        """
        #e should the checking be done in the caller (a helper function)?
        assert mfile.endswith(".dpb") # for now
        if os.path.exists(mfile):
            self.filename = mfile
            ###e do more... but what is needed? set alist? only if we need to play it, and we might not... (PlotTool doesn't)
            assert not part # this is nim; should call the validity checker
            return True
        else:
            pass #e env.history.message(redmsg(...)) -- is this a good idea? I think caller wants to do this... ###k
            self.destroy()
            return False
        pass

    # == methods for letting this object represent a movie (or xyz) file we're about to make, or just did make

    def set_alist(self, alist): #bruce 050325
        """
        Verify this list of atoms is legal (as an alist to make a movie from),
        and set it as this movie's alist. This only makes sense before making
        a moviefile, or after reading one we didn't make in the same session
        (or whose alist we lost) and figuring out somehow what existing
        atoms it should apply to. But nothing is checked about whether this
        alist fits the movie file, if we have one, and/or the other params
        we have -- that's assumed done by the caller.
        """
        alist = list(alist) # make our own copy (in case caller modifies its copy), and ensure type is list
        atm0 = alist[0]
        assert atm0.molecule.part, "atm0.molecule.part %r should exist" % atm0.molecule.part
        for atm in alist:
            assert atm.molecule.part == atm0.molecule.part, \
                   "atm %r.molecule.part %r should equal atm0.molecule.part %r" % \
                   (atm, atm.molecule.part, atm0.molecule.part)
        # all atoms have the same Part, which is not None, and there's at least one atom.
        self.alist = alist
        self.natoms = len(alist) #bruce 050404; note, some old code might reset this (perhaps wrongly?) when reading a .dpb file
        return

    def set_alist_from_entire_part(self, part):
        """
        Set self.alist to a list of all atoms of the given Part,
        in the order in which they would be written to a new mmp file.
        """
        # force recompute of part.alist, since it's not yet invalidated when it needs to be
        part.alist = None
        del part.alist
        ## self.alist = part.alist
        alist = part.alist # this recomputes it
        self.set_alist(alist) #e could optimize (bypass checks in that method)
        return

    # == methods for playing the movie file we know about (ie the movie we represent)

    # [bruce 050427 comments/warnings:
    #  These methods need to be refactored, since they intimately know about movieMode's dashboard (they update it).
    #  They also use and maintain state-of-playing variables in self (which might be useful for any manner of playing the movie).
    #  One of the methods does recursive processing of QEvents and doesn't return until the movie is paused (I think).
    # ]

    def _cueMovieCheck(self): #bruce 050427
        """
        Checks movie file to determine that its playable and that it's ok to
        start playing it.
        If not, emit complaints on history widget, don't set our state
        variables; return False. If so, return True.
        """
        # remember what to unset if things don't work when we return
        we_set_alist = False
        we_set_alist_and_moviefile = False
        if not self.might_be_playable():
            env.history.message( redmsg( "Movie is not playable.")) # can't happen, I think... if it can, it should give more info.
            return False
        if not self.alist_and_moviefile:
            # we haven't yet set up this correspondence. Do it now. Note that the filename needs to be set up here,
            # but the alist might or might not have been (and if it was, it might not be all the atoms in the current Part).
            if self.alist is None:
                # alist should equal current Part; then verify moviefile works for that.
                #e In future, we might also permit alist to come from selection now,
                # if this has right natoms for moviefile and whole part doesn't.
                # *Or* we might let it come from main part if we're in some other part which doesn't fit.
                # For now, nothing so fancy. [bruce 050427]
                self.set_alist_from_entire_part( self.assy.part)
                we_set_alist = True
            if self.ref_frame is None: #bruce 060112
                # make a reference frame the best we can from current atom positions
                # [note: only needed if movie file format needs one, so once we support new DPB file format,
                #  we should make this conditional on the format, or do it in a callback provided to alist_and_moviefile ###@@@]
                if self.currentFrame:
                    # should never happen once bug 1297 fix is completed
                    print "warning: making ref_frame from nonzero currentFrame %d" % self.currentFrame
                self.ref_frame = ( self.currentFrame, A(map(lambda a: a.sim_posn(), self.alist)) )
            else:
                # Client code supplied ref_frame, and that would be the fastest frame to start playing,
                # but we should not change currentFrame to match it since it should match actual atom posns,
                # not "desired frame to play next". Besides, I'm not sure this only runs when we cue the movie.
                # Some other code can play to frame 0 when cueing movie, if desired. For now that's nim;
                # it would require figuring out where movies are cued and when to autoplay to the desired frame
                # and which frame that was (presumably the reference frame). ###@@@ [bruce 060112]
                pass ## self.currentFrame = self.ref_frame[0]
            if len(self.ref_frame[1]) != len(self.alist):
                print "apparent bug: len(self.ref_frame[1]) != len(self.alist)" # should never happen, add %d if it does
            if self.currentFrame != 0:
                # As of 060111, this can happen (due to fixing bug 1273) if we moved atoms in realtime while creating movie.
                # Even after the complete fix of bug 1297 (just done, 060112) it will still happen unless we decide to
                # change the fix of bug 1273 to start playing from ref_frame (see comment above about that).
                #    Maybe it could also happen if we leave moviemode on a nonzero frame, then reenter it?
                # Should test, but right now my guess is that we're prohibited from leaving it in that case!
                env.history.message( greenmsg( "(Starting movie from frame %d.)" % self.currentFrame ))
                # [note: self.currentFrame is maintained independently of a similar variable
                #  inside a lower-level moviefile-related object.]
            self.alist_and_moviefile = alist_and_moviefile( self.assy, self.alist, self.filename, ref_frame = self.ref_frame )
                                                            ## curframe_in_alist = self.currentFrame)
                # if this detected an error in the file matching the alist, it stored this fact but didn't report it yet or fail #####@@@@@ doit
                # maybe it won't even check yet, until asked...
            we_set_alist_and_moviefile = True
            ok = self.alist_and_moviefile.valid()
        else:
            # we have an alist_and_moviefile but we need to recheck whether the alist still matches the file
            # (in case the file changed on disk).
            ok = self.alist_and_moviefile.recheck_valid() # in theory this might come up with a new larger totalFramesActual value (NIM)
        if not ok:
            # it emitted error messages already #####@@@@@ doit
            # reset what we set, in case user will try again later with altered file or different assy.part
            if we_set_alist_and_moviefile:
                self.alist_and_moviefile.destroy()
                self.alist_and_moviefile = None
            if we_set_alist:
                self.alist = None
            return False
        # change current part and/or arrange to warn if user does that? No, this is done later when we _play.
        return True

    def cueMovie(self, propMgr = None, hflag = True): #bruce 060112 revised retval documentation and specific values
        """
        Setup this movie for playing.

        @param propMgr: The movie property manager.
        @type  propMgr: MoviePropertyManager

        @param hflag: The history message flag. If True, print a history
                      message.
        @type  hflag: boolean

        @return: False if this worked, True if it failed
                 (warning: reverse of common boolean retvals).
        @rtype:  boolean

        [#doc whether it always prints error msg to history if it failed.]
        """
        # bruce 050427 comment:
        # what it did before today:
        # - figure out part to use for movie file (this is wrong and needs changing).
        # - check movie file for validity re that part (on error, print message and return true error code)
        # - freeze atoms (making some other operations on them illegal, I think, in the present code)
        # - possibly save frame 0 positions -- only if self.currentFrame is 0
        # - open movie file, read header
        # - update dashboard frame number info (SB, SL, label)
        # - history info: if hflag: self._info()
        # - self.startFrame = self.currentFrame
        if _DEBUG1:
            print "movie.cueMovie() called. filename = [" + self.filename + "]"

        self.propMgr = propMgr

        if self.isOpen and debug_flags.atom_debug:
            env.history.message( redmsg( "atom_debug: redundant cueMovie()? bug if it means atoms are still frozen"))

        kluge_ensure_natoms_correct( self.assy.part) # matters for some warn_if_other_part messages, probably not for anything else

        ok = self._cueMovieCheck()
        if not ok:
            # bruce 050427 doing the following disable under more circumstances than before
            # (since old code's errcodes 'r' 1 or 2 are no longer distinguished here, they're just both False) -- is that ok?
            self.isOpen = False #bruce 050427 added this
            return True

        #bruce 050427 extensively rewrote the following (and moved some of what was here into OldFormatMovieFile_startup)

        ## no longer needed: self.alist_and_moviefile.own_atoms() # older related terms: self.movsetup(), freeze the atoms
        self.isOpen = True

        self.totalFramesActual = self.alist_and_moviefile.get_totalFramesActual() # needed for dashboard controls
        self.natoms = len(self.alist) # needed for _info

        if hflag:
            self._info() # prints info to history

        # startFrame and currentFrame are compared in _close to determine if the assy has changed due to playing this movie. ###k
        self.startFrame = self.currentFrame

        return False

##        # Debugging Code [to enable, uncomment and remove prior 'return' statement]
##        if _DEBUG1:
##            msg = "Movie Ready: Number of Frames: " + str(self.totalFramesActual) + \
##                    ", Current Frame:" +  str(self.currentFrame) +\
##                    ", Number of Atoms:" + str(self.natoms)
##            env.history.message(msg)
##
##            ## filepos = self.fileobj.tell() # Current file position
##            msg = "Current frame:" + str(self.currentFrame) ## + ", filepos =" + str(filepos)
##            env.history.message(msg)
##        return False

    # ==

    def warn_if_other_part(self, part): #bruce 050427; to call when play is pressed, more or less...
        "warn the user if playing this movie won't move any (or all) atoms in the given part (or about other weird conditions too)."
        if self.alist is None:
            return
        # like .touches_part() in a subobject, but can be called sooner...
        yes = 0 # counts the ones we'll move
        other = killed = False
        for atm in self.alist:
            if atm.molecule.part == part:
                yes += 1
            elif atm.molecule.part:
                other = True
            else:
                killed = True
        if not (yes or other):
            if not killed:
                # should never happen, I think
                self.warning( "this movie has no atoms. Playing it anyway (no visible effect).")
            else:
                self.warning( "all of this movie's atoms have been deleted. Playing it anyway (no visible effect).")
        else:
            if not yes:
                self.warning( "to see this movie playing, you must display a different Part.") #e which one, or ones?
            elif other or killed:
                if killed:
                    self.warning( "some of this movie's atoms have been deleted. (Playing it still moves the remaining atoms.)")
                if other:
                    self.warning( "some of this movie's atoms have been moved to another Part (maybe one on the clipboard). " \
                                  "Playing it moves its atoms in whichever Parts they reside in." )
                if yes < part.natoms:
                    # (this assumes part.natoms has been properly updated by the caller; cueMovie() does this.)
                    self.warning( "some displayed atoms are not in this movie, and stay fixed while it plays.")
        return

    def warning(self, text):
        env.history.message( orangemsg( "Warning: " + text))

    def _close(self):
        """
        Close movie file and adjust atom positions.
        """
        #bruce 050427 comment: what this did in old code:
        # - if already closed, noop.
        # - pause (sets internal play-state variables, updates dashboard ###k)
        # - close file.
        # - unfreeze atoms.
        # - if frame moved while movie open this time, self.assy.changed()
        # - wanted to delete saved frame 0 but doesn't (due to crash during devel)
        if _DEBUG1:
            print_compact_stack( "movie._close() called. self.isOpen = %r" % self.isOpen)
        if not self.isOpen:
            return
        self._pause(0)
        ## self.fileobj.close() # Close the movie file.
        self.alist_and_moviefile.snuggle_singlets() #bruce 050427
        self.alist_and_moviefile.close_file() #bruce 050427
        self.isOpen = False #bruce 050425 guess: see if this fixes some bugs
        if _DEBUG1:
            print "self.isOpen = False #bruce 050425 guess: see if this fixes some bugs" ###@@@
        ## self.movend() # Unfreeze atoms.
        if self.startFrame != self.currentFrame:
            self.assy.changed()
                #bruce 050427 comment: this [i.e. having this condition rather than 'if 1' [060107]]
                # only helps if nothing else in playing a movie does this...
                # I'm not sure if that's still true (or if it was in the older code, either).
        return

    def _play(self, direction = FWD):
        """
        Start playing movie from the current frame.
        """
        #bruce 050427 comment: not changing this much

        if _DEBUG0:
            print "movie._play() called.  Direction = ", playDirection[ direction ]

        if not self.isOpen: #bruce 050428 not sure if this is the best condition to use here ###@@@
            if (not self.might_be_playable()) and 0: ## self.why_not_playable:
                msg = "Movie file is not presently playable: %s." ## % (self.why_not_playable,)
            else:
                msg = "Movie file is not presently playable." ###e needs more detail, especially when error happened long before.
            env.history.message( redmsg( msg )) #bruce 050425 mitigates bug 519 [since then, it got fixed -- bruce 050428]
            return

        if direction == FWD and self.currentFrame == self.totalFramesActual:
            return
        if direction == REV and self.currentFrame == 0:
            return

        self.playDirection = direction

        if self.currentFrame in [0, self.realtime_played_framenumber]:
            #bruce 060108 added realtime_played_framenumber; probably more correct would be only it, or a flag to emit this once
            env.history.message("Playing movie file [" + self.filename + "]")
            self._continue(hflag = False) #bruce 060108 revised call, should be equivalent
        else:
            self._continue()

    # josh 050815.
    # Changed name from _writeas to _write_povray_series.  mark 050908.
    # I plan to write a special Movie Maker dialog that would call this with arguments.
    # Mark 050908
    def _write_povray_series(self, name):
        """
        Writes the movie out as a series of POV-Ray files, starting with the
        current frame until the last frame, skipping frames using the
        "Skip" value from the dashboard.

        If your trajectory file was foobar.dpb, this will write, e.g.,
        foobar.000000.pov thru foobar.000999.pov (assuming your movie has
        1000 frames).

        If you have bash, you may then run:
            for FN in foobar.000*.pov; { povray +W800 +H600 +A -D $FN; } &> /dev/null &
        to generate the .png files.

        This is not to be done under NE1 because it typically takes several
        hours and will be best done on a renderfarm with commands appropriate
        to the renderfarm.

        You may then make a move of it with:
            mencoder "mf://*.png" -mf fps=25 -o output.avi -ovc lavc -lavcopts vcodec=mpeg4
        """
        from graphics.rendering.povray.writepovfile import writepovfile

        if not self.isOpen: #bruce 050428 not sure if this is the best condition to use here ###@@@
            if (not self.might_be_playable()) and 0: ## self.why_not_playable:
                msg = "Movie file is not presently playable: %s." ## % (self.why_not_playable,)
            else:
                msg = "Movie file is not presently playable." ###e needs more detail, especially when error happened long before.
            env.history.message( redmsg( msg )) #bruce 050425 mitigates bug 519 [since then, it got fixed -- bruce 050428]
            return

        self.playDirection = 1

        # Writes the POV-Ray series starting at the current frame until the last frame,
        # skipping frames if "Skip" (on the dashboard) is != 0.  Mark 050908
        nfiles = 0
        for i in range(self.currentFrame,
                       self.totalFramesActual+1,
                       self.propMgr.frameSkipSpinBox.value()):
            self.alist_and_moviefile.play_frame(i)
            filename = "%s.%06d.pov" % (name,i)
            # For 100s of files, printing a history message for each file is undesired.
            # Instead, I include a summary message below. Fixes bug 953.  Mark 051119.
            # env.history.message( "Writing file: " + filename )
            writepovfile(self.assy.part, self.assy.o, filename) #bruce 050927 revised arglist
            nfiles += 1
            self.framecounter  =  i #  gets the the last frame number of the file written. This will be passed in the history message ninad060809

        # Return to currentFrame. Fixes bug 1025.  Mark 051119
        self.alist_and_moviefile.play_frame(self.currentFrame)

        # Summary msgs tell user number of files saved and where they are located.
        msg = fix_plurals("%d file(s) written." % nfiles)
        env.history.message(msg)
        filenames = "%s.%06d.pov - %06d.pov" % (name, self.currentFrame, self.framecounter)#ninad060809 fixed bugs 2147 and 2148
        msg = "Files are named %s." % filenames
        env.history.message(msg)


    def _continue(self, hflag = True): # [bruce 050427 comment: only called from self._play]
        """
        Continue playing movie from current position.

        @param hflag: if True, print history message
        @type  hflag: boolean
        """
        if _DEBUG0:
            print "movie._continue() called. Direction = ", playDirection[ self.playDirection ]

        # In case the movie is already playing (usually the other direction).
        self._pause(0)

        if hflag:
            env.history.message("Movie continued: " + playDirection[ self.playDirection ])

        self.warn_if_other_part(self.assy.part) #bruce 050427

        self.showEachFrame = True #bruce 050428 comment: this is the only set of this var to True.

        # Continue playing movie.
        if self.playDirection == FWD:
            self._playToFrame(self.totalFramesActual)
            # If the pause button was pressed by the user, then this condition is True.
            if self.currentFrame != self.totalFramesActual:
                return
        else:
            self._playToFrame(0)
            # If the pause button was pressed by the user, then this condition is True.
            if self.currentFrame != 0:
                return

        # If "Loop" is checked, continue playing until user hits pause.  Mark 051101.
        while self.propMgr.movieLoop_checkbox.isChecked():
            if self.playDirection == FWD:
                self._reset() # Resets currentFrame to 0
                self.showEachFrame = True # _pause(), called by _playToFrame(), reset this to False.
                self._playToFrame(self.totalFramesActual)
                # If the pause button was pressed by the user, then this condition is True.
                if self.currentFrame != self.totalFramesActual:
                    break
            else: # playDirection == REV
                self._moveToEnd() # Resets currentFrame to totalFramesActual
                self.showEachFrame = True # _pause(), called by _playToFrame(), reset this to False.
                self._playToFrame(0)
                # If the pause button was pressed by the user, then this condition is True.
                if self.currentFrame != 0:
                    break

    def _pause(self, hflag = True):
        """
        Pause movie.
        hflag - if True, print history message
        """
        #bruce 050428 comment: I suspect it's required to call this in almost every event,
        # since it's the only place certain state variables gets reinitialized to default
        # values (e.g. showEachFrame to False). This should be analyzed and documented.
        if _DEBUG0:
            print "movie._pause() called"
        self.debug_dump("_pause called, not done")
        # bruce 050427 comment: no isOpen check, hope that's ok (this has several calls)
        self.isPaused = True
        self.win.movie_is_playing = False
        self.showEachFrame = False
        self.moveToEnd = False
        self.propMgr.moviePlayActiveAction.setVisible(0)
        self.propMgr.moviePlayAction.setVisible(1)
        self.propMgr.moviePlayRevActiveAction.setVisible(0)
        self.propMgr.moviePlayRevAction.setVisible(1)
        if hflag: env.history.message("Movie paused.")
        self.debug_dump("_pause call done")

    def debug_dump(self, heading = "debug_dump", **kws):
        if not _DEBUG_DUMP:
            return # disable when not needed -- but it's useful and nicelooking output, so keep it around as an example
        if heading:
            print "\n  %s:" % heading
        print "    movie_is_playing = %r, isPaused = %r, showEachFrame = %r, moveToEnd = %r, totalFramesActual = %r, currentFrame = %r, playDirection = %r" \
              % (self.win.movie_is_playing, self.isPaused, self.showEachFrame, self.moveToEnd, self.totalFramesActual, self.currentFrame, self.playDirection )
        if kws:
            print "  other args: %r" % kws
        print_compact_stack("    stack at that time: ", skip_innermost_n = 1) # skips this lineno and all internal ones

    def _playToFrame(self, fnum, from_slider = False):
        #bruce 050428 renamed this from _playFrame, since it plays all frames from current to fnum.
        """
        Main method for movie playing.
        When called due to the user sliding the movie dashboard frame-number slider, from_slider should be True.
        If "self.showEachFrame = True", it will play each frame of the movie between "fnum" and "currentFrame"
        (except for skipped frames due to the skip control on the dashboard).
        If "self.showEachFrame = False", it will advance to "fnum" from "currentFrame".
        fnum - frame number to play to in the movie.
        """
        #bruce 050427 revised docstring, added from_slider arg, merged most of _playSlider into this method.
        # I faithfully added conditions on from_slider to imitate the old code, though some of them might not
        # be needed or might even be bugs (if so, then presumably the non-from_slider cases are more correct).
        # If the differences are correct, it's probably better to separate the methods again;
        # the reason I merged them for now is to facilitate comparison so I (or Mark) can eventually review
        # whether the diffs are correct.

        if _DEBUG0:
            print "movie._playToFrame() called: from fnum = ", fnum, ", to currentFrame =", self.currentFrame

        #bruce 050427 comment: added an isOpen check, in case of bugs in callers (this has lots of calls)
        if not self.isOpen:
            return

        if not from_slider: #bruce 050427 comment: I'm suspicious of this condition.
            self.isPaused = False
            self.win.movie_is_playing = True # In case Bruce's suspicion is true.  Mark 051209.
            self.debug_dump()

        # Return immediately if already at desired frame.
        if fnum == self.currentFrame:
            if not from_slider: #bruce 050427 comment: I'm suspicious of this condition.
                self.isPaused = True # May not be needed.  Doing it anyway.
                self.win.movie_is_playing = False # May not be needed.  Doing it anyway. Mark 051209.
                self.debug_dump("fnum == self.currentFrame so paused", fnum = fnum)
            return

        # Don't let movie run out of bounds.
        if fnum < 0 or fnum > self.totalFramesActual:
            print "Warning: Slider or other fnum out of bounds.  fnum value =",fnum,", Number of frames =", self.totalFramesActual
            self.isPaused = True # May not be needed.  Doing it anyway.
            self.win.movie_is_playing = False # May not be needed.  Doing it anyway. Mark 051209.
            self.debug_dump("fnum out of range so paused", fnum = fnum)
            return

        # Reset movie to beginning (frame 0).  Executed when user types 0 in spinbox.
        #bruce 050427 comment: this might no longer be needed (it might be handled at a lower level). We'll see. ###@@@
        if not self.showEachFrame and fnum == 0 and not from_slider:
            self._reset()
            self.win.movie_is_playing = False # May not be needed.  Doing it anyway.  Mark 051209.
            return

        if False:
            # There are implications with the Undo system that I don't understand here. Bruce better look
            # this over before we switch it on.
            if self.currentFrame == 0:
                if not hasattr(self, 'origfile'):
                    errorcode, partdir = self.assy.find_or_make_part_files_directory()
                    if errorcode:
                        raise Exception("filename does not exist")
                    self.origfile = os.path.normpath(os.path.join(partdir,
                                                                  self.assy.filename[:-4] + '.orig' +
                                                                  self.assy.filename[-4:]))
                try:
                    env.mainwindow().fileOpen(self.origfile)
                except:
                    print 'cannot open original file'
                    raise

        # "inc" is the frame increment (FWD = 1, REV = -1) .
        if fnum > self.currentFrame:
            inc = FWD
            if not from_slider:
                self.propMgr.moviePlayActiveAction.setVisible(1)
                self.propMgr.moviePlayAction.setVisible(0)
        else:
            inc = REV
            if not from_slider:
                self.propMgr.moviePlayRevActiveAction.setVisible(1)
                self.propMgr.moviePlayRevAction.setVisible(0)

        # This addresses the situation when the movie file is large (> 1000 frames)
        # and the user drags the slider quickly, creating a large delta between
        # fnum and currentFrame.  Issue: playing this long of a range of the movie
        # may take some time.  We need to give feedback to the user as to what is happening:
        # 1). turn the cursor into WaitCursor (hourglass).
        # 2). print a history message letting the user know we are advancing the movie, but
        #       also let them know they can pause the movie at any time.
        #bruce 050427 comments:
        # - The above comment dates from the old code when this method didn't handle the slider,
        #   so my guess is, its reference to the slider is even older and is out of date.
        # - I'm now merging in _playSlider and adding "if from_slider" as needed for no change in behavior;
        #   no idea if the differences are good or bad.
        # - This might not be needed when we change to the new .dpb file format (NIM)
        #   or if we have cached enough frames in lower-level code (NIM). ###e
        waitCursor = False
        if not from_slider:
            if not self.showEachFrame:
                delta = abs(fnum - self.currentFrame)
                if delta != 1:
                    if delta > 1000:
                        waitCursor = True
                        env.history.message(playDirection[ inc ] + "ing to frame " + str(fnum) + ".  You may select Pause at any time.")
                    else:
                        env.history.message(playDirection[ inc ] + "ing to frame " + str(fnum))
        else:
            if abs(fnum - self.currentFrame) > 1000:
                env.history.message("Advancing to frame " + str(fnum) + ". Please wait...")
                env.history.h_update() #bruce 060707
                waitCursor = True
        if waitCursor:
            self.waitCursor = True
            QApplication.setOverrideCursor( QCursor(Qt.WaitCursor) )

        if _DEBUG0:
            print "BEGIN LOOP: fnum = ", fnum, ", currentFrame =", self.currentFrame, ", inc =",inc

        # This is the main loop to compute atom positions from the current frame to "fnum".
        # After this loop completes, we paint the model -- but also during it.
        # We also recursively process QEvents during it. [bruce 050427 revised this comment]
        # [bruce question 050516: do those events ever include movie dashboard slider
        #  or button events which call this method recursively?? ####@@@@]
        self.debug_dump("before playToFrame loop", fnum = fnum, inc = inc)
        if from_slider:
            # [bruce 050427: this case got a lot simpler.]
            self.currentFrame = fnum
            self.alist_and_moviefile.play_frame( self.currentFrame)
            self.debug_dump("just after play_frame for slider", fnum = fnum, inc = inc)
        # else...

        self.win.movie_is_playing = True # Starting the Movie...

        # Top of Main loop...
        while self.currentFrame != fnum:

            self.debug_dump("top of while loop body", fnum = fnum, inc = inc)
            assert not from_slider

            if self.isPaused:
                self.win.movie_is_playing = False # Probably not needed.  Doing it anyway. mark 051209.
                break

            ## self.currentFrame += inc -- doing this below [bruce 050427]

            if _DEBUG0:
                print "IN LOOP1: fnum = ", fnum, ", currentFrame =", self.currentFrame, ", inc =",inc

            #bruce 050427 totally revised the following in implem (not in behavior).
            # Note that we needn't worry about valid range of frames, since both currentFrame and fnum should be within range.
            # (Unless the surrounding code fails to check currentFrame well enough... I'm not sure! ###k)
            # We only need to worry about whether we reach fnum or not.

            skip_n = self.propMgr.frameSkipSpinBox.value() - 1 # Mark 060927
            if not self.showEachFrame:
                #bruce 050428 adding this to see if it speeds up "forward to end"
                ###e potential optim: increase skip, as long as time passed will not be too bad
                skip_n = max(19,skip_n) # seems ok, though value is surely too big for huge models and too small for tiny ones ###e
            delta_n = 1 + skip_n
            for ii in range(delta_n): # slowness should not be an issue compared to time it takes to scan file... until new file format??
                self.currentFrame += inc
                if self.currentFrame == fnum:
                    break
            # now self.currentFrame needs to be shown
            if 1: ## self.showEachFrame: ####@@@@ old code said if 1 for this... what's best? maybe update them every 0.1 sec?
                self.propMgr.updateCurrentFrame()
            if 1:
                self.alist_and_moviefile.play_frame( self.currentFrame) # doing this every time makes it a lot slower, vs doing nothing!
                ###e [bruce 050428 comments:]
                # potential optim: do we need to do this now, even if not drawing on glpane?
                # Warning: if we don't do it, the atom posns and self.currentFrame are out of sync;
                # at least this requires fixing the posns at the end (doing play_frame then),
                # but it might cause worse trouble, so really it's better to increase the "skip" above
                # (perhaps adaptively using time-measurements). The other updates could safely be
                # conditioned on how much time had passed, if they're always done at the end if needed.
            if self.showEachFrame:
                self.glpane.gl_update()

            # Process queued events [bruce comment 050516: note that this will do a paintGL from our earlier gl_update above ####@@@@]
            env.call_qApp_processEvents() #bruce 050908 replaced qApp.processEvents()
                #e bruce 050427 comment: should we check to see if the user changed the controls,
                # and (if so) change the fnum we're heading for?? ###@@@

        # End of loop
        self.debug_dump("after playToFrame loop", fnum = fnum, inc = inc)

        # Update cursor, slider and show frame.
        if self.waitCursor:
            QApplication.restoreOverrideCursor() # Restore the cursor
            self.waitCursor = False

        self.propMgr.updateCurrentFrame( )
            # [bruce 050428 comment: old code only updated slider here, but it did both SL and SB in loop above;
            #  now the update method decides which ones to update]

        # Set movie_is_playing to False right before it draws the last frame (fnum).
        self.win.movie_is_playing = False
        # This is the last frame (fnum).
        self.glpane.gl_update() #e bruce 050427 comment: we should optimize and only do this if we didn't just do it in the loop

        if 1: ## if not from_slider:
            #bruce 050428 always do this, since Mark agrees it'd be good for moving the slider to pause the movie
            if _DEBUG0:
                print "movie._playToFrame(): Calling _pause"
            self._pause(0) # Force pause. Takes care of variable and dashboard maintenance.
            if _DEBUG0:
                print "movie._playToFrame(): BYE!"

        return # from _playToFrame

    def _playSlider(self, fnum):
        """
        Slot for movie slider control.
        It will advance the movie to "fnum" from "currentFrame".
        fnum - frame number to advance to.
        """

        if _DEBUG0:
            print "movie._playSlider() called: fnum = ", fnum, ", currentFrame =", self.currentFrame
        self.debug_dump("_playSlider", fnum = fnum)
        self._playToFrame(fnum, from_slider = True) #bruce 050427 merged _playSlider into _playToFrame method, using from_slider arg


    def _reset(self):
        """
        Resets the movie to the beginning (frame 0).
        """
        if _DEBUG0:
            print "movie._reset() called"
        if self.currentFrame == 0:
            return

        #bruce 050427 comment: added an isOpen check, in case of bugs in callers
        if not self.isOpen:
            return

        self.currentFrame = 0

        # Restore atom positions.
        self.alist_and_moviefile.play_frame( self.currentFrame)

        self.propMgr.updateCurrentFrame()
        self._pause(0)
        self.glpane.gl_update()

    def _moveToEnd(self):
        """
        """
        if _DEBUG0:
            print "movie._moveToEnd() called"
        if self.currentFrame == self.totalFramesActual:
            return

        #bruce 050427 comment: added an isOpen check, in case of bugs in callers
        if not self.isOpen:
            return

        self._pause(0)
        self.moveToEnd = True
        self._playToFrame(self.totalFramesActual)

    # ==

    def _info(self):
        """
        Print info about movie to the history widget.
        """
        if _DEBUG0:
            print "movie._info() called."
        if not self.filename:
            env.history.message("No movie file loaded.")
            return
        env.history.message("Filename: [" + self.filename + "]")

        msg = "Number of Frames: " +  str(self.totalFramesActual) + \
            ".  Number of Atoms: " +  str(self.natoms)

        env.history.message(msg)
#        env.history.message("Temperature:" + str(self.temp) + "K")
#        env.history.message("Steps per Frame:" + str(self.stepsper))
#        env.history.message("Time Step:" + str(self.stepsper))

    def getMovieInfo(self):
        """
        Return the information about this movie.
        """
        fileName    = str(self.filename)
        numOfFrames = str(self.totalFramesActual)
        numOfAtoms  = str(self.natoms)

        return (fileName, numOfFrames, numOfAtoms)

    def getCurrentFrame(self):
        """
        Returns the current frame of the movie.
        """
        return self.currentFrame

    def getTotalFrames(self):
        """
        Returns the total frames of the movie.
        """
        return self.totalFramesActual

    def get_trace_filename(self):
        """
        Returns the trace filename for the current movie.
        """
        fullpath, ext = os.path.splitext(self.filename)
        if ext == '.xyz':
            #bruce 050407 new feature: ensure tracefilename differs when filename does
            # (see comment next to our caller in runSim.py for why this matters)
            suffix = "-xyztrace.txt"
        else:
            suffix = "-trace.txt"
        return fullpath + suffix

    def get_GNUplot_filename(self):
        """
        Returns the GNUplot filename for the current movie.
        """
        fullpath, ext = os.path.splitext(self.filename)
        return fullpath + "-plot.txt"

    def moveAtoms(self, newPositions): # used when reading xyz files
        """
        Move a list of atoms to newPosition. After all atoms moving
        [and singlet positions updated], bond updated, update display once.

        @param newPosition: a list of atom absolute position,
                            the list order is the same as self.alist
        @type  newPosition: list
        """
        if len(newPositions) != len(self.alist):
            #bruce 050225 added some parameters to this error message
            #bruce 050406 comment: but it probably never comes out, since readxyz checks this,
            # so I won't bother to print it to history here. But leaving it in is good for safety.
            #bruce 060108: it can come out for realtime minimize if you edit the model. hopefully we'll fix that soon.
            msg = "moveAtoms: The number of atoms from XYZ file (%d) is not matching with that of the current model (%d)" % \
                (len(newPositions), len(self.alist))
            print msg
            raise ValueError, msg
                #bruce 060108 reviewed/revised all 2 calls, added this exception to preexisting noop/errorprint (untested)
        move_atoms_and_normalize_bondpoints(self.alist, newPositions) #bruce 051221 fixed bug 1239 in this function, then split it out
        self.glpane.gl_update()
        return

    pass # end of class Movie

# ==

class MovableAtomList: #bruce 050426 splitting this out of class Movie... except it's entirely new code, as it turns out.
    """
    A list of atoms within an assy (perhaps in more than one Part or even
    including killed atoms), with methods for quickly and safely changing
    all their positions at once, updating their display, for "owning"
    those atoms or their chunks as needed to make it safe to reset their
    positions, and for tracking external changes to their structure relevant
    to safety and validity of resetting their positions. [For Alpha5 we're
    mainly worrying about safety from tracebacks rather than validity.]

    [Not yet handled here: ability to be told to move an H to one position,
    but to actually move a singlet into a different position computed
    from that (re bug 254). Caller might help by ordering singlets after
    their base atoms, or even by doing this work itself (none of that is
    decided yet). #e]
    """
    #e the plan is to later optimize this greatly
    # by making it totally own the involved atoms' posns and do its own fast redisplay.

    def __init__(self, assy, alist):
        self.assy = assy
        self.glpane = assy.o
        self.alist = list(alist) # use A()?
            # is alist a public attribute? (if so, no need for methods to prune its atoms by part or killedness, etc)
        self.natoms = len(self.alist)

    def get_sim_posns(self): #bruce 060111 renamed and revised this from get_posns, for use in approximate fix of bug 1297
        # note: this method is no longer called as of bruce 060112, but its comments are relevant and are referred to
        # from several files using the name of this method. It's also still correctly implemented, so we can leave it in for now.
        """
        Return an Array (mutable and owned by caller) of current
        positions-for-simulator of our atoms (like positions, except
        singlets pretend they're H's and correct their posns accordingly).
        (This must work even if some of our atoms have been killed,
        or moved into different Parts, since we were made, though the
        positions returned for killed atoms probably don't matter
        (#k not sure).)
        """
        # Problem: to fully fix bug 1297, we need to return the H position actually used in the sim, not the equilibrium H position
        # like a.sim_posn returns. For frame 0 it's the same; for other frames the only source for that info is the frame
        # returned from the sim (to the frame_callback), but it's only safe to use it if neither involved atom has been moved
        # since the sim was started. I might decide it's not worth fixing this except by moving to new DPB format which defines
        # absolute atom positions. Or I might have the realtime-motion code in runSim save frame 0, or any other frame
        # of a known number, and pass it to the movie object along with the "current frame number", for use instead of the array
        # returned by this method. For now, I won't bother with that, hoping this fix is good enough until we move to the new
        # DPB file format. ####@@@@
        res = map( lambda a: a.sim_posn(), self.alist )
        return A(res)

    def set_posns(self, newposns):
        """
        Set our atoms' positions (even killed ones) to those in the given
        array (but correct singlet positions); do all required invals but
        no redisplay

        @note: someday we might have a version which only does this for the
        atoms now in a given Part.
        """
        #e later we'll optimize this by owning atoms and speeding up or
        # eliminating invals
        #bruce 060109 replaced prior code with this recently split out routine,
        # so that singlet correction is done on every frame; could be optimized,
        # e.g. by precomputing singlet list and optimizing setposn on lists of
        # atoms
        #bruce 060111 comment: should probably be renamed set_sim_posns
        # since it corrects singlet posns
        move_atoms_and_normalize_bondpoints(self.alist, newposns)

    set_posns_no_inval = set_posns #e for now... later this can be faster, and require own/release around it

    def snuggle_singlets(self): #bruce 050427 made this from movend; 090107 renamed
        # terrible hack for singlets in simulator, which treats them as H
        for a in self.alist:
            if a.is_singlet() and a.bonds: # could check a.molecule.part instead, but a.bonds is more to the point and faster
                #bruce 050428 exclude killed atoms (a.killed() is too slow [not anymore, bruce 050702, but this check is better anyway])
                a.snuggle() # same code as in moveAtoms() except for killed-atom check
                    #bruce 051221 comment: note that to avoid an analog of bug 1239, it's critical that atoms are first all moved,
                    # and only then are singlets snuggled. This was already the case here, but not in moveAtoms() from which this
                    # code was copied.
            #e could optimize this (enough to do it continuously) by using Numeric to do them all at once
        self.glpane.gl_update() # needed because of the snuggle above
        return

    def update_displays(self):
        ###@@@ should use same glpane as in self.glpane.gl_update code above (one or the other is wrong) [bruce 050516 guess/comment]
        self.assy.o.gl_update() #stub? someday might need to update the MT as well if it's showing animated icons for involved Parts...

    def destroy(self):
        self.alist = None

    pass # end of class MovableAtomList

# ==

class alist_and_moviefile:
    """
    Set up and maintain a corresponding MovableAtomList and a MovieFile,
    and be able to move the atoms using the moviefile
    and know the state of their relationship at all times.
    (But let the two subobjects we create do most of the work.)

    Assume that we know the current valid frame better than the atoms do...
    even if something else moves them (unless it's another copy of the same
    movie, which we assume won't happen)... but this will become wrong once
    there's an Undo feature!
    So then, we'd want to advise the atom-state of this value (keyed to
    this object's moviefile-contents), so it'd be a part of the undone state.
    I'm not sure if I'll do that, or ignore it for now. ###k
    Or I might do *both*, by designating this object as the way the atom's
    real owner (their assy) remembers that state! In other words, this
    "playable movie" is sitting in the atoms as a "slidable handle"
    (metaphorically at least) to let anything adjust their posns using it,
    including (example 1) the moviemode dashboard controls
    (once it decides which movie object it wants to display and adjust)
    or (example 2) various cmenu ops (or even MT-embedded sliders?) on
    movie nodes in the MT.

    This class might be small enough to use as a Jig for actually being in
    the MT..., or it might still be better to let that be a separate object
    which represents one of these. #k
    """
    _valid = False
    def __init__(self, assy, alist, filename, ref_frame = None): #bruce 060112 removed curframe_in_alist, added ref_frame
        """
        Caller promises that filename exists. If it matches alist well enough
        to use with it, we set self.valid() true and fully init ourselves,
        i.e. set up the file/alist relationship and get ready to play
        specific frames (i.e. copy posns from file into alist's atoms)
        on request.

        If file doesn't match alist, we set self.valid() false and return early
        (but we might still be usable later if the file changes and some
        recheck method (NIM) is called (#e)).

        If provided, ref_frame is (frame_number, sim_posn_array) for some
        frame of the movie, which we should use as a reference for
        interpreting a purely-differential moviefile.
        Such moviefiles require that this argument be provided.
        [I'm not sure when this is checked -- leaving it out might cause
        later exception or (unlikely) wrong positions.]
        If the moviefile has its own abs positions, we can ignore this argument
        (#e but in future we might decide instead to check it, or to
        use it in some other way...).
        """
        self.alist = alist # needed for rechecking the match
        ## self.history = env.history # not yet used, but probably will be used for error messages [bruce 050913 removed this]
        self.moviefile = MovieFile( filename)
        self.movable_atoms = None
        if not self.moviefile:
            pass ## MovieFile will have emitted a history message (I hope)
            return
        self._valid = self.moviefile.matches_alist(alist) # this never emits a history message (for now)
        if not self._valid:
            # for now, we happen to know exactly why they're not valid... [bruce 050428]
            env.history.message( redmsg( "Movie file contents not valid for this Part (wrong number of atoms)."))
            self.moviefile.destroy()
            self.moviefile = None
            return # caller should check self.valid()
        self.movable_atoms = MovableAtomList( assy, alist)
##        if curframe_in_alist is not None:
##            n = curframe_in_alist
##            frame_n = self.movable_atoms.get_sim_posns()
##                #bruce 060111 replaced get_posns with new get_sim_posns to approximately fix bug 1297;
##                # see comment in definition of get_sim_posns for more info.
##            self.moviefile.donate_immutable_cached_frame( n, frame_n)
        if ref_frame:
            n, frame_n = ref_frame
            self.moviefile.donate_immutable_cached_frame( n, frame_n)
#bruce 060108 commenting out all sets of self.current_frame to avoid confusion, since nothing uses it;
# but I suggest leaving the commented-out code around until the next major rewrite.
##            self.current_frame = n
##                # warning [bruce 060108]: related or client code keeps its own version of this,
##                # called currentFrame, not necessarily in sync with this.
##        else:
##            self.current_frame = None # since it's unknown (#k ok for all callers?)
        return
    def destroy(self):
        try:
            if self.moviefile:
                self.moviefile.destroy()
            if self.movable_atoms:
                self.movable_atoms.destroy()
            self.alist = None
        except:
            if debug_flags.atom_debug:
                print_compact_traceback("atom_debug: exception in alist_and_moviefile.destroy() ignored: ")
        return
##    why_not_playable = "" #e need to set this to actual reasons when possible
    def might_be_playable(self):
        return self.valid() # from the last time this was checked -- it's not re-checked now
    def valid(self):
        return self._valid
    def recheck_valid(self):
        self._valid = self.moviefile.recheck_matches_alist( self.alist)
        #e also check whether atoms are all in same part and not killed? No! We'll play even if these conditions are false.
        return self._valid
    def snuggle_singlets(self):
        self.movable_atoms.snuggle_singlets()
    def play_frame(self, n):
        """
        Try to set atoms to positions in frame n.
        Return true if this works, false if n went beyond either end of moviefile.
        (For other errors, print some sort of error message and return false,
         or perhaps just raise an exception. #k)
        """
        mf = self.moviefile
        ma = self.movable_atoms
        if mf.frame_index_in_range(n):
            frame_n = mf.ref_to_transient_frame_n(n)
            ma.set_posns(frame_n) # now we no longer need frame_n
                # (note: set_posns did invals but not updates.)
##            self.current_frame = n #k might not be needed -- our caller keeps its own version of this (named currentFrame)
            return True ###k does caller, or this method, need to update dashboards and glpanes that care?
        else:
            ## self.pause() ###k guess -- since we presumably hit the end... maybe return errcode instead, let caller decide??
            return False
        pass
    def get_totalFramesActual(self):
        return self.moviefile.get_totalFramesActual()
    def close_file(self):
        self.moviefile.close_file()
    pass # end of class alist_and_moviefile


# == helper functions

def find_saved_movie( assy, mfile):
    "look for a .dpb file of the given name; if found, return a Movie object for it"
    movie = Movie(assy)
    if movie.represent_this_moviefile( mfile):
        # succeeded
        return movie
    # otherwise it failed but did NOT already emit error messages about that (should it? in future, only it knows why it failed)
    return None

def _checkMovieFile(part, filename): #bruce 050913 removed history arg since all callers pass env.history
    """
    Returns 0 if filename is (or might be) a valid movie file for the specified part.
    Returns 1 if filename does not exist.
    Returns 2 if the movie file does not match the part.
    Prints error messages to env.history
    (whenever return value is not zero).
    """
    #bruce 050427 comment: This should be merged with related code in moviefile.py,
    # but it looks correct, so I won't do this now. It's now only called from fileOpenMovie.
    #bruce 050324 made this a separate function, since it's not about a specific
    # Movie instance, just about a Part and a filename. Both args are now required,
    # and a new optional arg "history" is both where and whether to print errors
    # (both existing calls have been changed to pass it). [bruce 050913 hardcoded that arg to env.history.]
    # This function only checks number of atoms, and assumes all atoms of the Part
    # must be involved in the movie (in an order known to the Part, not checked,
    # though the order can easily be wrong).
    # It is not yet updated to handle the "new dpb format" (ie it doesn't get help
    # from either file keys or movie ids or atom positions) or movies made from
    # a possible future "simulate selection" operation.
    print_errors = True

    if _DEBUG1:
        print "movie._checkMovieFile() function called. filename = ", filename

    assert filename #bruce 050324
    if not os.path.exists(filename):
        if print_errors:
            msg = redmsg("Cannot play movie file [" + filename + "]. It does not exist.")
            env.history.message(msg)
        return 1

    #bruce 050411: protect against no part (though better if caller does this); see bug 497.
    # Might be better to let part be unspecified and figure out from the moviefile
    # which available Part to use, but this is not
    # currently possible -- if parts have same contents, it's not even possible in principle
    # until we have new DPB format, and not clear how to do it even then (if we only have
    # persistent names for files rather than parts).
    if part is None:
        if debug_flags.atom_debug:
            print_compact_stack( "atom_debug: possible bug: part is false (%r) in _checkMovieFile for %s" % (part,filename))
            ## can't do this, no movie arg!!! self.debug_print_movie_info()
        if print_errors:
            msg = redmsg("Movie file [" + filename + "] can't be played for current part.") # vaguer & different wording, since bug
            env.history.message(msg)
        return 2

    # start of code that should be moved into moviefile.py and merged with similar code there
    filesize = os.path.getsize(filename) - 4

    fp = open(filename,'rb')

    # Read header (4 bytes) from file containing the number of frames in the movie.
    nframes = unpack('i',fp.read(4))[0]
    fp.close()

    natoms = int(filesize/(nframes*3))
    # end of code that should be moved into moviefile.py

    kluge_ensure_natoms_correct( part)

    if natoms == part.natoms: ## bruce 050324 changed this from natoms == len(self.assy.alist)
        return 0
    else:
        if debug_flags.atom_debug:
            print "atom_debug: not natoms == part.natoms, %d %d" % (natoms, part.natoms)
        if print_errors:
            msg = redmsg("Movie file [" + filename + "] not valid for the current part.")
            env.history.message(msg)
            msg = redmsg("Movie is for %d frames, size is %d, natoms %d" % (nframes, filesize, natoms))
            env.history.message(msg)
            msg = redmsg("Current part has %d atoms" % (part.natoms))
            env.history.message(msg)
        return 2
    pass

def kluge_ensure_natoms_correct(part):
    ###@@@ kluge to work around bug in part.natoms not being invalidated enough
    part.natoms = None
    del part.natoms # force recompute when next needed
    return

# end