File: WavePropagation.py

package info (click to toggle)
esys-particle 2.1-4
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 7,284 kB
  • sloc: cpp: 77,304; python: 5,647; makefile: 1,176; sh: 10
file content (785 lines) | stat: -rw-r--r-- 28,307 bytes parent folder | download
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
#############################################################
##                                                         ##
## Copyright (c) 2003-2011 by The University of Queensland ##
## Earth Systems Science Computational Centre (ESSCC)      ##
## http://www.uq.edu.au/esscc                              ##
##                                                         ##
## Primary Business: Brisbane, Queensland, Australia       ##
## Licensed under the Open Software License version 3.0    ##
## http://www.opensource.org/licenses/osl-3.0.php          ##
##                                                         ##
#############################################################

from esys.lsm.util import *
from esys.lsm      import NRotBondPrms, DampingPrms, Runnable
import WavePropagationPy

import math
import weakref
from sets import Set

import itertools

class SourcePrms:
    """
    Base class for point-source distubance which generates elastic wave.
    """
    def __init__(self, posn):
        """
        @param posn: The desired location for the source disturbance.
        """
        self.initialPosn = Vec3(posn)

    def getInitialPosn(self):
        """
        Returns the desired location of the source disturbance.
        """
        return self.initialPosn

    def getPosn(self, t):
        """
        Returns relative position of source disturbance for time t.
        @param t: time.
        """
        raise NotImplementedException()

class ExpSourcePrms(SourcePrms):
    """
    Describes (time, position) trajectory information for a source particle.
    """
    def __init__(
        self,
        posn = Vec3(0.0, 0.0, 0.0),
        a  = (0.20, 0.25, 0.00),
        b  = (0.50, 0.50, 0.50),
        t0 = (5.00, 4.00, 6.00)
    ):
        """
        Constructs and exponential (Gaussian in time) source.
        @type posn: L{Vec3}
        @param posn: coordinate location of the source.
        @type a: L{list}/L{tuple}
        @param a: sequence of 3 elements defining the magnitude/amplitude of
                 the (x,y,z) components.
        @type b: L{list}/{tuple}
        @param b: sequence of 3 elements defining the period of the (x,y,z)
                 components.
        @type t0: float
        @param t0: sequence of 3 elements defining the time at which the maximum
                 amplitude is achieved for each (x,y,z) component.
        """
        SourcePrms.__init__(self, posn)
        self.a           = Vec3(a)
        self.b           = Vec3(b)
        self.t0          = Vec3(t0)

    def getPosn(self, t):
        d = Vec3()
        for i in range(0, 3):
            d[i] = self.a[i]*math.exp(-((t-self.t0[i])/self.b[i])**2)
        return d

class CircularSourcePrms(SourcePrms):
    """
    Defines circular trajectory (time, position) for source disturbance.
    """
    def __init__(self, posn, startTime=0.5, freq=0.05, radius=0.05):
        SourcePrms.__init__(self, posn)
        self.startTime = startTime
        self.freq      = freq
        self.radius    = radius

    def getPosn(self, t):
        d = Vec3(0,0,0)
        theta = self.freq*2.0*math.pi*(t-self.startTime)
        if ((t >= self.startTime) and (theta <= 2.0*math.pi)):
            d = \
                Vec3(
                    self.radius*math.cos(theta)-self.radius,
                    self.radius*math.sin(theta),
                    0
                )
            print "Moving source by |" + str(d) + "|=" + str(d.norm())
        return d

class WaveSource(Runnable):
    """
    Helper class which moves a source particle during each time step.
    """
    def __init__(self, prms, lsm):
        """
        Constructs the source Runnable.
        @param prms: object which governs the source particle trajectory.
                     This object is expected to define a getPosn(t) method
                     which returns a relative position for time t, and
                     also define a getInitialPosn() method which returns
                     the location of the source disturbance.
        @type lsm: L{esys.lsm.sim.WavePropagation.WavePropagation}
        @param lsm: a WavePropagation lattice solid model object.
                    A source particle in this model is moved according to
                    the trajectory specifed by the prms argument.
        """
        Runnable.__init__(self)
        self.prms = prms
        self.lsmProxy = weakref.proxy(lsm)
        self.particleId = \
            self.getLsm().findClosestParticle(
                self.getPrms().getInitialPosn()
            )
        self.initialPosn = \
            self.getLsm().getParticlePosn(self.getParticleId())

    def getLsm(self):
        return self.lsmProxy

    def getPrms(self):
        return self.prms

    def getParticleId(self):
        return self.particleId

    def getInitialPosn(self):
        return self.initialPosn

    def run(self):
        posn =                    \
            self.getInitialPosn() \
            +                     \
            self.getPrms().getPosn(self.getLsm().getTime())
        self.getLsm().moveParticleTo(self.getParticleId(), posn)

