Skip to content

Commit 949d342

Browse files
committed
added acceptance test for space-magnitude binning and catalog filtering
included tolerance for magnitude units on binning routine modified magnitude_bins function to reduce issues coming from floating point addition updated bin1d_vec function to account for absolute error in floating point numbers fixed ascii reader for cases where catalog_id is None removed io.py file and added those routines to __init__.py added more routines to __all__ in __init__.py
1 parent f3af6ed commit 949d342

File tree

11 files changed

+727
-329
lines changed

11 files changed

+727
-329
lines changed

csep/__init__.py

Lines changed: 299 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,38 @@
1+
import json
2+
import os
3+
import time
4+
5+
16
from csep.core import forecasts
27
from csep.core import catalogs
38
from csep.core import poisson_evaluations
49
from csep.core import catalog_evaluations
10+
from csep.core import regions
511
from csep.core.repositories import (
612
load_json,
713
write_json
814
)
9-
from csep.io import (
10-
load_stochastic_event_sets,
11-
load_catalog,
12-
query_comcat,
13-
load_evaluation_result,
14-
load_gridded_forecast,
15-
load_catalog_forecast
16-
)
1715

1816
from csep.utils import datasets
17+
from csep.utils import readers
18+
19+
from csep.core.forecasts import GriddedForecast, CatalogForecast
20+
from csep.models import EvaluationResult, CatalogNumberTestResult
21+
from csep.utils.time_utils import (
22+
utc_now_datetime,
23+
strptime_to_utc_datetime,
24+
datetime_to_utc_epoch,
25+
epoch_time_to_utc_datetime,
26+
utc_now_epoch
27+
)
1928

