Files
python3-damaris/src/gui/ResultReader.py
T
Markus Rosenstihl 28e6c9d1a0 * Make it work in bookworm, redid some of the changes from last commit
* Changed default number types to int16 for ADC_Result and float32 for
  Accumulations
2026-03-09 10:55:55 +01:00

577 lines
23 KiB
Python

# -*- coding: iso-8859-1 -*-
#############################################################################
# #
# Name: Class ResultReader #
# #
#############################################################################
import os
import os.path
import glob
import time
import sys
import base64
import numpy
try:
import xml.etree.cElementTree
ELEMENT_TREE = True
except:
import xml.parsers.expat
ELEMENT_TREE = False
import threading
from datetime import datetime
from damaris.data import ADC_Result
from damaris.data import Error_Result
from damaris.data import Temp_Result
from damaris.data import Config_Result
class ResultReader:
"""
starts at some point and returns result objects until none are there
"""
CONFIG_TYPE = 3
TEMP_TYPE = 2
ADCDATA_TYPE = 1
ERROR_TYPE = 0
def __init__(self, spool_dir=".", no=0, result_pattern="job.%09d.result", clear_jobs=False, clear_results=False):
self.spool_dir = spool_dir
self.start_no = no
self.no = self.start_no
self.result_pattern = result_pattern
self.clear_jobs=clear_jobs
self.clear_results=clear_results
self.quit_flag=threading.Event() # asychronous quit flag
def __iter__(self):
"""
get next job with iterator
"""
expected_filename=os.path.join(self.spool_dir,self.result_pattern%(self.no))
while os.access(expected_filename,os.R_OK):
yield self.get_result_object(expected_filename)
# purge result file
if self.clear_results:
if os.path.isfile(expected_filename): os.remove(expected_filename)
if self.clear_jobs:
if os.path.isfile(expected_filename[:-7]): os.remove(expected_filename[:-7])
self.no+=1
expected_filename=os.path.join(self.spool_dir,self.result_pattern%(self.no))
return
def get_result_object(self, in_filename):
"""
get result object
"""
# class-intern result-object currently being processed
retries=0
result_file=None
while result_file is None:
try:
result_file = open(in_filename, "r")
except IOError as e:
if retries>10:
raise e
print(e, "retry", retries)
time.sleep(0.05)
retries+=1
# get date of last modification
self.result_job_date = datetime.fromtimestamp(os.stat(in_filename)[8])
if ELEMENT_TREE:
self.__parseFile = self.__parseFile_cETree
else:
self.__parseFile = self.__parseFile_expat
self.__parseFile (result_file)
result_file.close()
result_file = None
r=self.result
self.result = None
return r
def __parseFile_cETree(self, in_file):
self.result = None
self.in_description_section=False
self.result_description = { }
self.result_job_number = None
# Job Date is set in __read_file()
self.__filetype = None
for elem in xml.etree.cElementTree.ElementTree(file=in_file).iter():
if elem.tag == 'result':
self.result_job_number = int(elem.get("job"))
pass
elif elem.tag == 'description':
if elem.text!=None:
self.result_description = {}
self.in_description_section=True
self.in_description_data=()
for an_item in elem:
self.in_description_data = (an_item.get("key"), an_item.get("type"), an_item.text)
# make item contents to dictionary item:
k,t,v=self.in_description_data
self.in_description_data=()
if t == "None":
self.result_description[k]=None
if t == "Float":
self.result_description[k]=float(v)
elif t == "Int":
self.result_description[k]=int(v)
elif t == "Long":
self.result_description[k]=int(v)
elif t == "Complex":
self.result_description[k]=complex(v)
elif t == "Boolean":
self.result_description[k]=bool(v)
elif t == "String":
self.result_description[k]=v
else:
# Anything else will be handled as a string
# Probably "repr".
self.result_description[k]=v
elif elem.tag == 'adcdata':
self.__filetype = ResultReader.ADCDATA_TYPE
self.adc_result_trailing_chars = ""
if self.result is None:
self.result = ADC_Result()
# None: new guess for adc data encoding
# "a": ascii
# "b": base64
self.adc_data_encoding = None
self.result.set_sampling_rate(float(elem.get("rate")))
self.result.set_job_id(self.result_job_number)
self.result.set_job_date(self.result_job_date)
self.result.set_nChannels(int(elem.get("channels")))
self.result.set_description_dictionary(self.result_description.copy())
title = "ADC-Result: job-id=%d"%int(self.result_job_number)
if len(self.result_description) > 0:
for k,v in self.result_description.items():
title += ", %s=%s"%(k,v)
self.result.set_title(title)
self.result_description = None
self.adc_result_sample_counter = 0
self.adc_result_parts = [] # will contain arrays of sampled intervals, assumes same sample rate
else:
if float(elem.get("rate")) != self.result.get_sampling_rate():
print("sample rate different in ADC_Result, found %f, former value %f"%\
(float(in_attribute["rate"]),self.result.get_sampling_rate()))
new_samples = int(elem.get("samples"))
self.adc_result_sample_counter += new_samples
self.adc_result_trailing_chars = "".join(elem.text.splitlines())
tmp_string = base64.standard_b64decode(self.adc_result_trailing_chars)
self.adc_result_trailing_chars = None
tmp = numpy.fromstring(tmp_string,dtype='int16')
tmp_string = None
self.adc_result_parts.append(tmp)
tmp = None
# we do not need this adcdata anymore, delete it
elem.clear()
elif elem.tag == 'error':
self.__filetype = ResultReader.ERROR_TYPE
self.result = Error_Result()
self.result.set_job_id(self.result_job_number)
self.result.set_job_date(self.result_job_date)
self.result.set_description_dictionary(self.result_description.copy())
self.result.set_error_message(elem.text)
elif elem.tag == 'temp':
self.__filetype = ResultReader.TEMP_TYPE
self.result = Error_Result()
self.result.set_job_id(self.result_job_number)
self.result.set_job_date(self.result_job_date)
elif elem.tag == 'conf':
self.__filetype = ResultReader.CONF_TYPE
self.result = Error_Result()
self.result.set_job_id(self.result_job_number)
self.result.set_job_date(self.result_job_date)
# xml file was traversed now prepare the data in one go
# prepare result data
if self.result is not None and \
self.__filetype == ResultReader.ADCDATA_TYPE and \
self.adc_result_sample_counter>0:
# fill the ADC_Result with collected data
# x data
self.result.x=numpy.arange(self.adc_result_sample_counter, dtype="float32")/\
self.result.get_sampling_rate()
self.result.y = []
nChannels = self.result.get_nChannels()
# initialise the y arrays
for i in range(nChannels):
self.result.y.append(numpy.empty(self.adc_result_sample_counter, dtype='int16'))
# remove from result stack
tmp_index = 0
while self.adc_result_parts:
tmp_part=self.adc_result_parts.pop(0)
tmp_size = int(tmp_part.size/nChannels)
for i in range(nChannels):
# split interleaved data
self.result.y[i][tmp_index:tmp_index+tmp_size] = tmp_part[i::nChannels]
self.result.y[i][tmp_index:tmp_index+tmp_size] = tmp_part[i::nChannels]
if self.result.index != []:
self.result.index.append((tmp_index, tmp_index+tmp_size-1))
else:
self.result.index = [(0,tmp_size-1)]
tmp_index += tmp_size
self.result.cont_data=True
tmp_part = None
def __parseFile_expat(self, in_file):
"Parses the given file, adding it to the result-queue"
self.result = None
self.in_description_section=False
self.result_description = { }
self.result_job_number = None
# Job Date is set in __read_file()
self.__filetype = None
# Expat XML-Parser & Binding handlers
self.xml_parser = xml.parsers.expat.ParserCreate()
self.xml_parser.StartElementHandler = self.__xmlStartTagFound
self.xml_parser.CharacterDataHandler = self.__xmlCharacterDataFound
self.xml_parser.EndElementHandler = self.__xmlEndTagFound
self.element_stack=[]
try:
# short version, but pyexpat buffers are awfully small
# self.xml_parser.ParseFile(in_file)
# read all, at least try
databuffer=in_file.read(-1)
# test wether really everything was read...
databuffer2=in_file.read(self.xml_parser.buffer_size)
if databuffer2=="":
# parse everything at once
self.xml_parser.Parse(databuffer,True)
else:
# do the first part ...
self.xml_parser.Parse(databuffer,False)
databuffer=databuffer2
# ... and again and again
while databuffer!="":
self.xml_parser.Parse(databuffer,False)
databuffer=in_file.read(-1)
self.xml_parser.Parse("",True)
except xml.parsers.expat.ExpatError as e:
print("result file %d: xml parser '%s' error at line %d, offset %d"%(self.no,
xml.parsers.expat.ErrorString(e.code),
e.lineno,
e.offset))
self.result = None
del databuffer
self.xml_parser.StartElementHandler=None
self.xml_parser.EndElementHandler=None
self.xml_parser.CharacterDataHandler=None
del self.xml_parser
# prepare result data
if self.result is not None and \
self.__filetype == ResultReader.ADCDATA_TYPE and \
self.adc_result_sample_counter>0:
# fill the ADC_Result with collected data
self.result.x=numpy.arange(self.adc_result_sample_counter, dtype="float32")/\
self.result.get_sampling_rate()
self.result.y=[]
self.result.index=[]
for i in range(2):
self.result.y.append(numpy.empty((self.adc_result_sample_counter,), dtype="int16"))
tmp_sample_counter=0
while self.adc_result_parts:
tmp_part=self.adc_result_parts.pop(0)
tmp_size=tmp_part.size/2
self.result.y[0][tmp_sample_counter:tmp_sample_counter+tmp_size]=tmp_part[::2]
self.result.y[1][tmp_sample_counter:tmp_sample_counter+tmp_size]=tmp_part[1::2]
self.result.index.append((tmp_sample_counter,tmp_sample_counter+tmp_size-1))
tmp_sample_counter+=tmp_size
self.result.cont_data=True
# Callback when a xml start tag is found
def __xmlStartTagFound(self, in_name, in_attribute):
# General Result-Tag
if in_name == "result":
self.result_job_number = int(in_attribute["job"])
# Job-Date is set in __read_file()
# Description
elif in_name == "description":
# old style description:
if len(in_attribute)!=0:
self.result_description = in_attribute.copy()
self.in_description_section=True
self.in_description_data=()
elif self.in_description_section and in_name == "item":
self.in_description_data=[in_attribute["key"], in_attribute["type"], ""]
# ADC_Results
elif in_name == "adcdata":
self.__filetype = ResultReader.ADCDATA_TYPE
self.adc_result_trailing_chars = ""
if self.result is None:
self.result = ADC_Result()
# None: new guess for adc data encoding
# "a": ascii
# "b": base64
self.adc_data_encoding = None
self.result.set_sampling_rate(float(in_attribute["rate"]))
self.result.set_job_id(self.result_job_number)
self.result.set_job_date(self.result_job_date)
self.result.set_description_dictionary(self.result_description.copy())
title="ADC-Result: job-id=%d"%int(self.result_job_number)
if len(self.result_description)>0:
for k,v in self.result_description.items():
title+=", %s=%s"%(k,v)
self.result.set_title(title)
self.result_description=None
self.adc_result_sample_counter = 0
self.adc_result_parts=[] # will contain arrays of sampled intervals, assumes same sample rate
else:
if float(in_attribute["rate"])!=self.result.get_sampling_rate():
print("sample rate different in ADC_Result, found %f, former value %f"%\
(float(in_attribute["rate"]),self.result.get_sampling_rate()))
new_samples=int(in_attribute["samples"])
self.adc_result_sample_counter += new_samples
# version depends on the inclusion of http://bugs.python.org/issue1137
if sys.hexversion>=0x020501f0:
# extend buffer to expected base64 size (2 channels, 2 byte)
required_buffer=int(new_samples*4/45+1)*62
if self.xml_parser.buffer_size < required_buffer:
try:
self.xml_parser.buffer_size=required_buffer
except AttributeError:
pass
# pass all chardata as one block
self.xml_parser.buffer_text = True
# do not change the contents
self.xml_parser.returns_unicode=False
# Error_Results
elif in_name == "error":
self.__filetype = ResultReader.ERROR_TYPE
self.result = Error_Result()
self.result.set_job_id(self.result_job_number)
self.result.set_job_date(self.result_job_date)
self.result.set_description_dictionary(self.result_description.copy())
# Temp_Results
elif in_name == "temp":
self.__filetype = ResultReader.TEMP_TYPE
self.result = Temp_Result()
self.result.set_job_id(self.result_job_number)
self.result.set_job_date(self.result_job_date)
# Config_Results
elif in_name == "conf":
self.__filetype = ResultReader.CONFIG_TYPE
self.result = Config_Result()
self.result.set_job_id(self.result_job_number)
self.result.set_job_date(self.result_job_date)
# maintain the stack
self.element_stack.append(in_name)
def __xmlCharacterDataFound(self, in_cdata):
if self.in_description_section and len(self.in_description_data):
self.in_description_data[2]+=in_cdata
# ADC_Result
elif self.__filetype == ResultReader.ADCDATA_TYPE and self.element_stack[-1]=="adcdata":
self.adc_result_trailing_chars+=in_cdata
# Error_Result
elif self.__filetype == ResultReader.ERROR_TYPE:
tmp_string = self.result.get_error_message()
if tmp_string is None: tmp_string = ""
tmp_string += in_cdata
self.result.set_error_message(tmp_string)
# Temp_Results
elif self.__filetype == ResultReader.TEMP_TYPE:
pass
# Config_Results
elif self.__filetype == ResultReader.CONFIG_TYPE:
pass
def __xmlEndTagFound(self, in_name):
# maintain the stack
self.element_stack.pop()
if in_name == "adcdata":
# ADC_Result
if self.__filetype == ResultReader.ADCDATA_TYPE:
# detect type of data encoding from first line
if self.adc_data_encoding is None:
self.adc_result_trailing_chars=self.adc_result_trailing_chars.strip()
first_line_end=self.adc_result_trailing_chars.find("\n")
first_line=""
if first_line_end!=-1:
first_line=self.adc_result_trailing_chars[:first_line_end]
else:
first_line=self.adc_result_trailing_chars
if len(first_line.lstrip("-0123456789 \t\n\r"))==0:
try:
list(map(int,list(filter(len,first_line.split()))))
except ValueError as e:
pass
else:
self.adc_data_encoding="a"
if self.adc_data_encoding is None and len(first_line)%4==0:
try:
base64.standard_b64decode(first_line)
except TypeError:
pass
else:
self.adc_data_encoding="b"
if self.adc_data_encoding is None:
print("unknown ADC data format \"%s\""%first_line)
tmp=None
if self.adc_data_encoding=="a":
values=list(map(int,self.adc_result_trailing_chars.split()))
tmp=numpy.array(values, dtype="int16")
elif self.adc_data_encoding=="b":
tmp_string=base64.standard_b64decode(self.adc_result_trailing_chars)
tmp=numpy.fromstring(tmp_string, dtype="int16")
del tmp_string
else:
print("unknown ADC data format")
self.adc_result_trailing_chars=""
self.adc_result_parts.append(tmp)
del tmp
return
elif in_name == "description":
self.in_description_section=False
elif self.in_description_section and in_name == "item":
# make item contents to dictionary item:
k,t,v=self.in_description_data
self.in_description_data=()
if t == "None":
self.result_description[k]=None
if t == "Float":
self.result_description[k]=float(v)
elif t == "Int":
self.result_description[k]=int(v)
elif t == "Long":
self.result_description[k]=int(v)
elif t == "Complex":
self.result_description[k]=complex(v)
elif t == "Boolean":
self.result_description[k]=bool(v)
elif t == "String":
self.result_description[k]=v
else:
# Anything else will be handled as a string
# Probably "repr".
self.result_description[k]=v
elif in_name == "result":
pass
# Error_Result
elif self.__filetype == ResultReader.ERROR_TYPE:
pass
# Temp_Result
elif self.__filetype == ResultReader.TEMP_TYPE:
pass
# Config_Result
elif self.__filetype == ResultReader.CONFIG_TYPE:
pass
class BlockingResultReader(ResultReader):
"""
to follow an active result stream
"""
def __init__(self, spool_dir=".", no=0, result_pattern="job.%09d.result", clear_jobs=False, clear_results=False):
ResultReader.__init__(self, spool_dir, no, result_pattern, clear_jobs=clear_jobs, clear_results=clear_results)
self.stop_no=None # end of job queue
self.poll_time=0.1 # sleep interval for polling results, <0 means no polling and stop
self.in_advance=0
def __iter__(self):
"""
get next job with iterator
block until result is available
"""
expected_filename=os.path.join(self.spool_dir,self.result_pattern%(self.no))
while (not self.quit_flag.isSet()) and (self.stop_no is None or self.stop_no>self.no):
if not os.access(expected_filename,os.R_OK):
# stop polling, if required
if self.poll_time<0: break
self.quit_flag.wait(self.poll_time)
continue
# find pending results
self.in_advance=max(self.no,self.in_advance)
in_advance_filename=os.path.join(self.spool_dir,self.result_pattern%(self.in_advance+1))
while os.access(in_advance_filename, os.R_OK) and (self.stop_no is None or self.stop_no>self.in_advance+1):
# do not more than 100 results in advance at one glance
if self.in_advance>self.no+100: break
self.in_advance+=1
in_advance_filename=os.path.join(self.spool_dir,self.result_pattern%(self.in_advance+1))
if self.quit_flag.isSet(): break
r=self.get_result_object(expected_filename)
if self.quit_flag.isSet(): break
if self.quit_flag.isSet(): break
yield r
if self.clear_results:
if os.path.isfile(expected_filename): os.remove(expected_filename)
if self.clear_jobs:
if os.path.isfile(expected_filename[:-7]): os.remove(expected_filename[:-7])
self.no+=1
expected_filename=os.path.join(self.spool_dir,self.result_pattern%(self.no))
return
def quit(self):
self.quit_flag.set()