class Seismograph:
    """
    Helper class whose objects represent seismographs.
    """
    def __init__(self, posn, lsm):
        """
        @type posn: Three float squence
        @param posn: the approximate location of the seismograph.
        @param lsm: a lattice solid model object.
        """
        self.lsmProxy = weakref.proxy(lsm)
        self.particleId = \
            self.getLsm().findClosestParticle(posn)
        self.posn = self.getLsm().getParticlePosn(self.particleId)

    def getLsm(self):
        """
        Returns LSM object which is used to obtain displacement,
        velocity and acceleration data.
        """
        return self.lsmProxy

    def getParticleId(self):
        """
        Returns the id of the particle which is used to obtain
        seismograph data.
        """
        return self.particleId

    def getInitialPosn(self):
        """
        Returns the initial position of the of this seismograph,
        this position is just the particle initial position.
        """
        return self.posn

def cmpRecordSetElemList(elemList1, elemList2):
    """
    Compare-function used with the list.sort method to order
    record section data by distance-to-source.
    """
    c = cmp(elemList1[1],elemList2[1])
    if (c == 0):
        return cmp(elemList1[0],elemList2[0])
    return c

class SeismographGroup:
    """
    Objects of this class represent a collection of seismographs.
    """
    def __init__(self, seismoList, name, fileNamePrefix, sourcePosn):
        """
        @type seismoList: list
        @param seismoList: List of Seismograph objects.
        @type name: string
        @param name: Name of this group.
        @type fileNamePrefix: string
        @param fileNamePrefix: For all Seismograph objects, data is
               saved to files with this prefix.
        @type sourcePosn: L{esys.lsm.util.FoundationPy.Vec3}
        @param sourcePosn: Location of source disturbance, used to
                           calculate distance from source to seismograph.
        """
        self.name           = name
        self.fileNamePrefix = fileNamePrefix
        self.seismoList     = seismoList
        self.seismoIdSet    = Set([s.getParticleId() for s in self.seismoList])
        self.sourcePosn     = Vec3(sourcePosn)

    def inGroup(self, seismoData):
        """
        Returns whether the specfied SeismographData object is
        from a Seismograph in this group.
        @type seismoData: L{SeismographData}
        @param seismoData: Determine whether this data belongs to a
                           Seismograph object in this group.
        @return: True if seismoData came from a seismograph in this group.
        """
        return (seismoData.particleId in self.seismoIdSet)

    def getRecordSectionFileName(self):
        """
        Returns the name of the file in which record section data
        is saved.
        """
        return self.fileNamePrefix + "record_section.txt"
    
    def getSeismoDataFileName(self, posn):
        """
        Returns the name of a seismograph data file for the specified posn.
        @param posn: A Vec3 object specifying the position of the seismograph.
        @rtype string
        @return: File name for seismograph data at spatial coordinate posn.
        """
        fileName = (self.fileNamePrefix + "%3.3f_%3.3f_%3.3f") % posn.toTuple()
        fileName += ".txt"
        return fileName

    def initialiseSeismoDataFiles(self):
        """
        Creates/overwrites empty seismograph data files.
        """
        fileName = self.getRecordSectionFileName()
        f = file(fileName, "w")
        f.close()
        for seismo in self.seismoList:
            fileName = self.getSeismoDataFileName(seismo.getInitialPosn())
            f = file(fileName, "w")
            f.write(
              "# " + ("%3.3f %3.3f %3.3f" %\
              seismo.getInitialPosn().toTuple())
            )
            f.close()

    def save(self, time, seismoDataList):
        """
        Saves seismograph data to files. Appends record section info
        as well as individual seismo data file.
        """
        for seismoData in itertools.ifilter(self.inGroup, seismoDataList):
            self.saveSeismoData(time, seismoData)
            self.saveRecordSectionData(time, seismoData)
    
    def saveSeismoData(self, time, seismoData):
        """
        Appends the specfied seismograph data to file. Each seismograph data
        record is appended to a different file. Each line in the file is
        of the form 't dx dy dz vx vy vz ax ay az' where d=(dx,dy,dz) is
        the displacement vector, v=(vx,vy,vz) is the velocity vector and
        a=(ax,ay,az) is the acceleration vector.
        @type time: float
        @param time: the time the data was recorded
        @type seismoData: L{SeismographData}
        @param seismoData: Data in this object is saved to file. The file
                           name is determined by the data's position.
        """
        fileName = \
            self.getSeismoDataFileName(
                seismoData.posn
            )
        f = file(fileName, "a")
        f.write(str(time))
        f.write(" " + str(seismoData.displacement))
        f.write(" " + str(seismoData.velocity))
        f.write(" " + str(seismoData.acceleration))
        f.write("\n")
        f.close()

    def saveRecordSectionData(
        self,
        time,
        seismoData
    ):
        """
        Appends record-set style seismograph data to file. Each seismograph data
        record is appended to a record-set file. Each line in the file is
        of the form 't d ux uy uz vx vy vz ax ay az' where t is the time, d is
        the distance from the seismograph to the sourcePosn d=(dx,dy,dz)
        is the displacement vector, v=(vx,vy,vz) is the velocity vector and
        a=(ax,ay,az) is the acceleration vector.
        @type time: float
        @param time: the time the data was recorded
        @type seismoData: L{SeismographData}
        @param seismoData: This data is saved to record the section file.
        """
        fileName = self.getRecordSectionFileName()
        f = file(fileName, "a")
        f.write(str(time))
        f.write(" " + str((seismoData.posn-self.sourcePosn).norm()))
        f.write(" " + str(seismoData.displacement))
        f.write(" " + str(seismoData.velocity))
        f.write(" " + str(seismoData.acceleration))
        f.write("\n")
        f.close()

    def writeReorderedRecordSectionData(self):
        """
        Re-orders record set data for more convenient plotting in gnuplot.
        Reads time-ordered record section data and re-orders according to
        distance-to-source.
        """
        f = file(self.getRecordSectionFileName(), "r")
        recordList = []
        for line in f.readlines():
            line = string.strip(line)
            if (len(line) > 0):
                recordList.append(map(float, string.split(line, " ")))
        f.close()
        #
        # Separate each seismo's data into a list
        #
        distDict = dict()
        for record in recordList:
            if (not distDict.has_key(record[1])):
                distDict[record[1]] = []
            distDict[record[1]].append(record)
        f = file(self.fileNamePrefix + "record_section_reordered.txt", "w")
        distKeyList = distDict.keys()
        distKeyList.sort()
        for dist in distKeyList:
            distDict[dist].sort(cmpRecordSetElemList)
            for recordElemList in distDict[dist]:
                f.write(string.join(map(str, recordElemList), " "))
                f.write("\n")
            f.write("\n")
        f.close()

