Skip to content

Commit c6f01f3

Browse files
authored
address catalog binning issue and minor hotfix for catalog evaluations (#89)
* fixes issue #86 and adds more stringent testing for spatial regions - allow calibration tests to properly handle lists of evaluation results with not-valid test results - change to numpy.testing.assert_array_equal() from assert_allclose() - include function to generate cleaner_range() to represent nodes of carteisan grid - added unit tests for cleaner_range() - updated CartesianGrid2D region to use cleaner_range() function to generate bbox grid - added test using example earthquakes listed in #86
1 parent 2304f7c commit c6f01f3

File tree

7 files changed

+96
-30
lines changed

7 files changed

+96
-30
lines changed

csep/core/catalog_evaluations.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ def spatial_test(forecast, observed_catalog):
124124

125125
return result
126126

127-
128127
def magnitude_test(forecast, observed_catalog):
129128
""" Performs magnitude test for catalog-based forecasts """
130129
test_distribution = []
@@ -292,7 +291,13 @@ def calibration_test(evaluation_results, delta_1=False):
292291

293292
# this is using "delta_2" which is the cdf value less-equal
294293
idx = 0 if delta_1 else 1
295-
quantiles = [result.quantile[idx] for result in evaluation_results]
294+
quantiles = []
295+
for result in evaluation_results:
296+
if result.status == 'not-valid':
297+
print(f'evaluation not valid for {result.name}. skipping in calibration test.')
298+
else:
299+
quantiles.append(result.quantile[idx])
300+
296301
ks, p_value = scipy.stats.kstest(quantiles, 'uniform')
297302

298303
result = CalibrationTestResult(

csep/core/forecasts.py

+1
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ def get_magnitude_index(self, mags, tol=0.00001):
217217
raise ValueError("mags outside the range of forecast magnitudes.")
218218
return idm
219219

220+
220221
class GriddedForecast(MarkedGriddedDataSet):
221222
""" Class to represent grid-based forecasts """
222223

csep/core/regions.py

+7-16
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import pyproj
1212

1313
# PyCSEP imports
14-
from csep.utils.calc import bin1d_vec
14+
from csep.utils.calc import bin1d_vec, cleaner_range
1515
from csep.utils.scaling_relationships import WellsAndCoppersmith
1616

1717
def california_relm_collection_region(dh_scale=1, magnitudes=None, name="relm-california-collection"):
@@ -172,14 +172,9 @@ def global_region(dh=0.1, name="global", magnitudes=None):
172172
csep.utils.CartesianGrid2D:
173173
"""
174174
# generate latitudes
175-
const = 1000000
176-
start_lat = numpy.floor(-90 * const)
177-
end_lat = numpy.floor(90 * const)
178-
start_lon = numpy.floor(-180 * const)
179-
end_lon = numpy.floor(180 * const)
180-
d = numpy.floor(const * dh)
181-
lats = numpy.arange(start_lat, end_lat, d) / const
182-
lons = numpy.arange(start_lon, end_lon, d) / const
175+
176+
lons = cleaner_range(-180.0, 179.9, dh)
177+
lats = cleaner_range(-90, 89.9, dh)
183178
coords = itertools.product(lons,lats)
184179
region = CartesianGrid2D([Polygon(bbox) for bbox in compute_vertices(coords, dh)], dh, name=name)
185180
if magnitudes is not None:
@@ -707,13 +702,9 @@ def _build_bitmask_vec(self):
707702
# get midpoints for hashing
708703
midpoints = numpy.array([poly.centroid() for poly in self.polygons])
709704

710-
# compute nx and ny
711-
nx = numpy.rint((bbox[1][0] - bbox[0][0]) / self.dh)
712-
ny = numpy.rint((bbox[1][1] - bbox[0][1]) / self.dh)
713-
714-
# set up grid of bounding box
715-
xs = self.dh * numpy.arange(nx + 1) + bbox[0][0]
716-
ys = self.dh * numpy.arange(ny + 1) + bbox[0][1]
705+
# set up grid over bounding box
706+
xs = cleaner_range(bbox[0][0], bbox[1][0], self.dh)
707+
ys = cleaner_range(bbox[0][1], bbox[1][1], self.dh)
717708

718709
# set up mask array, 1 is index 0 is mask
719710
a = numpy.ones([len(ys), len(xs), 2])

csep/utils/calc.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,25 @@ def _distribution_test(stochastic_event_set_data, observation_data):
192192
# score evaluation
193193
_, quantile = get_quantiles(test_distribution, d_obs)
194194

195-
return test_distribution, d_obs, quantile
195+
return test_distribution, d_obs, quantile
196+
197+
def cleaner_range(start, end, h):
198+
""" Returns array holding bin edges that doesn't contain floating point wander.
199+
200+
Floating point wander can occur when repeatedly adding floating point numbers together. The errors propogate and become worse over the sum. This function generates the
201+
values on an integer grid and converts back to floating point numbers through multiplication.
202+
203+
Args:
204+
start (float)
205+
end (float)
206+
h (float): magnitude spacing
207+
208+
Returns:
209+
bin_edges (numpy.ndarray)
210+
"""
211+
# convert to integers to prevent accumulating floating point errors
212+
const = 100000
213+
start = numpy.floor(const * start)
214+
end = numpy.floor(const * end)
215+
d = const * h
216+
return numpy.arange(start, end + d / 2, d) / const

run_tests.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#!/usr/bin/env bash
2-
pytest
2+
pytest -v

tests/test_calc.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
import unittest
2-
from csep.utils.calc import bin1d_vec
2+
import numpy
3+
from csep.utils.calc import bin1d_vec, cleaner_range
4+
5+
6+
class TestCleanerRange(unittest.TestCase):
7+
8+
def setUp(self):
9+
10+
self.start = 0.0
11+
self.end = 0.9
12+
self.dh = 0.1
13+
self.truth = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
14+
15+
def test_discrepancy_with_arange_catch_failure(self):
16+
17+
ar = numpy.arange(self.start, self.end + self.dh / 2, self.dh)
18+
cr = cleaner_range(self.start, self.end, self.dh)
19+
20+
self.assertRaises(AssertionError, numpy.testing.assert_array_equal, ar, cr)
21+
self.assertRaises(AssertionError, numpy.testing.assert_array_equal, ar, self.truth)
22+
23+
24+
def test_discrepancy_with_direct_input(self):
25+
26+
cr = cleaner_range(self.start, self.end, self.dh)
27+
numpy.testing.assert_array_equal(self.truth, cr)
328

429
class TestBin1d(unittest.TestCase):
530

tests/test_spatial.py

+32-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import numpy
66

77
from csep.core.regions import CartesianGrid2D, compute_vertex, compute_vertices, _bin_catalog_spatio_magnitude_counts, \
8-
_bin_catalog_spatial_counts, _bin_catalog_probability, Polygon
8+
_bin_catalog_spatial_counts, _bin_catalog_probability, Polygon, global_region
99

1010

1111
class TestPolygon(unittest.TestCase):
@@ -63,27 +63,31 @@ def test_object_creation(self):
6363
self.assertEqual(self.cart_grid.num_nodes, self.num_nodes, 'num nodes is not correct')
6464

6565
def test_xs_and_xy_correct(self):
66-
numpy.testing.assert_allclose(self.cart_grid.xs, numpy.arange(0,self.nx)*self.dh)
67-
numpy.testing.assert_allclose(self.cart_grid.ys, numpy.arange(0,self.ny)*self.dh)
66+
67+
test_xs = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]
68+
test_ys = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
69+
70+
numpy.testing.assert_array_equal(self.cart_grid.xs, test_xs)
71+
numpy.testing.assert_array_equal(self.cart_grid.ys, test_ys)
6872

6973
def test_bitmask_indices_mapping(self):
7074
test_idx = self.cart_grid.idx_map[1,0]
71-
numpy.testing.assert_allclose(test_idx, 0, err_msg='mapping for first polygon index (good) not correct')
75+
numpy.testing.assert_array_equal(test_idx, 0, err_msg='mapping for first polygon index (good) not correct')
7276

7377
test_idx = self.cart_grid.idx_map[0,1]
74-
numpy.testing.assert_allclose(test_idx, 9, err_msg='mapping for polygon (good) not correct.')
78+
numpy.testing.assert_array_equal(test_idx, 9, err_msg='mapping for polygon (good) not correct.')
7579

7680
test_idx = self.cart_grid.idx_map[2,0]
77-
numpy.testing.assert_allclose(test_idx, 1, err_msg='mapping for polygon (good) not correct.')
81+
numpy.testing.assert_array_equal(test_idx, 1, err_msg='mapping for polygon (good) not correct.')
7882

7983
test_idx = self.cart_grid.idx_map[0,2]
80-
numpy.testing.assert_allclose(test_idx, 19, err_msg='mapping for polygon (good) not correct.')
84+
numpy.testing.assert_array_equal(test_idx, 19, err_msg='mapping for polygon (good) not correct.')
8185

8286
test_idx = self.cart_grid.idx_map[-1,-1]
83-
numpy.testing.assert_allclose(test_idx, numpy.nan, err_msg='mapping for last index (bad) not correct.')
87+
numpy.testing.assert_array_equal(test_idx, numpy.nan, err_msg='mapping for last index (bad) not correct.')
8488

8589
test_idx = self.cart_grid.idx_map[0,0]
86-
numpy.testing.assert_allclose(test_idx, numpy.nan, err_msg='mapping for first index (bad) not correct.')
90+
numpy.testing.assert_array_equal(test_idx, numpy.nan, err_msg='mapping for first index (bad) not correct.')
8791

8892
def test_domain_mask(self):
8993
test_flag = self.cart_grid.bbox_mask[0, 0]
@@ -222,5 +226,24 @@ def test_bin_spatial_magnitudes(self):
222226
self.assertEqual(test_result[0, 1], 1)
223227
self.assertEqual(test_result[9, 0], 1)
224228

229+
230+
def test_global_region_binning(self):
231+
232+
gr = global_region()
233+
234+
# test points
235+
lons = numpy.array([-178.6, -178.6, -178.02, -177.73, -177.79])
236+
lats = numpy.array([-15.88, -51.75, -30.61, -29.98, -30.6])
237+
238+
# directly compute the indexes from the region object
239+
idxs = gr.get_index_of(lons, lats)
240+
for i, idx in enumerate(idxs):
241+
found_poly = gr.polygons[idx]
242+
lon = lons[i]
243+
lat = lats[i]
244+
245+
assert lon >= found_poly.points[1][0] and lon < found_poly.points[2][0]
246+
assert lat >= found_poly.points[0][1] and lat < found_poly.points[2][1]
247+
225248
if __name__ == '__main__':
226249
unittest.main()

0 commit comments

Comments
 (0)