openrvdas.logger.transforms.subsample_transform

Compute subsamples of input data.

  1#!/usr/bin/env python3
  2"""Compute subsamples of input data.
  3"""
  4
  5import logging
  6import time
  7
  8from logger.utils.das_record import DASRecord  # noqa: E402
  9from logger.utils.subsample import subsample  # noqa: E402
 10from logger.transforms.derived_data_transform import DerivedDataTransform  # noqa: E402
 11
 12
 13################################################################################
 14class SubsampleTransform(DerivedDataTransform):
 15    """Transform that computes subsamples of the specified variables.
 16    """
 17
 18    def __init__(self, field_spec, back_seconds=60*60,
 19                 metadata_interval=None, **kwargs):
 20        """
 21        ```
 22        field_spec - a dict that contains the fields to be subsampled,
 23                     the algorithms and parameters to be used, and the
 24                     names of the output values to be produced.
 25
 26          e.g.  {
 27                  field_1:{
 28                    output: smoothed_field_1,
 29                    subsample:{
 30                      'type':'boxcar_average', 'window': 30, 'interval': 30
 31                    }
 32                  },
 33                  field_2:{
 34                    output: smoothed_field_2,
 35                    subsample:{
 36                      'type':'boxcar_average', 'window': 15, 'interval': 5
 37                    }
 38                  }
 39                }
 40
 41        back_seconds - the number of seconds of data to cache for use by sampler
 42
 43        metadata_interval - how many seconds between when we attach field metadata
 44                     to a record we send out.
 45        ```
 46        """
 47        super().__init__(**kwargs)  # processes 'quiet' and type hints
 48
 49        logging.warning('SubsampleTransform is deprecated in favor of InterpolationTransform')
 50
 51        self.field_spec = field_spec
 52        self.back_seconds = back_seconds
 53        self.field_list = list(field_spec.keys())
 54
 55        # A dict of the cached values we're hanging onto
 56        self.cached_values = {f: [] for f in self.field_list}
 57
 58        # Last timestamp that's been emitted for each field
 59        self.last_timestamp = {f: 0 for f in self.field_list}
 60
 61        self.metadata_interval = metadata_interval
 62        self.last_metadata_send = 0
 63
 64    ############################
 65    def fields(self):
 66        """Which fields are we interested in to produce transformed data?"""
 67        return self.field_list
 68
 69    ############################
 70    def _metadata(self):
 71        """Return a dict of metadata for our derived fields."""
 72        metadata_fields = {
 73            self.field_spec[field]['output']: {
 74                'description':
 75                'Subsampled values of %s via %s' %
 76                (field, self.field_spec[field].get('subsample',
 77                                                   'unspecified algorithm')),
 78                'device': 'SubsampleTransform',
 79                'device_type': 'DerivedDataTransform',
 80                'device_type_field': field
 81            }
 82            for field in self.field_list
 83        }
 84        return metadata_fields
 85
 86    ############################
 87    def _add_record(self, record):
 88        """Cached the values contained in a new record."""
 89        if type(record) not in [DASRecord, dict]:
 90            logging.error('SubsampleTransform records must be dict or '
 91                          'DASRecord. Received type %s: %s', type(record), record)
 92            return
 93
 94        if type(record) is DASRecord:
 95            timestamp = record.timestamp
 96            fields = record.fields
 97        else:
 98            timestamp = record.get('timestamp')
 99            fields = record.get('fields')