class SeismographGroupCollection:
    """
    Represents a collection/list of SeismographGroup objects. 
    """
    def __init__(self, lsm):
        """
        @type lsm: L{esys.lsm.sim.WavePropagation.WavePropagation}
        @param lsm: Seismo data is collected from this object.
        """
        self.lsmRef = weakref.ref(lsm)
        self.idSeismoDict = dict()
        self.seismoGroupList = []

    def getLsm(self):
        """
        Returns the LSM object associated with this collection.
        @rtype: L{esys.lsm.sim.WavePropagation.WavePropagation}
        """
        return self.lsmRef()

    def createSeismograph(self, posn):
        """
        Creates a seismograph at the specified position.
        @type posn: L{Vec3}
        @param posn: Approximate location of seismograph.
        @rtype: L{Seismograph}
        @return: Newly created Seismograph object.
        """
        return Seismograph(posn, self.getLsm())

    def getSeismograph(self, posn):
        """
        Returns seismograph for the given position. Only allows a
        unique instance of a seismograph for a particular spatial
        region.
        @rtype: L{Seismograph}
        """
        seismo = self.createSeismograph(posn)
        if (not self.idSeismoDict.has_key(seismo.getParticleId())):
            self.idSeismoDict[seismo.particleId] = seismo
        return self.idSeismoDict[seismo.particleId]

    def createGroup(self, posnIterable, fileNamePrefix, sourcePosn):
        """
        Creates multiple seismographs from a sequence of locations.
        @type posnIterable: sequence of L{Vec3}
        @param posnIterable: sequence of seismograph approximate locations.
        @type fileNamePrefix: string
        @param fileNamePrefix: prefix of file where seimo data is saved.
        @type sourcePosn: L{Vec3}
        @param sourcePosn: Location of source disturbance.
        """
        seismoList = [self.getSeismograph(p) for p in posnIterable]
        self.seismoGroupList.append(
            SeismographGroup(
                seismoList,
                str(len(self.seismoGroupList)),
                fileNamePrefix,
                sourcePosn
            )
        )
        self.seismoGroupList[-1].initialiseSeismoDataFiles()

    def getSeismographData(self):
        """
        Returns a sequence of SeismographData objects representing
        seismograph data for the current time step.
        """
        visitor = ParticleDataVisitor()
        self.getLsm().visitParticlesWithId(self.idSeismoDict.keys(), visitor)
        return visitor.seismoDataList

    def saveSeismoData(self):
        """
        Saves seismograph data for each group of seismographs.
        """

        #
        # Start by getting the data for all seismographs so
        # there is only one MPI gather call to the worker processes.
        #
        time = self.getLsm().getTime()
        seismoDataList = self.getSeismographData()

        #
        # Now each group writes the data to the appropriate files.
        #
        for seismoGroup in self.seismoGroupList:
            seismoGroup.save(time, seismoDataList)

    def writeReorderedRecordSectionData(self):
        """
        Writes reordered seismo data to file, data is ordered
        by distance-to-source then by time.
        """
        for seismoGroup in self.seismoGroupList:
            seismoGroup.writeReorderedRecordSectionData()

