Repository: SCECcode/pycsep (language: python)

Issue:
accumulating floating point errors for magnitude bins
magnitude bins that are created using a function like `numpy.arange` can cause issues when computing offsets into the bins. this problem is exacerbated by the fact that most magnitudes are reported on bin edges and can cause representation issues.

first, magnitude bins should not be created by adding decimal bin widths to an offset. they should be created using integers and then converted to decimal. 

second, we can add a tolerance to the reported magnitude units and account for the `abs_err` in the floating point numbers.



Discussion / hints:

Fix patch:
diff --git a/csep/__init__.py b/csep/__init__.py
index 2068138c..e387f493 100644
--- a/csep/__init__.py
+++ b/csep/__init__.py
@@ -1,21 +1,30 @@
+import json
+import os
+import time
+
+
 from csep.core import forecasts
 from csep.core import catalogs
 from csep.core import poisson_evaluations
 from csep.core import catalog_evaluations
+from csep.core import regions
 from csep.core.repositories import (
     load_json,
     write_json
 )
-from csep.io import (
-    load_stochastic_event_sets,
-    load_catalog,
-    query_comcat,
-    load_evaluation_result,
-    load_gridded_forecast,
-    load_catalog_forecast
-)
 
 from csep.utils import datasets
+from csep.utils import readers
+
+from csep.core.forecasts import GriddedForecast, CatalogForecast
+from csep.models import EvaluationResult, CatalogNumberTestResult
+from csep.utils.time_utils import (
+    utc_now_datetime,
+    strptime_to_utc_datetime,
+    datetime_to_utc_epoch,
+    epoch_time_to_utc_datetime,
+    utc_now_epoch
+)
 
 # this defines what is imported on a `from csep import *`
 __all__ = [
@@ -23,6 +32,7 @@
     'write_json',
     'catalogs',
     'datasets',
+    'regions',
     'poisson_evaluations',
     'catalog_evaluations',
     'forecasts',
@@ -31,5 +41,284 @@
     'query_comcat',
     'load_evaluation_result',
     'load_gridded_forecast',
-    'load_catalog_forecast'
-]
\ No newline at end of file
+    'load_catalog_forecast',
+    'utc_now_datetime',
+    'strptime_to_utc_datetime',
+    'datetime_to_utc_epoch',
+    'epoch_time_to_utc_datetime',
+    'utc_now_epoch'
+]
+
+def load_stochastic_event_sets(filename, type='csv', format='native', **kwargs):
+    """ General function to load stochastic event sets
+
+    This function returns a generator to iterate through a collection of catalogs.
+    To load a forecast and include metadata use :func:`csep.load_catalog_forecast`.
+
+    Args:
+        filename (str): name of file or directory where stochastic event sets live.
+        type (str): either 'ucerf3' or 'csep' depending on the type of observed_catalog to load
+        format (str): ('csep' or 'native') if native catalogs are not converted to csep format.
+        kwargs (dict): see the documentation of that class corresponding to the type you selected
+                         for the kwargs options
+
+    Returns:
+        (generator): :class:`~csep.core.catalogs.AbstractBaseCatalog`
+
+    """
+    if type not in ('ucerf3', 'csv'):
+        raise ValueError("type must be one of the following: (ucerf3)")
+
+    # use mapping to dispatch to correct function
+    # in general, stochastic event sets are loaded with classmethods and single catalogs use the
+    # constructor
+    mapping = {'ucerf3': catalogs.UCERF3Catalog.load_catalogs,
+               'csv': catalogs.CSEPCatalog.load_ascii_catalogs}
+
+    # dispatch to proper loading function
+    result = mapping[type](filename, **kwargs)
+
+    # factory function to load catalogs from different classes
+    while True:
+        try:
+            catalog = next(result)
+        except StopIteration:
+            return
+        except Exception:
+            raise
+        if format == 'native':
+            yield catalog
+        elif format == 'csep':
+            yield catalog.get_csep_format()
+        else:
+            raise ValueError('format must be either "native" or "csep!')
+
+
+def load_catalog(filename, type='csep-csv', format='native', loader=None, **kwargs):
+    """ General function to load single catalog
+
+    See corresponding class documentation for additional parameters.
+
+    Args:
+        type (str): ('ucerf3', 'csep-csv', 'zmap', 'jma-csv', 'ndk') default is 'csep-csv'
+        format (str): ('native', 'csep') determines whether the catalog should be converted into the csep
+                      formatted catalog or kept as native.
+
+    Returns (:class:`~csep.core.catalogs.AbstractBaseCatalog`)
+    """
+
+    if type not in ('ucerf3', 'csep-csv', 'zmap', 'jma-csv', 'ndk') and loader is None:
+        raise ValueError("type must be one of the following: ('ucerf3', 'csep-csv', 'zmap', 'jma-csv', 'ndk').")
+
+    # map to correct catalog class, at some point these could be abstracted into configuration file
+    # this maps a human readable string to the correct catalog class and the correct loader function
+    class_loader_mapping = {
+        'ucerf3': {
+            'class': catalogs.UCERF3Catalog,
+            'loader': None
+        },
+        'csep-csv': {
+            'class': catalogs.CSEPCatalog,
+            'loader': readers.csep_ascii
+        },
+        'zmap': {
+            'class': catalogs.CSEPCatalog,
+            'loader': readers.zmap_ascii
+        },
+        'jma-csv': {
+            'class': catalogs.CSEPCatalog,
+            'loader': readers.jma_csv,
+        },
+        'ndk': {
+            'class': catalogs.CSEPCatalog,
+            'loader': readers.ndk
+        }
+    }
+
+    # treat json files using the from_dict() member instead of constructor
+    catalog_class = class_loader_mapping[type]['class']
+    if os.path.splitext(filename)[-1][1:] == 'json':
+        catalog = catalog_class.load_json(filename, **kwargs)
+    else:
+        if loader is None:
+            loader = class_loader_mapping[type]['loader']
+
+        catalog = catalog_class.load_catalog(filename=filename, loader=loader, **kwargs)
+
+    # convert to csep format if needed
+    if format == 'native':
+        return_val = catalog
+    elif format == 'csep':
+        return_val = catalog.get_csep_format()
+    else:
+        raise ValueError('format must be either "native" or "csep"')
+    return return_val
+
+
+def query_comcat(start_time, end_time, min_magnitude=2.50,
+                 min_latitude=31.50, max_latitude=43.00,
+                 min_longitude=-125.40, max_longitude=-113.10, verbose=True, **kwargs):
+    """
+    Access Comcat catalog through web service
+
+    Args:
+        start_time: datetime object of start of catalog
+        end_time: datetime object for end of catalog
+        min_magnitude: minimum magnitude to query
+        min_latitude:  maximum magnitude to query
+        max_latitude: max latitude of bounding box
+        min_longitude: min latitude of bounding box
+        max_longitude: max longitude of bounding box
+        region: :class:`csep.core.regions.CartesianGrid2D
+        verbose (bool): print catalog summary statistics
+
+    Returns:
+        :class:`csep.core.catalogs.ComcatCatalog
+    """
+
+    # Timezone should be in UTC
+    t0 = time.time()
+    eventlist = readers._query_comcat(start_time=start_time, end_time=end_time,
+                           min_magnitude=min_magnitude,
+                           min_latitude=min_latitude, max_latitude=max_latitude,
+                           min_longitude=min_longitude, max_longitude=max_longitude)
+    t1 = time.time()
+    comcat = catalogs.CSEPCatalog(data=eventlist, date_accessed=utc_now_datetime(), **kwargs)
+    print("Fetched ComCat catalog in {} seconds.\n".format(t1 - t0))
+    if verbose:
+        print("Downloaded catalog from ComCat with following parameters")
+        print("Start Date: {}\nEnd Date: {}".format(str(comcat.start_time), str(comcat.end_time)))
+        print("Min Latitude: {} and Max Latitude: {}".format(comcat.min_latitude, comcat.max_latitude))
+        print("Min Longitude: {} and Max Longitude: {}".format(comcat.min_longitude, comcat.max_longitude))
+        print("Min Magnitude: {}".format(comcat.min_magnitude))
+        print(f"Found {comcat.event_count} events in the ComCat catalog.")
+    return comcat
+
+
+def load_evaluation_result(fname):
+    """ Load evaluation result stored as json file
+
+    Returns:
+        :class:`csep.core.evaluations.EvaluationResult`
+
+    """
+    # tries to return the correct class for the evaluation result. if it cannot find the type simply returns the basic result.
+    evaluation_result_factory = {
+        'default': EvaluationResult,
+        'CatalogNumberTestResult': CatalogNumberTestResult
+    }
+    with open(fname, 'r') as json_file:
+        json_dict = json.load(json_file)
+        try:
+            evaluation_type = json_dict['named_type']
+        except:
+            evaluation_type = 'default'
+    eval_result = evaluation_result_factory[evaluation_type].from_dict(json_dict)
+    return eval_result
+
+
+def load_gridded_forecast(fname, loader=None, **kwargs):
+    """ Loads grid based forecast from hard-disk.
+
+    The function loads the forecast provided with at the filepath defined by fname. The function attempts to understand
+    the file format based on the extension of the filepath. Optionally, if loader function is provided, that function
+    will be used to load the forecast. The loader function should return a :class:`csep.core.forecasts.GriddedForecast`
+    class with the region and magnitude members correctly assigned.
+
+    File extensions:
+        .dat -> CSEP ascii format
+        .xml -> CSEP xml format (not yet implemented)
+        .h5 -> CSEP hdf5 format (not yet implemented)
+        .bin -> CSEP binary format (not yet implemented)
+
+    Args:
+        fname (str): path of grid based forecast
+        loader (func): function to load forecast in bespoke format needs to return :class:`csep.core.forecasts.GriddedForecast`
+                       and first argument should be required and the filename of the forecast to load
+                       called as loader(func, **kwargs).
+
+        **kwargs: passed into loader function
+
+    Throws:
+        FileNotFoundError: when the file extension is not known and a loader is not provided.
+        AttributeError: if loader is provided and is not callable.
+
+    Returns:
+        :class:`csep.core.forecasts.GriddedForecast`
+    """
+    # mapping from file extension to loader function, new formats by default they need to be added here
+    forecast_loader_mapping = {
+        'dat': GriddedForecast.load_ascii,
+        'xml': None,
+        'h5': None,
+        'bin': None
+    }
+
+    # sanity checks
+    if not os.path.exists(fname):
+        raise FileNotFoundError(f"Could not locate file {fname}. Unable to load forecast.")
+    # sanity checks
+    if loader is not None and not callable(loader):
+        raise AttributeError("Loader must be callable. Unable to load forecast.")
+    extension = os.path.splitext(fname)[-1][1:]
+    if extension not in forecast_loader_mapping.keys() and loader is None:
+        raise AttributeError("File extension should be in ('dat','xml','h5','bin') if loader not provided.")
+
+    if extension in ('xml','h5','bin'):
+        raise NotImplementedError
+
+    # assign default loader
+    if loader is None:
+        loader = forecast_loader_mapping[extension]
+    forecast = loader(fname, **kwargs)
+    # final sanity check
+    if not isinstance(forecast, GriddedForecast):
+        raise ValueError("Forecast not instance of GriddedForecast")
+    return forecast
+
+
+def load_catalog_forecast(fname, catalog_loader=None, format='native', type='ascii', **kwargs):
+    """ General function to handle loading catalog forecasts.
+
+        Currently, just a simple wrapper, but can contain more complex logic in the future.
+
+        Args:
+            fname (str): pathname to the forecast file or directory containing the forecast files
+            catalog_loader (func): callable that can load catalogs, see load_stochastic_event_sets above.
+            format (str): either 'native' or 'csep'. if 'csep', will attempt to be returned into csep catalog format. used to convert between
+                          observed_catalog type.
+            type (str): either 'ucerf3' or 'csep', determines the catalog format of the forecast. if loader is provided, then
+                        this parameter is ignored.
+            **kwargs: other keyword arguments passed to the :class:`csep.core.forecasts.CatalogForecast`.
+
+        Returns:
+            :class:`csep.core.forecasts.CatalogForecast`
+    """
+    # sanity checks
+    if not os.path.exists(fname):
+        raise FileNotFoundError(f"Could not locate file {fname}. Unable to load forecast.")
+    # sanity checks
+    if catalog_loader is not None and not callable(catalog_loader):
+        raise AttributeError("Loader must be callable. Unable to load forecast.")
+    # factory methods for loading different types of catalogs
+    catalog_loader_mapping = {
+        'ascii': catalogs.CSEPCatalog.load_ascii_catalogs,
+        'ucerf3': catalogs.UCERF3Catalog.load_catalogs
+    }
+    if catalog_loader is None:
+        catalog_loader = catalog_loader_mapping[type]
+    # try and parse information from filename and send to forecast constructor
+    if format == 'native' and type=='ascii':
+        try:
+            basename = str(os.path.basename(fname.rstrip('/')).split('.')[0])
+            split_fname = basename.split('_')
+            name = split_fname[0]
+            start_time = strptime_to_utc_datetime(split_fname[1], format="%Y-%m-%dT%H-%M-%S-%f")
+            # update kwargs
+            _ = kwargs.setdefault('name', name)
+            _ = kwargs.setdefault('start_time', start_time)
+        except:
+            pass
+    # create observed_catalog forecast
+    return CatalogForecast(filename=fname, loader=catalog_loader, catalog_format=format, catalog_type=type, **kwargs)
+
diff --git a/csep/core/catalogs.py b/csep/core/catalogs.py
index 0bcb40cc..fcdc86ba 100644
--- a/csep/core/catalogs.py
+++ b/csep/core/catalogs.py
@@ -377,7 +377,7 @@ def to_dataframe(self, with_datetime=False):
     def get_mag_idx(self):
         """ Return magnitude index from region magnitudes """
         try:
