Skip to content

pysmo.tools.iccs

Iterative Cross-Correlation and Stack (ICCS).

Warning

This module is being developed alongside a complete rewrite of AIMBAT. Expect major changes until the rewrite is complete.

The ICCS1 method is an iterative algorithm to rapidly determine the best fitting delay times between an arbitrary number of seismograms with minimal involvement by a human operator. Instead of looking at individual seismograms, parameters are set that control the algorithm, which then iteratively aligns seismograms, or discards them from further consideration if they are of poor quality.

The basic idea of ICCS, is that stacking all seismograms (aligned with respect to an initial, and later improved, phase arrival pick) will lead to the targeted phase arrival becoming visible in the stack. As the stack is generated from all input seismograms, the phase arrival in the stack may be considered a representation of the "best" mean arrival time. Each individual seismogram can then be cross-correlated with the stack to determine a time shift that best aligns them with the stack and thus each other.

The results of ICCS are similar to those produced by the mccc algorithm, while also requiring fewer cross-correlations to be computed (each individual seismogram is only cross-correlated with the stack, whereas in MCCC all seismograms are cross-correlated with each other). ICCS is therefore particularly useful to prepare data for a successful MCCC run (e.g. if the initial picks are calculated rather than hand picked).

Data requirements

The iccs module requires that seismograms contain extra attributes specific to the ICCS method. Hence it provides a protocol class (ICCSSeismogram) and corresponding Mini class (MiniICCSSeismogram). In addition to the common attributes of a Seismogram in pysmo, the following parameters are required:

Attribute Description
t0 Initial pick (typically computed). Serves as input only when t1 is not set.
t1 Improved pick. Serves as both input (if not None) and output (always) when running the ICCS algorithm. It should be set to None initially.
select Determines if a seismogram is used for the stack, and should therefore be True initially. It is set to False for poor quality seismograms automatically during a run if autoselect is True. Note that this flag does not exclude a seismogram from being cross-correlated with the stack. Recovery is therefore possible and previously de-selected seismograms may be selected again for the next iteration.
flip Determines if the seismogram data should be flipped (i.e. data are multiplied with -1) when using it in the stack and cross-correlation. Can be automatically toggled when autoflip is True during a run.

Tip

Functions and methods in this module do not modify any attributes other than the ones listed above. Preparation of seismograms for use in the cross-correlation and relevant visualisation functions happens internally, and does not affect the data of the original seismograms.

Ephemeral seismograms

As the ICCS algorithm operates on a window around the targeted phase arrival, only a small portion of the input seismogram data are used. These smaller portions are generated on the fly in two ways:

  • Cross-correlation seismograms are used for the execution of the ICCS algorithm. They consist of the windowed portion around the phase arrival and a tapered ramp up and down outside the window.
  • Context seismograms are used to provide extra context. They consist of a broader window around the phase arrival, and without any tapering applied.

Both share common processing steps, and are used to create a corresponding stack. As they are completely reproducable, they only exist for the lifetime of the ICCS instance that contains the input seismograms and parameters used in their creation.

Tip

Both types can be used for visualisation purposes. It is therefore possible to e.g. pick an updated arrival in the cross-correlation seismograms, and pick new time window boundaries in the context seismograms.

Execution flow

The diagram below shows execution flow, and how the above parameters are used when the ICCS algorithm is executed (see here for parameters and default values):