class SeismographData:
    """
    Objects of this class represent seismograph data at a particular
    instant in time.
    """
    def __init__(self, posn, displacement, velocity, acceleration, particleId):
        self.posn         = Vec3(posn)
        self.displacement = Vec3(displacement)
        self.velocity     = Vec3(velocity)
        self.acceleration = Vec3(acceleration)
        self.particleId   = particleId

class ParticleDataVisitor:
    """
    Helper class whose objects are used to collect
    seismograph data, from multiple locations,
    at a particular instant in time.
    Used in conjunction with the
    esys.lsm.sim.WavePropagation.WavePropagation.visitParticlesWithId
    method.
    """
    def __init__(self):
        self.seismoDataList = []

    def visitAParticle(self, p):
        """
        Converts the Particle data object p into a
        SeismographData object.
        @type p: L{esys.lsm.LsmPy.NRotSphere} or L{esys.lsm.LsmPy.RotSphere}
        @param p: Particle being visited.
        """
        self.seismoDataList.append(
            SeismographData(
                posn         = p.getInitialPosn(),
                displacement = p.getPosn() - p.getInitialPosn(),
                velocity     = p.getVelocity(),
                acceleration = p.getAcceleration(),
                particleId   = p.getId()
            )
        )

    def visitNRotSphere(self, p):
        self.visitAParticle(p)

    def visitRotSphere(self, p):
        self.visitAParticle(p)

class PVisitor:
    """
    Objects of this class are used in conjunction with
    the WavePropagation.visitParticlesWithId method to collect model
    particle data. Objects simply collect the visited particle
    data into a list.
    Used in conjunction with the
    esys.lsm.sim.WavePropagation.WavePropagation.visitParticlesWithId
    method.
    """
    def __init__(self):
        """
        Initialise with empty particle list.
        """
        self.particleList = []

    def __iter__(self):
        """
        Returns the iterator of the particle list.
        """
        return iter(self.particleList)

    def visitAParticle(self, particle):
        """
        Simply adds the specified particle data to the
        visited list.
        @type particle: L{esys.lsm.LsmPy.NRotSphere} or L{esys.lsm.LsmPy.RotSphere}
        @param particle: Particle data.
        """
        self.particleList.append(particle)

    def visitNRotSphere(self, p):
        self.visitAParticle(p)

    def visitRotSphere(self, p):
        self.visitAParticle(p)

