Skip to content

Commit 7206a3b

Browse files
committed
routed spatial_counts and spatial_magnitude_counts through region class
- added acceptance tests for these functions - fixed #121 -- issue with bin1d_vec for scalar values
1 parent 40c28bb commit 7206a3b

File tree

4 files changed

+92
-43
lines changed

4 files changed

+92
-43
lines changed

csep/core/catalogs.py

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -662,15 +662,12 @@ def spatial_counts(self):
662662
if self.region is None:
663663
raise CSEPSchedulerException("Cannot create binned rates without region information.")
664664

665-
# todo: this should be routed through self.region to allow for different types of regions
666-
output = regions._bin_catalog_spatial_counts(self.get_longitudes(),
667-
self.get_latitudes(),
668-
self.region.num_nodes,
669-
self.region.bbox_mask,
670-
self.region.idx_map,
671-
self.region.xs,
672-
self.region.ys)
673-
return output
665+
n_poly = self.region.num_nodes
666+
event_counts = numpy.zeros(n_poly)
667+
# this function could throw ValueError if points are outside of the region
668+
idx = self.region.get_index_of(self.get_longitudes(), self.get_latitudes())
669+
numpy.add.at(event_counts, idx, 1)
670+
return event_counts
674671

675672
def spatial_event_probability(self):
676673
# make sure region is specified with catalog
@@ -724,16 +721,14 @@ def magnitude_counts(self, mag_bins=None, tol=0.00001, retbins=False):
724721
else:
725722
return out
726723