flowchart TD
Start(["ICCSSeismograms with initial parameters."])
Stack0["Generate windowed seismograms and create stack from them."]
C["Cross-correlate windowed seismograms with stack to obtain updated picks and normalised correlation coefficients."]
FlipQ{"Is **autoflip**
True?"}
Flip["Toggle **flip** attribute of seismograms with negative correlation coefficients."]
QualQ{"Is **autoselect**
True?"}
Qual1["Toggle **select** attribute of seismograms based on correlation coefficient."]
Stack1["Recompute windowed seismograms and stack with updated parameters."]
H{"Convergence
criteria met?"}
I{"Maximum
iterations
reached?"}
End(["ICCSSeismograms with updated **t1**, **flip**, and **select** parameters."])
Start --> Stack0 --> C --> FlipQ -->|No| QualQ -->|No| Stack1 --> H -->|No| I -->|No| C
FlipQ -->|Yes| Flip --> QualQ
QualQ -->|Yes| Qual1 -->  Stack1
H -->|Yes| End
I -->|Yes| End

Convergence is reached when the stack itself is not changing significantly anymore between iterations. Typically this happens within a few iterations.

Operator involvement

The ICCS algorithm relies on a few parameters that need to be adjusted by the user. This module provides functions to visualise the stack and individual seismograms (all at the same time), and to update the parameters based on visual inspection.


  1. Lou, X., et al. “AIMBAT: A Python/Matplotlib Tool for Measuring Teleseismic Arrival Times.” Seismological Research Letters, vol. 84, no. 1, Jan. 2013, pp. 85–93, https://doi.org/10.1785/0220120033

Classes:

Name Description
ICCS

Class to store a list of ICCSSeismograms and run the ICCS algorithm.

ICCSSeismogram

Protocol class to define the ICCSSeismogram type.

MiniICCSSeismogram

Minimal implementation of the ICCSSeismogram type.

Functions:

Name Description
plot_seismograms

Plot the selected ICCS seismograms as an image.

plot_stack

Plot the ICCS stack.

update_all_picks

Update t1 in all seismograms by the same amount.

update_min_ccnorm

Interactively pick a new min_ccnorm.

update_pick

Manually pick t1 and apply it to all seismograms.

update_timewindow

Pick new time window limits.

ICCS

Class to store a list of ICCSSeismograms and run the ICCS algorithm.

The ICCS class serves as a container to store a list of seismograms (typically recordings of the same event at different stations), and to then run the ICCS algorithm when an instance of this class is called. Processing parameters that are common to all seismograms are stored as attributes (e.g. time window limits).

The seismograms stored in an instance are prepared in two distinct ways:

  • For cross-correlation: uses ramp_width to define how much of the seismogram should be used as taper before and after the time window of interest. These are the seismograms that form the stack and are used in the cross-correlation.
  • For added context: uses context_width to define how much of the seismogram should be used as extra context before and after the time window of interest. context_width should be chosen such that a large enough portion of the seismogram is shown to e.g. interactively pick new time window boundaries.

Apart from tapering the two types are processed the same. For performance, the prepared seismograms are cached and only calculated on a first call or if relevant parameters are updated.

Examples:

We begin with a set of SAC files of the same event, recorded at different stations. All files have a preliminary phase arrival estimate saved in the T0 SAC header, so we can use these files to create instances of the MiniICCSSeismogram class for use with the ICCS class:

>>> from pysmo.classes import SAC
>>> from pysmo.functions import clone_to_mini
>>> from pysmo.tools.iccs import MiniICCSSeismogram
>>> from pathlib import Path
>>>
>>> sacfiles = sorted(Path("iccs-example/").glob("*.bhz"))
>>>
>>> seismograms = []
>>> for sacfile in sacfiles:
...     sac = SAC.from_file(sacfile)
...     update = {"t0": sac.timestamps.t0}
...     iccs_seismogram = clone_to_mini(MiniICCSSeismogram, sac.seismogram, update=update)
...     seismograms.append(iccs_seismogram)
...
>>>

To better illustrate the different modes of running the ICCS algorithm, we modify the data and picks in the seismograms to make them worse than they actually are:

>>> from pandas import Timedelta
>>> from copy import deepcopy
>>> import numpy as np
>>>
>>> # change the sign of the data in the first seismogram
>>> seismograms[0].data *= -1
>>>
>>> # move the initial pick 2 seconds earlier in second seismogram
>>> seismograms[1].t0 += Timedelta(seconds=-2)
>>>
>>> # move the initial pick 2 seconds later in third seismogram
>>> seismograms[2].t0 += Timedelta(seconds=2)
>>>
>>> # create a seismogram with completely random data
>>> iccs_random: MiniICCSSeismogram = deepcopy(seismograms[-1])
>>> np.random.seed(42)  # set this for consistent results during testing
>>> iccs_random.data = np.random.rand(len(iccs_random.data))
>>> seismograms.append(iccs_random)
>>>

We can now create an instance of the ICCS class and plot the initial stack and cc_seismograms:

>>> from pysmo.tools.iccs import ICCS, plot_stack
>>> iccs = ICCS(seismograms)
>>> plot_stack(iccs, context=False)
>>>

Initial stack Initial stack

The phase emergence is not visible in the stack, and the (absolute) correlation coefficients of the seismograms are not very high. This shows the initial picks are not very good and/or that the data are of low quality. To run the ICCS algorithm, we simply call (execute) the ICCS instance:

>>> convergence_list = iccs()  # this runs the ICCS algorithm and returns
>>>                            # a list of the convergence value after each
>>>                            # iteration.
>>> plot_stack(iccs, context=False)
>>>

Stack after first run Stack after first run

Despite the random noise seismogram, the phase arrival is now visible in the stack. Seismograms with low correlation coefficients can automatically be deselected from the calculations by running ICCS again with autoselect=True:

>>> _ = iccs(autoselect=True)
>>> plot_stack(iccs, context=False)
>>>

Stack after run with autoselect Stack after run with autoselect

Seismograms that fit better with their polarity reversed can be flipped automatically by setting autoflip=True:

>>> _ = iccs(autoflip=True)
>>> plot_stack(iccs, context=False)
>>>

Stack after run with autoflip Stack after run with autoflip

To further improve results, you can interactively update the picks, time window, and minimum correlation coefficient using update_pick, update_timewindow, and update_min_ccnorm, respectively, and then run the ICCS algorithm again.

Methods:

Name Description
__call__

Run the iccs algorithm.

validate_pick

Check if a new pick is valid.

validate_time_window

Check if a new time window (relative to pick) is valid.

Attributes:

Name Type Description
bandpass_apply bool

Filter seismograms with a bandpass filter before running ICCS.

bandpass_fmax float

Bandpass filter maximum frequency (Hz). Only used if bandpass_apply is True.

bandpass_fmin float

Bandpass filter minimum frequency (Hz). Only used if bandpass_apply is True.

cc_seismograms list[_EphemeralSeismogram]

Returns the seismograms as used for the cross-correlation.

ccnorms ndarray

Returns an array of the normalised cross-correlation coefficients.

context_seismograms list[_EphemeralSeismogram]

Returns the seismograms with extra context for plotting.

context_stack MiniSeismogram

Returns the stacked context_seismograms.

context_width Timedelta

Context padding to apply before and after the time window.

min_ccnorm floating | float

Minimum normalised cross-correlation coefficient for seismograms.

ramp_width NonNegativeTimedelta | NonNegativeNumber

Width of taper ramp up and down.

seismograms Sequence[ICCSSeismogram]

Input seismograms.

stack MiniSeismogram

Returns the stacked cc_seismograms).

window_post Timedelta

End of the time window relative to the pick.

window_pre Timedelta

Beginning of the time window relative to the pick.

Source code in src/pysmo/tools/iccs/_iccs.py
 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
@beartype
@define(slots=True)
class ICCS:
    """Class to store a list of [`ICCSSeismograms`][pysmo.tools.iccs.ICCSSeismogram] and run the ICCS algorithm.

    The [`ICCS`][pysmo.tools.iccs.ICCS] class serves as a container to store a
    list of seismograms (typically recordings of the same event at different
    stations), and to then run the ICCS algorithm when an instance of this
    class is called. Processing parameters that are common to all seismograms
    are stored as attributes (e.g. time window limits).

    The seismograms stored in an instance are prepared in two distinct ways:

    - **For cross-correlation:** uses [`ramp_width`][pysmo.tools.iccs.ICCS.ramp_width]
      to define how much of the seismogram should be used as *taper* before and
      after the time window of interest. These are the seismograms that form
      the stack and are used in the cross-correlation.
    - **For added context:** uses [`context_width`][pysmo.tools.iccs.ICCS.context_width]
      to define how much of the seismogram should be used as *extra context*
      before and after the time window of interest.
      [`context_width`][pysmo.tools.iccs.ICCS.context_width] should be chosen
      such that a large enough portion of the seismogram is shown to e.g.
      interactively pick new time window boundaries.

    Apart from tapering the two types are processed the same. For performance,
    the prepared seismograms are cached and only calculated on a first call or
    if relevant parameters are updated.

    Examples:
        We begin with a set of SAC files of the same event, recorded at different
        stations. All files have a preliminary phase arrival estimate saved in the
        `T0` SAC header, so we can use these files to create instances of the
        [`MiniICCSSeismogram`][pysmo.tools.iccs.MiniICCSSeismogram] class for use
        with the [`ICCS`][pysmo.tools.iccs.ICCS] class:

        ```python
        >>> from pysmo.classes import SAC
        >>> from pysmo.functions import clone_to_mini
        >>> from pysmo.tools.iccs import MiniICCSSeismogram
        >>> from pathlib import Path
        >>>
        >>> sacfiles = sorted(Path("iccs-example/").glob("*.bhz"))
        >>>
        >>> seismograms = []
        >>> for sacfile in sacfiles:
        ...     sac = SAC.from_file(sacfile)
        ...     update = {"t0": sac.timestamps.t0}
        ...     iccs_seismogram = clone_to_mini(MiniICCSSeismogram, sac.seismogram, update=update)
        ...     seismograms.append(iccs_seismogram)
        ...
        >>>
        ```

        To better illustrate the different modes of running the ICCS algorithm,
        we modify the data and picks in the seismograms to make them **worse**
        than they actually are:

        ```python
        >>> from pandas import Timedelta
        >>> from copy import deepcopy
        >>> import numpy as np
        >>>
        >>> # change the sign of the data in the first seismogram
        >>> seismograms[0].data *= -1
        >>>
        >>> # move the initial pick 2 seconds earlier in second seismogram
        >>> seismograms[1].t0 += Timedelta(seconds=-2)
        >>>
        >>> # move the initial pick 2 seconds later in third seismogram
        >>> seismograms[2].t0 += Timedelta(seconds=2)
        >>>
        >>> # create a seismogram with completely random data
        >>> iccs_random: MiniICCSSeismogram = deepcopy(seismograms[-1])
        >>> np.random.seed(42)  # set this for consistent results during testing
        >>> iccs_random.data = np.random.rand(len(iccs_random.data))
        >>> seismograms.append(iccs_random)
        >>>
        ```

        We can now create an instance of the [`ICCS`][pysmo.tools.iccs.ICCS]
        class and plot the initial [`stack`][pysmo.tools.iccs.ICCS.stack] and
        [`cc_seismograms`][pysmo.tools.iccs.ICCS.cc_seismograms]:

        ```python
        >>> from pysmo.tools.iccs import ICCS, plot_stack
        >>> iccs = ICCS(seismograms)
        >>> plot_stack(iccs, context=False)
        >>>
        ```

        <!-- invisible-code-block: python
        ```
        >>> import matplotlib.pyplot as plt
        >>> plt.close("all")
        >>> if savedir:
        ...     plt.style.use("dark_background")
        ...     fig, _ = plot_stack(iccs, context=False, return_fig=True)
        ...     fig.savefig(savedir / "iccs_stack_initial-dark.png", transparent=True)
        ...
        ...     plt.style.use("default")
        ...     fig, _ = plot_stack(iccs, context=False, return_fig=True)
        ...     fig.savefig(savedir / "iccs_stack_initial.png", transparent=True)
        >>>
        ```
        -->

        ![Initial stack](../../../images/tools/iccs/iccs_stack_initial.png#only-light){ loading=lazy }
        ![Initial stack](../../../images/tools/iccs/iccs_stack_initial-dark.png#only-dark){ loading=lazy }

        The phase emergence is not visible in the stack, and the (absolute)
        correlation coefficients of the seismograms are not very high. This
        shows the initial picks are not very good and/or that the data are of
        low quality. To run the ICCS algorithm, we simply call (execute) the
        ICCS instance:

        ```python
        >>> convergence_list = iccs()  # this runs the ICCS algorithm and returns
        >>>                            # a list of the convergence value after each
        >>>                            # iteration.
        >>> plot_stack(iccs, context=False)
        >>>
        ```

        <!-- invisible-code-block: python
        ```
        >>> import matplotlib.pyplot as plt
        >>> plt.close("all")
        >>> if savedir:
        ...     plt.style.use("dark_background")
        ...     fig, _ = plot_stack(iccs, context=False, return_fig=True)
        ...     fig.savefig(savedir / "iccs_stack_first_run-dark.png", transparent=True)
        ...
        ...     plt.style.use("default")
        ...     fig, _ = plot_stack(iccs, context=False, return_fig=True)
        ...     fig.savefig(savedir / "iccs_stack_first_run.png", transparent=True)
        >>>
        ```
        -->

        ![Stack after first run](../../../images/tools/iccs/iccs_stack_first_run.png#only-light){ loading=lazy }
        ![Stack after first run](../../../images/tools/iccs/iccs_stack_first_run-dark.png#only-dark){ loading=lazy }

        Despite the random noise seismogram, the phase arrival is now visible in
        the stack. Seismograms with low correlation coefficients can automatically
        be deselected from the calculations by running ICCS again with
        `autoselect=True`:


        ```python
        >>> _ = iccs(autoselect=True)
        >>> plot_stack(iccs, context=False)
        >>>
        ```

        <!-- invisible-code-block: python
        ```
        >>> import matplotlib.pyplot as plt
        >>> plt.close("all")
        >>> if savedir:
        ...     plt.style.use("dark_background")
        ...     fig, _ = plot_stack(iccs, context=False, return_fig=True)
        ...     fig.savefig(savedir / "iccs_stack_autoselect-dark.png", transparent=True)
        ...
        ...     plt.style.use("default")
        ...     fig, _ = plot_stack(iccs, context=False, return_fig=True)
        ...     fig.savefig(savedir / "iccs_stack_autoselect.png", transparent=True)
        >>>
        ```
        -->

        ![Stack after run with autoselect](../../../images/tools/iccs/iccs_stack_autoselect.png#only-light){ loading=lazy }
        ![Stack after run with autoselect](../../../images/tools/iccs/iccs_stack_autoselect-dark.png#only-dark){ loading=lazy }


        Seismograms that fit better with their polarity reversed can be flipped
        automatically by setting `autoflip=True`:

        ```python
        >>> _ = iccs(autoflip=True)
        >>> plot_stack(iccs, context=False)
        >>>
        ```

        <!-- invisible-code-block: python
        ```
        >>> import matplotlib.pyplot as plt
        >>> plt.close("all")
        >>> if savedir:
        ...     plt.style.use("dark_background")
        ...     fig, _ = plot_stack(iccs, context=False, return_fig=True)
        ...     fig.savefig(savedir / "iccs_stack_autoflip-dark.png", transparent=True)
        ...
        ...     plt.style.use("default")
        ...     fig, _ = plot_stack(iccs, context=False, return_fig=True)
        ...     fig.savefig(savedir / "iccs_stack_autoflip.png", transparent=True)
        >>>
        ```
        -->

        ![Stack after run with autoflip](../../../images/tools/iccs/iccs_stack_autoflip.png#only-light){ loading=lazy }
        ![Stack after run with autoflip](../../../images/tools/iccs/iccs_stack_autoflip-dark.png#only-dark){ loading=lazy }


        To further improve results, you can interactively update the picks,
        time window, and minimum correlation coefficient using
        [`update_pick`][pysmo.tools.iccs.update_pick],
        [`update_timewindow`][pysmo.tools.iccs.update_timewindow], and
        [`update_min_ccnorm`][pysmo.tools.iccs.update_min_ccnorm],
        respectively, and then run the ICCS algorithm again.
    """

    seismograms: Sequence[ICCSSeismogram] = field(
        factory=lambda: list[ICCSSeismogram](), validator=_clear_cache_on_update
    )
    """Input seismograms.

    These seismograms provide the input data for ICCS. They are used to store
    processing parameters and create shorter seismograms (based on pick and
    time window) that are then used for cross-correlation. The shorter
    seismograms are created on the fly and then cached within an [`ICCS`]
    [pysmo.tools.iccs.ICCS] instance.
    """

    window_pre: Timedelta = field(
        default=ICCS_DEFAULTS.window_pre,
        validator=[
            validators.lt(Timedelta(seconds=0)),
            _validate_window_pre,
            _clear_cache_on_update,
        ],
    )
    """Beginning of the time window relative to the pick."""

    window_post: Timedelta = field(
        default=ICCS_DEFAULTS.window_post,
        validator=[
            validators.gt(Timedelta(seconds=0)),
            _validate_window_post,
            _clear_cache_on_update,
        ],
    )
    """End of the time window relative to the pick."""

    context_width: Timedelta = field(
        default=ICCS_DEFAULTS.context_width,
        validator=[validators.gt(Timedelta(seconds=0)), _clear_cache_on_update],
    )
    """Context padding to apply before and after the time window.

    This padding is *not* used for the cross-correlation."""

    ramp_width: NonNegativeTimedelta | NonNegativeNumber = field(
        default=ICCS_DEFAULTS.ramp_width, validator=_clear_cache_on_update
    )
    """Width of taper ramp up and down.

    Can be either a timedelta or a float - see [`pysmo.functions.window()`][pysmo.functions.window]
    for details.
    """

    bandpass_apply: bool = field(
        default=ICCS_DEFAULTS.bandpass_apply, validator=_clear_cache_on_update
    )
    """Filter seismograms with a bandpass filter before running ICCS.

    Setting this to [`True`][] will apply a
    [`bandpass`][pysmo.tools.signal.bandpass] filter (with `zerophase` set to
    [`True`][]) to the [`cc_seismograms`][pysmo.tools.iccs.ICCS.cc_seismograms]
    and [`context_seismograms`][pysmo.tools.iccs.ICCS.context_seismograms].

    As the [`seismograms`][pysmo.tools.iccs.ICCS.seismograms] may have already
    been pre-processed (i.e. already filtered) the default value for this
    parameter is [`False`][].
    """

    bandpass_fmin: float = field(
        default=ICCS_DEFAULTS.bandpass_fmin, validator=_clear_cache_on_update
    )
    """Bandpass filter minimum frequency (Hz). Only used if [`bandpass_apply`][pysmo.tools.iccs.ICCS.bandpass_apply] is `True`."""

    bandpass_fmax: float = field(
        default=ICCS_DEFAULTS.bandpass_fmax, validator=_clear_cache_on_update
    )
    """Bandpass filter maximum frequency (Hz). Only used if [`bandpass_apply`][pysmo.tools.iccs.ICCS.bandpass_apply] is `True`."""

    min_ccnorm: np.floating | float = ICCS_DEFAULTS.min_ccnorm
    """Minimum normalised cross-correlation coefficient for seismograms.

    When the ICCS algorithm is [executed][pysmo.tools.iccs.ICCS.__call__],
    the cross-correlation coefficient for each seismogram is calculated after
    each iteration. If `autoselect` is set to `True`, the
    [`select`][pysmo.tools.iccs.ICCSSeismogram.select] attribute of seismograms
    with with correlation coefficients below this value is set to `False`, and
    they are no longer used for the [`stack`][pysmo.tools.iccs.ICCS.stack].
    """

    # The following attributes are cached to prevent unnecessary processing.
    # Setting the caches to None will force a new calculation when they are
    # requested.
    _cc_seismograms: list[_EphemeralSeismogram] | None = field(init=False)
    _context_seismograms: list[_EphemeralSeismogram] | None = field(init=False)
    _ccnorms: np.ndarray | None = field(init=False)
    _cc_stack: MiniSeismogram | None = field(init=False)
    _context_stack: MiniSeismogram | None = field(init=False)
    _valid_pick_range: tuple[timedelta, timedelta] | None = field(init=False)
    _valid_time_window_range: tuple[Timedelta, Timedelta] | None = field(init=False)

    def __attrs_post_init__(self) -> None:
        self._clear_caches()

    def _clear_caches(self) -> None:
        """Clear all cached attributes."""
        self._cc_seismograms = None
        self._context_seismograms = None
        self._ccnorms = None
        self._cc_stack = None
        self._context_stack = None
        self._valid_pick_range = None
        self._valid_time_window_range = None

    def _prepare_seismograms(
        self,
        add_context: bool = False,
    ) -> list[_EphemeralSeismogram]:
        """Prepare cc_seismograms or context_seismograms."""

        ephemeral_seismograms: list[_EphemeralSeismogram] = []

        min_delta = min((s.delta for s in self.seismograms))

        for seismogram in self.seismograms:
            pick = seismogram.t1 or seismogram.t0
            window_start = pick + self.window_pre
            window_end = pick + self.window_post

            ephemeral_seismogram = _EphemeralSeismogram(parent_seismogram=seismogram)

            if self.bandpass_apply:
                bandpass(
                    ephemeral_seismogram,
                    self.bandpass_fmin,
                    self.bandpass_fmax,
                    zerophase=True,
                )

            if not np.isclose(
                ephemeral_seismogram.delta.total_seconds(), min_delta.total_seconds()
            ):
                resample(ephemeral_seismogram, min_delta)

            if add_context:
                context_window_start = window_start - self.context_width
                context_window_end = window_end + self.context_width

                if (
                    context_window_start < seismogram.begin_time
                    or context_window_end > seismogram.end_time
                ):
                    pad(
                        ephemeral_seismogram,
                        context_window_start,
                        context_window_end,
                        mode="linear_ramp",
                        end_values=(0, 0),
                    )

                crop(ephemeral_seismogram, context_window_start, context_window_end)
                detrend(ephemeral_seismogram)
                normalize(ephemeral_seismogram, window_start, window_end)
            else:
                window(ephemeral_seismogram, window_start, window_end, self.ramp_width)
                normalize(ephemeral_seismogram)

            if seismogram.flip:
                ephemeral_seismogram.data *= -1

            ephemeral_seismograms.append(ephemeral_seismogram)

        # If all seismograms have the same length, return them now.
        if len(lengths := set(len(s.data) for s in ephemeral_seismograms)) == 1:
            return ephemeral_seismograms

        # Shorten seismograms if necessary and return (floating-point precision
        # can cause small differences in length after resampling). We cut off
        # at the end so the `begin_time` will not change.
        for s in ephemeral_seismograms:
            s.data = s.data[: min(lengths)]
        return ephemeral_seismograms

    @property
    def cc_seismograms(self) -> list[_EphemeralSeismogram]:
        """Returns the seismograms as used for the cross-correlation.

        These seismograms are derived from the input seismograms and used for
        the cross-correlation steps. Starting with the input seismograms, they
        are processed as follows:

        1. Bandpass filtered if
           [`bandpass_apply`][pysmo.tools.iccs.ICCS.bandpass_apply] is
           [`True`][].
        2. Resampled to the minimum sampling interval of all input seismograms
           (only if it is not equal in all seismograms).
        3. Cropped to `ramp_width` +  current time window + `ramp_width`.
        4. Detrended.
        5. Tapered using [`ramp_width`][pysmo.tools.iccs.ICCS.ramp_width]
           (tapered sections are *outside* time window).
        6. Normalised based on the highest absolute value within the cropped
           window. This step is done slightly differently in
           [`context_seismograms`][pysmo.tools.iccs.ICCS.context_seismograms]
           --see the documentation of that property for details.
        """

        if self._cc_seismograms is None:
            self._cc_seismograms = self._prepare_seismograms(add_context=False)
        return self._cc_seismograms

    @property
    def context_seismograms(self) -> list[_EphemeralSeismogram]:
        """Returns the seismograms with extra context for plotting.

        These seismograms are derived from the input seismograms and used
        primarily for plotting with extra context (e.g. when selecting new
        time window boundaries). Starting with the input seismograms, they are
        processed as follows:

        1. Bandpass filtered if
           [`bandpass_apply`][pysmo.tools.iccs.ICCS.bandpass_apply] is
           [`True`][].
        2. Resampled to the minimum sampling interval of all input seismograms
           (only if it is not equal in all seismograms).
        3. Cropped and/or padded to `context_width` +  current time window +
           `context_width`.
        4. Detrended.
        5. Normalised based on the highest absolute value within the selected
           time window (i.e. without the context).
        """

        if self._context_seismograms is None:
            self._context_seismograms = self._prepare_seismograms(add_context=True)
        return self._context_seismograms

    @property
    def ccnorms(self) -> np.ndarray:
        """Returns an array of the normalised cross-correlation coefficients."""

        if self._ccnorms is None:
            matrix = np.array([s.data for s in self.cc_seismograms])
            self._ccnorms = pearson_matrix_vector(matrix, self.stack.data)
        return self._ccnorms

    @property
    def stack(self) -> MiniSeismogram:
        """Returns the stacked [`cc_seismograms`][pysmo.tools.iccs.ICCS.cc_seismograms]).

        The stack is calculated as the average of all seismograms with the
        attribute [`select`][pysmo.tools.iccs.ICCSSeismogram.select] set to
        [`True`][True]. The [`begin_time`][pysmo.MiniSeismogram.begin_time] of
        the returned stack is the average of the [`begin_time`]
        [pysmo.tools.iccs.ICCSSeismogram.begin_time] of the input seismograms.

        Returns:
            Stacked input seismograms.
        """
        if self._cc_stack is None:
            self._cc_stack = _create_stack(self.cc_seismograms)
        return self._cc_stack

    @property
    def context_stack(self) -> MiniSeismogram:
        """Returns the stacked [`context_seismograms`][pysmo.tools.iccs.ICCS.context_seismograms].

        Returns:
            Stacked input seismograms with context padding.
        """
        if self._context_stack is None:
            self._context_stack = _create_stack(self.context_seismograms)
        return self._context_stack

    def validate_pick(self, pick: Timedelta) -> bool:
        """Check if a new pick is valid.

        This checks if a new manual pick is valid for all selected seismograms.

        Args:
            pick: New pick to validate.

        Returns:
            Whether the new pick is valid.
        """

        # first calculate valid pick range if not done yet
        if self._valid_pick_range is None:
            self._valid_pick_range = _calc_valid_pick_range(self)

        return self._valid_pick_range[0] <= pick <= self._valid_pick_range[1]

    def validate_time_window(
        self, window_pre: Timedelta, window_post: Timedelta
    ) -> bool:
        """Check if a new time window (relative to pick) is valid.

        Args:
            window_pre: New window start time to validate.
            window_post: New window end time to validate.

        Returns:
            Whether the new time window is valid.
        """

        if window_pre >= window_post:
            return False

        if window_pre > -self.stack.delta:
            return False

        if window_post < self.stack.delta:
            return False

        # Calculate valid time window range if not done yet
        if self._valid_time_window_range is None:
            self._valid_time_window_range = _calc_valid_time_window_range(self)

        return (
            self._valid_time_window_range[0] <= window_pre
            and window_post <= self._valid_time_window_range[1]
        )

    def __call__(
        self,
        autoflip: bool = False,
        autoselect: bool = False,
        convergence_limit: float = ICCS_DEFAULTS.convergence_limit,
        convergence_method: ConvergenceMethod = ICCS_DEFAULTS.convergence_method,
        max_iter: int = ICCS_DEFAULTS.max_iter,
        max_shift: Timedelta | None = None,
    ) -> np.ndarray:
        """Run the iccs algorithm.

        Args:
            autoflip: Automatically toggle [`flip`][pysmo.tools.iccs.ICCSSeismogram.flip] attribute of seismograms.
            autoselect: Automatically set `select` attribute to `False` for poor quality seismograms.
            convergence_limit: Convergence limit at which the algorithm stops.
            convergence_method: Method to calculate convergence criterion.
            max_iter: Maximum number of iterations.
            max_shift: Maximum shift in seconds (see [`delay()`][pysmo.tools.signal.delay]).

        Returns:
            convergence: Array of convergence criterion values.
        """
        convergence_list = []

        for _ in range(max_iter):
            # Save the previous stack to calculate convergence criterion after updating the seismograms.
            prev_stack = clone_to_mini(MiniSeismogram, self.stack)

            # Get delays and correlation coefficients for all seismograms in one go
            delays, ccnorms = multi_delay(
                self.stack, self.cc_seismograms, abs_max=autoflip
            )

            # Update seismograms based on results and settings.
            for delay, ccnorm, cc_seismogram in zip(
                delays, ccnorms, self.cc_seismograms
            ):
                _update_seismogram(
                    delay,
                    ccnorm,
                    cc_seismogram.parent_seismogram,
                    autoflip,
                    autoselect,
                    self.min_ccnorm,
                    (self.window_pre, self.window_post),
                )

            self._clear_caches()

            convergence = _calc_convergence(self.stack, prev_stack, convergence_method)
            convergence_list.append(convergence)
            if convergence <= convergence_limit:
                break

        return np.array(convergence_list)

bandpass_apply class-attribute instance-attribute

bandpass_apply: bool = field(
    default=bandpass_apply, validator=_clear_cache_on_update
)

Filter seismograms with a bandpass filter before running ICCS.

Setting this to True will apply a bandpass filter (with zerophase set to True) to the cc_seismograms and context_seismograms.

As the seismograms may have already been pre-processed (i.e. already filtered) the default value for this parameter is False.

bandpass_fmax class-attribute instance-attribute

bandpass_fmax: float = field(
    default=bandpass_fmax, validator=_clear_cache_on_update
)

Bandpass filter maximum frequency (Hz). Only used if bandpass_apply is True.

bandpass_fmin class-attribute instance-attribute

bandpass_fmin: float = field(
    default=bandpass_fmin, validator=_clear_cache_on_update
)

Bandpass filter minimum frequency (Hz). Only used if bandpass_apply is True.

cc_seismograms property

cc_seismograms: list[_EphemeralSeismogram]

Returns the seismograms as used for the cross-correlation.

These seismograms are derived from the input seismograms and used for the cross-correlation steps. Starting with the input seismograms, they are processed as follows:

  1. Bandpass filtered if bandpass_apply is True.
  2. Resampled to the minimum sampling interval of all input seismograms (only if it is not equal in all seismograms).
  3. Cropped to ramp_width + current time window + ramp_width.
  4. Detrended.
  5. Tapered using ramp_width (tapered sections are outside time window).
  6. Normalised based on the highest absolute value within the cropped window. This step is done slightly differently in context_seismograms --see the documentation of that property for details.

ccnorms property

ccnorms: ndarray

Returns an array of the normalised cross-correlation coefficients.

context_seismograms property

context_seismograms: list[_EphemeralSeismogram]

Returns the seismograms with extra context for plotting.

These seismograms are derived from the input seismograms and used primarily for plotting with extra context (e.g. when selecting new time window boundaries). Starting with the input seismograms, they are processed as follows:

  1. Bandpass filtered if bandpass_apply is True.
  2. Resampled to the minimum sampling interval of all input seismograms (only if it is not equal in all seismograms).
  3. Cropped and/or padded to context_width + current time window + context_width.
  4. Detrended.
  5. Normalised based on the highest absolute value within the selected time window (i.e. without the context).

context_stack property

context_stack: MiniSeismogram

Returns the stacked context_seismograms.

Returns:

Type Description
MiniSeismogram

Stacked input seismograms with context padding.

context_width class-attribute instance-attribute

context_width: Timedelta = field(
    default=context_width,
    validator=[
        gt(Timedelta(seconds=0)),
        _clear_cache_on_update,
    ],
)

Context padding to apply before and after the time window.

This padding is not used for the cross-correlation.

min_ccnorm class-attribute instance-attribute

min_ccnorm: floating | float = min_ccnorm

Minimum normalised cross-correlation coefficient for seismograms.

When the ICCS algorithm is executed, the cross-correlation coefficient for each seismogram is calculated after each iteration. If autoselect is set to True, the select attribute of seismograms with with correlation coefficients below this value is set to False, and they are no longer used for the stack.

ramp_width class-attribute instance-attribute

ramp_width: NonNegativeTimedelta | NonNegativeNumber = (
    field(
        default=ramp_width, validator=_clear_cache_on_update
    )
)

Width of taper ramp up and down.

Can be either a timedelta or a float - see pysmo.functions.window() for details.

seismograms class-attribute instance-attribute

seismograms: Sequence[ICCSSeismogram] = field(
    factory=lambda: list[ICCSSeismogram](),
    validator=_clear_cache_on_update,
)

Input seismograms.

These seismograms provide the input data for ICCS. They are used to store processing parameters and create shorter seismograms (based on pick and time window) that are then used for cross-correlation. The shorter seismograms are created on the fly and then cached within an ICCS instance.

stack property

Returns the stacked cc_seismograms).

The stack is calculated as the average of all seismograms with the attribute select set to True. The begin_time of the returned stack is the average of the begin_time of the input seismograms.

Returns:

Type Description
MiniSeismogram

Stacked input seismograms.

window_post class-attribute instance-attribute

window_post: Timedelta = field(
    default=window_post,
    validator=[
        gt(Timedelta(seconds=0)),
        _validate_window_post,
        _clear_cache_on_update,
    ],
)

End of the time window relative to the pick.

window_pre class-attribute instance-attribute

window_pre: Timedelta = field(
    default=window_pre,
    validator=[
        lt(Timedelta(seconds=0)),
        _validate_window_pre,
        _clear_cache_on_update,
    ],
)

Beginning of the time window relative to the pick.

__call__

__call__(
    autoflip: bool = False,
    autoselect: bool = False,
    convergence_limit: float = convergence_limit,
    convergence_method: ConvergenceMethod = convergence_method,
    max_iter: int = max_iter,
    max_shift: Timedelta | None = None,
) -> ndarray

Run the iccs algorithm.

Parameters:

Name Type Description Default
autoflip bool

Automatically toggle flip attribute of seismograms.

False
autoselect bool

Automatically set select attribute to False for poor quality seismograms.

False
convergence_limit float

Convergence limit at which the algorithm stops.

convergence_limit
convergence_method ConvergenceMethod

Method to calculate convergence criterion.

convergence_method
max_iter int

Maximum number of iterations.

max_iter
max_shift Timedelta | None

Maximum shift in seconds (see delay()).

None

Returns:

Name Type Description
convergence ndarray

Array of convergence criterion values.

Source code in src/pysmo/tools/iccs/_iccs.py
def __call__(
    self,
    autoflip: bool = False,
    autoselect: bool = False,
    convergence_limit: float = ICCS_DEFAULTS.convergence_limit,
    convergence_method: ConvergenceMethod = ICCS_DEFAULTS.convergence_method,
    max_iter: int = ICCS_DEFAULTS.max_iter,
    max_shift: Timedelta | None = None,
) -> np.ndarray:
    """Run the iccs algorithm.

    Args:
        autoflip: Automatically toggle [`flip`][pysmo.tools.iccs.ICCSSeismogram.flip] attribute of seismograms.
        autoselect: Automatically set `select` attribute to `False` for poor quality seismograms.
        convergence_limit: Convergence limit at which the algorithm stops.
        convergence_method: Method to calculate convergence criterion.
        max_iter: Maximum number of iterations.
        max_shift: Maximum shift in seconds (see [`delay()`][pysmo.tools.signal.delay]).

    Returns:
        convergence: Array of convergence criterion values.
    """
    convergence_list = []

    for _ in range(max_iter):
        # Save the previous stack to calculate convergence criterion after updating the seismograms.
        prev_stack = clone_to_mini(MiniSeismogram, self.stack)

        # Get delays and correlation coefficients for all seismograms in one go
        delays, ccnorms = multi_delay(
            self.stack, self.cc_seismograms, abs_max=autoflip
        )

        # Update seismograms based on results and settings.
        for delay, ccnorm, cc_seismogram in zip(
            delays, ccnorms, self.cc_seismograms
        ):
            _update_seismogram(
                delay,
                ccnorm,
                cc_seismogram.parent_seismogram,
                autoflip,
                autoselect,
                self.min_ccnorm,
                (self.window_pre, self.window_post),
            )

        self._clear_caches()

        convergence = _calc_convergence(self.stack, prev_stack, convergence_method)
        convergence_list.append(convergence)
        if convergence <= convergence_limit:
            break

    return np.array(convergence_list)

validate_pick

validate_pick(pick: Timedelta) -> bool

Check if a new pick is valid.

This checks if a new manual pick is valid for all selected seismograms.

Parameters:

Name Type Description Default
pick Timedelta

New pick to validate.

required

Returns:

Type Description
bool

Whether the new pick is valid.

Source code in src/pysmo/tools/iccs/_iccs.py
def validate_pick(self, pick: Timedelta) -> bool:
    """Check if a new pick is valid.

    This checks if a new manual pick is valid for all selected seismograms.

    Args:
        pick: New pick to validate.

    Returns:
        Whether the new pick is valid.
    """

    # first calculate valid pick range if not done yet
    if self._valid_pick_range is None:
        self._valid_pick_range = _calc_valid_pick_range(self)

    return self._valid_pick_range[0] <= pick <= self._valid_pick_range[1]

validate_time_window

validate_time_window(
    window_pre: Timedelta, window_post: Timedelta
) -> bool

Check if a new time window (relative to pick) is valid.

Parameters:

Name Type Description Default
window_pre Timedelta

New window start time to validate.

required
window_post Timedelta

New window end time to validate.

required

Returns:

Type Description
bool

Whether the new time window is valid.

Source code in src/pysmo/tools/iccs/_iccs.py
def validate_time_window(
    self, window_pre: Timedelta, window_post: Timedelta
) -> bool:
    """Check if a new time window (relative to pick) is valid.

    Args:
        window_pre: New window start time to validate.
        window_post: New window end time to validate.

    Returns:
        Whether the new time window is valid.
    """

    if window_pre >= window_post:
        return False

    if window_pre > -self.stack.delta:
        return False

    if window_post < self.stack.delta:
        return False

    # Calculate valid time window range if not done yet
    if self._valid_time_window_range is None:
        self._valid_time_window_range = _calc_valid_time_window_range(self)

    return (
        self._valid_time_window_range[0] <= window_pre
        and window_post <= self._valid_time_window_range[1]
    )

ICCSSeismogram

Bases: Seismogram, Protocol

Protocol class to define the ICCSSeismogram type.

The ICCSSeismogram type extends the Seismogram type with the addition of parameters that are required for ICCS.

Attributes:

Name Type Description
begin_time Timestamp

Seismogram begin time.

data ndarray

Seismogram data.

delta Timedelta

The sampling interval.

end_time Timestamp

Seismogram end time.

flip bool

Data in seismogram should be flipped for ICCS.

select bool

Use seismogram to create stack.

t0 Timestamp

Initial pick.

t1 Timestamp | None

Updated pick.

Source code in src/pysmo/tools/iccs/_types.py
@runtime_checkable
class ICCSSeismogram(Seismogram, Protocol):
    """Protocol class to define the `ICCSSeismogram` type.

    The `ICCSSeismogram` type extends the [`Seismogram`][pysmo.Seismogram] type
    with the addition of parameters that are required for ICCS.
    """

    t0: Timestamp
    """Initial pick."""

    t1: Timestamp | None
    """Updated pick."""

    flip: bool
    """Data in seismogram should be flipped for ICCS."""

    select: bool
    """Use seismogram to create stack."""

begin_time instance-attribute

begin_time: Timestamp

Seismogram begin time.

data instance-attribute

data: ndarray

Seismogram data.

delta instance-attribute

delta: Timedelta

The sampling interval.

Should be a positive Timedelta instance.

end_time property

end_time: Timestamp

Seismogram end time.

flip instance-attribute

flip: bool

Data in seismogram should be flipped for ICCS.

select instance-attribute

select: bool

Use seismogram to create stack.

t0 instance-attribute

Initial pick.

t1 instance-attribute

t1: Timestamp | None

Updated pick.

MiniICCSSeismogram

Bases: SeismogramEndtimeMixin, ICCSSeismogram

Minimal implementation of the ICCSSeismogram type.

The MiniICCSSeismogram class provides a minimal implementation of a class that is compatible with the ICCSSeismogram protocol.

Examples:

Because ICCSSeismogram inherits from Seismogram, we can easily create MiniICCSSeismogram instances from existing seismograms using the clone_to_mini() function, whereby the update parameter is used to provide the extra information needed:

>>> from pysmo.classes import SAC
>>> from pysmo.functions import clone_to_mini
>>> from pysmo.tools.iccs import MiniICCSSeismogram
>>> from pandas import Timedelta
>>> sac = SAC.from_file("example.sac")
>>> sac_seis = sac.seismogram
>>> # Use existing pick or set a new one 10 seconds after begin time
>>> update = {"t0": sac.timestamps.t0 or sac_seis.begin_time + Timedelta(seconds=10)}
>>> mini_iccs_seis = clone_to_mini(MiniICCSSeismogram, sac_seis, update=update)
>>>

Attributes:

Name Type Description
begin_time Timestamp

Seismogram begin time.

data ndarray

Seismogram data.

delta PositiveTimedelta

Seismogram sampling interval.

end_time Timestamp

Seismogram end time.

flip bool

Data in seismogram should be flipped for ICCS.

select bool

Use seismogram to create stack.

t0 Timestamp

Initial pick.

t1 Timestamp | None

Updated pick.

Source code in src/pysmo/tools/iccs/_types.py
@beartype
@define(kw_only=True, slots=True)
class MiniICCSSeismogram(SeismogramEndtimeMixin, ICCSSeismogram):
    """Minimal implementation of the [`ICCSSeismogram`][pysmo.tools.iccs.ICCSSeismogram] type.

    The [`MiniICCSSeismogram`][pysmo.tools.iccs.ICCSSeismogram] class provides
    a minimal implementation of a class that is compatible with the
    [`ICCSSeismogram`][pysmo.tools.iccs.ICCSSeismogram] protocol.

    Examples:
        Because [`ICCSSeismogram`][pysmo.tools.iccs.ICCSSeismogram] inherits
        from [`Seismogram`][pysmo.Seismogram], we can easily create
        [`MiniICCSSeismogram`][pysmo.tools.iccs.MiniICCSSeismogram] instances
        from existing seismograms using the
        [`clone_to_mini()`][pysmo.functions.clone_to_mini] function, whereby
        the `update` parameter is used to provide the extra information needed:

        ```python
        >>> from pysmo.classes import SAC
        >>> from pysmo.functions import clone_to_mini
        >>> from pysmo.tools.iccs import MiniICCSSeismogram
        >>> from pandas import Timedelta
        >>> sac = SAC.from_file("example.sac")
        >>> sac_seis = sac.seismogram
        >>> # Use existing pick or set a new one 10 seconds after begin time
        >>> update = {"t0": sac.timestamps.t0 or sac_seis.begin_time + Timedelta(seconds=10)}
        >>> mini_iccs_seis = clone_to_mini(MiniICCSSeismogram, sac_seis, update=update)
        >>>
        ```
    """

    begin_time: Timestamp = field(
        default=SEISMOGRAM_DEFAULTS.begin_time.value, validator=datetime_is_utc
    )
    """Seismogram begin time."""

    delta: PositiveTimedelta = SEISMOGRAM_DEFAULTS.delta.value
    """Seismogram sampling interval."""

    data: np.ndarray = field(factory=lambda: np.array([]))
    """Seismogram data."""

    t0: Timestamp = field(validator=datetime_is_utc)
    """Initial pick."""

    t1: Timestamp | None = field(
        default=None, validator=validators.optional(datetime_is_utc)
    )
    """Updated pick."""

    flip: bool = False
    """Data in seismogram should be flipped for ICCS."""

    select: bool = True
    """Use seismogram to create stack."""

begin_time class-attribute instance-attribute

begin_time: Timestamp = field(
    default=begin_time.value, validator=datetime_is_utc
)

Seismogram begin time.

data class-attribute instance-attribute

data: ndarray = field(factory=lambda: array([]))

Seismogram data.

delta class-attribute instance-attribute

delta: PositiveTimedelta = delta.value

Seismogram sampling interval.

end_time property

end_time: Timestamp

Seismogram end time.

flip class-attribute instance-attribute

flip: bool = False

Data in seismogram should be flipped for ICCS.

select class-attribute instance-attribute

select: bool = True

Use seismogram to create stack.

t0 class-attribute instance-attribute

t0: Timestamp = field(validator=datetime_is_utc)

Initial pick.

t1 class-attribute instance-attribute

t1: Timestamp | None = field(
    default=None, validator=optional(datetime_is_utc)
)

Updated pick.

plot_seismograms

plot_seismograms(
    iccs: ICCS,
    context: bool = True,
    show_all: bool = False,
    return_fig: bool = False,
) -> tuple[Figure, Axes] | None

Plot the selected ICCS seismograms as an image.

Parameters:

Name Type Description Default
iccs ICCS

Instance of the ICCS class.

required
context bool

Determines which seismograms are used: - True: context_seismograms are used. - False: cc_seismograms are used.

True
show_all bool

If True, all seismograms are shown in the plot instead of the selected ones only.

False
return_fig bool

If True, the Figure and Axes objects are returned instead of shown.

False

Returns:

Type Description
tuple[Figure, Axes] | None

Figure of the selected seismograms as an image if return_fig is True.

Examples:

The default plotting mode is to pad the seismograms beyond the time window used for the cross-correlations. This is particularly useful for narrow time windows.

>>> from pysmo.tools.iccs import ICCS, plot_seismograms
>>> iccs = ICCS(iccs_seismograms)
>>> _ = iccs(autoselect=True, autoflip=True)
>>>
>>> plot_seismograms(iccs)
>>>

View the seismograms with context View the seismograms with context

To view the seismograms exactly as they are used in the cross-correlations, set the context argument to False:

>>> plot_seismograms(iccs, context=False)
>>>

View the seismograms with context View the seismograms with context

Source code in src/pysmo/tools/iccs/_functions.py
def plot_seismograms(
    iccs: ICCS, context: bool = True, show_all: bool = False, return_fig: bool = False
) -> tuple[Figure, Axes] | None:
    """Plot the selected ICCS seismograms as an image.

    Args:
        iccs: Instance of the [`ICCS`][pysmo.tools.iccs.ICCS] class.
        context: Determines which seismograms are used:
            - `True`: [`context_seismograms`][pysmo.tools.iccs.ICCS.context_seismograms] are used.
            - `False`: [`cc_seismograms`][pysmo.tools.iccs.ICCS.cc_seismograms] are used.
        show_all: If `True`, all seismograms are shown in the plot instead of the
            selected ones only.
        return_fig: If `True`, the [`Figure`][matplotlib.figure.Figure] and
            [`Axes`][matplotlib.axes.Axes] objects are returned instead of
            shown.

    Returns:
        Figure of the selected seismograms as an image if `return_fig` is `True`.

    Examples:
        The default plotting mode is to pad the seismograms beyond the time
        window used for the cross-correlations. This is particularly useful
        for narrow time windows.

        ```python
        >>> from pysmo.tools.iccs import ICCS, plot_seismograms
        >>> iccs = ICCS(iccs_seismograms)
        >>> _ = iccs(autoselect=True, autoflip=True)
        >>>
        >>> plot_seismograms(iccs)
        >>>
        ```

        ![View the seismograms with context](../../../images/tools/iccs/iccs_plot_seismograms_context.png#only-light){ loading=lazy }
        ![View the seismograms with context](../../../images/tools/iccs/iccs_plot_seismograms_context-dark.png#only-dark){ loading=lazy }

        To view the seismograms exactly as they are used in the
        cross-correlations, set the `context` argument to `False`:

        ```python
        >>> plot_seismograms(iccs, context=False)
        >>>
        ```

        ![View the seismograms with context](../../../images/tools/iccs/iccs_plot_seismograms.png#only-light){ loading=lazy }
        ![View the seismograms with context](../../../images/tools/iccs/iccs_plot_seismograms-dark.png#only-dark){ loading=lazy }
    """
    fig, ax = plt.subplots(figsize=(10, 5), layout="constrained")
    _draw_common_image(ax, iccs, context, show_all)
    if return_fig:
        return fig, ax
    plt.show()
    return None

plot_stack

plot_stack(
    iccs: ICCS,
    context: bool = True,
    show_all: bool = False,
    return_fig: bool = False,
) -> tuple[Figure, Axes] | None

Plot the ICCS stack.

Parameters:

Name Type Description Default
iccs ICCS

Instance of the ICCS class.

required
context bool

Determines which seismograms are used: - True: context_seismograms are used. - False: cc_seismograms are used.

True
show_all bool

If True, all seismograms are shown in the plot instead of the selected ones only.

False
return_fig bool

If True, the Figure and Axes objects are returned instead of shown.

False

Returns:

Type Description
tuple[Figure, Axes] | None

Figure of the stack with the seismograms if return_fig is True.

Examples:

The default plotting mode is to pad the stack beyond the time window used for the cross-correlations (highlighted in light green). This is useful particularly useful for narrow time windows. Note that because of the padding, the displayed stack isn't exactly what is used for the cross-correlations.

>>> from pysmo.tools.iccs import ICCS, plot_stack
>>> iccs = ICCS(iccs_seismograms)
>>> _ = iccs(autoselect=True, autoflip=True)
>>>
>>> plot_stack(iccs)
>>>

View the stack with context View the stack with context

To view the stack exactly as it is used in the cross-correlations, set the context argument to False:

>>> plot_stack(iccs, context=False)
>>>

View the stack with taper View the stack with taper

Source code in src/pysmo/tools/iccs/_functions.py
def plot_stack(
    iccs: ICCS, context: bool = True, show_all: bool = False, return_fig: bool = False
) -> tuple[Figure, Axes] | None:
    """Plot the ICCS stack.

    Args:
        iccs: Instance of the [`ICCS`][pysmo.tools.iccs.ICCS] class.
        context: Determines which seismograms are used:
            - `True`: [`context_seismograms`][pysmo.tools.iccs.ICCS.context_seismograms] are used.
            - `False`: [`cc_seismograms`][pysmo.tools.iccs.ICCS.cc_seismograms] are used.
        show_all: If `True`, all seismograms are shown in the plot instead of the
            selected ones only.
        return_fig: If `True`, the [`Figure`][matplotlib.figure.Figure] and
            [`Axes`][matplotlib.axes.Axes] objects are returned instead of
            shown.

    Returns:
        Figure of the stack with the seismograms if `return_fig` is `True`.

    Examples:
        The default plotting mode is to pad the stack beyond the time window
        used for the cross-correlations (highlighted in light green). This is
        useful particularly useful for narrow time windows. Note that because
        of the padding, the displayed stack isn't exactly what is used for the
        cross-correlations.

        ```python
        >>> from pysmo.tools.iccs import ICCS, plot_stack
        >>> iccs = ICCS(iccs_seismograms)
        >>> _ = iccs(autoselect=True, autoflip=True)
        >>>
        >>> plot_stack(iccs)
        >>>
        ```

        ![View the stack with context](../../../images/tools/iccs/iccs_view_stack_context.png#only-light){ loading=lazy }
        ![View the stack with context](../../../images/tools/iccs/iccs_view_stack_context-dark.png#only-dark){ loading=lazy }

        To view the stack exactly as it is used in the cross-correlations, set
        the `context` argument to `False`:

        ```python
        >>> plot_stack(iccs, context=False)
        >>>
        ```

        ![View the stack with taper](../../../images/tools/iccs/iccs_view_stack.png#only-light){ loading=lazy }
        ![View the stack with taper](../../../images/tools/iccs/iccs_view_stack-dark.png#only-dark){ loading=lazy }
    """
    fig, ax = plt.subplots(figsize=(10, 5), layout="constrained")
    _draw_common_stack(ax, iccs, context, show_all)
    if return_fig:
        return fig, ax
    plt.show()
    return None

update_all_picks

update_all_picks(iccs: ICCS, pickdelta: Timedelta) -> None

Update t1 in all seismograms by the same amount.

Parameters:

Name Type Description Default
iccs ICCS

Instance of the ICCS class.

required
pickdelta Timedelta

delta applied to all picks.

required

Raises:

Type Description
ValueError

If the new t1 is outside the valid range.

Source code in src/pysmo/tools/iccs/_functions.py
def update_all_picks(iccs: ICCS, pickdelta: Timedelta) -> None:
    """Update [`t1`][pysmo.tools.iccs.ICCSSeismogram.t1] in all seismograms by the same amount.

    Args:
        iccs: Instance of the [`ICCS`][pysmo.tools.iccs.ICCS] class.
        pickdelta: delta applied to all picks.

    Raises:
        ValueError: If the new t1 is outside the valid range.
    """

    if not iccs.validate_pick(pickdelta):
        raise ValueError(
            "New t1 is outside the valid range. Consider reducing the time window."
        )

    for seismogram in iccs.seismograms:
        current_pick = seismogram.t1 or seismogram.t0
        seismogram.t1 = current_pick + pickdelta
    iccs._clear_caches()  # seismograms and stack need to be refreshed

update_min_ccnorm

update_min_ccnorm(
    iccs: ICCS,
    context: bool = True,
    show_all: bool = False,
    return_fig: bool = False,
) -> (
    tuple[
        Figure,
        Axes,
        tuple[
            Cursor,
            Line2D,
            Button,
            Button,
            _ScrollIndexTracker,
        ],
    ]
    | None
)

Interactively pick a new min_ccnorm.

This function launches an interactive figure to manually pick a new min_ccnorm, which is used when running the ICCS algorithm with autoselect set to True.

Parameters:

Name Type Description Default
iccs ICCS

Instance of the ICCS class.

required
context bool

Determines which seismograms are used: - True: context_seismograms are used. - False: cc_seismograms are used.

True
show_all bool

If True, all seismograms are shown in the plot instead of the selected ones only.

False
return_fig bool

If True, the Figure and Axes objects are returned instead of shown.

False

Returns:

Type Description
tuple[Figure, Axes, tuple[Cursor, Line2D, Button, Button, _ScrollIndexTracker]] | None

Figure with the selector widgets if return_fig is True.

Examples:

>>> from pysmo.tools.iccs import ICCS, update_min_ccnorm
>>> iccs = ICCS(iccs_seismograms)
>>> _ = iccs()
>>> update_min_ccnorm(iccs)
>>>

Picking a new time window in stack Picking a new time window in stack

Source code in src/pysmo/tools/iccs/_functions.py
def update_min_ccnorm(
    iccs: ICCS, context: bool = True, show_all: bool = False, return_fig: bool = False
) -> (
    tuple[Figure, Axes, tuple[Cursor, Line2D, Button, Button, _ScrollIndexTracker]]
    | None
):
    """Interactively pick a new [`min_ccnorm`][pysmo.tools.iccs.ICCS.min_ccnorm].

    This function launches an interactive figure to manually pick a new
    [`min_ccnorm`][pysmo.tools.iccs.ICCS.min_ccnorm], which is used when
    [running][pysmo.tools.iccs.ICCS.__call__] the ICCS algorithm with
    `autoselect` set to `True`.

    Args:
        iccs: Instance of the [`ICCS`][pysmo.tools.iccs.ICCS] class.
        context: Determines which seismograms are used:
            - `True`: [`context_seismograms`][pysmo.tools.iccs.ICCS.context_seismograms] are used.
            - `False`: [`cc_seismograms`][pysmo.tools.iccs.ICCS.cc_seismograms] are used.
        show_all: If `True`, all seismograms are shown in the plot instead of the
            selected ones only.
        return_fig: If `True`, the [`Figure`][matplotlib.figure.Figure] and
            [`Axes`][matplotlib.axes.Axes] objects are returned instead of
            shown.

    Returns:
        Figure with the selector widgets if `return_fig` is `True`.

    Examples:
        ```python
        >>> from pysmo.tools.iccs import ICCS, update_min_ccnorm
        >>> iccs = ICCS(iccs_seismograms)
        >>> _ = iccs()
        >>> update_min_ccnorm(iccs)
        >>>
        ```

        ![Picking a new time window in stack](../../../images/tools/iccs/iccs_update_min_ccnorm.png#only-light){ loading=lazy }
        ![Picking a new time window in stack](../../../images/tools/iccs/iccs_update_min_ccnorm-dark.png#only-dark){ loading=lazy }
    """
    fig, ax = plt.subplots(figsize=(10, 6))
    matrix = _draw_common_image(ax, iccs, context, show_all)
    fig.subplots_adjust(bottom=0.2, left=0.05, right=0.95, top=0.93)

    ax.set_title("Pick a new minimal cross-correlation coefficient.")
    pending_val = [iccs.min_ccnorm]

    def handle_valid_pick(new_val: float) -> None:
        pending_val[0] = new_val
        ax.set_title(f"Click save to set min_ccnorm to {new_val:.4f}")

    cursor, pick_line, tracker = _setup_ccnorm_picker(
        ax, iccs, show_all, len(matrix) - 1, handle_valid_pick
    )

    def on_save(_: Event) -> None:
        iccs.min_ccnorm = pending_val[0]
        iccs._clear_caches()
        plt.close()

    b_save, b_cancel = _add_save_cancel_buttons(fig, on_save)

    if return_fig:
        return fig, ax, (cursor, pick_line, b_save, b_cancel, tracker)
    plt.show()
    return None

update_pick

update_pick(
    iccs: ICCS,
    context: bool = True,
    show_all: bool = False,
    use_seismogram_image: bool = False,
    return_fig: bool = False,
) -> (
    tuple[
        Figure, Axes, tuple[Cursor, Line2D, Button, Button]
    ]
    | None
)

Manually pick t1 and apply it to all seismograms.

This function launches an interactive figure to manually pick a new phase arrival, and then apply it to all seismograms.

Parameters:

Name Type Description Default
iccs ICCS

Instance of the ICCS class.

required
context bool

Determines which seismograms are used: - True: context_seismograms are used. - False: cc_seismograms are used.

True
show_all bool

If True, all seismograms are shown in the plot instead of the selected ones only.

False
use_seismogram_image bool

Use the seismogram image instead of the stack).

False
return_fig bool

If True, the Figure and Axes objects are returned instead of shown.

False

Returns:

Type Description
tuple[Figure, Axes, tuple[Cursor, Line2D, Button, Button]] | None

Figure of the stack with the picker if return_fig is True.

Examples:

>>> from pysmo.tools.iccs import ICCS, update_pick
>>> iccs = ICCS(iccs_seismograms)
>>> _ = iccs(autoselect=True, autoflip=True)
>>>
>>> update_pick(iccs)
>>>

Picking a new T1 Picking a new T1

Source code in src/pysmo/tools/iccs/_functions.py
def update_pick(
    iccs: ICCS,
    context: bool = True,
    show_all: bool = False,
    use_seismogram_image: bool = False,
    return_fig: bool = False,
) -> tuple[Figure, Axes, tuple[Cursor, Line2D, Button, Button]] | None:
    """Manually pick [`t1`][pysmo.tools.iccs.ICCSSeismogram.t1] and apply it to all seismograms.

    This function launches an interactive figure to manually pick a new phase
    arrival, and then apply it to all seismograms.

    Args:
        iccs: Instance of the [`ICCS`][pysmo.tools.iccs.ICCS] class.
        context: Determines which seismograms are used:
            - `True`: [`context_seismograms`][pysmo.tools.iccs.ICCS.context_seismograms] are used.
            - `False`: [`cc_seismograms`][pysmo.tools.iccs.ICCS.cc_seismograms] are used.
        show_all: If `True`, all seismograms are shown in the plot instead of the
            selected ones only.
        use_seismogram_image: Use the
            [seismogram image][pysmo.tools.iccs.plot_seismograms]
            instead of the [stack][pysmo.tools.iccs.plot_stack]).
        return_fig: If `True`, the [`Figure`][matplotlib.figure.Figure] and
            [`Axes`][matplotlib.axes.Axes] objects are returned instead of
            shown.

    Returns:
        Figure of the stack with the picker if `return_fig` is `True`.

    Examples:
        ```python
        >>> from pysmo.tools.iccs import ICCS, update_pick
        >>> iccs = ICCS(iccs_seismograms)
        >>> _ = iccs(autoselect=True, autoflip=True)
        >>>
        >>> update_pick(iccs)
        >>>
        ```

        ![Picking a new T1](../../../images/tools/iccs/iccs_update_pick.png#only-light){ loading=lazy }
        ![Picking a new T1](../../../images/tools/iccs/iccs_update_pick-dark.png#only-dark){ loading=lazy }
    """
    fig, ax = plt.subplots(figsize=(10, 6))
    if use_seismogram_image:
        _draw_common_image(ax, iccs, context, show_all)
        fig.subplots_adjust(bottom=0.2, left=0.05, right=0.95, top=0.93)
    else:
        _draw_common_stack(ax, iccs, context, show_all)
        fig.subplots_adjust(bottom=0.2, left=0.09, right=1.05, top=0.93)

    ax.set_title("Update t1 for all seismograms.")
    pending_pick = [0.0]

    def handle_valid_pick(xdata: float) -> None:
        pending_pick[0] = xdata
        ax.set_title(f"Click save to adjust t1 by {xdata:.3f} seconds.")

    cursor, pick_line = _setup_phase_picker(ax, iccs, handle_valid_pick)

    def on_save(_: Event) -> None:
        plt.close()
        update_all_picks(iccs, Timedelta(seconds=pending_pick[0]))

    b_save, b_cancel = _add_save_cancel_buttons(fig, on_save)

    if return_fig:
        return fig, ax, (cursor, pick_line, b_save, b_cancel)
    plt.show()
    return None

update_timewindow

update_timewindow(
    iccs: ICCS,
    context: bool = True,
    show_all: bool = False,
    use_seismogram_image: bool = False,
    return_fig: bool = False,
) -> (
    tuple[Figure, Axes, tuple[SpanSelector, Button, Button]]
    | None
)

Pick new time window limits.

This function launches an interactive figure to pick new values for window_pre and window_post.

Parameters:

Name Type Description Default
iccs ICCS

Instance of the ICCS class.

required
context bool

Determines which seismograms are used: - True: context_seismograms are used. - False: cc_seismograms are used.

True
show_all bool

If True, all seismograms are shown in the plot instead of the selected ones only.

False
use_seismogram_image bool

Use the seismogram image instead of the stack).

