Added final details for offline processing
Added merge buffer methods and a (very inefficient) nearest neighbor system.
This commit is contained in:
2 files changed
+75
-4
No files matched your search
@@ -2,6 +2,7 @@
|
|||||||
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
# Copyright (C) 2026 Association Exergie <association.exergie@gmail.com>
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
from bisect import bisect_left
|
from bisect import bisect_left
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import csv
|
import csv
|
||||||
@@ -20,6 +21,10 @@ class ValueBuffer:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
self._samples.append((timestamp, uuid, value))
|
self._samples.append((timestamp, uuid, value))
|
||||||
|
|
||||||
|
def merge(self, buffer: ValueBuffer) -> None:
|
||||||
|
with self._lock:
|
||||||
|
self._samples += buffer._samples
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._samples.clear()
|
self._samples.clear()
|
||||||
@@ -32,6 +37,32 @@ class ValueBuffer:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
return list({signal_id for _, signal_id, _ in self._samples})
|
return list({signal_id for _, signal_id, _ in self._samples})
|
||||||
|
|
||||||
|
def get_nearest_neighbor_no_lock(self, uuid: UUID, timestamp: int) -> float | None:
|
||||||
|
previous: tuple[int, UUID, float] | None = None
|
||||||
|
|
||||||
|
for sample in self._samples:
|
||||||
|
if sample[1] != uuid:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if sample[0] >= timestamp:
|
||||||
|
if previous is None:
|
||||||
|
return sample[2]
|
||||||
|
|
||||||
|
d1 = abs(sample[0] - timestamp)
|
||||||
|
d2 = abs(previous[0] - timestamp)
|
||||||
|
|
||||||
|
if d1 < d2:
|
||||||
|
return sample[2]
|
||||||
|
else:
|
||||||
|
return previous[2]
|
||||||
|
|
||||||
|
previous = sample
|
||||||
|
|
||||||
|
if previous is not None:
|
||||||
|
return previous[2]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def get_nearest_neighbor(self, uuid: UUID, timestamp: int) -> float | None:
|
def get_nearest_neighbor(self, uuid: UUID, timestamp: int) -> float | None:
|
||||||
previous: tuple[int, UUID, float] | None = None
|
previous: tuple[int, UUID, float] | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from copy import copy
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
import inspect
|
import inspect
|
||||||
@@ -200,9 +202,12 @@ class DeriveUnit:
|
|||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
return not self._stopped_event.is_set()
|
return not self._stopped_event.is_set()
|
||||||
|
|
||||||
def process_offline(self, buffer: ValueBuffer) -> ValueBuffer:
|
def _process_offline_worker(
|
||||||
|
self, buffer: ValueBuffer, start: int, end: int
|
||||||
|
) -> ValueBuffer:
|
||||||
|
samples = buffer._samples.copy()
|
||||||
output_buffer: ValueBuffer = ValueBuffer()
|
output_buffer: ValueBuffer = ValueBuffer()
|
||||||
for sample in buffer._samples:
|
for sample in samples[start:end]:
|
||||||
if sample[1] in [signal.id for signal in self._input_signals]:
|
if sample[1] in [signal.id for signal in self._input_signals]:
|
||||||
input_args: list[ValueDescriptor] = []
|
input_args: list[ValueDescriptor] = []
|
||||||
for signal in self._input_signals:
|
for signal in self._input_signals:
|
||||||
@@ -214,12 +219,47 @@ class DeriveUnit:
|
|||||||
input_args.append(
|
input_args.append(
|
||||||
ValueDescriptor(
|
ValueDescriptor(
|
||||||
signal.id,
|
signal.id,
|
||||||
buffer.get_nearest_neighbor(signal.id, sample[0]),
|
buffer.get_nearest_neighbor_no_lock(
|
||||||
|
signal.id, sample[0]
|
||||||
|
),
|
||||||
sample[0],
|
sample[0],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
result = self._process_function(*input_args, self._return_signal)
|
result = self._process_function(*input_args, self._return_signal)
|
||||||
output_buffer.append(result.timestamp, self._return_signal.id, result.value)
|
output_buffer.append(
|
||||||
|
result.timestamp, self._return_signal.id, result.value
|
||||||
|
)
|
||||||
|
return output_buffer
|
||||||
|
|
||||||
|
def process_offline(self, buffer: ValueBuffer) -> ValueBuffer:
|
||||||
|
num_samples = len(buffer._samples)
|
||||||
|
num_chunks = 16
|
||||||
|
chunk_size = num_samples // num_chunks
|
||||||
|
|
||||||
|
output_buffer: ValueBuffer = ValueBuffer()
|
||||||
|
with ThreadPoolExecutor(max_workers=num_chunks) as executor:
|
||||||
|
ranges: list[tuple[int, int]] = []
|
||||||
|
|
||||||
|
for i in range(num_chunks):
|
||||||
|
start = i * chunk_size
|
||||||
|
|
||||||
|
if i == num_chunks - 1:
|
||||||
|
end = num_samples
|
||||||
|
else:
|
||||||
|
end = (i + 1) * chunk_size
|
||||||
|
|
||||||
|
ranges.append((start, end))
|
||||||
|
|
||||||
|
futures = [
|
||||||
|
executor.submit(self._process_offline_worker, buffer, *range)
|
||||||
|
for range in ranges
|
||||||
|
]
|
||||||
|
|
||||||
|
results = [future.result() for future in futures]
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
output_buffer.merge(result)
|
||||||
|
|
||||||
return output_buffer
|
return output_buffer
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user