100
101        if not fields:
102            logging.error('SubsampleTransform: no fields found in record: %s', record)
103            return
104
105        # First, copy the new data into our cache
106        for field in self.field_list:
107            new_vals = fields.get(field)
108            if not new_vals:
109                continue
110            # If list, we assume it's [(ts, value), (ts, value),...]
111            if type(new_vals) is list:
112                self.cached_values[field].extend(new_vals)
113                # If not list, assume DASRecord or simple field dict; add tuple
114            elif timestamp:
115                self.cached_values[field].append((timestamp, new_vals))
116            else:
117                logging.error('SubsampleTransform found no timestamp in '
118                              'record: %s', record)
119
120    ############################
121    def _clean_cache(self):
122        """Which fields are we interested in to produce transformed data?"""
123        now = time.time()
124        for field in self.field_list:
125            # Iterate forward through field cache until we find a timestamp
126            # that is recent enough to keep
127            cache = self.cached_values[field]
128            for keep_index in range(len(cache)):
129                if cache[keep_index][0] > now - self.back_seconds:
130                    # Throw away everything before that index
131                    self.cached_values[field] = cache[keep_index:]
132                    break
133
134    ############################
135    def transform(self, record):
136        """Incorporate any useable fields in this record, and if it gives
137        us any new subsampled values, aggregate and return them.
138        """
139
140        # If we've got a list, hope it's a list of records. Recurse,
141        # calling transform() on each of the list elements in order and
142        # return the resulting list.
143        if type(record) is list:
144            results = []
145            for single_record in record:
146                results.append(self.transform(single_record))
147            return results
148
149        # Clean out old data
150        self._add_record(record)
151
152        # Clean out old data
153        self._clean_cache()
154
155        now = time.time()
156
157        result_fields = {}
158        for field in self.field_list:
159            if not self.cached_values[field]:
160                continue
161
162            output_field = self.field_spec[field].get('output')
163            if not output_field:
164                logging.warning('No "output" spec found for field %s', field)
165                continue
166            algorithm = self.field_spec[field].get('subsample')
167            if not algorithm:
168                logging.warning('No "subsample" spec found for field %s', field)
169                continue
170            field_result = subsample(algorithm, self.cached_values[field],
171                                     self.last_timestamp[field], now)
172            if field_result:
173                result_fields[output_field] = field_result
174                self.last_timestamp[field] = field_result[-1][0]
175
176        if not result_fields:
177            return None
178
179        # Form the response, adding in metadata if so specified and it's
180        # been long enough since we last sent it.
181        result = {'fields': result_fields}
182        if self.metadata_interval and \
183           now - self.metadata_interval > self.last_metadata_send:
184            result['metadata'] = {'fields': self._metadata()}
185            self.last_metadata_send = now
186
187        return result
class SubsampleTransform(logger.transforms.derived_data_transform.DerivedDataTransform):
 15class SubsampleTransform(DerivedDataTransform):
 16    """Transform that computes subsamples of the specified variables.
 17    """
 18
 19    def __init__(self, field_spec, back_seconds=60*60,
 20                 metadata_interval=None, **kwargs):
 21        """
 22        ```
 23        field_spec - a dict that contains the fields to be subsampled,
 24                     the algorithms and parameters to be used, and the
 25                     names of the output values to be produced.
 26
 27          e.g.  {
 28                  field_1:{
 29                    output: smoothed_field_1,
 30                    subsample:{
 31                      'type':'boxcar_average', 'window': 30, 'interval': 30
 32                    }
 33                  },
 34                  field_2:{
 35                    output: smoothed_field_2,
 36                    subsample:{
 37                      'type':'boxcar_average', 'window': 15, 'interval': 5
 38                    }
 39                  }
 40                }
 41
 42        back_seconds - the number of seconds of data to cache for use by sampler
 43
 44        metadata_interval - how many seconds between when we attach field metadata
 45                     to a record we send out.
 46        ```
 47        """
 48        super().__init__(**kwargs)  # processes 'quiet' and type hints
 49
 50        logging.warning('SubsampleTransform is deprecated in favor of InterpolationTransform')
 51
 52        self.field_spec = field_spec
 53        self.back_seconds = back_seconds
 54        self.field_list = list(field_spec.keys())
 55
 56        # A dict of the cached values we're hanging onto
 57        self.cached_values = {f: [] for f in self.field_list}
 58
 59        # Last timestamp that's been emitted for each field
 60        self.last_timestamp = {f: 0 for f in self.field_list}
 61
 62        self.metadata_interval = metadata_interval
 63        self.last_metadata_send = 0
 64
 65    ############################
 66    def fields(self):
 67        """Which fields are we interested in to produce transformed data?"""
 68        return self.field_list
 69
 70    ############################
 71    def _metadata(self):
 72        """Return a dict of metadata for our derived fields."""
 73        metadata_fields = {
 74            self.field_spec[field]['output']: {
 75                'description':
 76                'Subsampled values of %s via %s' %
 77                (field, self.field_spec[field].get('subsample',
 78                                                   'unspecified algorithm')),
 79                'device': 'SubsampleTransform',
 80                'device_type': 'DerivedDataTransform',
 81                'device_type_field': field
 82            }
 83            for field in self.field_list
 84        }
 85        return metadata_fields
 86
 87    ############################
 88    def _add_record(self, record):
 89        """Cached the values contained in a new record."""
 90        if type(record) not in [DASRecord, dict]:
 91            logging.error('SubsampleTransform records must be dict or '
 92                          'DASRecord. Received type %s: %s', type(record), record)
 93            return
 94
 95        if type(record) is DASRecord:
 96            timestamp = record.timestamp
 97            fields = record.fields
 98        else:
 99            timestamp = record.get('timestamp')