False
return_fig bool

If True, the Figure and Axes objects are returned instead of shown.

False

Returns:

Type Description
tuple[Figure, Axes, tuple[SpanSelector, Button, Button]] | None

Figure of the stack with the picker if return_fig is True.

Info

The new time window may not be chosen such that the pick lies outside the the window. The picker will therefore automatically correct itself for invalid window choices.

Examples:

>>> from pysmo.tools.iccs import ICCS, update_timewindow
>>> iccs = ICCS(iccs_seismograms)
>>> _ = iccs(autoselect=True, autoflip=True)
>>>
>>> update_timewindow(iccs)
>>>

Picking a new time window Picking a new time window

Source code in src/pysmo/tools/iccs/_functions.py
def update_timewindow(
    iccs: ICCS,
    context: bool = True,
    show_all: bool = False,
    use_seismogram_image: bool = False,
    return_fig: bool = False,
) -> tuple[Figure, Axes, tuple[SpanSelector, Button, Button]] | None:
    """Pick new time window limits.

    This function launches an interactive figure to pick new values for
    [`window_pre`][pysmo.tools.iccs.ICCS.window_pre] and
    [`window_post`][pysmo.tools.iccs.ICCS.window_post].

    Args:
        iccs: Instance of the [`ICCS`][pysmo.tools.iccs.ICCS] class.
        context: Determines which seismograms are used:
            - `True`: [`context_seismograms`][pysmo.tools.iccs.ICCS.context_seismograms] are used.
            - `False`: [`cc_seismograms`][pysmo.tools.iccs.ICCS.cc_seismograms] are used.
        show_all: If `True`, all seismograms are shown in the plot instead of the
            selected ones only.
        use_seismogram_image: Use the
            [seismogram image][pysmo.tools.iccs.plot_seismograms]
            instead of the [stack][pysmo.tools.iccs.plot_stack]).
        return_fig: If `True`, the [`Figure`][matplotlib.figure.Figure] and
            [`Axes`][matplotlib.axes.Axes] objects are returned instead of
            shown.

    Returns:
        Figure of the stack with the picker if `return_fig` is `True`.

    Info:
        The new time window may not be chosen such that the pick lies
        outside the the window. The picker will therefore automatically correct
        itself for invalid window choices.

    Examples:
        ```python
        >>> from pysmo.tools.iccs import ICCS, update_timewindow
        >>> iccs = ICCS(iccs_seismograms)
        >>> _ = iccs(autoselect=True, autoflip=True)
        >>>
        >>> update_timewindow(iccs)
        >>>
        ```

        ![Picking a new time window](../../../images/tools/iccs/iccs_update_timewindow.png#only-light){ loading=lazy }
        ![Picking a new time window](../../../images/tools/iccs/iccs_update_timewindow-dark.png#only-dark){ loading=lazy }
    """
    fig, ax = plt.subplots(figsize=(10, 6))
    if use_seismogram_image:
        _draw_common_image(ax, iccs, context, show_all)
        fig.subplots_adjust(bottom=0.2, left=0.05, right=0.95, top=0.93)
    else:
        _draw_common_stack(ax, iccs, context, show_all)
        fig.subplots_adjust(bottom=0.2, left=0.09, right=1.05, top=0.93)

    ax.set_title("Pick a new time window.")
    pending_window = [iccs.window_pre.total_seconds(), iccs.window_post.total_seconds()]

    def handle_valid_selection(xmin: float, xmax: float) -> None:
        pending_window[0], pending_window[1] = xmin, xmax
        ax.set_title(f"Click save to set window at {xmin:.3f} to {xmax:.3f} seconds.")

    span = _setup_timewindow_picker(ax, iccs, handle_valid_selection)

    def on_save(_: Event) -> None:
        iccs.window_pre = Timedelta(seconds=pending_window[0])
        iccs.window_post = Timedelta(seconds=pending_window[1])
        plt.close()

    b_save, b_cancel = _add_save_cancel_buttons(fig, on_save)

    if return_fig:
        return fig, ax, (span, b_save, b_cancel)
    plt.show()
    return None