2029
# this defines what is imported on a `from csep import *`
2130
__all__ = [
2231
'load_json',
2332
'write_json',
2433
'catalogs',
2534
'datasets',
35+
'regions',
2636
'poisson_evaluations',
2737
'catalog_evaluations',
2838
'forecasts',
@@ -31,5 +41,284 @@
3141
'query_comcat',
3242
'load_evaluation_result',
3343
'load_gridded_forecast',
34-
'load_catalog_forecast'
35-
]
44+
'load_catalog_forecast',
45+
'utc_now_datetime',
46+
'strptime_to_utc_datetime',
47+
'datetime_to_utc_epoch',
48+
'epoch_time_to_utc_datetime',
49+
'utc_now_epoch'
50+
]
51+
52+
def load_stochastic_event_sets(filename, type='csv', format='native', **kwargs):
53+
""" General function to load stochastic event sets
54+
55+
This function returns a generator to iterate through a collection of catalogs.
56+
To load a forecast and include metadata use :func:`csep.load_catalog_forecast`.
57+
58+
Args:
59+
filename (str): name of file or directory where stochastic event sets live.
60+
type (str): either 'ucerf3' or 'csep' depending on the type of observed_catalog to load
61+
format (str): ('csep' or 'native') if native catalogs are not converted to csep format.
62+
kwargs (dict): see the documentation of that class corresponding to the type you selected
63+
for the kwargs options
64+
65+
Returns:
66+
(generator): :class:`~csep.core.catalogs.AbstractBaseCatalog`
67+
68+
"""
69+
if type not in ('ucerf3', 'csv'):
70+
raise ValueError("type must be one of the following: (ucerf3)")
71+
72+
# use mapping to dispatch to correct function
73+
# in general, stochastic event sets are loaded with classmethods and single catalogs use the
74+
# constructor
75+
mapping = {'ucerf3': catalogs.UCERF3Catalog.load_catalogs,
76+
'csv': catalogs.CSEPCatalog.load_ascii_catalogs}
77+
78+
# dispatch to proper loading function
79+
result = mapping[type](filename, **kwargs)
80+
81+
# factory function to load catalogs from different classes
82+
while True:
83+
try:
84+
catalog = next(result)
85+
except StopIteration:
86+
return
87+
except Exception:
88+
raise
89+
if format == 'native':
90+
yield catalog
91+
elif format == 'csep':
92+
yield catalog.get_csep_format()
93+
else:
94+
raise ValueError('format must be either "native" or "csep!')
95+
96+
97+
def load_catalog(filename, type='csep-csv', format='native', loader=None, **kwargs):
98+
""" General function to load single catalog
99+
100+
See corresponding class documentation for additional parameters.
101+
102+
Args:
103+
type (str): ('ucerf3', 'csep-csv', 'zmap', 'jma-csv', 'ndk') default is 'csep-csv'
104+
format (str): ('native', 'csep') determines whether the catalog should be converted into the csep
105+
formatted catalog or kept as native.
106+
107+
Returns (:class:`~csep.core.catalogs.AbstractBaseCatalog`)
108+
"""
109+
110+
if type not in ('ucerf3', 'csep-csv', 'zmap', 'jma-csv', 'ndk') and loader is None:
111+
raise ValueError("type must be one of the following: ('ucerf3', 'csep-csv', 'zmap', 'jma-csv', 'ndk').")
112+
113+
# map to correct catalog class, at some point these could be abstracted into configuration file
114+
# this maps a human readable string to the correct catalog class and the correct loader function
115+
class_loader_mapping = {
116+
'ucerf3': {
117+
'class': catalogs.UCERF3Catalog,
118+
'loader': None
119+
},
120+
'csep-csv': {
121+
'class': catalogs.CSEPCatalog,
122+
'loader': readers.csep_ascii
123+
},
124+
'zmap': {
125+
'class': catalogs.CSEPCatalog,
126+
'loader': readers.zmap_ascii
127+
},
128+
'jma-csv': {
129+
'class': catalogs.CSEPCatalog,
130+
'loader': readers.jma_csv,
131+
},
132+
'ndk': {
133+
'class': catalogs.CSEPCatalog,
134+
'loader': readers.ndk
135+
}
136+
}
137+
138+
# treat json files using the from_dict() member instead of constructor
139+
catalog_class = class_loader_mapping[type]['class']
140+
if os.path.splitext(filename)[-1][1:] == 'json':
141+
catalog = catalog_class.load_json(filename, **kwargs)
142+
else:
143+
if loader is None:
144+
loader = class_loader_mapping[type]['loader']
145+
146+
catalog = catalog_class.load_catalog(filename=filename, loader=loader, **kwargs)
147+
148+
# convert to csep format if needed
149+
if format == 'native':
150+
return_val = catalog
151+
elif format == 'csep':
152+
return_val = catalog.get_csep_format()
153+
else:
154+
raise ValueError('format must be either "native" or "csep"')
155+
return return_val
156+
157+
158+
def query_comcat(start_time, end_time, min_magnitude=2.50,
159+
min_latitude=31.50, max_latitude=43.00,
160+
min_longitude=-125.40, max_longitude=-113.10, verbose=True, **kwargs):
161+
"""
162+
Access Comcat catalog through web service
163+
164+
Args:
165+
start_time: datetime object of start of catalog
166+
end_time: datetime object for end of catalog
167+
min_magnitude: minimum magnitude to query
168+
min_latitude: maximum magnitude to query
169+
max_latitude: max latitude of bounding box
170+
min_longitude: min latitude of bounding box
171+
max_longitude: max longitude of bounding box
172+
region: :class:`csep.core.regions.CartesianGrid2D
173+
verbose (bool): print catalog summary statistics
174+
175+
Returns:
176+
:class:`csep.core.catalogs.ComcatCatalog
177+
"""
178+
179+
# Timezone should be in UTC
180+
t0 = time.time()
181+
eventlist = readers._query_comcat(start_time=start_time, end_time=end_time,
182+
min_magnitude=min_magnitude,
183+
min_latitude=min_latitude, max_latitude=max_latitude,
184+
min_longitude=min_longitude, max_longitude=max_longitude)
185+
t1 = time.time()
186+
comcat = catalogs.CSEPCatalog(data=eventlist, date_accessed=utc_now_datetime(), **kwargs)
187+
print("Fetched ComCat catalog in {} seconds.\n".format(t1 - t0))
188+
if verbose:
189+
print("Downloaded catalog from ComCat with following parameters")
190+
print("Start Date: {}\nEnd Date: {}".format(str(comcat.start_time), str(comcat.end_time)))
191+
print("Min Latitude: {} and Max Latitude: {}".format(comcat.min_latitude, comcat.max_latitude))
192+
print("Min Longitude: {} and Max Longitude: {}".format(comcat.min_longitude, comcat.max_longitude))
193+
print("Min Magnitude: {}".format(comcat.min_magnitude))
194+
print(f"Found {comcat.event_count} events in the ComCat catalog.")
195+
return comcat
196+
197+
198+
def load_evaluation_result(fname):
199+
""" Load evaluation result stored as json file
200+
201+
Returns:
202+
:class:`csep.core.evaluations.EvaluationResult`
203+
204+
"""
205+
# tries to return the correct class for the evaluation result. if it cannot find the type simply returns the basic result.
206+
evaluation_result_factory = {
207+
'default': EvaluationResult,
208+
'CatalogNumberTestResult': CatalogNumberTestResult
209+
}
210+
with open(fname, 'r') as json_file:
211+
json_dict = json.load(json_file)
212+
try:
213+
evaluation_type = json_dict['named_type']
214+
except:
215+
evaluation_type = 'default'
216+
eval_result = evaluation_result_factory[evaluation_type].from_dict(json_dict)
217+
return eval_result
218+
219+
220+
def load_gridded_forecast(fname, loader=None, **kwargs):
221+
""" Loads grid based forecast from hard-disk.
222+
223+
The function loads the forecast provided with at the filepath defined by fname. The function attempts to understand
224+
the file format based on the extension of the filepath. Optionally, if loader function is provided, that function
225+
will be used to load the forecast. The loader function should return a :class:`csep.core.forecasts.GriddedForecast`
226+
class with the region and magnitude members correctly assigned.
227+
228+
File extensions:
229+
.dat -> CSEP ascii format
230+
.xml -> CSEP xml format (not yet implemented)
231+
.h5 -> CSEP hdf5 format (not yet implemented)
232+
.bin -> CSEP binary format (not yet implemented)
233+
234+
Args:
235+
fname (str): path of grid based forecast
236+
loader (func): function to load forecast in bespoke format needs to return :class:`csep.core.forecasts.GriddedForecast`
237+
and first argument should be required and the filename of the forecast to load
238+
called as loader(func, **kwargs).
239+
240+
**kwargs: passed into loader function
241+
242+
Throws:
243+
FileNotFoundError: when the file extension is not known and a loader is not provided.
244+
AttributeError: if loader is provided and is not callable.
245+
246+
Returns:
247+
:class:`csep.core.forecasts.GriddedForecast`
248+
"""
249+
# mapping from file extension to loader function, new formats by default they need to be added here
250+
forecast_loader_mapping = {
251+
'dat': GriddedForecast.load_ascii,
252+
'xml': None,
253+
'h5': None,
254+
'bin': None
255+
}
256+
257+
# sanity checks
258+
if not os.path.exists(fname):
259+
raise FileNotFoundError(f"Could not locate file {fname}. Unable to load forecast.")
260+
# sanity checks
261+
if loader is not None and not callable(loader):
262+
raise AttributeError("Loader must be callable. Unable to load forecast.")
263+
extension = os.path.splitext(fname)[-1][1:]
264+
if extension not in forecast_loader_mapping.keys() and loader is None:
265+
raise AttributeError("File extension should be in ('dat','xml','h5','bin') if loader not provided.")
266+
267+
if extension in ('xml','h5','bin'):
268+
raise NotImplementedError
269+
270+
# assign default loader
271+
if loader is None:
272+
loader = forecast_loader_mapping[extension]
273+
forecast = loader(fname, **kwargs)
274+
# final sanity check
275+
if not isinstance(forecast, GriddedForecast):
276+
raise ValueError("Forecast not instance of GriddedForecast")
277+
return forecast
278+
279+
280+
def load_catalog_forecast(fname, catalog_loader=None, format='native', type='ascii', **kwargs):
281+
""" General function to handle loading catalog forecasts.
282+
283+
Currently, just a simple wrapper, but can contain more complex logic in the future.
284+
285+
Args:
286+
fname (str): pathname to the forecast file or directory containing the forecast files
287+
catalog_loader (func): callable that can load catalogs, see load_stochastic_event_sets above.
288+
format (str): either 'native' or 'csep'. if 'csep', will attempt to be returned into csep catalog format. used to convert between
289+
observed_catalog type.
290+
type (str): either 'ucerf3' or 'csep', determines the catalog format of the forecast. if loader is provided, then
291+
this parameter is ignored.
292+
**kwargs: other keyword arguments passed to the :class:`csep.core.forecasts.CatalogForecast`.
293+
294+
Returns:
295+
:class:`csep.core.forecasts.CatalogForecast`
296+
"""
297+
# sanity checks
298+
if not os.path.exists(fname):
299+
raise FileNotFoundError(f"Could not locate file {fname}. Unable to load forecast.")
300+
# sanity checks
301+
if catalog_loader is not None and not callable(catalog_loader):
302+
raise AttributeError("Loader must be callable. Unable to load forecast.")
303+
# factory methods for loading different types of catalogs
304+
catalog_loader_mapping = {
305+
'ascii': catalogs.CSEPCatalog.load_ascii_catalogs,
306+
'ucerf3': catalogs.UCERF3Catalog.load_catalogs
307+
}
308+
if catalog_loader is None:
309+
catalog_loader = catalog_loader_mapping[type]
310+
# try and parse information from filename and send to forecast constructor
311+
if format == 'native' and type=='ascii':
312+
try:
313+
basename = str(os.path.basename(fname.rstrip('/')).split('.')[0])
314+
split_fname = basename.split('_')
315+
name = split_fname[0]
316+
start_time = strptime_to_utc_datetime(split_fname[1], format="%Y-%m-%dT%H-%M-%S-%f")
317+
# update kwargs
318+
_ = kwargs.setdefault('name', name)
319+
_ = kwargs.setdefault('start_time', start_time)
320+
except:
321+
pass
322+
# create observed_catalog forecast
323+
return CatalogForecast(filename=fname, loader=catalog_loader, catalog_format=format, catalog_type=type, **kwargs)
324+

0 commit comments

Comments
 (0)