mirror of
https://github.com/OMGeeky/flucto-heisskleber.git
synced 2025-12-31 16:50:28 +01:00
143 lines
5.4 KiB
Python
143 lines
5.4 KiB
Python
import math
|
|
from datetime import datetime, timedelta
|
|
from queue import Queue
|
|
|
|
import numpy as np
|
|
|
|
from heisskleber.mqtt import MqttSubscriber
|
|
|
|
|
|
def round_dt(dt, delta):
|
|
"""Round a datetime object based on a delta timedelta."""
|
|
return datetime.min + math.floor((dt - datetime.min) / delta) * delta
|
|
|
|
|
|
def timestamp_generator(start_epoch, timedelta_in_ms):
|
|
"""Generate increasing timestamps based on a start epoch and a delta in ms."""
|
|
timestamp_start = datetime.fromtimestamp(start_epoch)
|
|
delta = timedelta(milliseconds=timedelta_in_ms)
|
|
delta_half = timedelta(milliseconds=timedelta_in_ms // 2)
|
|
next_timestamp = round_dt(timestamp_start, delta) + delta_half
|
|
while True:
|
|
yield datetime.timestamp(next_timestamp)
|
|
next_timestamp += delta
|
|
|
|
|
|
def interpolate(t1, y1, t2, y2, t_target):
|
|
"""Perform linear interpolation between two data points."""
|
|
y1, y2 = np.array(y1), np.array(y2)
|
|
fraction = (t_target - t1) / (t2 - t1)
|
|
interpolated_values = y1 + fraction * (y2 - y1)
|
|
return interpolated_values.tolist()
|
|
|
|
|
|
class Resampler:
|
|
"""
|
|
Synchronously resample data based on a fixed rate. Can handle upsampling and downsampling.
|
|
|
|
Parameters:
|
|
----------
|
|
config : namedtuple
|
|
Configuration for the resampler.
|
|
subscriber : MqttSubscriber
|
|
Synchronous Subscriber
|
|
"""
|
|
|
|
def __init__(self, config, subscriber: MqttSubscriber):
|
|
self.config = config
|
|
self.subscriber = subscriber
|
|
self.buffer = Queue()
|
|
self.resample_rate = self.config.resample_rate
|
|
self.delta_t = round(self.resample_rate / 1_000, 3)
|
|
|
|
def run(self):
|
|
topic, message = self.subscriber.receive()
|
|
self.buffer.put(self._pack_data(message))
|
|
self.data_keys = message.keys()
|
|
|
|
while True:
|
|
topic, message = self.subscriber.receive()
|
|
self.buffer.put(self._pack_data(message))
|
|
|
|
def resample(self):
|
|
aggregated_data = []
|
|
aggregated_timestamps = []
|
|
# Get first element to determine timestamp
|
|
timestamp, message = self.buffer.get()
|
|
timestamps = timestamp_generator(timestamp, self.resample_rate)
|
|
|
|
# step through interpolation timestamps
|
|
for next_timestamp in timestamps:
|
|
# last_timestamp, last_message = timestamp, message
|
|
|
|
# append new data to buffer until the most recent data
|
|
# is newer than the next interplation timestamp
|
|
while timestamp < next_timestamp:
|
|
aggregated_timestamps.append(timestamp)
|
|
aggregated_data.append(message)
|
|
timestamp, message = self.buffer.get()
|
|
|
|
return_timestamp = round(next_timestamp - self.delta_t / 2, 3)
|
|
|
|
# Case 1: Only one new data point was received
|
|
if len(aggregated_data) == 1:
|
|
last_timestamp, last_message = (
|
|
aggregated_timestamps[0],
|
|
aggregated_data[0],
|
|
)
|
|
|
|
# Case 1a Upsampling:
|
|
# The data point is not within our time interval
|
|
# We step through time intervals, yielding interpolated data points
|
|
while timestamp - next_timestamp > self.delta_t:
|
|
last_message = interpolate(
|
|
last_timestamp,
|
|
last_message,
|
|
timestamp,
|
|
message,
|
|
return_timestamp,
|
|
)
|
|
last_timestamp = return_timestamp
|
|
return_timestamp += self.delta_t
|
|
next_timestamp = next(timestamps)
|
|
yield self._unpack_data(last_timestamp, last_message)
|
|
|
|
# Case 1b: The data point is within our time interval
|
|
# We simply yield the data point
|
|
# Note, this will also be the case once we have advanced the time interval by upsampling
|
|
last_message = interpolate(
|
|
last_timestamp,
|
|
last_message,
|
|
timestamp,
|
|
message,
|
|
return_timestamp,
|
|
)
|
|
last_timestamp = return_timestamp
|
|
return_timestamp += self.delta_t
|
|
yield self._unpack_data(last_timestamp, last_message)
|
|
|
|
# Case 2 - downsampling: Multiple data points were during the resampling timeframe
|
|
# We simply yield the mean of the data points, which is more robust and performant than interpolation
|
|
if len(aggregated_data) > 1:
|
|
# yield self._handle_downsampling(return_timestamp, aggregated_data)
|
|
mean_message = np.mean(np.array(aggregated_data), axis=0)
|
|
yield self._unpack_data(return_timestamp, mean_message)
|
|
|
|
# reset the aggregator
|
|
aggregated_data.clear()
|
|
aggregated_timestamps.clear()
|
|
|
|
def _handle_downsampling(self, return_timestamp, aggregated_data) -> dict:
|
|
"""Handle the downsampling case."""
|
|
mean_message = np.mean(np.array(aggregated_data), axis=0)
|
|
return self._unpack_data(return_timestamp, mean_message)
|
|
|
|
def _pack_data(self, data) -> tuple[int, list]:
|
|
# pack data from dict to tuple list
|
|
ts = data.pop("epoch")
|
|
return (ts, list(data.values()))
|
|
|
|
def _unpack_data(self, ts, values) -> dict:
|
|
# from tuple
|
|
return {"epoch": round(ts, 3), **dict(zip(self.data_keys, values))}
|