-            return bin1d_vec(self.get_magnitudes(), self.region.magnitudes, right_continuous=True)
+            return bin1d_vec(self.get_magnitudes(), self.region.magnitudes, tol=0.000001, right_continuous=True)
         except AttributeError:
             raise CSEPCatalogException("Cannot return magnitude index without self.region.magnitudes")
 
@@ -472,21 +472,6 @@ def filter(self, statements=None, in_place=True):
         if not self.filters and statements is None:
             raise CSEPCatalogException("Must provide filter statements to function or class to filter")
 
-        def parse_datetime_to_origin_time(dt_input):
-            """ Parses datetime strings with various formats. Handles datetime objects and datetime strings.
-            If datetime object is not time aware will assume that time is UTC, if it is timezone aware will convert to UTC timezone. """
-            if type(dt_input) == datetime.datetime:
-                # check for time zone
-                if dt_input.tzinfo == None:
-                    return create_utc_datetime(dt_input)
-            # handle it as string
-            try:
-                format = parse_string_format(dt_input)
-            except:
-                raise CSEPIOException(
-                    "Supported time-string formats are '%Y-%m-%dT%H:%M:%S.%f' and '%Y-%m-%dT%H:%M:%S'")
-            return strptime_to_utc_datetime(dt_input, format)
-
         # programmatically assign operators
         operators = {'>': operator.gt,
                      '<': operator.lt,
@@ -515,7 +500,6 @@ def parse_datetime_to_origin_time(dt_input):
             # slower but at the convenience of not having to call multiple times
             filters = list(statements)
             filtered = numpy.copy(self.catalog)
-            idx = numpy.ones(self.event_count, dtype=numpy.bool)
             for filt in filters:
                 name = filt.split(' ')[0]
                 # create indexing array, start with all events
@@ -569,7 +553,6 @@ def filter_spatial(self, region=None, update_stats=False, in_place=True):
         filtered = self.catalog[~mask]
         if in_place:
             self.catalog = filtered
-
             if update_stats:
                 self.update_catalog_stats()
             return self
@@ -691,7 +674,7 @@ def spatial_event_probability(self):
                                                   self.region.ys)
         return output
 
-    def magnitude_counts(self, mag_bins=None, retbins=False):
+    def magnitude_counts(self, mag_bins=None, tol=0.00001, retbins=False):
         """ Computes the count of events within mag_bins
 
 
@@ -719,14 +702,14 @@ def magnitude_counts(self, mag_bins=None, retbins=False):
                 return (mag_bins, out)
             else:
                 return out
-        idx = bin1d_vec(self.get_magnitudes(), mag_bins, right_continuous=True)
+        idx = bin1d_vec(self.get_magnitudes(), mag_bins, tol=tol, right_continuous=True)
         numpy.add.at(out, idx, 1)
         if retbins:
             return (mag_bins, out)
         else:
             return out
 
-    def spatial_magnitude_counts(self, mag_bins=None, ret_skipped=False):
+    def spatial_magnitude_counts(self, mag_bins=None, tol=0.00001, ret_skipped=False):
         """ Return counts of events in space-magnitude region.
 
         We figure out the index of the polygons and create a map that relates the spatial coordinate in the
@@ -774,7 +757,8 @@ def spatial_magnitude_counts(self, mag_bins=None, ret_skipped=False):
                                                                            self.region.idx_map,
                                                                            self.region.xs,
                                                                            self.region.ys,
-                                                                           mag_bins)
+                                                                           mag_bins,
+                                                                           tol=tol)
         if ret_skipped:
             return output, skipped
         else:
diff --git a/csep/core/forecasts.py b/csep/core/forecasts.py
index 72474508..49a2908a 100644
--- a/csep/core/forecasts.py
+++ b/csep/core/forecasts.py
@@ -15,9 +15,15 @@
 from csep.utils.plots import plot_spatial_dataset
 
 
-# idea: should this be a SpatialDataSet and the class below SpaceMagnitudeDataSet
+# idea: should this be a SpatialDataSet and the class below SpaceMagnitudeDataSet, bc of functions like
+#       get_latitudes(), and get_longitudes()
+#       or this class should be refactored as to use the underlying region
+
 # idea: this needs to handle non-carteisan regions, so maybe (lons, lats) should be a single variable like locations
+
 # note: these are specified to 2D data sets and some minor refactoring needs to happen here.
+
+# todo: add mask to dataset that has the shape of data. consider using numpy.ma module to hold these values
 class GriddedDataSet(LoggingMixin):
     """Represents space-magnitude discretized seismicity implementation.
 
@@ -192,7 +198,7 @@ def magnitude_counts(self):
         """ Returns counts of events in magnitude bins """
         return numpy.sum(self.data, axis=0)
 
-    def get_magnitude_index(self, mags):
+    def get_magnitude_index(self, mags, tol=0.00001):
         """ Returns the indices into the magnitude bins of selected magnitudes
 
         Note: the right-most bin is treated as extending to infinity.
@@ -206,12 +212,11 @@ def get_magnitude_index(self, mags):
         Raises:
             ValueError
         """
-        idm = bin1d_vec(mags, self.magnitudes, right_continuous=True)
+        idm = bin1d_vec(mags, self.magnitudes, tol=tol, right_continuous=True)
         if numpy.any(idm == -1):
             raise ValueError("mags outside the range of forecast magnitudes.")
         return idm
 
-
 class GriddedForecast(MarkedGriddedDataSet):
     """ Class to represent grid-based forecasts """
 
@@ -443,7 +448,7 @@ def __init__(self, filename=None, catalogs=None, name=None,
                  filter_spatial=False, filters=None, apply_mct=False,
                  region=None, expected_rates=None, start_time=None, end_time=None,
                  n_cat=None, event=None, loader=None, catalog_type='ascii',
-                 catalog_format='native'):
+                 catalog_format='native', store=True):
 
 
         """