100            fields = record.get('fields')
101
102        if not fields:
103            logging.error('SubsampleTransform: no fields found in record: %s', record)
104            return
105
106        # First, copy the new data into our cache
107        for field in self.field_list:
108            new_vals = fields.get(field)
109            if not new_vals:
110                continue
111            # If list, we assume it's [(ts, value), (ts, value),...]
112            if type(new_vals) is list:
113                self.cached_values[field].extend(new_vals)
114                # If not list, assume DASRecord or simple field dict; add tuple
115            elif timestamp:
116                self.cached_values[field].append((timestamp, new_vals))
117            else:
118                logging.error('SubsampleTransform found no timestamp in '
119                              'record: %s', record)
120
121    ############################
122    def _clean_cache(self):
123        """Which fields are we interested in to produce transformed data?"""
124        now = time.time()
125        for field in self.field_list:
126            # Iterate forward through field cache until we find a timestamp
127            # that is recent enough to keep
128            cache = self.cached_values[field]
129            for keep_index in range(len(cache)):
130                if cache[keep_index][0] > now - self.back_seconds:
131                    # Throw away everything before that index
132                    self.cached_values[field] = cache[keep_index:]
133                    break
134
135    ############################
136    def transform(self, record):
137        """Incorporate any useable fields in this record, and if it gives
138        us any new subsampled values, aggregate and return them.
139        """
140
141        # If we've got a list, hope it's a list of records. Recurse,
142        # calling transform() on each of the list elements in order and
143        # return the resulting list.
144        if type(record) is list:
145            results = []
146            for single_record in record:
147                results.append(self.transform(single_record))
148            return results
149
150        # Clean out old data
151        self._add_record(record)
152
153        # Clean out old data
154        self._clean_cache()
155
156        now = time.time()
157
158        result_fields = {}
159        for field in self.field_list:
160            if not self.cached_values[field]:
161                continue
162
163            output_field = self.field_spec[field].get('output')
164            if not output_field:
165                logging.warning('No "output" spec found for field %s', field)
166                continue
167            algorithm = self.field_spec[field].get('subsample')
168            if not algorithm:
169                logging.warning('No "subsample" spec found for field %s', field)
170                continue
171            field_result = subsample(algorithm, self.cached_values[field],
172                                     self.last_timestamp[field], now)
173            if field_result:
174                result_fields[output_field] = field_result
175                self.last_timestamp[field] = field_result[-1][0]
176
177        if not result_fields:
178            return None
179
180        # Form the response, adding in metadata if so specified and it's
181        # been long enough since we last sent it.
182        result = {'fields': result_fields}
183        if self.metadata_interval and \
184           now - self.metadata_interval > self.last_metadata_send:
185            result['metadata'] = {'fields': self._metadata()}
186            self.last_metadata_send = now
187
188        return result

Transform that computes subsamples of the specified variables.

