Files
controller-wisetopdataserver/python/biopro/util/analysis.py
T
2021-12-20 14:52:55 +08:00

746 lines
23 KiB
Python

"""data analysis module.
This module is some kinds of out of date and less maintain.
"""
import abc
from collections import Counter, deque, defaultdict
from datetime import datetime
from statistics import mean, median, pstdev
from typing import Set, List, Iterable, Tuple, Dict, Optional
from .cli import *
from .console import hex_value
from .iter import zip_next2, flat_iter, counter_diff, counter_total, counter_product_sum
from .text import list_padding, Table
try:
# noinspection PyStatementEffect
datetime.fromisoformat
def _from_iso_format(expr: str) -> datetime:
return datetime.fromisoformat(expr)
except AttributeError:
import re
def _from_iso_format(expr: str) -> datetime:
m = re.match(r'(\d{4})-(\d{2})-(\d{2}) *(?:(\d{2})(?::(\d{2})(?::(\d{2})(?:\.(\d{3,6}))?)?)?)?', expr)
if not m:
raise ValueError()
dy = int(m.group(1))
dm = int(m.group(2))
dd = int(m.group(3))
th = int(m.group(4))
tm = int(m.group(5))
ts = int(m.group(6))
tf = int(m.group(7))
return datetime(dy, dm, dd, th, tm, ts, tf)
def _percent(a, b) -> str:
return '%.2f%%' % (100 * a / b)
def _diff_continuous_data(it, dimension: int = 1, overflow: Optional[int] = None) -> Counter:
ret = Counter(map(lambda v: v[1] - v[0], zip_next2(it, dimension - 1)))
if overflow is not None:
ret[1] += ret[-overflow + 1]
del ret[-overflow + 1]
return ret
def _max_continuous_data(it, dimension: int = 1, overflow: Optional[int] = None) -> Counter:
ret = Counter()
s = 0
for a, b in zip_next2(it, dimension - 1):
if b - a == 1 or a - b + 1 == overflow:
s += 1
else:
ret[s] += 1
s = 0
return ret
# noinspection PyUnusedLocal
class Main(CliMain):
def __init__(self):
super().__init__()
@cli_flags('-h', '--help', force_return=True)
def _help(self, opt: str):
"""print help document"""
self.print_help()
@cli_command('txt')
def _txt_data_command(self, cmd: str, argv: List[str]):
"""analysis txt data file"""
return _TextDataCommand
@cli_command("dump")
def _dump_data_command(self, cmd: str, argv: List[str]):
"""analysis sync data dump file"""
return _DumpDataCommand
def run(self):
self.print_help()
# noinspection PyUnusedLocal
class _AbstractDataCommand(CliSubCommandMain, metaclass=abc.ABCMeta):
def __init__(self, command: str):
super().__init__(command)
self.data_file: str = None
# flag and options
self.ramp_data = False
self.avg_data = False
self.overview = True
self.chip_id_table = False
self.time_diff_table = False
self.time_diff_threshold = 10
self.ramp_data_threshold = 10
self.avg_data_time_step = 10
self.avg_data_duration = 100
self.avg_data_bin = 10
@cli_flags('-h', '--help', force_return=True)
def _help(self, opt: str):
"""print help document"""
self.print_help()
@cli_flags('--ramp')
def _ramp_data(self, opt: str):
"""analysis ramp data"""
self.ramp_data = True
@cli_flags('--avg', '--average')
def _avg_data(self, opt: str):
"""average data"""
self.avg_data = True
@cli_flags('--no-overview')
def _no_overview(self, opt: str):
"""do not print overview table"""
self.overview = False
@cli_flags('--chip')
def _chip_id(self, opt: str):
"""chip ID table"""
self.chip_id_table = True
@cli_flags('--time-diff')
def _time_diff_table(self, opt: str):
"""time diff table"""
self.time_diff_table = True
@cli_options('--time-diff-threshold', value='VALUE')
def _time_diff_threshold(self, opt: str, value: str):
"""the threshold to print address in time diff table"""
self.time_diff_threshold = int(value)
@cli_options('--ramp-threshold', value='VALUE')
def _ramp_data_threshold(self, opt: str, value: str):
"""the threshold to print address in ramp data table"""
self.ramp_data_threshold = int(value)
@cli_options('--average-time-step', value='VALUE')
def _avg_data_time_step(self, opt: str, value: str):
"""the time step for avg data"""
self.avg_data_time_step = int(value)
@cli_options('--average-duration', value='VALUE')
def _avg_data_duration(self, opt: str, value: str):
"""the duration to move average for avg data"""
self.avg_data_duration = int(value)
@cli_options('--average-bin', value='VALUE')
def _avg_data_bin(self, opt: str, value: str):
"""the bin for avg data"""
self.avg_data_bin = int(value)
@cli_arguments(0, value='FILE')
def _dump_data_file(self, pos: int, value: str):
"""set configuration ny recording file"""
self.data_file = value
def run(self):
if self.data_file is None:
raise RuntimeError('lost FILE')
self.run_collect()
if self.overview:
self.run_table_overview()
if self.chip_id_table:
print()
self.run_table_chip_id()
if self.time_diff_table:
print()
self.run_table_time_diff()
if self.ramp_data:
print()
self.run_table_ramp_data()
elif self.avg_data:
print()
self.run_table_avg_data()
@abc.abstractmethod
def run_collect(self):
pass
@abc.abstractmethod
def run_table_overview(self):
pass
@abc.abstractmethod
def chip_id_list(self) -> Iterable[int]:
pass
def run_table_chip_id(self):
counter = Counter(self.chip_id_list())
total = counter_total(counter)
print('chip id, total', total)
table = Table('ID', 'count', '(count%)')
for chip, count in sorted(counter.items(), key=lambda it: it[0]):
table.append(str(chip), str(count), _percent(count, total))
table.set_format(1, split=' : ')
table.print(' ')
@abc.abstractmethod
def data_time_list(self) -> Iterable[float]:
pass
@abc.abstractmethod
def data_time_address(self, delta: float) -> str:
pass
def run_table_time_diff(self):
time_list = list(self.data_time_list())
time_diff = list(map(lambda v: round(v, 2),
map(counter_diff, zip_next2(time_list, 0))))
time_diff_counter = Counter(time_diff)
total = counter_total(time_diff_counter)
print('time diff [int, ms]', 'total', total)
table = Table('diff', 'count', 'count%', 'address (line)')
for k, v in sorted(time_diff_counter.items(), key=lambda it: abs(it[0])):
if abs(k) < self.time_diff_threshold:
a = ''
else:
a = self.data_time_address(k)
table.append(k, v, _percent(v, total), a)
table.set_format(1, split=' : ')
table.set_format(3, align_right=False)
table.print(' ')
@abc.abstractmethod
def data_value_list(self) -> Iterable[float]:
pass
@abc.abstractmethod
def data_value_address(self, delta: float) -> str:
pass
def run_table_ramp_data(self):
counter = _diff_continuous_data(self.data_value_list(), overflow=1024)
product = counter_product_sum(counter)
missing = counter_product_sum(counter, key_mapper=lambda k: k - 1)
total = counter_total(counter)
print('data content, total', product,
', missing', missing,
', rate', _percent(missing, product))
table = Table('gap', 'count', 'count%', 'total%', 'address (line)')
for k, v in sorted(counter.items(), key=lambda it: abs(it[0])):
if abs(k) < self.ramp_data_threshold:
a = ''
else:
a = self.data_value_address(k)
table.append(k, v, _percent(v, total), _percent(k * v, product), a)
table.set_format(1, split=' : ')
table.set_format(4, align_right=False)
table.print(' ')
#
print()
counter = _max_continuous_data(self.data_value_list(), overflow=1024)
total = counter_total(counter)
product = counter_product_sum(counter)
print('max continuous data',
', total', product)
table = Table('block size', 'count', 'count%', 'total%')
for k, v in sorted(counter.items(), key=lambda it: it[0], reverse=True):
table.append(k, v, _percent(v, total), _percent(k * v, product))
table.set_format(1, split=' : ')
table.print(' ')
def run_table_avg_data(self):
counter = Counter()
ls = deque()
time_next = None
for time, data in zip(self.data_time_list(), self.data_value_list()):
if time_next is None:
ls.append((time, data))
time_next = time + self.avg_data_time_step
elif time < time_next:
ls.append((time, data))
continue
else:
old_time = time - self.avg_data_duration
try:
while True:
if ls[0][0] < old_time:
ls.popleft()
else:
break
except IndexError:
pass
else:
if len(ls):
counter[int(sum(map(lambda it: it[1], ls)) / len(ls) / self.avg_data_bin)] += 1
ls.append((time, data))
time_next += self.avg_data_time_step
total = counter_total(counter)
print('data average global :', round(mean(self.data_value_list()), 4),
', total segment :', total,
', average duration :', self.avg_data_duration, '+', self.avg_data_time_step, 'ms')
table = Table('avg range', 'count', 'count%')
for k, v in sorted(counter.items(), key=lambda it: -abs(it[1])):
table.append('[%d, %d)' % (k * self.avg_data_bin, (k + 1) * self.avg_data_bin), v, _percent(v, total))
table.set_format(1, split=' : ')
table.print(' ')
# noinspection PyUnusedLocal
class _DumpDataCommand(_AbstractDataCommand):
def __init__(self, command: str):
super().__init__(command)
self.time_diff_threshold = 1000
self.ramp_data_threshold = 100
# data
self.sync_count = 0
self.byte_total = 0
self.byte_invalid = 0
self.time_diff = []
self.group_count: List[int] = []
self.group_size: List[int] = []
self.byte_count: List[int] = []
self.counter_list: List[List[int]] = []
self._chip_id_list: List[List[int]] = []
self._data_list: List[List[List[int]]] = []
self._data_time_list: List[List[float]] = []
def run_collect(self):
with open(self.data_file) as dump:
self.sync_count = 0
self.byte_invalid = 0
self.byte_total = 0
self.time_diff = []
self.group_count: List[int] = []
self.group_size: List[int] = []
self.byte_count: List[int] = []
self.counter_list: List[List[int]] = []
self._chip_id_list: List[List[int]] = []
self._data_list: List[List[List[int]]] = []
self._data_time_list: List[List[float]] = []
time_stamp = None
number = 0
try:
for number, line in enumerate(dump):
line = line.strip()
if line.startswith('#'):
prev_time = time_stamp
time_stamp = _from_iso_format(line[2:].strip())
if prev_time is not None:
delta = time_stamp - prev_time
self.time_diff.append(delta.total_seconds() * 1000 + 0.001 * delta.microseconds)
else:
self.sync_count += 1
data = line.split(' ')
self.byte_total += len(data)
self.byte_count.append(len(data))
group = self._parsing_group(data)
if len(group) > 0:
self.group_count.append(len(group))
self.group_size.extend(map(len, group))
else:
self.byte_invalid += len(data)
self.counter_list.append(self._parsing_counter(group))
self._chip_id_list.append(self._parsing_chip_id(group))
self._data_list.append(self._parsing_data(group))
self._data_time_list.append(self._parsing_data_time(group))
except BaseException as e:
raise RuntimeError('line ' + str(number + 1), ':', *e.args) from e
def run_table_overview(self):
print('sync count', self.sync_count, ', total byte', self.byte_total, ', invalid', self.byte_invalid)
print()
data_time_diff = _diff_continuous_data(self._data_time_list, dimension=2)
data_time_int_diff = list(map(counter_diff,
map(lambda it: (int(it[0]), int(it[1])),
zip_next2(self._data_time_list, 1))))
table = [
'',
'msec / data [f]',
'msec / data [i]',
'msec / sync',
'group / sync',
'bytes / group',
'bytes / sync',
]
list_padding(table)
data = [data_time_diff, data_time_int_diff, self.time_diff, self.group_count, self.group_size, self.byte_count]
add = ['mean']
add.extend(map(lambda it: '%.2f' % it, map(mean, data)))
list_padding(table, add, align_right=True)
list_padding(table)
add = ['median']
add.extend(map(lambda it: '%.2f' % it, map(median, data)))
list_padding(table, add, align_right=True)
list_padding(table)
add = ['min']
add.extend(map(lambda it: '%.2f' % it, map(min, data)))
list_padding(table, add, align_right=True)
list_padding(table)
add = ['max']
add.extend(map(lambda it: '%.2f' % it, map(max, data)))
list_padding(table, add, align_right=True)
list_padding(table)
add = ['std']
add.extend(map(lambda it: '%.2f' % it, map(pstdev, data)))
list_padding(table, add, align_right=True)
list_padding(table)
for line in table:
print(line)
def chip_id_list(self) -> Iterable[int]:
return flat_iter(self._chip_id_list)
def _run_table_counter(self):
miss_counter = _diff_continuous_data(self.counter_list, dimension=2, overflow=256)
product = counter_product_sum(miss_counter) + 1
missing = counter_product_sum(miss_counter, key_mapper=lambda k: k - 1)
total = counter_total(miss_counter)
print('header counter, total', product,
', missing', missing,
', rate', _percent(missing, product))
table = Table('gap', 'count', 'count%', 'total%', 'address (line:group)')
for k, v in sorted(miss_counter.items(), key=lambda it: (it[1], -it[0]), reverse=True):
if k == 1:
a = ''
else:
a = ' '.join(map(lambda it: '%d:%d' % it, self._find_diff2(self.counter_list, k)))
table.append(k, v, _percent(v, total), _percent(k * v, product), a)
table.set_format(1, split=' : ')
table.set_format(4, align_right=False)
table.print(' ')
def data_time_list(self) -> Iterable[float]:
return flat_iter(self._data_time_list)
def data_time_address(self, delta: float) -> str:
return ' '.join(map(lambda it: '%d:%d' % it, self._find_diff2(self._data_time_list, delta, 2)))
def data_value_list(self) -> Iterable[float]:
return flat_iter(flat_iter(self._data_list))
def data_value_address(self, delta: float) -> str:
return ' '.join(map(lambda it: '%d:%d+%d' % it, self._find_diff3(self._data_list, delta)))
@staticmethod
def _parsing_group(data: List[str]) -> List[List[str]]:
length = len(data)
ret = []
i = 0
while i < length:
header = data[i]
if header != 'FF':
break
else:
size = hex_value(data[i + 2])
ret.append(data[i:i + size + 3])
i += 3 + size
return ret
@staticmethod
def _parsing_counter(data: List[List[str]]) -> List[int]:
return list(map(lambda g: hex_value(g[1]), data))
@staticmethod
def _parsing_chip_id(data: List[List[str]]) -> List[int]:
return list(map(lambda g: hex_value(g[3]), data))
@staticmethod
def _parsing_data(data: List[List[str]]) -> List[List[int]]:
ret = []
for group in data:
tmp = []
for i in range(9, len(group) - 1, 2):
value = ((hex_value(group[i]) & 0b0000_1111) << 6) | ((hex_value(group[i + 1]) & 0b1111_1100) >> 2)
flag = hex_value(group[i + 1]) & 0b0011
if flag == 0:
tmp.append(value - 512)
ret.append(tmp)
return ret
@staticmethod
def _parsing_data_time(data: List[List[str]]) -> List[float]:
ret = []
for group in data:
v0 = hex_value(group[5])
v1 = hex_value(group[6])
v2 = hex_value(group[7])
v3 = hex_value(group[8])
value = v0 | (v1 << 8) | (v2 << 16) | (v3 << 24)
ret.append(value / 32)
return ret
@staticmethod
def _find_diff2(ls: List[List[float]], diff_value: float, n=2) -> Iterable[Tuple[int, int]]:
prev = None
for i, group in enumerate(ls):
for j, value in enumerate(group):
if prev is not None and round(value - prev, n) == round(diff_value, n):
yield 2 * (i + 1), j + 1
prev = value
@staticmethod
def _find_diff3(ls: List[List[List[float]]], diff_value: float, n=2) -> Iterable[Tuple[int, int, int]]:
prev = None
for i, row in enumerate(ls):
for j, frame in enumerate(row):
for k, value in enumerate(frame):
if prev is not None and round(value - prev, n) == round(diff_value, n):
yield 2 * (i + 1), j + 1, k + 1
prev = value
# noinspection PyUnusedLocal
class _TextDataCommand(_AbstractDataCommand):
def __init__(self, command: str):
super().__init__(command)
self.error_line_table = False
#
self.start_address = None
self.data_count = 0
self.time_list: List[int] = []
self._chip_id_list: List[int] = []
# self.channel_counter: Dict[int, Counter] = {}
self._data_list: List[int] = []
self.comment = []
self.error_line: Dict[int, Set[str]] = {}
@cli_flags('--error-line')
def _error_line_table(self, opt: str):
"""error line table"""
self.error_line_table = True
def run(self):
super().run()
if self.error_line_table:
print()
self._run_error_line_table()
def run_collect(self):
with open(self.data_file) as dump:
self.data_count = 0
self.time_list = []
self._chip_id_list = []
# self.channel_counter = defaultdict(Counter)
self._data_list = []
self.comment = []
self.error_line = defaultdict(set)
try:
for number, line in enumerate(dump):
line = line.strip()
if len(line) == 0:
pass
elif line.startswith('#'):
self.comment.append(line)
else:
if self.start_address is None:
self.start_address = number
self.data_count += 1
data = line.split(' ')
t = float(data[0])
d = int(data[1])
c = int(data[2])
v = float(data[3])
self.time_list.append(t)
self._chip_id_list.append(d)
# self.channel_counter[d][c] += 1
self._data_list.append(v)
except BaseException as e:
raise RuntimeError('line ' + str(number + 1), ':', *e.args) from e
def run_table_overview(self):
for comment in self.comment:
print(comment)
print('total', self.data_count)
def chip_id_list(self) -> Iterable[int]:
return self._chip_id_list
def data_time_list(self) -> Iterable[float]:
return self.time_list
def data_time_address(self, delta: float) -> str:
error_line = list(map(lambda it: '%d' % it, self._find_diff2(self.time_list, delta, n=2)))
ret = ' '.join(map(lambda it: '%d' % it, error_line))
if self.error_line_table:
for line in error_line:
self.error_line[line].add('time_diff')
return ret
def data_value_list(self) -> Iterable[float]:
return self._data_list
def data_value_address(self, delta: float) -> str:
error_line = list(self._find_diff2(self._data_list, delta))
ret = ' '.join(map(lambda it: '%d' % it, error_line))
if self.error_line_table:
for line in error_line:
self.error_line[line].add('data_diff')
return ret
def _run_error_line_table(self):
error_type_set = list(set(flat_iter(self.error_line.values())))
error_line: Dict[str, List[int]] = defaultdict(list)
for line, error_type in self.error_line.items():
a = ' '.join(map(lambda e: e if e in error_type else ' ' * len(e), error_type_set))
error_line[a].append(line)
total = sum(map(len, error_line.values()))
print('error type',
', total', total,
', total lines', self.data_count,
', rate', _percent(total, self.data_count))
table = Table('error_type', 'count', 'count%', 'line')
for k, v in sorted(error_line.items(), key=lambda it: len(it[1])):
table.append(k, len(v),
_percent(len(v), total),
' '.join(map(str, sorted(v))))
table.set_format(1, split=' : ')
table.set_format(3, align_right=False)
table.print(' ')
def _find_diff2(self, ls: List[int], diff_value: float, n=0) -> Iterable[int]:
prev = None
for i, value in enumerate(ls):
if prev is not None and round(value - prev, n) == diff_value:
yield i + self.start_address
prev = value
if __name__ == '__main__':
Main().main()