@@ -469,6 +474,8 @@ def __init__(self, filename=None, catalogs=None, name=None,
             name (str): name of the forecast, will be used for defaults in plotting and other places
             n_cat (int): number of catalogs in the forecast
             event (:class:`csep.models.Event`): if the forecast is associated with a particular event
+            store (bool): if true, will store catalogs on object in memory. this should only be made false if working
+                          with very large forecast files that cannot be stored in memory
 
         """
 
@@ -482,6 +489,7 @@ def __init__(self, filename=None, catalogs=None, name=None,
 
         # should be iterable
         self.catalogs = catalogs or []
+        self._catalogs = []
 
         # should be a generator function
         self.loader = loader
@@ -489,6 +497,9 @@ def __init__(self, filename=None, catalogs=None, name=None,
         # used if the forecast is associated with a particular event
         self.event = event
 
+        # if true, no filters will be applied when iterating though forecast
+        self.apply_filters = True
+
         # these can be used to filter catalogs to a desired experiment region
         self.filters = filters or []
 
@@ -509,10 +520,12 @@ def __init__(self, filename=None, catalogs=None, name=None,
         self.start_time = start_time
         self.end_time = end_time
 
+        # stores catalogs in memory
+        self.store = store
+
         # time horizon in years
         if self.start_time is not None and self.end_time is not None:
             self.time_horizon_years = (self.end_epoch - self.start_epoch) / SECONDS_PER_ASTRONOMICAL_YEAR / 1000
-            # add time filters only if filters are not provied and user wants to filter in time
 
         # number of simulated catalogs
         self.n_cat = n_cat
@@ -531,8 +544,10 @@ def __next__(self):
         """ Allows the class to be used in a for-loop. Handles the case where the catalogs are stored as a list or
         loaded in using a generator function. The latter solves the problem where memory is a concern or all of the
         catalogs should not be held in memory at once. """
+        is_generator = True
         try:
             n_items = len(self.catalogs)
+            is_generator = False
             assert self.n_cat == n_items
             # here, we have reached the end of the list, simply reset the index to the front
             if self._idx >= self.n_cat:
@@ -547,18 +562,28 @@ def __next__(self):
                 self._idx += 1
             except StopIteration:
                 # gets a new generator function after the old one is exhausted
-                self.catalogs = self.loader(format=self.catalog_format, filename=self.filename,
-                                            region=self.region, name=self.name)
+                if not self.store:
+                    self.catalogs = self.loader(format=self.catalog_format, filename=self.filename,
+                                                region=self.region, name=self.name)
+                else:
+                    self.catalogs = self._catalogs
+
                 self.n_cat = self._idx
                 self._idx = 0
                 raise StopIteration()
+
         # apply filtering to catalogs, these can throw errors if not configured properly
-        if self.filters:
-            catalog = catalog.filter(self.filters)
-        if self.apply_mct:
-            catalog = catalog.apply_mct(self.event.magnitude, datetime_to_utc_epoch(self.event.time))
-        if self.filter_spatial:
-            catalog = catalog.filter_spatial(self.region)
+        if self.apply_filters:
+            if self.filters:
+                catalog = catalog.filter(self.filters)
+            if self.apply_mct:
+                catalog = catalog.apply_mct(self.event.magnitude, datetime_to_utc_epoch(self.event.time))
+            if self.filter_spatial:
+                catalog = catalog.filter_spatial(self.region)
+
+        if is_generator and self.store:
+            self._catalogs.append(catalog)
+
         # return potentially filtered data
         return catalog
 
diff --git a/csep/core/regions.py b/csep/core/regions.py
index 0d3d3db9..a961dedd 100644
--- a/csep/core/regions.py
+++ b/csep/core/regions.py
@@ -7,11 +7,10 @@
 # Third-party imports
 import matplotlib.path
 import numpy
-
-# PyCSEP imports
 import numpy as np
 import pyproj
 
+# PyCSEP imports
 from csep.utils.calc import bin1d_vec
 from csep.utils.scaling_relationships import WellsAndCoppersmith
 
@@ -195,13 +194,20 @@ def magnitude_bins(start_magnitude, end_magnitude, dmw):
     Returns:
         bin_edges (numpy.ndarray)
     """
-    return numpy.arange(start_magnitude, end_magnitude+dmw/2, dmw)
+    # convert to integers to prevent accumulating floating point errors
+    const = 10000
+    start = numpy.floor(const * start_magnitude)
+    end = numpy.floor(const * end_magnitude)
+    d = const * dmw
+    return (numpy.arange(start, end + d / 2, d) / const)
 
 def create_space_magnitude_region(region, magnitudes):
     """Simple wrapper to create space-magnitude region """
     if not isinstance(region, CartesianGrid2D):
         raise TypeError("region must be CartesianGrid2D")
     # bind to region class
+    if magnitudes is None:
+        raise ValueError("magnitudes should not be None if creating space-magnitude region.")
     region.magnitudes = magnitudes
     region.num_mag_bins = len(region.magnitudes)
     return region
@@ -370,7 +376,7 @@ def compute_vertices(origin_points, dh, tol=numpy.finfo(float).eps):
     """
     return list(map(lambda x: compute_vertex(x, dh, tol=tol), origin_points))
 
-def _bin_catalog_spatio_magnitude_counts(lons, lats, mags, n_poly, mask, idx_map, binx, biny, mag_bins):
+def _bin_catalog_spatio_magnitude_counts(lons, lats, mags, n_poly, mask, idx_map, binx, biny, mag_bins, tol=0.00001):
     """
     Returns a list of event counts as ndarray with shape (n_poly, n_cat) where each value
     represents the event counts within the polygon.
@@ -388,7 +394,7 @@ def _bin_catalog_spatio_magnitude_counts(lons, lats, mags, n_poly, mask, idx_map
     # index in 2d grid
     idx = bin1d_vec(lons, binx)
     idy = bin1d_vec(lats, biny)
-    mag_idxs = bin1d_vec(mags, mag_bins, right_continuous=True)
+    mag_idxs = bin1d_vec(mags, mag_bins, tol=tol, right_continuous=True)
     # start with zero event counts in each bin
     event_counts = numpy.zeros((n_poly, len(mag_bins)))
     # does not seem that we can vectorize this part
diff --git a/csep/io.py b/csep/io.py
deleted file mode 100644
index 6ca7a4ee..00000000
--- a/csep/io.py
+++ /dev/null
@@ -1,279 +0,0 @@
-import json
-import os
-import time
-
-# CSEP imports
-from csep.models import EvaluationResult, CatalogNumberTestResult
-from csep.core.forecasts import CatalogForecast, GriddedForecast
-from csep.core.catalogs import UCERF3Catalog, CSEPCatalog
-from csep.utils import readers
-from csep.utils.time_utils import strptime_to_utc_datetime, utc_now_datetime
-
-
-def load_stochastic_event_sets(filename, type='csv', format='native', **kwargs):
-    """ General function to load stochastic event sets
-
-    This function returns a generator to iterate through a collection of catalogs.
-    To load a forecast and include metadata use :func:`csep.load_catalog_forecast`.
-
-    Args:
-        filename (str): name of file or directory where stochastic event sets live.
-        type (str): either 'ucerf3' or 'csep' depending on the type of observed_catalog to load
-        format (str): ('csep' or 'native') if native catalogs are not converted to csep format.
-        kwargs (dict): see the documentation of that class corresponding to the type you selected
-                         for the kwargs options
-
-    Returns:
-        (generator): :class:`~csep.core.catalogs.AbstractBaseCatalog`
-
-    """
-    if type not in ('ucerf3', 'csv'):
-        raise ValueError("type must be one of the following: (ucerf3)")
-
-    # use mapping to dispatch to correct function
-    # in general, stochastic event sets are loaded with classmethods and single catalogs use the
-    # constructor
-    mapping = {'ucerf3': UCERF3Catalog.load_catalogs,
-               'csv': CSEPCatalog.load_ascii_catalogs}
-
-    # dispatch to proper loading function
-    result = mapping[type](filename, **kwargs)
-
-    # factory function to load catalogs from different classes
-    while True:
-        try:
-            catalog = next(result)
-        except StopIteration:
-            return
-        except Exception:
-            raise
-        if format == 'native':
-            yield catalog
-        elif format == 'csep':
-            yield catalog.get_csep_format()
-        else:
-            raise ValueError('format must be either "native" or "csep!')
-
-
-
-def load_catalog(filename, type='csep-csv', format='native', loader=None, **kwargs):
-    """ General function to load single catalog
-
-    See corresponding class documentation for additional parameters.
-
-    Args:
-        type (str): ('ucerf3', 'csep-csv', 'zmap', 'jma-csv', 'ndk') default is 'csep-csv'
-        format (str): ('native', 'csep') determines whether the catalog should be converted into the csep
-                      formatted catalog or kept as native.
-
-    Returns (:class:`~csep.core.catalogs.AbstractBaseCatalog`)
-    """
-
-    if type not in ('ucerf3', 'csep-csv', 'zmap', 'jma-csv', 'ndk') and loader is None:
-        raise ValueError("type must be one of the following: ('ucerf3', 'csep-csv', 'zmap', 'jma-csv', 'ndk').")
-
-    # map to correct catalog class, at some point these could be abstracted into configuration file
-    # this maps a human readable string to the correct catalog class and the correct loader function
-    class_loader_mapping = {
-        'ucerf3': {
-            'class': UCERF3Catalog,
-            'loader': None
-        },
-        'csep-csv': {
-            'class': CSEPCatalog,
-            'loader': readers.csep_ascii
-        },
-        'zmap': {
-            'class': CSEPCatalog,
-            'loader': readers.zmap_ascii
-        },
-        'jma-csv': {
-            'class': CSEPCatalog,
-            'loader': readers.jma_csv,
-        },
-        'ndk': {
-            'class': CSEPCatalog,
-            'loader': readers.ndk
-        }
-    }
-
-    # treat json files using the from_dict() member instead of constructor
-    catalog_class = class_loader_mapping[type]['class']
-    if os.path.splitext(filename)[-1][1:] == 'json':
-        catalog = catalog_class.load_json(filename, **kwargs)
-    else:
-        if loader is None:
-            loader = class_loader_mapping[type]['loader']
-
-        catalog = catalog_class.load_catalog(filename=filename, loader=loader, **kwargs)
-
-    # convert to csep format if needed
-    if format == 'native':
-        return_val = catalog
-    elif format == 'csep':
-        return_val = catalog.get_csep_format()
-    else:
-        raise ValueError('format must be either "native" or "csep"')
-    return return_val
-
-def query_comcat(start_time, end_time, min_magnitude=2.50,
-                 min_latitude=31.50, max_latitude=43.00,
-                 min_longitude=-125.40, max_longitude=-113.10, verbose=True, **kwargs):
-    """
-    Access Comcat catalog through web service
-
-    Args:
-        start_time: datetime object of start of catalog
-        end_time: datetime object for end of catalog
-        min_magnitude: minimum magnitude to query
-        min_latitude:  maximum magnitude to query
-        max_latitude: max latitude of bounding box
-        min_longitude: min latitude of bounding box
-        max_longitude: max longitude of bounding box
-        region: :class:`csep.core.regions.CartesianGrid2D
-        verbose (bool): print catalog summary statistics
-
-    Returns:
-        :class:`csep.core.catalogs.ComcatCatalog
-    """
-
-    # Timezone should be in UTC
-    t0 = time.time()
-    eventlist = readers._query_comcat(start_time=start_time, end_time=end_time,
-                           min_magnitude=min_magnitude,
-                           min_latitude=min_latitude, max_latitude=max_latitude,
-                           min_longitude=min_longitude, max_longitude=max_longitude)
-    t1 = time.time()
-    comcat = CSEPCatalog(data=eventlist, date_accessed=utc_now_datetime(), **kwargs)
-    print("Fetched ComCat catalog in {} seconds.\n".format(t1 - t0))
-    if verbose:
-        print("Downloaded catalog from ComCat with following parameters")
-        print("Start Date: {}\nEnd Date: {}".format(str(comcat.start_time), str(comcat.end_time)))
-        print("Min Latitude: {} and Max Latitude: {}".format(comcat.min_latitude, comcat.max_latitude))
-        print("Min Longitude: {} and Max Longitude: {}".format(comcat.min_longitude, comcat.max_longitude))
-        print("Min Magnitude: {}".format(comcat.min_magnitude))
-        print(f"Found {comcat.event_count} events in the ComCat catalog.")
-    return comcat
-
-def load_evaluation_result(fname):
-    """ Load evaluation result stored as json file
-
-    Returns:
-        :class:`csep.core.evaluations.EvaluationResult`
-
-    """
-    # tries to return the correct class for the evaluation result. if it cannot find the type simply returns the basic result.
-    evaluation_result_factory = {
-        'default': EvaluationResult,
-        'CatalogNumberTestResult': CatalogNumberTestResult
-    }
-    with open(fname, 'r') as json_file:
-        json_dict = json.load(json_file)
-        try:
-            evaluation_type = json_dict['named_type']
-        except:
-            evaluation_type = 'default'
-    eval_result = evaluation_result_factory[evaluation_type].from_dict(json_dict)
-    return eval_result
-
-def load_gridded_forecast(fname, loader=None, **kwargs):
-    """ Loads grid based forecast from hard-disk.
-
-    The function loads the forecast provided with at the filepath defined by fname. The function attempts to understand
-    the file format based on the extension of the filepath. Optionally, if loader function is provided, that function
-    will be used to load the forecast. The loader function should return a :class:`csep.core.forecasts.GriddedForecast`
-    class with the region and magnitude members correctly assigned.
-
-    File extensions:
-        .dat -> CSEP ascii format
-        .xml -> CSEP xml format (not yet implemented)
-        .h5 -> CSEP hdf5 format (not yet implemented)
-        .bin -> CSEP binary format (not yet implemented)
-
-    Args:
-        fname (str): path of grid based forecast
-        loader (func): function to load forecast in bespoke format needs to return :class:`csep.core.forecasts.GriddedForecast`
-                       and first argument should be required and the filename of the forecast to load
-                       called as loader(func, **kwargs).
-
-        **kwargs: passed into loader function
-
-    Throws:
-        FileNotFoundError: when the file extension is not known and a loader is not provided.
-        AttributeError: if loader is provided and is not callable.
-
-    Returns:
-        :class:`csep.core.forecasts.GriddedForecast`
-    """
-    # mapping from file extension to loader function, new formats by default they need to be added here
-    forecast_loader_mapping = {
-        'dat': GriddedForecast.load_ascii,
-        'xml': None,
-        'h5': None,
-        'bin': None
-    }
-
-    # sanity checks
-    if not os.path.exists(fname):
-        raise FileNotFoundError(f"Could not locate file {fname}. Unable to load forecast.")
-    # sanity checks
-    if loader is not None and not callable(loader):
-        raise AttributeError("Loader must be callable. Unable to load forecast.")
-    extension = os.path.splitext(fname)[-1][1:]
-    if extension not in forecast_loader_mapping.keys() and loader is None:
-        raise AttributeError("File extension should be in ('dat','xml','h5','bin') if loader not provided.")
-
-    if extension in ('xml','h5','bin'):
-        raise NotImplementedError
-
-    # assign default loader
-    if loader is None:
-        loader = forecast_loader_mapping[extension]
-    forecast = loader(fname, **kwargs)
-    # final sanity check
-    if not isinstance(forecast, GriddedForecast):
-        raise ValueError("Forecast not instance of GriddedForecast")
-    return forecast
-
-def load_catalog_forecast(fname, catalog_loader=None, format='native', type='ascii', **kwargs):
-    """ General function to handle loading catalog forecasts.
-
-        Currently, just a simple wrapper, but can contain more complex logic in the future.
-
-        Args:
-            fname (str): pathname to the forecast file or directory containing the forecast files
-            catalog_loader (func): callable that can load catalogs, see load_stochastic_event_sets above.
-            format (str): either 'native' or 'csep'. if 'csep', will attempt to be returned into csep catalog format. used to convert between
-                          observed_catalog type.
-            type (str): either 'ucerf3' or 'csep', determines the catalog format of the forecast. if loader is provided, then
-                        this parameter is ignored.
-            **kwargs: other keyword arguments passed to the :class:`csep.core.forecasts.CatalogForecast`.
-
-        Returns:
-            :class:`csep.core.forecasts.CatalogForecast`
-    """
-    # sanity checks
-    if not os.path.exists(fname):
-        raise FileNotFoundError(f"Could not locate file {fname}. Unable to load forecast.")
-    # sanity checks
-    if catalog_loader is not None and not callable(catalog_loader):
-        raise AttributeError("Loader must be callable. Unable to load forecast.")
-    # factory methods for loading different types of catalogs
-    catalog_loader_mapping = {
-        'ascii': CSEPCatalog.load_ascii_catalogs,
-        'ucerf3': UCERF3Catalog.load_catalogs
-    }
-    if catalog_loader is None:
-        catalog_loader = catalog_loader_mapping[type]
-    # try and parse information from filename and send to forecast constructor
-    if format == 'native' and type=='ascii':
-        # this works for unix how windows?
-        basename = str(os.path.basename(fname.rstrip('/')).split('.')[0])
-        split_fname = basename.split('_')
-        name = split_fname[0]
-        start_time = strptime_to_utc_datetime(split_fname[1], format="%Y-%m-%dT%H-%M-%S-%f")
-        # update kwargs
-        _ = kwargs.setdefault('name', name)
-        _ = kwargs.setdefault('start_time', start_time)
-    # create observed_catalog forecast
-    return CatalogForecast(filename=fname, loader=catalog_loader, catalog_format=format, catalog_type=type, **kwargs)
\ No newline at end of file
diff --git a/csep/utils/calc.py b/csep/utils/calc.py
index 91860cda..9c7773d5 100644
--- a/csep/utils/calc.py
+++ b/csep/utils/calc.py
@@ -51,7 +51,7 @@ def discretize(data, bin_edges, right_continuous=False):
     x_new = bin_edges[idx]
     return x_new
 
-def bin1d_vec(p, bins, right_continuous=False):
+def bin1d_vec(p, bins, tol=None, right_continuous=False):
     """Efficient implementation of binning routine on 1D Cartesian Grid.
 
     Returns the indices of the points into bins. Bins are inclusive on the lower bound
@@ -69,12 +69,24 @@ def bin1d_vec(p, bins, right_continuous=False):
     Raises:
         ValueError:
     """
+    bins = numpy.array(bins)
+    p = numpy.array(p)
     a0 = numpy.min(bins)
     h = bins[1] - bins[0]
-    eps = numpy.finfo(numpy.float).eps
+
+    a0_tol = numpy.abs(a0) * numpy.finfo(numpy.float).eps
+    h_tol = numpy.abs(h) * numpy.finfo(numpy.float).eps
+    p_tol = numpy.abs(p) * numpy.finfo(numpy.float).eps
+
+    # absolute tolerance
+    if tol is None:
+        idx = numpy.floor((p + (p_tol + a0_tol) - a0) / (h - h_tol))
+    else:
+        idx = numpy.floor((p + (tol + a0_tol) - a0) / (h - h_tol))
     if h < 0:
         raise ValueError("grid spacing must be positive and monotonically increasing.")
-    idx = numpy.floor((p + eps - a0) / h)
+    # account for floating point uncertainties by considering extreme case
+
     if right_continuous:
         # set upper bin index to last
         try:
@@ -89,7 +101,10 @@ def bin1d_vec(p, bins, right_continuous=False):
         except (TypeError):
             if idx < 0 or idx >= len(bins):
                 idx = -1
-    idx = idx.astype(numpy.int)
+    try:
+        idx = idx.astype(numpy.int)
+    except (AttributeError):
+        idx = int(idx)
     return idx
 
 def _compute_likelihood(gridded_data, apprx_rate_density, expected_cond_count, n_obs):
diff --git a/csep/utils/readers.py b/csep/utils/readers.py
index 4237a921..e336e93d 100644
--- a/csep/utils/readers.py
+++ b/csep/utils/readers.py
@@ -444,7 +444,10 @@ def parse_datetime(dt_string):
             # maybe fractional seconds are not included
             origin_time = parse_datetime(line[3])
             depth = float(line[4])
-            catalog_id = int(line[5])
+            try:
+                catalog_id = int(line[5])
+            except ValueError:
+                catalog_id = int(-1)
             event_id = line[6]
             try:
                 event_id = event_id.decode('utf-8')
@@ -642,7 +645,6 @@ def jma_csv(fname):
             is_first_event = False
     return events
 
-
 def _query_comcat(start_time, end_time, min_magnitude=2.50,
                  min_latitude=31.50, max_latitude=43.00,
                  min_longitude=-125.40, max_longitude=-113.10, extra_comcat_params=None):
diff --git a/docs/conf.py b/docs/conf.py
index 7cff07d1..4b3032cc 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -28,9 +28,9 @@
 html_show_sphinx = False
 
 # The short X.Y version
-version = 'v0.1'
+version = 'v0.2'
 # The full version, including alpha/beta/rc tags
-release = 'v0.1.0'
+release = 'v0.2.0'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/docs/index.rst b/docs/index.rst
index aaff32f9..1ea04aa1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,4 +1,4 @@
-PyCSEP: Tools for Earthquake Forecast Developers
+pyCSEP: Tools for Earthquake Forecast Developers
 ================================================
 
 .. toctree::


Test patch:
diff --git a/tests/artifacts/Comcat/test_catalog.csv b/tests/artifacts/Comcat/test_catalog.csv
new file mode 100644
index 00000000..7ace7107
--- /dev/null
+++ b/tests/artifacts/Comcat/test_catalog.csv
@@ -0,0 +1,338 @@
+lon,lat,mag,time_string,depth,catalog_id,event_id
+-121.4566667,37.0103333,3.7,1986-01-06T19:52:42.880000,8.9,,nc62905
+-122.9186667,38.9375,3.65,1986-01-09T16:33:10.510000,4.097,,nc1134632
+-121.1781667,36.5828333,4.8,1986-01-14T03:09:36.040000,6.2,,nc63490
+-125.328,40.4481667,3.5,1986-01-21T11:57:29.930000,2.182,,nc64064
+-121.285,36.8043333,5.5,1986-01-26T19:20:50.950000,8.153,,nc64626
+-121.2926667,36.8221667,4.0,1986-01-26T23:46:54.410000,4.861,,nc64912
+-124.7043333,40.5995,3.5,1986-02-05T12:00:10.630000,17.363,,nc66662
+-115.793,31.862,4.43,1986-02-07T00:09:23.570000,5.681,,ci115319
+-115.804,31.877,4.17,1986-02-07T00:40:03.640000,5.681,,ci115322
+-115.798,31.868,3.6,1986-02-07T07:40:01.900000,5.472,,ci115338
+-116.03,34.116,3.94,1986-02-17T02:12:33.480000,10.585,,ci116008
+-124.7853333,40.7056667,4.0,1986-02-19T00:25:39.610000,1.14,,nc67435
+-117.555,32.508,3.91,1986-02-19T00:47:24.490000,5.574,,ci117185
+-124.6656667,40.5701667,3.5,1986-02-28T10:31:35.940000,12.003,,nc67191
+-115.809,31.779,3.91,1986-02-28T15:28:41.640000,5.883,,ci142267
+-124.659,40.2308333,3.8,1986-03-03T17:56:46.750000,4.098,,nc67354
+-124.4561667,40.2846667,3.8,1986-03-07T22:26:33.160000,-0.799,,nc1136446
+-119.802,34.402,4.14,1986-03-10T15:33:16.270000,17.095,,ci118360
+-124.3788333,40.2675,3.6,1986-03-11T00:05:09.600000,20.061,,nc67871
+-125.3786667,40.437,3.9,1986-03-11T15:34:38.670000,8.162,,nc1136528
+-123.7843333,40.8625,3.8,1986-03-20T22:41:38.240000,23.944,,nc68412
+-125.233,40.93,3.5,1986-03-21T06:37:27.300000,5.0,,usp0002s0v
+-123.1625,40.9018333,3.7,1986-03-21T06:37:48.270000,3.019,,nc1136719
+-122.8103333,38.8225,3.6,1986-03-23T04:58:02.150000,0.445,,nc68586
+-113.893,32.446,3.64,1986-03-24T17:29:57.340000,5.967,,ci119822
+-122.2038333,37.878,4.1,1986-03-29T16:24:03.710000,11.603,,nc69034
+-121.6866667,37.4791667,5.7,1986-03-31T11:55:39.810000,8.502,,nc68932
+-123.8223333,41.535,3.6,1986-04-03T06:40:48,3.816,,nc10086327
+-118.01,33.73,3.67,1986-04-05T06:50:40.370000,13.432,,ci121108
+-115.709,33.336,3.53,1986-04-05T17:21:49.520000,3.144,,ci121045
+-125.1076667,40.3896667,4.4,1986-04-15T00:13:19.350000,4.069,,nc70318
+-121.3185,36.6898333,3.6,1986-04-15T09:25:56.520000,4.228,,nc70320
+-125.0158333,40.4658333,3.5,1986-04-15T14:04:45.800000,0.319,,nc1137577
+-115.254,32.446,3.52,1986-04-24T02:01:48.770000,5.991,,ci122743
+-115.253,32.438,3.63,1986-04-24T02:03:33.680000,5.991,,ci142256
+-115.287,32.474,3.61,1986-04-24T02:31:24.180000,5.991,,ci122746
+-121.6863333,37.4701667,3.5,1986-04-28T17:33:47.620000,8.252,,nc71312
+-121.2668333,36.8065,3.6,1986-04-28T22:18:40.340000,8.461,,nc71451
+-124.5078333,40.74,3.9,1986-04-30T22:37:30.960000,22.815,,nc71533
+-117.121,31.567,3.77,1986-05-06T03:40:21.920000,5.446,,ci123749
+-123.351,39.1541667,3.62,1986-05-07T02:51:44.420000,26.256,,nc1138129
+-124.5081667,40.1451667,4.3,1986-05-07T06:09:09.570000,4.033,,nc72055
+-122.58,39.3091667,4.13,1986-05-08T14:14:45.930000,2.994,,nc1138173
+-118.019,35.806,3.86,1986-05-23T11:41:55.070000,8.89,,ci125030
+-121.2511667,36.6381667,3.7,1986-05-31T08:47:06.950000,5.721,,nc73775
+-121.2496667,36.6373333,4.7,1986-05-31T08:47:55.590000,5.264,,nc73777
+-121.2356667,36.6278333,3.58,1986-06-01T06:49:03.760000,4.991,,nc73901
+-121.233,36.6268333,3.5,1986-06-01T06:49:34.700000,5.288,,nc73902
+-116.344,33.789,3.74,1986-06-03T14:14:49.270000,9.85,,ci125717
+-116.948,37.409,3.94,1986-06-04T15:07:37.320000,3.599,,ci126022
+-121.2505,36.6338333,3.5,1986-06-11T15:08:59.290000,7.231,,nc74419
+-120.0278333,36.0291667,3.5,1986-06-13T13:25:15.070000,8.907,,nc10052616
+-123.4818333,41.5278333,3.61,1986-06-14T07:30:45.500000,3.724,,nc1138852
+-124.5826667,40.6926667,4.0,1986-06-22T20:34:52.900000,17.559,,nc75147
+-123.468,40.7121667,3.5,1986-06-22T23:57:06.490000,27.822,,nc75153
+-115.133,32.132,3.9,1986-06-23T23:45:58.080000,5.963,,ci700278
+-115.174,32.161,3.57,1986-06-24T13:10:00.080000,5.946,,ci700297
+-115.109,32.149,4.28,1986-06-24T13:13:02.300000,5.969,,ci700298
+-119.77,39.533,3.9,1986-06-28T02:06:29.800000,13.0,,usp0002vg5
+-116.293,31.976,3.67,1986-06-29T15:00:10.990000,5.526,,ci700525
+-124.4646667,41.0118333,3.7,1986-07-04T19:55:37.430000,20.096,,nc10086215
+-121.874,36.0531667,4.4,1986-07-08T00:40:22.200000,15.47,,nc76037
+-116.608,33.999,6.0,1986-07-08T09:20:44.560000,9.468,,ci700917
+-116.654,34.032,4.2,1986-07-08T09:24:12.790000,7.846,,ci700919
+-116.554,33.971,3.5,1986-07-08T09:27:09.260000,12.87,,ci748407
+-116.533,33.95,3.96,1986-07-08T09:28:13.750000,16.668,,ci127338
+-116.686,34.023,3.55,1986-07-08T09:30:22.480000,10.55,,ci748414
+-116.561,33.997,3.72,1986-07-08T09:49:49.740000,8.568,,ci127340
+-116.581,33.96,3.71,1986-07-08T10:04:52.940000,4.308,,ci700932
+-116.579,33.977,4.29,1986-07-08T10:09:02.900000,7.188,,ci700934
+-116.67,34.023,3.96,1986-07-08T10:11:00.170000,2.835,,ci700935
+-116.627,34.028,4.3,1986-07-08T10:22:39.440000,9.847,,ci748481
+-116.674,34.025,3.57,1986-07-08T10:34:14.510000,9.655,,ci700942
+-116.618,34.02,3.5,1986-07-08T11:24:58.410000,10.238,,ci700964
+-116.685,34.038,3.89,1986-07-08T15:55:19.800000,10.025,,ci701032
+-116.583,33.993,3.68,1986-07-08T16:39:44.080000,11.438,,ci701050
+-116.556,33.968,3.52,1986-07-08T18:04:14.460000,8.208,,ci701078
+-116.615,34.016,4.07,1986-07-08T19:36:20.080000,10.798,,ci701105
+-116.567,33.976,4.11,1986-07-09T00:12:32.140000,7.818,,ci701181
+-116.574,33.972,3.71,1986-07-09T09:41:21.050000,7.468,,ci701320
+-116.565,33.975,3.57,1986-07-09T11:32:21.080000,8.318,,ci127268
+-124.6715,40.4538333,3.7,1986-07-10T20:52:44.110000,23.689,,nc10086024
+-116.652,33.986,4.05,1986-07-12T05:45:27.510000,5.748,,ci701985
+-116.613,33.952,3.73,1986-07-13T01:41:38.230000,10.728,,ci702132
+-117.874,32.971,5.45,1986-07-13T13:47:08.210000,5.828,,ci127298
+-117.828,32.963,3.97,1986-07-13T13:53:27.860000,5.756,,ci601530
+-117.843,32.993,4.37,1986-07-13T14:01:33.090000,5.688,,ci127300
+-117.777,32.978,3.74,1986-07-13T14:11:01.250000,5.756,,ci601533
+-117.78,32.961,3.62,1986-07-13T14:26:01.650000,5.688,,ci601537
+-117.766,32.891,3.59,1986-07-13T15:50:04.990000,5.756,,ci601556
+-117.775,32.955,3.66,1986-07-13T23:53:43.010000,5.756,,ci601643
+-117.82,32.963,4.14,1986-07-14T00:32:46.010000,5.756,,ci601649
+-120.403,35.293,3.77,1986-07-14T00:33:30.810000,15.279,,nc1139411
+-117.83,32.967,3.79,1986-07-14T01:11:10.560000,5.756,,ci601652
+-117.81,32.959,3.77,1986-07-14T07:17:35.450000,5.756,,ci702404
+-117.819,32.948,3.74,1986-07-14T09:07:53.940000,5.756,,ci702418
+-117.819,32.969,3.82,1986-07-14T14:44:08.760000,5.756,,ci601753
+-117.77,32.983,3.56,1986-07-14T17:14:43.220000,5.692,,ci601776
+-117.817,32.944,3.88,1986-07-16T12:47:01.620000,5.756,,ci602008
+-116.647,33.989,4.46,1986-07-17T20:35:14.970000,5.768,,ci702983
+-116.647,33.993,4.43,1986-07-17T21:54:45.140000,6.378,,ci702997
+-118.4365,37.5668333,3.9,1986-07-18T16:00:07.900000,4.44,,nc76662
+-117.849,36.095,3.74,1986-07-18T17:00:36.810000,1.116,,ci602301
+-118.4381667,37.5666667,5.9,1986-07-20T14:29:45.440000,5.044,,nc99451
+-118.4393333,37.58,3.51,1986-07-20T14:36:56.490000,3.034,,nc99480
+-118.435,37.5746667,4.1,1986-07-20T14:46:07.300000,6.134,,nc99197
+-118.4658333,37.5538333,3.5,1986-07-20T15:29:27.560000,7.424,,nc99983
+-118.4561667,37.5433333,3.5,1986-07-20T16:23:01.180000,5.334,,nc1139527
+-118.406,37.5628333,3.9,1986-07-20T16:32:33.540000,2.412,,nc1139529
+-118.4663333,37.5588333,3.7,1986-07-20T16:37:24.530000,6.02,,nc98963
+-118.4323333,37.5313333,3.51,1986-07-20T16:43:02.060000,6.614,,nc1139536
+-118.4641667,37.5533333,3.5,1986-07-20T17:41:55.240000,7.294,,nc100613
+-118.4533333,37.5328333,3.8,1986-07-20T18:36:53.580000,4.154,,nc99421
+-118.436,37.5353333,4.7,1986-07-20T18:38:52.360000,7.124,,nc10085761
+-118.417,37.583,3.68,1986-07-20T18:40:50.960000,5.127,,ci127672
+-118.4241667,37.5368333,3.7,1986-07-21T02:27:53.720000,7.324,,nc97840
+-118.4651667,37.5581667,4.4,1986-07-21T03:12:10.840000,6.994,,nc97843
+-118.457,37.5735,3.56,1986-07-21T06:50:40.860000,7.384,,nc97658
+-118.4538333,37.5321667,4.1,1986-07-21T08:10:30.530000,4.334,,nc97746
+-118.444,37.5723333,3.6,1986-07-21T08:20:43.660000,6.234,,nc97749
+-118.4588333,37.5666667,4.3,1986-07-21T11:15:21.240000,6.954,,nc97588
+-118.4428333,37.538,6.4,1986-07-21T14:42:26,8.804,,nc10085763
+-118.417,37.583,4.25,1986-07-21T14:45:20.710000,4.405,,ci127724
+-118.417,37.583,3.76,1986-07-21T14:46:08.310000,4.405,,ci142331
+-118.667,37.5,3.81,1986-07-21T14:46:52.980000,4.803,,ci127725
+-118.0,37.617,4.09,1986-07-21T14:47:55.630000,4.803,,ci127726
+-118.417,37.583,4.03,1986-07-21T14:48:23.490000,4.803,,ci142332
+-118.4538333,37.4931667,5.7,1986-07-21T14:51:09.370000,8.482,,nc10085764
+-118.417,37.583,4.02,1986-07-21T14:52:11.360000,5.127,,ci142334
+-118.302,37.493,4.54,1986-07-21T14:53:35.620000,4.803,,ci127728
+-118.42,37.58,4.9,1986-07-21T14:53:58.100000,6.0,,usp0002w9q
+-118.0,37.5,4.24,1986-07-21T14:54:40.300000,4.405,,ci127729
+-118.417,37.583,3.54,1986-07-21T14:55:33.990000,4.803,,ci142335
+-118.417,37.583,3.83,1986-07-21T14:56:19.800000,4.803,,ci127730
+-118.417,37.583,3.73,1986-07-21T14:57:18.410000,4.803,,ci144280
+-118.3918333,37.5036667,4.8,1986-07-21T14:57:48.500000,2.48,,nc10085765
+-118.5781667,37.5605,3.76,1986-07-21T14:58:57.880000,7.984,,nc10085790
+-118.4726667,37.6126667,3.9,1986-07-21T15:00:42.700000,1.911,,nc10085766
+-118.5098333,37.5935,3.5,1986-07-21T15:03:01.890000,5.265,,nc10085767
+-118.4971667,37.5288333,4.1,1986-07-21T15:05:40.790000,-1.326,,nc10085768
+-118.5445,37.5113333,3.68,1986-07-21T15:06:51.330000,3.597,,nc97439
+-118.5176667,37.5365,3.8,1986-07-21T15:10:15.220000,3.234,,nc97496
+-118.405,37.682,3.7,1986-07-21T15:10:30.260000,10.0,,usp0002w9v
+-118.5046667,37.5376667,4.4,1986-07-21T15:11:30.540000,11.316,,nc97497
+-118.6593333,37.68,3.51,1986-07-21T15:15:32.100000,6.82,,nc1139698
+-118.4458333,37.4335,3.58,1986-07-21T15:19:01.310000,-1.355,,nc1139700
+-118.4376667,37.525,4.7,1986-07-21T15:19:34.650000,-0.482,,nc10085769
+-118.417,37.583,3.59,1986-07-21T15:22:39.960000,5.127,,ci127872
+-118.5103333,37.5106667,3.75,1986-07-21T15:24:31.410000,3.424,,nc97444
+-118.4418333,37.5128333,4.6,1986-07-21T15:26:48.260000,9.872,,nc97382
+-118.5448333,37.5298333,3.52,1986-07-21T15:27:49.050000,3.329,,nc97420
+-118.4746667,37.5906667,3.8,1986-07-21T15:29:10.350000,2.663,,nc97421
+-118.4378333,37.5975,3.6,1986-07-21T15:31:04.080000,7.786,,nc10085770
+-118.437,37.5503333,4.0,1986-07-21T15:36:49.320000,5.214,,nc97384
+-118.4003333,37.543,4.2,1986-07-21T15:41:21.290000,2.634,,nc97385
+-118.4386667,37.5415,4.45,1986-07-21T15:46:15.670000,6.837,,nc97386
+-118.446,37.6058333,4.5,1986-07-21T15:46:23.200000,5.962,,nc1139705
+-118.3695,37.446,3.86,1986-07-21T15:50:13.250000,11.126,,nc10085771
+-118.4935,37.6278333,3.8,1986-07-21T15:50:21.820000,9.469,,nc10085789
+-118.228,35.913,3.52,1986-07-21T15:57:37.900000,5.022,,ci703694
+-118.4158333,37.5055,3.67,1986-07-21T16:19:04.610000,4.626,,nc97758
+-118.383,37.4866667,4.6,1986-07-21T16:26:44.110000,5.136,,nc97702
+-118.4521667,37.5396667,3.8,1986-07-21T16:36:43.290000,9.36,,nc97652
+-118.4738333,37.529,4.5,1986-07-21T17:05:33.100000,9.974,,nc97418
+-118.439,37.5563333,3.5,1986-07-21T17:08:32.660000,5.644,,nc97482
+-118.4823333,37.5296667,3.52,1986-07-21T17:08:54.770000,9.0,,nc97483
+-118.3778333,37.4653333,4.5,1986-07-21T17:19:59.930000,6.751,,nc97380
+-118.4025,37.5458333,3.9,1986-07-21T17:30:29.600000,3.034,,nc1139730
+-118.4433333,37.5885,3.56,1986-07-21T17:43:00.050000,6.417,,nc1139733
+-118.4593333,37.5796667,4.4,1986-07-21T17:48:51.830000,6.484,,nc97150
+-118.4446667,37.5876667,4.3,1986-07-21T17:48:55.270000,8.294,,nc97362
+-118.4678333,37.5755,3.5,1986-07-21T17:53:02.500000,6.274,,nc97151
+-118.4585,37.6165,4.0,1986-07-21T18:13:56.980000,3.255,,nc97370
+-118.446,37.616,3.58,1986-07-21T18:15:06.590000,3.539,,nc97371
+-118.4201667,37.5288333,3.5,1986-07-21T18:18:32.560000,5.964,,nc10086029
+-118.3895,37.5396667,3.61,1986-07-21T18:20:05.970000,3.604,,nc10086033
+-117.815,33.005,4.02,1986-07-21T18:29:30.960000,5.688,,ci602813
+-118.4453333,37.5265,4.0,1986-07-21T20:36:04.620000,8.684,,nc95536
+-118.3795,37.458,3.6,1986-07-21T20:40:24.460000,6.927,,nc96449
+-118.4781667,37.5026667,3.6,1986-07-21T20:40:43.090000,10.496,,nc96461
+-118.4551667,37.586,3.6,1986-07-21T21:08:41.180000,5.804,,nc95495
+-118.434,37.6,3.7,1986-07-21T21:51:44.330000,2.085,,nc95456
+-118.3425,37.6213333,5.6,1986-07-21T22:07:15.640000,-0.076,,nc95459
+-118.573,37.628,4.41,1986-07-21T22:09:22.220000,3.629,,ci602845
+-118.475,37.594,4.2,1986-07-21T23:43:05.950000,6.311,,nc94295
+-118.4321667,37.6063333,4.5,1986-07-22T00:09:53.210000,2.445,,nc94559
+-118.482,37.5978333,3.6,1986-07-22T02:21:30.990000,6.975,,nc10083392
+-118.4766667,37.5816667,3.8,1986-07-22T03:02:10.270000,5.675,,nc94084
+-118.4398333,37.5326667,3.9,1986-07-22T03:18:47.670000,6.364,,nc94287
+-118.4191667,37.5523333,3.6,1986-07-22T05:24:07.400000,5.144,,nc92149
+-118.4531667,37.5631667,4.1,1986-07-22T05:40:44.910000,6.834,,nc93915
+-118.3855,37.4525,4.2,1986-07-22T06:21:52.370000,10.79,,nc93530
+-118.4356667,37.531,3.5,1986-07-22T06:33:38.690000,9.754,,nc93533
+-118.4716667,37.6091667,3.6,1986-07-22T06:58:10.680000,6.657,,nc93409
+-118.4455,37.5335,4.0,1986-07-22T08:29:15.650000,3.354,,nc93088
+-118.3748333,37.4546667,3.5,1986-07-22T09:34:15.630000,8.28,,nc92184
+-118.4733333,37.5266667,4.4,1986-07-22T12:24:49.600000,9.234,,nc93078
+-118.4696667,37.5191667,3.66,1986-07-22T12:26:14.240000,8.174,,nc93104
+-118.4731667,37.5276667,4.7,1986-07-22T13:33:59.320000,8.744,,nc93040
+-118.4715,37.5258333,5.0,1986-07-22T13:48:59.150000,10.653,,nc92934
+-118.4776667,37.5301667,3.6,1986-07-22T13:57:34.290000,9.824,,nc92968
+-118.4301667,37.5136667,3.7,1986-07-22T17:17:20.900000,8.784,,nc92870
+-118.4755,37.574,3.6,1986-07-22T17:39:17.910000,8.364,,nc92742
+-118.4766667,37.5806667,3.5,1986-07-22T18:16:52.560000,6.635,,nc92750
+-118.373,37.49,4.2,1986-07-22T18:19:35.760000,6.336,,nc92751
+-118.3753333,37.4981667,4.7,1986-07-22T18:29:43.240000,4.241,,nc92872
+-118.432,37.5706667,4.2,1986-07-22T20:16:59.390000,3.854,,nc76910
+-118.4668333,37.6113333,4.3,1986-07-22T20:22:25.790000,6.285,,nc76912
+-118.369,37.4876667,3.5,1986-07-22T20:49:14.200000,7.946,,nc92624
+-118.3953333,37.4905,4.2,1986-07-22T22:06:41.330000,8.527,,nc92398
+-118.374,37.494,3.66,1986-07-22T23:38:15.850000,3.436,,nc92359
+-118.4698333,37.5205,3.6,1986-07-23T02:12:13.410000,10.172,,nc77272
+-117.808,32.96,3.61,1986-07-23T02:57:58.580000,5.756,,ci127376
+-118.4258333,37.532,3.8,1986-07-23T02:58:19.830000,7.231,,nc77277
+-118.4885,37.6065,3.7,1986-07-23T03:01:01.850000,6.9,,nc77292
+-119.5778333,38.6496667,4.3,1986-07-23T07:39:10.770000,2.567,,nc76976
+-117.831,33.001,3.65,1986-07-23T10:29:01.900000,5.688,,ci127377
+-118.4373333,37.537,4.7,1986-07-23T15:39:11.360000,12.124,,nc77040
+-118.489,37.5618333,3.5,1986-07-23T16:27:40.460000,12.68,,nc77046
+-117.758,32.987,3.54,1986-07-23T17:22:16.870000,5.756,,ci603237
+-118.472,37.577,3.5,1986-07-24T01:17:09.240000,8.115,,nc81924
+-118.4651667,37.6041667,4.1,1986-07-24T02:43:10.340000,2.909,,nc81926
+-118.37,37.4678333,4.2,1986-07-24T06:10:04.870000,4.876,,nc91853
+-118.4003333,37.5466667,3.6,1986-07-24T09:22:29.790000,3.077,,nc91860
+-118.3713333,37.4883333,3.7,1986-07-24T14:58:44.270000,7.642,,nc91864
+-118.4433333,37.547,3.5,1986-07-24T16:44:40.230000,8.382,,nc91662
+-118.3678333,37.4718333,4.3,1986-07-24T19:03:25.350000,7.262,,nc90460
+-118.4735,37.5828333,3.6,1986-07-25T10:11:03.980000,7.348,,nc1140679
+-118.4731667,37.5321667,4.2,1986-07-26T14:39:40.450000,6.506,,nc89571
+-118.4503333,37.5871667,3.6,1986-07-26T21:58:46.800000,7.688,,nc1141068
+-118.2856667,37.3778333,4.2,1986-07-27T03:49:39.880000,3.738,,nc77495
+-118.3933333,37.5091667,3.9,1986-07-27T10:08:01.610000,5.384,,nc89370
+-117.822,32.945,3.74,1986-07-28T02:54:46.020000,5.622,,ci603764
+-118.4393333,37.5468333,4.2,1986-07-29T07:11:58.260000,7.45,,nc89208
+-117.841,32.934,4.31,1986-07-29T08:17:41.600000,5.622,,ci603888
+-118.467,37.5998333,4.6,1986-07-29T09:57:56.670000,6.405,,nc89211
+-117.83,32.941,3.62,1986-07-29T11:22:22.680000,5.622,,ci603902
+-118.3748333,37.4523333,4.2,1986-07-29T15:14:24.810000,7.38,,nc89090
+-117.823,32.963,3.58,1986-07-30T00:26:30.420000,5.622,,ci603947
+-118.5025,37.5323333,3.6,1986-07-30T00:46:50.100000,11.329,,nc89074
+-118.47,37.607,4.0,1986-07-30T06:03:31.690000,7.105,,nc77678
+-118.4565,37.5846667,4.8,1986-07-30T06:41:52.330000,6.585,,nc77679
+-117.801,32.985,4.15,1986-07-30T22:51:13.090000,5.756,,ci604030
+-117.829,32.972,4.04,1986-07-31T01:06:19.900000,5.024,,ci705123
+-118.3686667,37.4731667,5.8,1986-07-31T07:22:39.770000,5.754,,nc78895
+-118.347,37.4446667,4.5,1986-07-31T07:28:03.820000,6.794,,nc10085942
+-118.374,37.4751667,4.0,1986-07-31T07:36:01.800000,6.224,,nc88746
+-118.37,37.4925,3.8,1986-07-31T07:50:45.600000,4.474,,nc88812
+-116.573,33.965,3.52,1986-07-31T07:51:42.910000,4.455,,ci127475
+-118.3581667,37.4451667,3.9,1986-07-31T08:10:42.440000,5.394,,nc10085943
+-118.3733333,37.4825,4.0,1986-07-31T08:15:38.660000,6.582,,nc10085944
+-124.087,40.459,3.61,1986-08-01T02:31:38.400000,15.262,,nc1141979
+-123.3615,39.4885,4.67,1986-08-01T04:53:28.990000,58.145,,nc1142000
+-123.9956667,40.8936667,4.88,1986-08-01T05:30:01.520000,18.112,,nc1142006
+-118.4488333,37.5585,3.9,1986-08-01T06:34:42.710000,10.215,,nc86978
+-118.3928333,37.5141667,4.8,1986-08-01T14:27:15.890000,4.324,,nc85659
+-118.399,37.5103333,5.1,1986-08-01T14:28:18.150000,7.964,,nc85660
+-118.3903333,37.509,3.8,1986-08-01T15:10:43.670000,4.574,,nc85360
+-116.696,34.041,3.56,1986-08-02T11:36:57.650000,11.502,,ci604316
+-118.4313333,37.6151667,3.6,1986-08-02T14:51:36.540000,6.056,,nc85010
+-118.3926667,37.5125,3.55,1986-08-03T01:37:30.890000,4.424,,nc78187
+-118.4465,37.6173333,4.3,1986-08-03T10:33:04.750000,5.846,,nc84836
+-118.3541667,37.44,3.7,1986-08-05T02:32:07.680000,6.34,,nc84448
+-123.7133333,40.8663333,3.8,1986-08-08T17:31:38.910000,16.976,,nc10086481
+-118.3785,37.497,3.7,1986-08-12T09:29:47.700000,4.184,,nc83014
+-118.478,37.5068333,3.8,1986-08-12T15:37:27.500000,3.218,,nc82832
+-123.7268333,40.119,3.7,1986-08-14T00:50:48.260000,21.945,,nc82239
+-117.8,33.007,3.58,1986-08-15T13:09:47.380000,5.492,,ci605642
+-117.801,32.951,3.8,1986-08-15T18:45:16.550000,5.492,,ci605671
+-118.4433333,37.5455,3.9,1986-08-18T10:49:38.440000,10.398,,nc79568
+-118.3773333,37.4981667,3.9,1986-08-23T03:01:29.680000,4.534,,nc80304
+-123.0873333,39.128,3.6,1986-08-26T15:27:29.200000,4.232,,nc1142921
+-118.4341667,37.5296667,3.6,1986-08-26T19:07:20.630000,10.1,,nc80793
+-117.854,32.912,3.81,1986-08-26T19:11:49.920000,5.756,,ci606444
+-118.4631667,37.6131667,3.5,1986-08-27T09:20:40.220000,5.473,,nc80580
+-123.9426667,39.199,3.99,1986-08-27T15:52:52.920000,4.054,,nc1142942
+-117.41,32.505,3.7,1986-08-28T10:14:16.600000,5.562,,ci606558
+-116.599,33.955,3.87,1986-08-29T07:46:54.480000,4.448,,ci606616
+-118.4591667,37.6088333,3.5,1986-09-02T00:49:48,4.816,,nc80967
+-121.6526667,37.2803333,4.0,1986-09-03T04:31:14.510000,8.733,,nc80996
+-115.156,32.392,3.82,1986-09-07T01:03:31.130000,5.963,,ci607077
+-115.124,32.422,3.77,1986-09-07T01:11:01.940000,5.981,,ci607079
+-115.113,32.443,3.63,1986-09-07T04:32:35.520000,5.969,,ci607112
+-115.094,32.439,3.65,1986-09-07T06:40:10.790000,5.969,,ci607133
+-117.07,37.394,3.81,1986-09-07T12:55:12.370000,3.599,,ci708814
+-115.109,32.378,3.62,1986-09-07T13:14:02.050000,5.984,,ci607170
+-116.569,33.962,3.68,1986-09-09T16:22:50.610000,5.598,,ci607306
+-122.869,39.4656667,4.49,1986-09-11T07:39:32.020000,4.1,,nc1143317
+-118.4323333,37.6183333,3.5,1986-09-16T06:36:57.140000,5.526,,nc82687
+-118.4301667,37.6141667,3.5,1986-09-16T13:14:25.500000,5.386,,nc82742
+-118.4298333,37.614,3.5,1986-09-17T05:58:23.630000,5.916,,nc82807
+-118.4323333,37.6201667,4.0,1986-09-18T07:59:47.390000,5.016,,nc82905
+-121.8608333,36.0496667,3.7,1986-09-23T19:27:56.010000,14.48,,nc83498
+-118.4731667,37.7115,4.02,1986-09-24T04:57:39.990000,77.033,,nc1143886
+-117.228,37.332,3.69,1986-09-24T14:35:57.130000,4.208,,ci608088
+-117.798,32.99,4.18,1986-09-30T09:52:11.950000,5.743,,ci608328
+-117.846,32.981,3.99,1986-10-01T20:12:18.580000,5.622,,ci710131
+-117.75,33.002,3.79,1986-10-02T15:23:29.890000,5.75,,ci710150
+-115.366,32.425,3.72,1986-10-07T04:40:23.860000,6.017,,ci710413
+-118.3815,37.3648333,4.4,1986-10-09T05:37:25.190000,11.048,,nc85252
+-121.966,37.8211667,4.2,1986-10-11T05:17:35.640000,9.025,,nc85580
+-123.4978333,38.6898333,4.0,1986-10-12T06:43:01.860000,6.391,,nc85535
+-116.577,33.948,4.0,1986-10-15T02:28:47.760000,5.719,,ci710934
+-116.17,31.784,3.71,1986-10-17T00:28:47.550000,5.446,,ci711116
+-116.39,34.37,3.53,1986-10-17T18:56:16.350000,1.795,,ci711160
+-119.2986667,38.4083333,3.7,1986-10-19T00:42:39.980000,3.995,,nc86065
+-117.786,32.952,3.52,1986-10-20T22:35:30.010000,5.622,,ci711371
+-121.5686667,37.1775,3.6,1986-10-27T02:06:45.410000,6.721,,nc86584
+-117.144,32.604,4.48,1986-10-29T02:38:15.280000,14.834,,ci711952
+-119.2408333,38.0965,3.6,1986-10-30T04:01:52.770000,5.324,,nc86908
+-119.263,38.4641667,4.6,1986-10-31T03:57:27.910000,8.191,,nc87087
+-117.183,35.581,3.77,1986-10-31T14:27:05.160000,5.753,,ci712099
+-121.5626667,36.9536667,3.5,1986-10-31T18:46:14.080000,5.292,,nc86929
+-119.54,38.715,4.6,1986-11-01T19:23:38.300000,17.0,,usp0002zk7
+-123.767,40.0301667,4.11,1986-11-05T21:05:58.030000,36.831,,nc1145074
+-120.157,34.739,3.97,1986-11-06T09:19:58.450000,4.351,,ci129146
+-122.774,38.7925,3.67,1986-11-10T15:22:34.100000,-0.142,,nc87708
+-118.5138333,37.4786667,3.9,1986-11-13T16:55:37.890000,3.399,,nc87971
+-118.5108333,37.4756667,3.5,1986-11-13T18:45:21.940000,3.639,,nc87973
+-124.9835,40.3786667,3.9,1986-11-19T16:19:07.300000,12.815,,nc88348
+-124.4061667,40.3605,5.1,1986-11-21T23:33:01.700000,15.295,,nc88508
+-124.3978333,40.359,5.1,1986-11-21T23:34:18.970000,15.305,,nc88509
+-124.5143333,40.428,3.5,1986-11-22T00:05:22.270000,4.072,,nc88511
+-124.4298333,40.3596667,3.8,1986-11-22T03:17:58.840000,16.835,,nc88501
+-124.4208333,40.366,3.5,1986-11-23T01:21:48.970000,18.845,,nc88548
+-124.4746667,40.3563333,4.0,1986-11-23T05:41:06.720000,18.455,,nc88612
+-124.4371667,40.3573333,3.5,1986-11-26T08:22:15.610000,17.262,,nc88763
+-118.4393333,37.5858333,3.6,1986-11-29T13:10:40.440000,2.896,,nc89012
+-121.67,37.5598333,4.1,1986-12-11T14:18:04.950000,2.365,,nc89901
+-118.5141667,37.4786667,3.8,1986-12-12T02:19:40.040000,3.383,,nc90064
+-121.8001667,37.4548333,3.6,1986-12-20T02:19:41.590000,6.68,,nc90864
+-117.824,32.977,3.68,1986-12-20T12:19:36.930000,5.622,,ci714791
+-124.4041667,40.372,3.7,1986-12-20T19:45:34.700000,18.555,,nc90672
+-116.288,32.99,3.7,1986-12-25T17:35:22.870000,7.387,,ci715002
+-118.403,37.5715,4.0,1986-12-26T09:56:27,2.909,,nc91007
+-121.796,37.4521667,4.0,1986-12-29T15:28:04.650000,6.85,,nc91072
+-115.772,33.011,3.52,1986-12-29T16:05:14,4.884,,ci129393
diff --git a/tests/test_catalog.py b/tests/test_catalog.py
index bcbcef14..b9ec36a6 100644
--- a/tests/test_catalog.py
+++ b/tests/test_catalog.py
@@ -1,11 +1,19 @@
 import copy
 import unittest
+import os
 
 import numpy
 
+import csep
+from csep.core import regions, forecasts
 from csep.utils.time_utils import strptime_to_utc_epoch, strptime_to_utc_datetime
 from csep.core.catalogs import CSEPCatalog
 
+def comcat_path():
+    root_dir = os.path.dirname(os.path.abspath(__file__))
+    data_dir = os.path.join(root_dir, 'artifacts', 'Comcat',
+                            'test_catalog.csv')
+    return data_dir
 
 class CatalogFiltering(unittest.TestCase):
     def setUp(self):
@@ -64,6 +72,36 @@ def test_filter_with_datetime(self):
         filtered_test_cat = test_cat.filter(filters, in_place=False)
         numpy.testing.assert_equal(numpy.array([b'1', b'2'], dtype='S256').T, filtered_test_cat.get_event_ids())
 
+    def test_catalog_binning_and_filtering_acceptance(self):
+        # create space-magnitude region
+        region = regions.create_space_magnitude_region(
+            regions.california_relm_region(),
+            regions.magnitude_bins(4.5, 10.05, 0.1)
+        )
+
+        # read catalog
+        comcat = csep.load_catalog(comcat_path(), region=region)
+
+        # create data set from data set
+        d = forecasts.MarkedGriddedDataSet(
+            data=comcat.spatial_magnitude_counts(),
+            region=comcat.region,
+            magnitudes=comcat.region.magnitudes
+        )
+
+        for idm, m_min in enumerate(d.magnitudes):
+            # catalog filtered cumulative
+            c = comcat.filter([f'magnitude >= {m_min}'], in_place=False)
+            # catalog filtered incrementally
+            c_int = comcat.filter([f'magnitude >= {m_min}', f'magnitude < {m_min + 0.09999999}'], in_place=False)
+            # sum from overall data set
+            gs = d.data[:, idm:].sum()
+            # incremental counts
+            gs_int = d.data[:, idm].sum()
+            # event count from filtered catalog and events in binned data should be the same
+            numpy.testing.assert_almost_equal(gs, c.event_count)
+            numpy.testing.assert_almost_equal(gs_int, c_int.event_count)
+
 
 if __name__ == '__main__':
     unittest.main()