-
Notifications
You must be signed in to change notification settings - Fork 4
/
globalETAS.py
2878 lines (2797 loc) · 142 KB
/
globalETAS.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'''
#globalETAS.py:
#author: mark yoder
#email: [email protected], [email protected]
#institution: dept. of physics, UC Davis
#
# summary:
# these scripts and modules constitute a second generation implementation of the 'mean-field' or
# 'rupture-length' variant of ETAS published by Yoder et al. (2014) in which initial ETAS (aftershock)
# productivity from known earthquake scaling relations, earthquake finite extents, and independent
# observational constraints.
#
# in this version, we adapt the model for large scale, global deployment. we:
# 1) start with the basic ETAS model from Yoder et al. (2014)
# 2) we, again, divide the earth into a large set of discrete locations (basically, a lattice), and calculate
# the ETAS contribution (aftershock rate) of each earthquake to each lattice site.
# 3) we mitigate the computational requirements of the model by indexing the map production from the earthquake catalog
# and limiting the spatil range of each earthquake according to its magnitude. for example, we calculate the
# ETAS contribution of each earthquake to sites up to 5*L_r from the epicenter (and eventually, we'd like to be
# smarter for large earthquakes with long ruptures, etc.).
# -- the ETAS contribution drops off pretty quickly with distance, so this should do a pretty good job of reducing
# computational requirements by skipping the calculation of very small contributions of very small earthquakes
# a great distance from a given lattice site.
# 4) eventually add temporal indexing as well?
# 5) we're also interested in convolving ETAS with finite source models (aka, map ETAS to faults or some other
# irregular geometry). we may do something with that here.
#
# TODO notes:
# 1: revisit Bindex model; i think it will be faster, simpler, and lower memory footprint than rtree, and i think the main obstacles to making it work properly
# have actually been resolved.
# 2: revise the boundary selection framework for rtree. basically, there is a problem constraining the boundaries when lat/lon parameters are provided to ETAS.
# 3: implement (or confirm implementation of) the brute-force version of ETAS, where we loop-loop over all events,locations. for small ETAS, we probably
# want to do this anyway and it will be faster because we forego calculating indices.
# 4: revise the spatial limits: instead of just using n*L_r for spatial extetns, actually calculate the distance for z -> x*z0, aka the distance for 90% reduction.
# this will be similar to the L_r approach, bit will account for proper scaling and should improve the output appearance.
# 5: modify the MPP handler(s) to inherit from Pool() and actually work like a Pool(), rather than explicitly handling Process() objects. the problem is that when
# we divide up the job, we inevitably give one processor a light job and one processor a heavy job, so we spend a lot of time waiting for one process to finish.
# yes, there is a bit of overhead in running with n_cpu > n_processors, but it is minor in comparison. also, by dividing the job into lots of small little
# jobs, we can probably also reduce the real-time memory footprint, so we can run on smaller systems.
'''
#
import datetime as dtm
import matplotlib.dates as mpd
import pytz
tzutc = pytz.timezone('UTC')
import operator
import math
import random
import numpy
import scipy
import scipy.optimize as spo
import itertools
import sys
#import scipy.optimize as spo
import sys
import os
#from PIL import Image as ipp
import multiprocessing as mpp
#
import matplotlib as mpl
import matplotlib.pyplot as plt
import functools
#
# let's see if we can compile some of thes functions.
import numba
#
#import shapely.geometry as sgp
#
from mpl_toolkits.basemap import Basemap as Basemap
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from geographiclib.geodesic import Geodesic as ggp
#
#
#import ANSStools as atp
from yodiipy import ANSStools as atp
import bindex
#import contours2kml.contours2kml as contours2kml
from yodiipy import contours2kml as c2kml
#
import rtree
from rtree import index
import geopy
from geopy.distance import great_circle
#
# python3 vs python2 issues:
# a bit of version 3-2 compatibility:
if sys.version_info.major>=3:
xrange=range
from eq_params import *
#
# TODO: add a p_map (or p, p0 distinction) variable to distinguish the p value for calculating ETAS parameters (in catalog) and p to calculate
# ETAS itself. this will facilitate time independent ETAS, etc., which we need to proprly evaluate geo-spatial ROC.
# (see p_etas...)
#
# TODO: some of these constants can be found in scipy.constants (and other places). they should be drawn from those
# libraries when possible.
days2secs = 60.*60.*24.
year2secs = 60.*60.*24.*365.
deg2km=111.31
deg2rad = 2.0*math.pi/360.
alpha_0 = 2.28
#
calipoly = [[-129.0, 42.75], [-121.5, 29.5], [-113.5, 29.5], [-114.0, 35.6], [-119.3, 39.4], [-119.5, 42.75], [-129.0, 42.75]]
tz_utc = pytz.timezone('UTC')
# a lookup dictionary for ditance types:
dist_calc_types_dict={}
dist_calc_types_dict.update({key:'spherical' for key in ['sph', 'spherical', 'sphere']})
dist_calc_types_dict.update({key:'cartesian' for key in ['cartesian', 'cart', 'xy', 'rect']})
dist_calc_types_dict.update({key:'euclidian' for key in ['euc', 'euclidian', 'euclid']})
#dist_calc_types_dict.update({key:'cartesian2' for key in ['cartesian2']})
dist_calc_types_dict.update({key:'geo' for key in ['geo', 'geodesic', 'geodesiclib', 'glib', 'g_lib']})
# and this one to return the x,y components for cartesian measurements:
dist_calc_types_dict.update({key:'dx_dy' for key in ['dxdy', 'dx_dy']})
#
# Developer notes and status:
# as of 9 oct 2015: Global_ETAS_model and the various subclasses (which use different indexing) run (apprently) successfully, but the figures they turn out look a bit
# odd, so i'm working on diagnostic scripts (maybe i just need to run a bigger model... or maybe just one more directly comparable to some standard runs like nor/so CA.
# see also etas_testing.py. it looks like the Earthquake() object produces the correct local_intensity(), as compared to the former BASScast/ETASmf model.
# so maybe the rates are not aggregating correctly, or maybe the indexing is screwing up something.
# the Bindex() indexing approach appears to be having some float-integer indexing problems (basically, error in floating point accuracy causing some bins to go the wrong
# way. probably, we need a more explicit approach that maps out the bins by counting (aka, instead of 35.5 --> 35.0/.1 --> j=350, we just need to line up the values we've got
# and count off the bins. note, skipping Bindex and just using the rtree approach... or use Bindex, but pre-calc the domain. maybe also look into using
# numpy.digitize(x,bins) or there are some intesting examples of using hist() to make bins.
#
# Classes:
class Global_ETAS_model(object):
# guts of ETAS. this will contain a catalog, lattice sites, etc. graphical tools, plotting, etc. will likely
# be kept elsewhere.
# questions: use equal distance or equal angel bin spacing? we can nominally do a spherical->cartesian transformation like: x = lon*cos(lat)*deg2km
# but it behaves sort of funny when we try to calculate disanctes between points... which we don't necessaryily do; we map (x,y) -> (lon, lat) and then
# do proper geodetic transformations, but 1) it becomes easy to make mistakes, and 2) the only gain is that we have fewer points in the higher latitudes.
# as long as 1) we have sufficient spatial resolution in the lower latitudes and 2) the problem remains computational tractable (equal spacing can reduce
# the grid size by maybe halfish??), there's no real harm in just using (a much simpler) lon,lat lattice with equal angular spacing.
#
def __init__(self, catalog=None, lats=None, lons=None, mc=2.5, mc_etas=None, d_lon=.1, d_lat=.1, bin_lon0=0., bin_lat0=0., etas_range_factor=25.0, etas_range_padding=1.5, etas_fit_factor=1.0, t_0=dtm.datetime(1990,1,1, tzinfo=tz_utc), t_now=dtm.datetime.now(tzutc), t_future=None, transform_type='equal_area', transform_ratio_max=2.5, cat_len=10.*365., calc_etas=True, n_contours=15, cmap_contours='jet', etas_cat_range=None, etas_xyz_range=None, p_cat=1.1, q_cat=1.5, ab_ratio_expon=.25, p_etas=None, D_fract=1.5, dmstar=1.0, n_cpu_cat=None, **kwargs):
'''
#
# 21 Nov 2018, yoder: add t_future parameter. if not None, the Earthquake.local_intensity() will compute the expected total number of earthqukes, between t_now (=t_forecast)
# and t_future, as opposed to the instantaneous rate at t_now.
# 22 July 2019, yoder: TODO: confirm or modify this so that we can separately define the catalog
# temporal extent and the cumulative t_from and t_to. The main idea is that we want to be able
# to compare the expected number of events (or cumulative ETAS intensity) to the observed number of
# events (so <N> with and without events in the forecast interval).
#
# basically: if we are given a catalog, use it. try to extract mc, etc. data from catalog if it's not
# externally provided. otherwise, get a catalog from ANSS or OH based oon the parameters provided.
# note: we want to do this properly in an x-y projection, not lat-lon. our earthquakes will come back to us with lat/lon coordinates
# so we'll need to translate. we'll also want lat/lon as well as x/y positions for the lattice sites, so we can get even spacing and accurate distances
#
# etas_cat_range: range of catalog to do ETAS. mainly, this is to facilitate MPP processing; each process will have a full catalog of earthquakes and
# (nominally) a full array of etas lattice sites (though we might be able to improve upon this requirement later -- probably the best appraoch is to
# yoder 23 July 2019: Now I need to double check this etas_cat_range stuff. as i recall, we provide
# a full catalog and split up the map for MPP processing. We tested the mpp.Array() shared memory
# approach, but it was just not helpful; this is not a fast way to parallelize.
# accept this limitation and write a make_etas() that uses an mpp.Array() object (simplified script).
#
# p_,q_cat: p,q values passed to the catalog getter. these will affect not only the p,q values in the catalog, but the
# other calculated ETAS parameters.
#
# p_etas: use this to control p for etas calculations, separate for p calculated in catalog (aka, p to solve initial rates). use p_etas to make time-independent maps.
'''
print("begin globalETAS.__init()__")
# dx=1., dy=1., x0=0., y0=0., leaf_type=float)
# load all the input parameters into class variables (note i believe locals() is all currently defined variables in the function scope,
# so this behaves differently if we execute now or down-stream. if we really only want to record the input values, then
# execute now. if we want to record corrected inputs, then execute after corrections; if we execute at the end, every declared
# variable becomes a class member.
#self.__dict__.update(locals())
#
# we might just want the last N days, as a consistent standard. note we might, later on, make this a bit more sophisticated
# by processing the full t_0 -> t_now catalog, but only doing ETAS for the most recent cat_len days. BUT, to do this, we have
# to enforce in all the do_ETAS() functions
t_now = (t_now or dtm.datetime.now(pytz.timezone('UTC')))
if cat_len is not None:
t_0=t_now - dtm.timedelta(days=cat_len)
print("Overriding t_0 (etas catalog start date/time) for ETAS calculations. using catalog start, t_0 = t_now - catlen (%f) = %s" % (cat_len, t_0))
#
if lats is None and catalog is None: lats = [-89.9, 89.9]
if lons is None and catalog is None: lons = [-180., 180.]
#
# for now, assume the catalog is string-indexed -- aka, recarray, PANDAS,etc.
if lats is None and not (catalog is None or len(catalog) == 0): lats = [min(catalog['lat']), numpy.max(catalog['lat'])]
if lons is None and not (catalog is None or len(catalog) == 0): lons = [min(catalog['lon']), numpy.max(catalog['lon'])]
#
if mc is None and not (catalog is None or len(catalog) == 0): mc = min(catalog['mag'])
#
# and handle some specific cases...
if isinstance(t_now, float):
self.t_forecast = t_now
elif isinstance(t_now, numpy.datetime64):
# TODO: .tolist() may no longer accomplish the desired conversion...
self.t_forecast = mpd.date2num(t_now.astype(dtm.datetime))
#self.t_forecast = mpd.date2num(t_now.tolist())
else:
self.t_forecast = mpd.date2num(t_now)
#
if not t_future is None:
if isinstance(t_future, float):
t_future = t_future
elif isinstance(t_future, numpy.datetime64):
# TODO: .tolist() may no longer accomplish the desired conversion...
t_future = mpd.date2num(t_future.tolist())
else:
t_future = mpd.date2num(t_future)
self.t_future = t_future
#
#mc_etas = (mc_etas or mc) # mc_eats: minimum mag. for etas calculations -- aka, mc for the catalog, but only do etas for m>mc_eatas.
if mc_etas is None:
mc_etas = mc
#
# inputs massaged; now update class dictionary.
self.__dict__.update(locals())
#
self.latses = numpy.arange(lats[0], lats[1], d_lat) # note: if we want lats[1], lons[1] inclusive, we need to add +d_lat, +d_lon
self.lonses = numpy.arange(lons[0], lons[1], d_lon) # to the range().
self.n_lat = len(self.latses)
self.n_lon = len(self.lonses)
#
# calculate xyz_range (for mpp applications where we split up the geo-spatial array to processes):
if etas_xyz_range is None: etas_xyz_range = [0,self.n_lat*self.n_lon]
if etas_xyz_range[0] is None: etas_xyz_range[0] = 0
if etas_xyz_range[1] is None: etas_xyz_range[1] = self.n_lat*self.n_lon
#etas_xyz_range[0] = (etas_xyz_range[0] or 0)
#etas_xyz_range[1] = (etas_xyz_range[1] or self.n_lat*self.n_lon)
#
self.ETAS_array = numpy.array([])
# [etas_xyz_range[0]:etas_xyz_range[1]]
#
# TODO: consider wrapping this two-step into one-step. can we save memory (even if intermittently) by doing so? this might be one of those cases where we can't nest these steps; we need to instantiate
# the interim output object before we input it into the next function (aka, make the ND-array, then wrap as a recarray).
self.ETAS_array = numpy.array([[lon, lat, 0.] for j, (lat,lon) in enumerate(itertools.product(self.latses, self.lonses)) if (j>= etas_xyz_range[0] and j<etas_xyz_range[1])])
self.ETAS_array = numpy.core.records.fromarrays(zip(*self.ETAS_array), dtype = [('x', '>f8'), ('y', '>f8'), ('z', '>f8')])
#
# self.lats=lats
# self.lons=lons
# self.mc=mc
# self.d_x=d_x
# self.d_y=d_y
# self.bin_x0=bin_x0
# self.bin_y0=bin_y0
# self.etas_range_factor = etas_range_factor
# self.t_0=t_0
# self.t_now=t_now
# self.transform_type=transform_type
# self.transform_ratio_max=transform_ratio_max
#
if catalog is None:
print("fetch and process catalog for dates: {}-{}, mc={}, lats={}, lons={}".format(t_0, t_now, mc, lats, lons))
#catalog = make_ETAS_catalog(incat=None, lats=lats, lons=lons, mc=mc, date_range=[t_0, t_now], fit_factor=etas_fit_factor) # and note there are other variables to consider...
catalog = make_ETAS_catalog_mpp(incat=None, lats=lats, lons=lons, mc=mc, date_range=[t_0, t_now], fit_factor=etas_fit_factor, p=p_cat, q=q_cat, dmstar=dmstar, D_fract=D_fract, n_cpu=n_cpu_cat) # and note there are other variables to consider...
print("catalog fetched and processed.")
self.catalog = catalog
#
# set etas_cat_range as necessary:
if etas_cat_range is None: etas_cat_range = [0,len(catalog)]
# yoder: note that this "or" syntax is a bit twitchy when things can be zero,
# so it's progbably better to just "if" it:
if etas_cat_range[0] is None:
etas_cat_range[0] = 0
#etas_cat_range[0] = (etas_cat_range[0] or 0)
if etas_cat_range[1] is None:
etas_cat_range[1] = len(catalog)
#etas_cat_range[1] = (etas_cat_range[1] or len(catalog))
self.etas_cat_range = etas_cat_range
print("ETAS over etas_cat_range/xyz_range: ", (self.etas_cat_range, self.etas_xyz_range))
#
# ... and here is really where we do derived classes for different ETAS forcast objects. so for ETAS_bindex(ETAS), we do:
if not hasattr(self, 'make_etas'):
# instantiating directly from this parent class script, or for some other reason we don't get a make_etas() method defined.
self.make_etas = self.make_etas_bindex
# ... and for ETAS_brute(ETAS), we'd do:
# self.make_etas = self.make_etas_all
#
if calc_etas:
print("make_etas():")
self.make_etas()
print("ETAS complete.")
#
@property
def X(self):
return self.ETAS_array['x']
@property
def Y(self):
return self.ETAS_array['y']
@property
def Z(self):
return self.ETAS_array['z']
#
@property
def lattice_sites(self):
X = self.ETAS_array['z']
X.shape=(len(self.latses), len(self.lonses))
return X
#
def draw_map(self, fignum=0, fig_size=(6.,6.), map_resolution='i', map_projection='cyl', d_lon_range=None, d_lat_range=None, lats_map=None, lons_map=None, ax=None, do_states=True, do_rivers=True, lake_color='blue', lat_label_indices=[1,1,0,0], lon_label_indices=[0,0,1,1]):
'''
# TODO: we end up matching up a bunch of procedural calls, which is a big pain. we should write an ETAS_Map() class
# which includes the contour,etc. figures... but we can keep the variables, like lon_label_indices, etc.
# in one place...
#
# plot contours over a map.
'''
#lons_map = (lons_map or self.lons)
#lats_map = (lats_map or self.lats)
if lons_map is None: lons_map = self.lons
if lats_map is None: lats_map = self.lats
#
# first, get contours:
#etas_contours = self.calc_etas_contours(n_contours=n_contours, fignum=fignum, contour_fig_file=contour_fig_file, contour_kml_file=contour_kml_file, kml_contours_bottom=kml_contours_bottom, kml_contours_top=kml_contours_top, alpha_kml=alpha_kml, refresh_etas=refresh_etas)
#
# now, clear away the figure and set up the basemap...
#
d_lon_range = (d_lon_range or 1.)
d_lat_range = (d_lat_range or 1.)
#
if ax==None:
plt.figure(fignum, fig_size)
plt.clf()
ax=plt.gca()
#
#lons, lats = self.lons, self.lats
#cntr = [numpy.mean(lons), numpy.mean(lats)]
cntr = [numpy.mean(lons_map), numpy.mean(lats_map)]
#cm = Basemap(llcrnrlon=self.lons[0], llcrnrlat=self.lats[0], urcrnrlon=self.lons[1], urcrnrlat=self.lats[1], resolution=map_resolution, projection=map_projection, lon_0=cntr[0], lat_0=cntr[1])
cm = Basemap(llcrnrlon=lons_map[0], llcrnrlat=lats_map[0], urcrnrlon=lons_map[1], urcrnrlat=lats_map[1], resolution=map_resolution, projection=map_projection, lon_0=cntr[0], lat_0=cntr[1], ax=ax)
#
#cm.drawlsmask(land_color='0.8', ocean_color='b', resolution=map_resolution)
cm.drawcoastlines(color='gray', zorder=1)
cm.drawcountries(color='black', zorder=1)
if do_states: cm.drawstates(color='black', zorder=1)
if do_rivers: cm.drawrivers(color='blue', zorder=1)
cm.fillcontinents(color='beige', lake_color=lake_color, zorder=0)
# drawlsmask(land_color='0.8', ocean_color='w', lsmask=None, lsmask_lons=None, lsmask_lats=None, lakes=True, resolution='l', grid=5, **kwargs)
#cm.drawlsmask(land_color='0.8', ocean_color='c', lsmask=None, lsmask_lons=None, lsmask_lats=None, lakes=True, resolution=self.mapres, grid=5)
# lat_label_indices
#cm.drawmeridians(numpy.arange(int(lons_map[0]/d_lon_range)*d_lon_range, lons_map[1], d_lon_range), color='k', labels=[0,0,1,1])
#cm.drawparallels(numpy.arange(int(lats_map[0]/d_lat_range)*d_lat_range, lats_map[1], d_lat_range), color='k', labels=[1, 1, 0, 0])
cm.drawmeridians(numpy.arange(int(lons_map[0]/d_lon_range)*d_lon_range, lons_map[1], d_lon_range), color='k', labels=lon_label_indices)
cm.drawparallels(numpy.arange(int(lats_map[0]/d_lat_range)*d_lat_range, lats_map[1], d_lat_range), color='k', labels=lat_label_indices)
#
return cm
def make_etas_contour_map(self, n_contours=None, fignum=0, fig_size=(6.,6.), contour_fig_file=None, contour_kml_file=None, kml_contours_bottom=0., kml_contours_top=1.0, alpha=.5, alpha_kml=.5, refresh_etas=False, map_resolution='i', map_projection='cyl', map_cmap=None, lat_interval=None, lon_interval=None, lats_map=None, lons_map=None, ax=None, do_colorbar=True, do_states=True, do_rivers=True, lake_color='blue', Z=None ):
#
#map_cmap = map_cmap or self.map_cmap
if map_cmap is None: map_cmap = self.cmap_contours
#
n_contours = (n_contours or self.n_contours)
if ax is None:
fg=plt.figure(fignum)
ax=fg.gca()
#
# mm.draw_map(d_lat_range=10., d_lon_range=20., fignum=0)
#cm = self.draw_map(fignum=fignum, fig_size=fig_size, map_resolution=map_resolution, map_projection=map_projection)
cm = self.draw_map(fignum=fignum, fig_size=fig_size, map_resolution=map_resolution, map_projection=map_projection, d_lon_range=lon_interval, d_lat_range=lat_interval, lons_map=lons_map, lats_map=lats_map, ax=ax, do_states=do_states, do_rivers=do_rivers, lake_color=lake_color)
#
fg=plt.gcf()
#
X,Y = cm(numpy.array(self.lonses), numpy.array(self.latses))
#print("xylen: ", len(X), len(Y))
#
# yoder 2017-06-10: allow Z values to be passed in, so we can plot derived values. now, is it bettter to pass Z directly, or lattice sites? probably Z, so we can use log/not-log values.
if Z is None: Z = numpy.log10(self.lattice_sites)
#etas_contours = ax.contourf(X,Y, numpy.log10(self.lattice_sites), n_contours, zorder=8, alpha=alpha, cmap=map_cmap)
etas_contours = ax.contourf(X,Y, Z, n_contours, zorder=8, alpha=alpha, cmap=map_cmap)
# ax.colorbar() ??
if do_colorbar:
#plt.colorbar(ax)
# getting a few cases in extended scripts where this fails due to... don't know maybe another
# error where a bunch of figures get stacked on top of one another. let's just error-trap
# it for now and sort it out later.
try:
plt.colorbar(etas_contours, cax=None, ax=ax, cmap=map_cmap)
except:
print('DEBUG: error creating colorbar() in globalETAS.make_etas_contourmap()')
#mpl.colorbar.ColorbarBase(ax=ax, cmap=map_cmap, values=sorted(Z.ravel()), orientation="vertical")
#
self.cm=cm
self.etas_contours = etas_contours
#
return cm
#
#
def make_etas_boxy_map(self, n_contours=None, fignum=0, fig_size=(6.,6.), contour_fig_file=None, contour_kml_file=None, kml_contours_bottom=0., kml_contours_top=1.0, alpha=.6, alpha_kml=.5, refresh_etas=False, map_resolution='i', map_projection='cyl', map_cmap='jet'):
#
cm = self.draw_map(fignum=fignum, fig_size=fig_size, map_resolution=map_resolution, map_projection=map_projection)
c_map = plt.get_cmap(map_cmap)
zs = numpy.log10(self.ETAS_array['z'])
cNorm = mpl.colors.Normalize(vmin=min(zs), vmax=numpy.nanmax(zs))
scalarMap = mpl.cm.ScalarMappable(norm=cNorm, cmap=c_map)
#
for rw in self.ETAS_array:
plt.fill_between(x=[rw['x']-self.d_lon/2., rw['x']+self.d_lon/2.], y1=[rw['y']-.5*self.d_lat, rw['y']-.5*self.d_lat], y2=[rw['y']+.5*self.d_lat, rw['y']+.5*self.d_lat], color=scalarMap.to_rgba(numpy.log10(rw['z'])))
#plt.colorbar() # not sure how to make this work for non-contour plot...
#
return cm
#
def plot_mainshock_and_aftershocks(self, m0=6.0, n_contours=25, mainshock=None, fignum=0, ax=None):
#
map_etas = self.make_etas_contour_map(n_contours=n_contours, fignum=fignum, ax=ax)
if mainshock is None:
mainshock = self.catalog[0]
for rw in self.catalog:
if rw['mag']>mainshock['mag']: mainshock=rw
ms=mainshock
#
ax = (ax or plt.gca())
#
for eq in self.catalog:
if eq['mag']<m0 or eq['event_date']<ms['event_date']: continue
if eq==ms:
x,y = map_etas(eq['lon'], eq['lat'])
ax.plot([x], [y], 'k*', zorder=7, ms=20, alpha=.8)
ax.plot([x], [y], 'r*', zorder=8, ms=18, label='mainshock', alpha=.8)
if eq['event_date']>eq['event_date']:
x,y = map_etas(eq['lon'], eq['lat'])
ax.plot([x], [y], 'o', zorder=7, ms=20, alpha=.8)
#
#return plt.gca()
return ax
#
def calc_etas_contours(self, n_contours=None, fignum=0, contour_fig_file=None, contour_kml_file=None, kml_contours_bottom=0., kml_contours_top=1.0, alpha_kml=.5, refresh_etas=False):
# wrapper for one-stop-shopping ETAS calculations.
# (and these calc_contours schemes need to be cleaned up a bit. there is a bit of redundancy and disorganization)
#
n_contours = (n_contours or self.n_contours)
#
if refresh_etas or ('ETAS_array' not in self.__dict__.keys()):
self.make_etas()
#
plt.figure(fignum)
plt.clf()
#
self.etas_contours = plt.contourf(self.lonses, self.latses, numpy.log10(self.lattice_sites), n_contours)
plt.colorbar()
#
if contour_fig_file!=None:
p_name, f_name = os.path.split(contour_fig_file)
if not os.path.isdir(p_name): os.makedirs(p_name)
plt.savefig(contour_fig_file)
#
if contour_kml_file!=None:
# make kml and write to file like:
# kml_str = kml_from_contours(cset=contours, colorbarname='napa_colorbar.png', open_file=True, close_file=True, contour_labels=None, top=top, bottom=bottom, fname_out=fname_out, alpha_kml=alpha_kml)
# ... and this is maybe not the best coding framework, but this function will automatically output to a file if we
# give it fname_out!=None. BUT, since it's sort of a crappy way to write code, and we'll likely 'fix' it later,
# let's just return a string and write our own file. we'll even dump the KML to the class dict. maybe... or maybe we should
# use the built in file-writer, since i think we need/want to also include the colorbar...
#
self.contours_kml_str = c2kml.kml_from_contours(cset=self.etas_contours, colorbarname=None, open_file=True, close_file=True, contour_labels=None, top=kml_contours_top, bottom=kml_contours_bottom, alpha_kml=alpha_kml, fname_out=contour_kml_file)
p_name, f_name = os.path.split(contour_kml_file)
if not os.path.isdir(p_name): os.makedirs(p_name)
with open(contour_kml_file, 'w') as f_kml:
f_kml.write(self.contours_kml_str)
#pass
#
return self.etas_contours
#
def export_kml(self, fout='etas_contours.kml', kml_contours_bottom=0., kml_contours_top=1.0, alpha_kml=.5):
#
self.contours_kml_str = c2kml.kml_from_contours(cset=self.etas_contours, colorbarname=None, open_file=True, close_file=True, contour_labels=None, top=kml_contours_top, bottom=kml_contours_bottom, alpha_kml=alpha_kml, fname_out=None)
p_name, f_name = os.path.split(fout)
if not os.path.isdir(p_name): os.makedirs(p_name)
with open(fout,'w') as f:
f.write(self.contours_kml_str)
#
#
def kml_from_contours(self, contours=None, contour_kml_file=None, kml_contours_bottom=0., kml_contours_top=1.0, alpha_kml=.5, refresh_etas=False):
if contours is None: contours = self.etas_contours
if (contours is None or refresh_etas): contours = plt.contourf(self.lonses, self.latses, numpy.log10(self.lattice_sites), n_contours)
#
self.contours_kml_str = c2kml.kml_from_contours(cset=self.etas_contours, colorbarname=None, open_file=True, close_file=True, contour_labels=None, top=kml_contours_top, bottom=kml_contours_bottom, alpha_kml=alpha_kml, fname_out=contour_kml_file)
p_name, f_name = os.path.split(contour_kml_file)
if not os.path.isdir(p_name): os.makedirs(p_name)
with open(contour_kml_file, 'w') as f_kml:
f_kml.write(self.contours_kml_str)
return contours_kml_str
#
def export_xyz(self, fout='etas_xyz.xyz'):
#
# export ETAS_array as xyx format.
#
p_name, f_name = os.path.split(fout)
#
if not os.path.isdir(p_name):
os.makedirs(os.path.split(p_name))
#
with open(fout, 'w') as f:
f.write('#globalETAS xyz (lon, lat, z_etas) export\n')
f.write('#eventually, add metadata\n')
f.write('#!x\t\y\tz\n')
[f.write('\t'.join([str(x) for x in rw]) + '\n') for rw in self.ETAS_array.tolist()]
#
def total_rate(self, mc=None):
# compute total expected rate of seismicity for this map/catalog (rate of m>mc):
# NOTE: for cumulative call (with t_future != None, this is the total count.
mc = mc or self.mc_etas
#
#return numpy.sum([z*numpy.cos(y*deg2rad) for x,y,z in self.ETAS_array])*self.d_lon*self.d_lat*(deg2km**2)*10**(self.mc_etas - mc)
# ... or in more pure numpy?
return numpy.sum(self.ETAS_array['z']*(deg2km*deg2km*self.d_lat*self.d_lon*numpy.cos(self.ETAS_array['y']*deg2rad)))
#
def make_etas_rtree(self):
# use the same basic framework as etas_all (aka, instantiate a lattice), but map an rtree index to the lattice, then use a delta_lat, delta_lon
# approach (like etas_bindex) bit loop only over the rtree.intersection() of the delta_lat/lon window.
#
print("begin make_etas_rtree()")
#
latses=self.latses
lonses=self.lonses
n_lat=self.n_lat
n_lon=self.n_lon
#
# note: it's important to do the (latses, lonses) in the same sequence so we have consistency in the indices.
# this would make a reasonable case to use a conventional for-loop instead of multiple list comprehensions.
#print("initialize ETAS array and indices.")
#if not hasattr(self, 'ETAS_array') or len(self.ETAS_array)==0:
# self.ETAS_array = [[lon, lat, 0.] for lat,lon in itertools.product(latses, lonses)]
# #self.ETAS_array = numpy.array([[lon, lat, 0.] for lat,lon in itertools.product(self.latses, self.lonses)])
# self.ETAS_array = numpy.core.records.fromarrays(zip(*self.ETAS_array), dtype = [('x', '>f8'), ('y', '>f8'), ('z', '>f8')])
#
# make a dict of lattice sites and their x,y coordinates.
# ... but in the end, we don't appear to really do anything with this, so i think we can get rid of it, with maybe some patches where it once was...
#
# NOTE: since i is just sequential, we can use sorted order, and therefore a numpy array.
#lattice_dict = {i:{'lat':lat, 'lon':lon, 'j_lon':int(i%n_lon), 'j_lat':int(i/n_lon)} for i, (lon,lat,z) in enumerate(self.ETAS_array)}
#self.lattice_dict=lattice_dict
#print("len(local_lattice_dict): ", len(self.lattice_dict))
print('** len(self.ETAS_array)[{}] = {}'.format(os.getpid(), len(self.ETAS_array)))
#
# make an rtree index:
lattice_index = index.Index()
# build index. we should build directly from ETAS_array, particualrly since we'll be (maybe) building an mpp version that splits up ETAS_array between proceses.
#[lattice_index.insert(j, (lon, lat, lon, lat)) for j, (lat,lon) in enumerate(itertools.product(latses,lonses))] # like [[lat0,lon0],[lat0,lon1], [lat0,lon2]...]
[lattice_index.insert(j, (lon, lat, lon, lat)) for j, (lon, lat, z) in enumerate(self.ETAS_array)] # like [[lat0,lon0],[lat0,lon1], [lat0,lon2]...]
#
print("Indices initiated. begin ETAS[{}] :: range: {}".format(os.getpid(), self.etas_cat_range))
#
#for quake in self.catalog:
# yoder 2019_07_23: filter m<mc by index, not if, continue; we should do the same with the event_date filter as well.
#for quake in self.catalog[self.etas_cat_range[0]:self.etas_cat_range[1]]:
for quake in (self.catalog[self.etas_cat_range[0]:self.etas_cat_range[1]])[self.catalog['mag']>=self.mc_etas]:
#if quake['mag']<self.mc_etas: continue
#
# TODO: handle the case where we have t_future. we do want to catch t<0 or \Delta t<0 in the Omori calculations.
# how did we format t_future? What is the float version?
#if quake['event_date_float']>self.t_forecast: continue
# this should do it; t_future should be a float.,
if quake['event_date_float'] > max(self.t_forecast, (self.t_forecast or self.t_future)): continue
#if quake['event_date_float']>self.t_forecast and self.t_future is None: continue
#
# yoder 28 July 2019: add t_1, t_2 parameters -- one step closer to pre-calcing orate:
eq = Earthquake(quake, transform_type=self.transform_type, transform_ratio_max=self.transform_ratio_max, ab_ratio_expon=self.ab_ratio_expon, t_1=self.t_forecast, t_2=self.t_future)
#
# ab_ratio_expon is, indeed, making it successfully to Earthquake().
#print('**debug: eq abexpon: {}'.format(eq.ab_ratio_expon))
#
# get lat/lon range:
# TODO: introduce a new lat/lon range model; use the spatial-omori distribution; calc. to r = r(x=.9x0)
delta_lat = self.etas_range_padding + eq.L_r*self.etas_range_factor/deg2km
if abs(eq.lat)==90.:
delta_lon=180.
else:
delta_lon = self.etas_range_padding + delta_lat/math.cos(eq.lat*deg2rad)
#
# and let's also assume we want to limit our ETAS map to the input lat/lon:
# this formulation can get confused if lons, lats, and a catalog are provided separately. look for a smarter way...
#lon_min, lon_max = max(eq.lon - delta_lon, self.lons[0]), min(eq.lon + delta_lon, self.lons[1])
#lat_min, lat_max = max(eq.lat - delta_lat, self.lats[0]), min(eq.lat + delta_lat, self.lats[1])
# for now, just let the space be big.
lon_min, lon_max = eq.lon - delta_lon, eq.lon + delta_lon
lat_min, lat_max = eq.lat - delta_lat, eq.lat + delta_lat
#
#print('rtree indexing: ', quake, lon_min, lat_min, lon_max, lat_max, self.lons, self.lats, delta_lon, delta_lat)
#
# yoder 2019_07_23: these should probaby be numpy.arrays. good guess is that .intersection() returns an array,
# so we should probably use that, and then use numpy.append().
site_indices = numpy.array(list(lattice_index.intersection((lon_min, lat_min, lon_max, lat_max)))).astype(int)
if len(site_indices)==0: continue
# ... and if we wrap around the other side of the world...
# there's probably a smarter way to do this...
if lon_min<-180.:
#new_lon_min = lon_min%(180.)
#new_lon_min = 180.+(lon_min+180.)
new_lon_min = 360. + lon_min
#site_indices += list(lattice_index.intersection((new_lon_min, lat_min, 180., lat_max)))
site_indices = numpy.append(site_indices, list(lattice_index.intersection((new_lon_min, lat_min, 180., lat_max)))).astype(int)
if lon_max>180.:
#new_lon_max = lon_max%(-180.)
#new_lon_max = -180. + lon_max-180.
new_lon_max = -360. + lon_max
#site_indices += list(lattice_index.intersection((-180., lat_min, new_lon_max, lat_max)))
site_indices = numpy.append(site_indices, list(lattice_index.intersection((-180., lat_min, new_lon_max, lat_max)))).astype(int)
#
#site_indices=site_indices.astype(int)
#print("LLRange: ", lon_min, lat_min, lon_max, lat_max, len(list(site_indices)))
#
# TODO: I think we can get a performance boost by doing the elliptical transform on all (sub-set of) points at once, in a
# linear algebra operation, so we have the relevant lattice sites; now transform them, and loop through XY_prime=dot(E, XY)
# ... but I think to do this, we need to do all of the computations here, in a sequence that replaces this loop (aka, rewrite the
# local_intensity(), elliptical_transform(), etc. bits here.
#Xs = (self.ETAS_array[['x'], ['y']])[site_indices]
#
# TODO: does this work???
# will this auto-vectorize?
#local_intensities = eq.local_intensity(t=self.t_forecast, t_to=self.t_future, lon=Xs['lon'], lat=Xs['lat'], p=self.p_etas)
# ... probably not, but we have the newer local_intensiteis() function, which should work. note it is designed to take an array
# of times as well...
#
#print('*** DEBUG: ', len(self.ETAS_array['y']), len(self.ETAS_array['x']), len(site_indices))
# if numpy.isnan(site_indices).any() or len(site_indices)==0:
# print('*** ERROR: site_indices: ', site_indices)
# raise Exception('site_indices exception.')
#
local_intensities = eq.local_intensities(ts=numpy.atleast_1d(self.t_forecast), ts_to=(self.t_future if self.t_future is None else numpy.atleast_1d(self.t_future)), lons=self.ETAS_array['x'][site_indices], lats=self.ETAS_array['y'][site_indices], p=self.p_etas)
#
# handle nan values?
local_intensities[numpy.isnan(local_intensities)] = 0.
#
(self.ETAS_array['z'])[site_indices] += local_intensities.reshape((self.ETAS_array['z'])[site_indices].shape)
#
# for site_index in site_indices:
# #X = lattice_dict[site_index]
# #x,y = (self.ETAS_array[['lon', 'lat']])[site_index]
# #print('*** DEBUG dtype: ', self.ETAS_array.dtype)
# #
# # do we need to do this, or does the Earthquake self-transform? Earthquake does, but actually it looks like we might move all of
# # the local_intensity() calculation here, so we can vectorize.
# # yoder 2019_07_23: this eq.ellipitical_transform() computation is done in local_intensity(); we don't do anything with it here.
# # TODO: (however), we might to well to reorganize this to do the ET (or at least part of it) in one combined LinAlg. operation
# # to improve speed.
# #
# #local_intensity = eq.local_intensity(t=self.t_forecast, lon=X['lon'], lat=X['lat'], p=self.p_etas)
# # TODO: since we are passing t_1, t_2 in the eq initialization, we can pass the "use pre-calc" code
# # (which might not yet be set), which should give a speed boost.
# #local_intensity = eq.local_intensity(t=self.t_forecast, t_to=self.t_future, lon=X['lon'], lat=X['lat'], p=self.p_etas)
# local_intensity = eq.local_intensity(t=self.t_forecast, t_to=self.t_future, lon=self.ETAS_array['x'][site_index], lat=self.ETAS_array['y'][site_index], p=self.p_etas)
# #
# if numpy.isnan(local_intensity):
# #print("NAN encountered: ", site_index, self.t_forecast, X['lon'], X['lat'], eq.lon, eq.lat, eq.__dict__)
# continue
# #
# #self.lattice_sites.add_to_bin(bin_x=lon_bin['index'], bin_y=lat_bin['index'], z=local_intensity)
# #self.lattice_sites[j_lon][j_lat] += local_intensity
# #
# # TODO: generalize this; write a function like: self.add_to_ETAS(site_index, local_intensity).
# # for current, SPP or non-memory shared versions, this will be the same as below -- directly add to the array.
# # however, if we 1) write a c++ extension version or 2) use a shared memory array (or both), we can override the class
# # definition of self.add_to_ETAS() to be something like, self.shared_ETAS_array[3*site_index+2]+=local_intensity.
# # that said, it might be possible to accomplish this in the __init__ by declaring the ETAS_array as a list from
# # the shared array by reff. (??)
# self.ETAS_array[site_index][2] += local_intensity
# #
#
print('[{}]: finished calculateing ETAS (rtree). wrap up in recarray and return.'.format(os.getpid()))
# this conversion to the 2d array should probably be moved to a function or @property in the main class scope.
#self.lattice_sites = numpy.array([rw[2] for rw in self.ETAS_array])
#
#self.lattice_sites.shape=(len(latses), len(lonses))
#
# Getting a Warning about passing lists, not tuples, for recarray conversion (aka, .fromrecords() ), so maybe consider
# passing this as an ary.T rather than zip?
#self.ETAS_array = numpy.core.records.fromarrays(zip(*self.ETAS_array), dtype = [('x', '>f8'), ('y', '>f8'), ('z', '>f8')])
#self.ETAS_array = numpy.core.records.fromarrays(self.ETAS_array.T, dtype = [('x', '>f8'), ('y', '>f8'), ('z', '>f8')])
#
def make_etas_all(self):
# TODO: this does not appear to work correctly, or at least not in MPP mode. might be because i'm trying to hijack the
# rtree MPP model; the best approach might be to more directly hijack the rtree model and just circumvent the indexing
# or burn a little bit of memory and impose a default index (of all elements).
# loop-loop over the whole lattice space...
# first, make an empty rates-lattice:
#
#latses = numpy.arange(self.lats[0], self.lats[1]+self.d_lat, self.d_lat)
#lonses = numpy.arange(self.lons[0], self.lons[1]+self.d_lon, self.d_lon)
latses = self.latses
lonses = self.lonses
n_lat = self.n_lat
n_lon = self.n_lon
#
#for quake in self.catalog:
print("Begin ETAS all/brute :: ", self.etas_cat_range)
#
#for quake in self.catalog:
for quake in self.catalog[self.etas_cat_range[0]:self.etas_cat_range[1]]:
#for quake in self.catalog[etas_cat_range[0]:etas_cat_range[1]]:
if quake['mag']<self.mc_etas: continue
if quake['event_date_float']>self.t_forecast: continue
#
eq = Earthquake(quake, transform_type=self.transform_type, transform_ratio_max=self.transform_ratio_max, ab_ratio_expon=self.ab_ratio_expon)
#for j, lat_lon in enumerate(itertools.product(latses, lonses)):
for j, (lat,lon) in enumerate(itertools.product(latses, lonses)):
# TODO: for some reason, this is running over the site index (j=len(catalog) or something).
if j>=len(self.catalog):continue
#
#self.ETAS_array[j][2] += eq.local_intensity(t=self.t_forecast, lon=lat_lon[1], lat=lat_lon[0], p=self.p_etas)
#self.ETAS_array[j][2] += eq.local_intensity(t=self.t_forecast, lon=lon, lat=lat, p=self.p_etas)
#
self.ETAS_array[j][2] += eq.local_intensity(t=self.t_forecast, t_to=self.t_future, lon=X['lon'], lat=X['lat'], p=self.p_etas)
#
#for lat_tpl,lon_tpl in itertools.product(enumerate(latses), enumerate(lonses)):
# local_intensity = eq.local_intensity(t=self.t_forecast, lon=lon_tpl[1], lat=lat_tpl[1])
# #
# #self.lattice_sites.add_to_bin(bin_x=lon_bin['index'], bin_y=lat_bin['index'], z=local_intensity)
# self.lattice_sites[lon_tpl[0]][lat_tpl[0]] += local_intensity
# #
#
#
#self.lattice_sites = numpy.array([rw[2] for rw in self.ETAS_array])
#self.lattice_sites.shape=(len(latses), len(lonses))
#
#self.ETAS_array = []
#
self.ETAS_array = numpy.core.records.fromarrays(zip(*self.ETAS_array), dtype = [('x', '>f8'), ('y', '>f8'), ('z', '>f8')])
def make_etas_bindex(self):
# rewrite this version; minimize the use of functional indexing; aka, get initial position from functional index; then just count.
#
# do the ETAS calculation. for each earthquake, figure out what bins it contributes to and calc. etas for each bin.
# (new in p3?) can't seem to set this attribute... maybe in __init__()? generally, i think we're going to handle this a bit differently anyway...
self.lattice_sites = None
bb=bindex.Bindex2D(dx=self.d_lon, dy=self.d_lat, x0=self.bin_lon0, y0=self.bin_lat0) # list of lattice site objects, and let's index it by...
self.lattce_sites=bb # probably (i_x/lon, j_y/lat)
# copy class level variables:
#
print( "calc bindex etas...")
#for quake in self.catalog:
for quake in self.catalog[etas_cat_range[0]:etas_cat_range[1]]:
if quake['mag']<self.mc_etas: continue
#
eq = Earthquake(quake, transform_type=self.transform_type, transform_ratio_max=self.transform_ratio_max, ab_ratio_expon=self.ab_ratio_expon)
# calculate the bins within range of this earthquake:
# nominally, the proper thing to do now (or at lest one proper approach) is to compute a proper geodesic north and east to get the lat/lon bounds
# near the poles, however, this will create a problem, that is a bit difficult to track, where the geodesic reaches over the pole and to lat<90
# on the other side. if we just use a recta-linear approximation, we can use [max(-90, lat-d_lat), min(90, lat+d_lat)]
#x,y = lon_lat_2xy(lon=quake['lon'], lat=quake['lat'])
#
# range of influence:
delta_lat = self.etas_range_padding + eq.L_r*self.etas_range_factor/deg2km
if abs(eq.lat) == 90.:
delta_lon = 180.
else:
delta_lon = delta_lat/math.cos(eq.lat*deg2rad)
#
# and let's also assume we want to limit our ETAS map to the input lat/lon:
lon_min, lon_max = numpy.nanmax(eq.lon - delta_lon, self.lons[0]), numpy.nanmin(eq.lon + delta_lon, self.lons[1])
lat_min, lat_max = numpy.nanmax(eq.lat - delta_lat, self.lats[0]), numpy.nanmin(eq.lat + delta_lat, self.lats[1])
#
#print ("lon, lat range: (%f, %f), (%f, %f):: m=%f, L_r=%f, dx=%f/%f" % (lon_min, lon_max, lat_min, lat_max, eq.mag, eq.L_r, eq.L_r*self.etas_range_factor, eq.L_r*self.etas_range_factor/deg2km))
#
# - choose an elliptical transform: equal-area, rotational, etc.
# - calculate initial rate-density
# - distribute over relevant sites:
# - for each site, D' = D_spherical * R'/R_xy of course this will be approximately equal to R'.
#
#for lon_site,lat_site in [[j,k] for j in numpy.arange(lon_min, lon_max, d_lon) for k in numpy.arange(lat_min, lat_max, d_lat)]:
for lon_site, lat_site in itertools.product(numpy.arange(lon_min+self.bin_lon0, lon_max+self.bin_lon0, self.d_lon), numpy.arange(lat_min+self.bin_lat0, lat_max+self.bin_lat0, self.d_lat)):
# so we make a square (or maybe a circle later on) and calc. etas at each site. use Bindex() to correct for any misalignment.
#
lon_bin = self.lattice_sites.get_xbin_center(lon_site) # returns a dict like {'index':j, 'center':x}
lat_bin = self.lattice_sites.get_ybin_center(lat_site)
#
#print("lon-lat bins: ", lon_bin, lat_bin)
#
#bin_lonlat = xy2_lon_lat(x_bin['center'], y_bin['center'])
#bin_lonlat=[lon_bin, lat_bin]
#
# now, calculate local ETAS intensity from this eq at this site...
# (this should be defined in the Earthquake() object:
# note: at this time, we have only the self-similar type local intensity. eventually, we need to split this up. nominally,
# this can occur at the Earthquake level, so -- if we so desire, different earthquakes can have differend spatial distributions.
#
#local_intensity = eq.local_intensity(t=self.t_forecast, lon=lon_bin['center'], lat=lat_bin['center'], p=self.p_etas) # anything else?
local_intensity = eq.local_intensity(t=self.t_forecast, t_to=self.t_future, lon=X['lon'], lat=X['lat'], p=self.p_etas)
#
#self.lattice_sites.add_to_bin(x=x_bin['center'], y=y_bin['center'], z=1.0/distances['geo'])
#
#self.lattice_sites.add_to_bin(x=lon_bin['center'], y=lat_bin['center'], z=local_intensity) # note: if we give x,y instead of the bin index, bindex
self.lattice_sites.add_to_bin(bin_x=lon_bin['index'], bin_y=lat_bin['index'], z=local_intensity)
#
self.ETAS_array = self.lattice_sites.to_array()
print("finished calculating ETAS")
#
'''
# i think these are all handled in the Bindex() object.
def get_bin_id_x(self, x):
return self.get_bin_id(x,dx=self.d_x, x0=self.bin_x0)
#
def get_bin_id_y(self, x):
return self.get_bin_id(y,dx=self.d_y, x0=self.bin_y0)
#
def get_bin_id(self, x, dx=1., x0=None, bin_0=0):
# return bin_id/index along a single axis.
x0 = (x0 or self.bin_x0)
#
return int(round(bin_0) + int(round(round((x-x0)/dx))
def bin2x(bin_num, dx=1., x0=0., bin_0=0):
# return the position x of a bin along one axis.
return (bin_num - bin_0)*dx + x0x
'''
#
class ETAS_bindex(Global_ETAS_model):
# default case...
def __init__(self, *args, **kwargs):
self.make_etas=self.make_etas_bindex
super(ETAS_bindex,self).__init__(*args, **kwargs)
#
class ETAS_brute(Global_ETAS_model):
def __init__(self, *args, **kwargs):
self.make_etas = self.make_etas_all
super(ETAS_brute, self).__init__(*args, **kwargs)
#
#
#
class ETAS_rtree(Global_ETAS_model):
def __init__(self, *args, **kwargs):
self.make_etas = self.make_etas_rtree
super(ETAS_rtree, self).__init__(*args, **kwargs)
#
#
#
class ETAS_rtree_mpp(ETAS_rtree, mpp.Process):
'''
# an mpp version of ETAS_rtree (to be run as an mpp process, aka not an mpp handler, but an mpp worker**).
'''
def __init__(self, pipe_r, *args, **kwargs):
self.make_etas = self.make_etas_rtree
kwargs['calc_etas']=False
self.pipe_r = pipe_r
#print('initting worker, xyz_range = ', locals().get('etas_xyz_range', 'none'), kwargs.get('etas_xyz_range', 'nnonee'))
super(ETAS_rtree_mpp, self).__init__(*args, **kwargs)
mpp.Process.__init__(self)
#
#
def run(self):
# the mpp run bit.
# i think we need to disable the 'execute make etas' bit in __init__ (super); we need to kick off ETAS in run()
self.make_etas()
#
# now, pipe back the results
print('etas complete (from mpp_rtree run() loop); now pipe back(%s)' % self.etas_cat_range)
self.pipe_r.send(self.ETAS_array) # might need to pipe back in a simpler form. eventually, we want to use a shared memory mpp.Array()...
self.pipe_r.close()
#
class ETAS_mpp_handler(Global_ETAS_model):
# a sub/container class to manage and collect results from a bunch of mpp_etas instances.
# Note: this version works, but can be super memory-intensive for large catalogs. its cousin class, ETAS_mpp_handler_xyz
# is recommended for almost all operations (to the extent that this version should probalby be depricated and removed).
#
def __init__(self, n_cpu=None, *args, **kwargs):
#self.make_etas = self.make_etas_rtree
#
self.etas_kwargs = {key:val for key,val in kwargs.items() if key not in ['n_proocesses']} # or any other kwargs we want to skip. note: might need to handle
# the ETAS_range parameter carefully...
#
#if n_cpu is None: n_cpu = max(1, mpp.cpu_count()-1)
n_cpu = (n_cpu or 2*mpp.cpu_count())
self.n_cpu = n_cpu
#
# go ahead and run the base class __init__. this basically handles lats, lons, forecast_time, and some other bits and then kicks off make_etas(), where we'll
#kwargs['make_etas']=False
self.make_etas = self.make_etas_mpp
#
# now, when we run __init__, it will build the catalog (unless one is provided). we'll build up a set of etas_rtree objects. we need to sort out how
# to generalize. nominally, we set up a sort of parallel-inherited class structure for mpp_in_general and mpp_specific inheritance.
#
# define the ETAS worker class:
self.ETAS_worker = ETAS_rtree_mpp
#
#
super(ETAS_mpp_handler, self).__init__(*args, **kwargs)
#
def make_etas_mpp(self):
#
#
cat_len = len(self.catalog)
#proc_len = cat_len/self.n_cpu
proc_len = (self.etas_cat_range[1]-self.etas_cat_range[0])/self.n_cpu
# first, gather a bunch of rtree processes.
#
etas_workers = []
etas_pipes = []
prams = self.etas_kwargs.copy()
prams['catalog']=self.catalog # .copy() ??
for j in range(self.n_cpu):
pipe_r, pipe_s = mpp.Pipe()
prams['etas_cat_range'] = [int(self.etas_cat_range[0]+j*proc_len), int((j+1)*proc_len)] # (sort out these indices to be sure we always get the whole catalog...)
prams['pipe_r'] = pipe_r
etas_pipes += [pipe_s]
#
etas_workers += [self.ETAS_worker(**prams)]
#
# now, go through the list again and start each intance (this can probably be done when they're instantiated):
for j,p in enumerate(etas_workers):
p.start()
#etas_workers[j].start()
#
#
# and join() them? do we need to join if we're doing send()-recv()? (see vq code for examples):
# in fact, i think we don't need this, and be careful with the join() syntax (see notes in vq_analyzer code).
# now, processes are finished; they return ETAS_array[] like [[x,y,z]]... ETAS_array should be initiated. we can aggregate them by index, but they should all
# have the same indexing (they should all be a complete set of lats, lons. so we should check for that here... and i think actually we should have an empty copy
# from __init__(), but for now, just add them row by row (maybe sort first). in later versions (maybe this one??) all processes will write directly to an mpp.Array()
# shared memory object.
#
print('now gather sub-arrays...')
for j,(etas,pp) in enumerate(zip(etas_workers, etas_pipes)):
ary = pp.recv()
for k,rw in enumerate(ary): self.ETAS_array['z'][k]+=rw['z']
pp.close()
#
# still not sure if we need this...
#for j,p in enumerate(etas_workers):
# p.join()
#
del etas_workers
#
#
class ETAS_mpp_handler_xyz(Global_ETAS_model):
# a sub/container class to manage and collect results from a bunch of mpp_etas instances.
# this is a semi-memory friendly version in which the forecast lattice is divided amongst the processes,
# as opposed to ETAS_mpp_handler() in which the catalog is split up, which uses a lot of memory (and probably lots
# of time piping results) for large maps.
#
# TODO: Figure out how to derive this from mpp.Pool() and, rather than manually managing processes, run as a Pool().
# the main problem is that we divide the job into jobs based on geography (aka, slice up the map and give each processor
# a section of map), but this is not an accurate representation of the actual compute requirements. seismically active
# regions end up doing way more flops than quiescent regions. a good, simple approach, then, is to divide into, say
# 2*n_cpu() jobs, which we process n_cpu() at a time using a Pool(). this way, non-intensive jobs will be discarded and replaced
# quickly, and the compute intensive jobs will be smaller, as a product of smaller geometry.
#
def __init__(self, n_cpu=None, worker_class=ETAS_rtree_mpp, *args, **kwargs):
#self.make_etas = self.make_etas_rtree
#
self.etas_kwargs = {key:val for key,val in kwargs.items() if key not in ['n_proocesses']} # or any other kwargs we want to skip. note: might need to handle
# the ETAS_range parameter carefully...
#
if n_cpu is None: n_cpu = max(1, mpp.cpu_count()-1)
n_cpu = (n_cpu or mpp.cpu_count())
self.n_cpu = int(numpy.ceil(n_cpu))
#
# go ahead and run the base class __init__. this basically handles lats, lons, forecast_time, and some other bits and then kicks off make_etas(), where we'll
#kwargs['make_etas']=False
self.make_etas = self.make_etas_mpp
#
# now, when we run __init__, it will build the catalog (unless one is provided). we'll build up a set of etas_rtree objects. we need to sort out how
# to generalize. nominally, we set up a sort of parallel-inherited class structure for mpp_in_general and mpp_specific inheritance.
#
# define the ETAS worker class:
#self.ETAS_worker = ETAS_rtree_mpp
self.ETAS_worker = worker_class
#
#
super(ETAS_mpp_handler_xyz, self).__init__(*args, **kwargs)
#
def make_etas_mpp(self):
#
cat_len = len(self.catalog)
##proc_len = cat_len/self.n_cpu
#proc_len = (self.etas_cat_range[1]-self.etas_cat_range[0])/self.n_cpu
#
xyz_len = self.n_lat*self.n_lon/self.n_cpu
#
# first, gather a bunch of rtree processes.
#
etas_workers = []
etas_pipes = []
prams = self.etas_kwargs.copy()
prams['etas_cat_range']=None
prams['catalog']=self.catalog # .copy() ??
for j in range(self.n_cpu):
pipe_r, pipe_s = mpp.Pipe()
prams['etas_xyz_range'] = [int(j*xyz_len), int((j+1)*xyz_len)] # (sort out these indices to be sure we always get the whole catalog...)
print('etas_mpp worker xyz_range: ', prams['etas_xyz_range'])
prams['pipe_r'] = pipe_r
etas_pipes += [pipe_s]
#
etas_workers += [self.ETAS_worker(**prams)]
#
# now, go through the list again and start each intance (this can probably be done when they're instantiated):
for j,p in enumerate(etas_workers):
p.start()
#
#