class WavePropagation(WavePropagationPy.WavePropagation):
    """
    Wave propagation model, extends the
    esys.lsm.sim.WavePropagationPy.WavePropagation by providing
    convenient methods for saving data, creating partices/bonds and
    creating seismographs.
    """
    def __init__(
        self,
        domainBox,
        numWorkerProcesses = 2,
        mpiDimList = [0,0,0],
        do2d = False,
        timeStepSize = 0.05,
        dampingViscosity = 0.0
    ):
        """
        Initialise a wave propagation model.
        @type domainBox: L{BoundingBox}
        @param domainBox: a BoundingBox which specifies the model
                          particle domain.
        @type numWorkerProcesses: int
        @param numWorkerProcesses: the number of MPI worker processes used
                                   in model computations.
        @type mpiDimList: list
        @param mpiDimList: sequence of 3 int elements specifying the domain
                           decomposition used for the MPI worker processes.
        @type do2d: bool
        @param do2d: if True, forces the model to perform 2d calculations
        (particles move only in the x-y plane).
        @type timeStepSize: float
        @param timeStepSize: size of the time-step in the explicit integration
                             scheme.
        @type dampingViscosity: float
        @param dampingViscosity: if > 0 a damping viscosity is applied to
                                 particles.
        """
        WavePropagationPy.WavePropagation.__init__(
            self,
            numWorkerProcesses,
            mpiDimList
        )
        self.initVerletModel(
            "NRotSphere",
            2.5,
            0.20
        )
        self.setTimeStepSize(dt=timeStepSize)
        self.setSpatialDomain(domainBox)
        self.force2dComputations(do2d)
        if (dampingViscosity > 0.0):
            self.createDamping(
                DampingPrms(
                    type="Damping",
                    name="dampie-damp",
                    viscosity = dampingViscosity,
                    maxIterations=100
                )
            )
        self.sourceList      = []
        self.seismoGroups = SeismographGroupCollection(self)

    def createParticles(self, particles):
        """
        Creates model particles.
        @type particles: sequence
        @param particles: A sequence of L{esys.lsm.geometry.SimpleSphere}
                          objects.
        """
        WavePropagationPy.WavePropagation.createParticles(self, particles)

    def createBonds(self, connections, tagBondPrmDict):
        """
        Creates elastic bonds between particles.
        @type connections: sequence of
                           L{esys.lsm.geometry.GeometryPy.TaggedIdConnection}
        @param connections: create a bond for each connection which has
                            a tag key in the tagBondPrmDict dictionary.
        @type tagBondPrmDict: dict of tag:float
        @param tagBondPrmDict: a dictionary of (connectionTag:normalK)
                              pairs. Connections with tag connectionTag
                              will have a corresponding linear elastic
                              bond created with spring constant normalK.
        """
        self.createConnections(connections)
        for tag in tagBondPrmDict.keys():
            if (isinstance(tagBondPrmDict[tag], float)):
                bondPrms = \
                    NRotBondPrms(
                        name = "BondageTag_" + str(tag),
                        normalK = tagBondPrmDict[tag],
                        breakDistance = 10.0,
                        tag = tag
                    )
            else:
                bondPrms = tagBondPrmDict[tag]
            self.createInteractionGroup(bondPrms)

    def addSource(self, source):
        """
        Add a source disturbance to the model.
        @type source: L{WaveSource}
        @param source: source disturbance object
        """
        self.sourceList.append(source)
        self.addPreTimeStepRunnable(source)

    def createSource(self, prm):
        """
        Creates a propagation source (a WaveSource object)
        within the model.
        @type prm: L{SourcePrms}
        @param prm: parameters for creating a WaveSource object.
        """
        self.addSource(WaveSource(prm, self))

    def getTime(self):
        """
        Returns simulation time value (simply number of time steps
        multiplied by the time step size).
        @rtype: float
        @return: Simulation time.
        """
        return self.getTimeStep()*self.getTimeStepSize()

    def createSources(self, sourcesPrms):
        """
        Creates multiple propagation sources.
        @type sourcesPrms: sequence of L{SourcePrms} objects
        @param sourcesPrms: A WaveSource is created for each object in
                            this sequence.
        """
        if (not hasattr(sourcesPrms, "__iter__")):
            sourcesPrms = [sourcesPrms]
        for prm in sourcesPrms:
            self.createSource(prm)

    def createSeismographGroup(
        self,
        posnIteratable,
        fileNamePrefix,
        sourcePosn
    ):
        """
        Creates a group of seismographs (a SeismographGroup obect)
        from a specified sequence of spatial locations.
        @type posnIteratable: sequence of L{Vec3}
        @param posnIteratable: a sequence of coordinates which indicate
                               the locations of seismographs. A Seismograph
                               object is created for each point in
                               posnIteratable.
        @type fileNamePrefix: string
        @param fileNamePrefix: Seismograph data is saved to files with this
                              file name prefix.
        @type sourcePosn: L{Vec3}
        @param sourcePosn: location of the point-source disturbance.
        """
        self.seismoGroups.createGroup(
            posnIteratable,
            fileNamePrefix,
            sourcePosn
        )

    def saveSeismoData(self):
        """
        Appends seismograph data to file for the current time step.
        """
        self.seismoGroups.saveSeismoData()

    def writeReorderedRecordSectionData(self):
        """
        Reads time-ordered record-section data from file
        and writes a distance-from-source ordered file.
        """
        self.seismoGroups.writeReorderedRecordSectionData()

    def getParticleDataFileName(self, idx, fileNamePrefix):
        """
        Returns the name of the file where particle data is saved
        for the specifed index.
        @type idx: int
        @param idx: The snap-shot/frame number
        @type fileNamePrefix: string
        @param fileNamePrefix: The returned file name is a concatenation
                              of this prefix with a suffix formed using
                              the index argument.
        """
        return (fileNamePrefix + "%d.txt") % (idx,)

    def writeParticleDataOrig(self, idList, index, fileNamePrefix="particle_"):
        """
        Pure python-implemented version of saving particle displacement
        data, the C++ implementation provided by
        esys.lsm.sim.WavePropagation.WavePropagation class is faster.
        Writes particle displacement and velocity data to file.
        Each line of the file is
        'px py pz dx dy dz vx vy vz' where p=(px,py,pz) is the particle
        position, d=(dx,dy,dz) is the current particle displacement
        (ie position relative to initial position) and (vx,vy,vz) is the
        particle velocity.
        @type idList: sequence of int
        @param idList: list of particle id's for which data will be saved.
        @type index: int
        @param index: integer used to generate file name.
        @type fileNamePrefix: string
        @param fileNamePrefix: prefix of file where displacement data is saved.
        """
        #
        # Collect data for all particles with id's specified
        # in the idList.
        #
        visitor = PVisitor()
        self.visitParticlesWithId(idList, visitor)
        f = file(self.getParticleDataFileName(index, fileNamePrefix), "w")
        for p in visitor:
            f.write(
                str(p.getPosn()) +\
                " " +\
                str(p.getPosn()-p.getInitialPosn()) +\
                " " +\
                str(p.getVelocity()) +\
                "\n"
            )
        f.close()

    def writeParticleData(self, index, fileNamePrefix="particle_"):
        """
        Writes particle displacement and velocity data to file.
        Each line of the file is
        'px py pz dx dy dz vx vy vz' where p=(px,py,pz) is the particle
        position, d=(dx,dy,dz) is the current particle displacement
        (ie position relative to initial position) and (vx,vy,vz) is the
        particle velocity. Particle data is saved for all particles with an
        id in the list returned by the self.getParticleDataIdList.
        @type index: int
        @param index: integer used to generate file name.
        @type fileNamePrefix: string
        @param fileNamePrefix: prefix of file where displacement data is saved.
        """
        #
        # Collect data for all particles with id's specified
        # in the idList.
        #
        self.writeParticleDataToFile(
            self.getParticleDataFileName(index, fileNamePrefix)
        )