SubsampleTransform(field_spec, back_seconds=3600, metadata_interval=None, **kwargs)
19    def __init__(self, field_spec, back_seconds=60*60,
20                 metadata_interval=None, **kwargs):
21        """
22        ```
23        field_spec - a dict that contains the fields to be subsampled,
24                     the algorithms and parameters to be used, and the
25                     names of the output values to be produced.
26
27          e.g.  {
28                  field_1:{
29                    output: smoothed_field_1,
30                    subsample:{
31                      'type':'boxcar_average', 'window': 30, 'interval': 30
32                    }
33                  },
34                  field_2:{
35                    output: smoothed_field_2,
36                    subsample:{
37                      'type':'boxcar_average', 'window': 15, 'interval': 5
38                    }
39                  }
40                }
41
42        back_seconds - the number of seconds of data to cache for use by sampler
43
44        metadata_interval - how many seconds between when we attach field metadata
45                     to a record we send out.
46        ```
47        """
48        super().__init__(**kwargs)  # processes 'quiet' and type hints
49
50        logging.warning('SubsampleTransform is deprecated in favor of InterpolationTransform')
51
52        self.field_spec = field_spec
53        self.back_seconds = back_seconds
54        self.field_list = list(field_spec.keys())
55
56        # A dict of the cached values we're hanging onto
57        self.cached_values = {f: [] for f in self.field_list}
58
59        # Last timestamp that's been emitted for each field
60        self.last_timestamp = {f: 0 for f in self.field_list}
61
62        self.metadata_interval = metadata_interval
63        self.last_metadata_send = 0
field_spec - a dict that contains the fields to be subsampled,
             the algorithms and parameters to be used, and the
             names of the output values to be produced.

  e.g.  {
          field_1:{
            output: smoothed_field_1,
            subsample:{
              'type':'boxcar_average', 'window': 30, 'interval': 30
            }
          },
          field_2:{
            output: smoothed_field_2,
            subsample:{
              'type':'boxcar_average', 'window': 15, 'interval': 5
            }
          }
        }

back_seconds - the number of seconds of data to cache for use by sampler

metadata_interval - how many seconds between when we attach field metadata
             to a record we send out.
field_spec
back_seconds
field_list
cached_values
last_timestamp
metadata_interval
last_metadata_send
def fields(self):
66    def fields(self):
67        """Which fields are we interested in to produce transformed data?"""
68        return self.field_list

Which fields are we interested in to produce transformed data?

def transform(self, record):
136    def transform(self, record):
137        """Incorporate any useable fields in this record, and if it gives
138        us any new subsampled values, aggregate and return them.
139        """
140
141        # If we've got a list, hope it's a list of records. Recurse,
142        # calling transform() on each of the list elements in order and
143        # return the resulting list.
144        if type(record) is list:
145            results = []
146            for single_record in record:
147                results.append(self.transform(single_record))
148            return results
149
150        # Clean out old data
151        self._add_record(record)
152
153        # Clean out old data
154        self._clean_cache()
155
156        now = time.time()
157
158        result_fields = {}
159        for field in self.field_list:
160            if not self.cached_values[field]:
161                continue
162
163            output_field = self.field_spec[field].get('output')
164            if not output_field:
165                logging.warning('No "output" spec found for field %s', field)
166                continue
167            algorithm = self.field_spec[field].get('subsample')
168            if not algorithm:
169                logging.warning('No "subsample" spec found for field %s', field)
170                continue
171            field_result = subsample(algorithm, self.cached_values[field],
172                                     self.last_timestamp[field], now)
173            if field_result:
174                result_fields[output_field] = field_result
175                self.last_timestamp[field] = field_result[-1][0]
176
177        if not result_fields:
178            return None
179
180        # Form the response, adding in metadata if so specified and it's
181        # been long enough since we last sent it.
182        result = {'fields': result_fields}
183        if self.metadata_interval and \
184           now - self.metadata_interval > self.last_metadata_send:
185            result['metadata'] = {'fields': self._metadata()}
186            self.last_metadata_send = now
187
188        return result

Incorporate any useable fields in this record, and if it gives us any new subsampled values, aggregate and return them.