727-
def spatial_magnitude_counts(self, mag_bins=None, tol=0.00001, ret_skipped=False):
724+
def spatial_magnitude_counts(self, mag_bins=None, tol=0.00001):
728725
""" Return counts of events in space-magnitude region.
729726
730727
We figure out the index of the polygons and create a map that relates the spatial coordinate in the
731728
Cartesian grid with with the polygon in region.
732729
733730
Args:
734731
mag_bins: magnitude bins (optional). tries to use magnitue bins associated with region
735-
ret_skipped (bool): if true, will return list of (lon, lat, mw) tuple of skipped points
736-
737732
738733
Returns:
739734
output: unnormalized event count in each bin, 1d ndarray where index corresponds to midpoints
@@ -747,33 +742,22 @@ def spatial_magnitude_counts(self, mag_bins=None, tol=0.00001, ret_skipped=False
747742
if self.region.magnitudes is None and mag_bins is None:
748743
raise CSEPCatalogException("Region must have magnitudes or mag_bins must be defined to "
749744
"compute space magnitude binning.")
750-
745+
# prefer user supplied mag_bins
746+
if mag_bins is None:
747+
mag_bins = self.region.magnitudes
751748
# short-circuit if zero-events in catalog... return array of zeros
752-
if self.event_count == 0:
753-
n_poly = self.region.num_nodes
754-
n_mws = self.region.num_mag_bins
755-
output = numpy.zeros((n_poly, n_mws))
756-
skipped = []
757-
else:
758-
if mag_bins is None:
759-
mag_bins = self.region.magnitudes
760-
761-
# compute if not
762-
# todo: this should be routed through self.region to allow for different types of regions
763-
output, skipped = regions._bin_catalog_spatio_magnitude_counts(self.get_longitudes(),
764-
self.get_latitudes(),
765-
self.get_magnitudes(),
766-
self.region.num_nodes,
767-
self.region.bbox_mask,
768-
self.region.idx_map,
769-
self.region.xs,
770-
self.region.ys,
771-
mag_bins,
772-
tol=tol)
773-
if ret_skipped:
774-
return output, skipped
775-
else:
776-
return output
749+
n_poly = self.region.num_nodes
750+
event_counts = numpy.zeros((n_poly, len(mag_bins)))
751+
if self.event_count != 0:
752+
# this will throw ValueError if points outside range
753+
spatial_idx = self.region.get_index_of(self.get_longitudes(), self.get_latitudes())
754+
# also throwing the same error
755+
mag_idx = bin1d_vec(self.get_magnitudes(), mag_bins, tol=tol, right_continuous=True)
756+
for idx in range(spatial_idx.shape[0]):
757+
if mag_idx[idx] == -1:
758+
raise ValueError("at least one magnitude value outside of the valid region.")
759+
event_counts[(spatial_idx[idx], mag_idx[idx])] += 1
760+
return event_counts
777761

778762
def length_in_seconds(self):
779763
""" Returns catalog length in seconds assuming that the catalog is sorted by time. """

csep/utils/calc.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,13 @@ def bin1d_vec(p, bins, tol=None, right_continuous=False):
9090
if right_continuous:
9191
# set upper bin index to last
9292
try:
93-
idx[(idx >= len(bins) - 1)] = len(bins) - 1
9493
idx[(idx < 0)] = -1
94+
idx[(idx >= len(bins) - 1)] = len(bins) - 1
9595
except (TypeError):
9696
if idx >= len(bins) - 1:
9797
idx = len(bins) - 1
98+
if idx < 0:
99+
idx = -1
98100
else:
99101
try:
100102
idx[((idx < 0) | (idx >= len(bins)))] = -1

tests/test_calc.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,18 @@ def test_less_and_greater_than(self):
126126
expected = [-1, 3, -1]
127127
self.assertListEqual(test.tolist(), expected)
128128

129+
def test_scalar_outside(self):
130+
from csep.utils.calc import bin1d_vec
131+
mbins = numpy.arange(5.95, 9, 0.1) # This gives bins from 5.95 to 8.95
132+
idx = bin1d_vec(5.95, mbins, tol=0.00001, right_continuous=True)
133+
self.assertEqual(idx, 0)
134+
135+
idx = bin1d_vec(6, mbins, tol=0.00001, right_continuous=True) # This would give 0: Which is fine.
136+
self.assertEqual(idx, 0)
137+
138+
idx = bin1d_vec(5, mbins, tol=0.00001, right_continuous=True)
139+
self.assertEqual(idx, -1)
140+
141+
idx = bin1d_vec(4, mbins, tol=0.00001, right_continuous=True)
142+
self.assertEqual(idx, -1)
143+

tests/test_catalog.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import csep
99
from csep.core import regions, forecasts
1010
from csep.utils.time_utils import strptime_to_utc_epoch, strptime_to_utc_datetime
11-
from csep.core.catalogs import CSEPCatalog
11+
from csep.core.catalogs import CSEPCatalog, AbstractBaseCatalog
1212
from csep.core.regions import CartesianGrid2D, Polygon, compute_vertices
1313

1414
def comcat_path():
@@ -17,6 +17,20 @@ def comcat_path():
1717
'test_catalog.csv')
1818
return data_dir
1919

20+
class MockCatalog(AbstractBaseCatalog):
21+
22+
def __init__(self):
23+
super().__init__()
24+
25+
def get_number_of_events(self):
26+
return len(self.get_latitudes())
27+
28+
def get_longitudes(self):
29+
return numpy.array([0.05, 0.15])
30+
31+
def get_latitudes(self):
32+
return numpy.array([0.15, 0.05])
33+
2034
class CatalogFiltering(unittest.TestCase):
2135
def setUp(self):
2236

@@ -108,7 +122,7 @@ def test_catalog_binning_and_filtering_acceptance(self):
108122
)
109123

110124
# read catalog
111-
comcat = csep.load_catalog(comcat_path(), region=region)
125+
comcat = csep.load_catalog(comcat_path(), region=region).filter(f"magnitude >= 4.5")
112126

113127
# create data set from data set
114128
d = forecasts.MarkedGriddedDataSet(
@@ -119,6 +133,7 @@ def test_catalog_binning_and_filtering_acceptance(self):
119133

120134
for idm, m_min in enumerate(d.magnitudes):
121135
# catalog filtered cumulative
136+
print(m_min)
122137
c = comcat.filter([f'magnitude >= {m_min}'], in_place=False)
123138
# catalog filtered incrementally
124139
c_int = comcat.filter([f'magnitude >= {m_min}', f'magnitude < {m_min + 0.1}'], in_place=False)
@@ -127,9 +142,42 @@ def test_catalog_binning_and_filtering_acceptance(self):
127142
# incremental counts
128143
gs_int = d.data[:, idm].sum()
129144
# event count from filtered catalog and events in binned data should be the same
130-
numpy.testing.assert_almost_equal(gs, c.event_count)
131-
numpy.testing.assert_almost_equal(gs_int, c_int.event_count)
145+
numpy.testing.assert_equal(gs, c.event_count)
146+
numpy.testing.assert_equal(gs_int, c_int.event_count)
132147

148+
def test_bin_spatial_counts(self):
149+
""" this will test both good and bad points within the region.
150+
151+
1) 2 inside the domain
152+
2) outside the bbox but not in domain
153+
3) completely outside the domain
154+
155+
we will check that only 1 event is placed in the grid and ensure its location is correct.
156+
"""
157+
# create some arbitrary grid
158+
nx = 8
159+
ny = 10
160+
dh = 0.1
161+
x_points = numpy.arange(nx) * dh
162+
y_points = numpy.arange(ny) * dh
163+
origins = list(itertools.product(x_points, y_points))
164+
# grid is missing first and last block
165+
origins.pop(0)
166+
origins.pop(-1)
167+
cart_grid = CartesianGrid2D(
168+
[Polygon(bbox) for bbox in compute_vertices(origins, dh)],
169+
dh
170+
)
171+
catalog = MockCatalog()
172+
catalog.region = cart_grid
173+
test_result = catalog.spatial_counts()
174+
175+
# we have tested 2 inside the domain
176+
self.assertEqual(numpy.sum(test_result), 2)
177+
# we know that (0.05, 0.15) corresponds to index 0
178+
self.assertEqual(test_result[0], 1)
179+
# we know that (0.15, 0.05) corresponds too index 9
180+
self.assertEqual(test_result[9], 1)
133181

134182
if __name__ == '__main__':
135183
unittest.main()

0 commit comments

Comments
 (0)