#!.venv/bin/python3
import xml.etree.cElementTree as ET
from collections.abc import Sequence
import numpy as np
from .peak import Peak
from .fitintegralcoll import FitIntegralCollection
[docs]
class Fitlist(Sequence):
"""
Representation of a Fitlist as a Sequence of Peaks, grouped in Fits.
Encompasses all neccessary information which is saved in a fitlist,
most importantly the peak information. Those can be accessed as a
flattened array via their consecutive indices. As a Sequence,
Fitlists can be accessed backwards via negative-Indexing,
just like standard python Lists.
Calibrations and integrals are saved in separate structures and accessed
through object methods.
Methods
-------
- get_integral_by_peakID
- get_integral_list_by_type
- get_all_integrals
- closest
"""
def __init__(self, path: str):
"""
Create Fitlist instance from a path to the fitlist.xml file.
Parses the xml-tree and flattens the created objects to
a one dimensional list.
Parameters
----------
path : str
a relative or absolute path to the fitlist.xml file.
"""
if not isinstance(path, str):
raise TypeError("'path' attribute expects a string (of a path)")
try:
self.tree = ET.parse(path)
self.root = self.tree.getroot()
except:
raise ValueError(f"Could not read xml-file at {path}.")
# len(Fitlist) should enumerate peaks, but the xml-file groups a different
# number of peaks in a fit (it has to if you think about it).
# So its structure is roughly:
#{xml: [fit0: (p0), (p1) ], [fit1: (p0), (p1), (p2) ], [fit2: (p0) ], [fit3: (p0) ], [fit4: (p0), (p1) ], ... }
# We Iterate simply through each fit and total all occurences of "peak"
# to get the total length. But for indexing each peak, we have to know in
# which fit the peak is contained. For easier indexing, we build a list
# where each consequtive peak-index maps to a tuple of the index of its
# encompassing fit and the offset in this fit.
# So the structure of this list (from the above example) looks like this:
#[(0,0), (0,1), (1,0), (1,1), (1,2), (2,0), (3,0), (4,0), (4,1), ...]
# In general, the subindexer looks like this:
#[(fitID(p0),Offset(p0)), (fitID(p1),Offset(p1)), (fitID(p2),Offset(p2)), (fitID(p3),Offset(p3)), ...]
self.__subindexer = []
self.__spectra_collection = []
self.__integral_collection = FitIntegralCollection()
self.__length = 0
index = 0
for fit in self.root:
if fit.tag == "fit":
spc = fit.find("spectrum").attrib
self.__spectra_collection.append((spc["name"], spc["calibration"]))
self.__integral_collection.create_new_integral(fit)
offset = 0
for child in fit:
if child.tag == "peak":
self.__subindexer.append((index, offset))
self.__length += 1
offset += 1
index += 1
self.__peakcache = [False]*self.__length # once generated, Peak-object overwrites the 'False' entry
super().__init__()
[docs]
def __len__(self):
return self.__length
[docs]
def __getitem__(self, i):
if i >= self.__length or i < -self.__length:
raise IndexError(f"{i} is out of range for Fitlist (len={self.__length})")
if i<0:
i = self.__length + i
if self.__peakcache[i]: # caching
return self.__peakcache[i]
else:
p = Peak(self.root[self.__subindexer[i][0]], self.__subindexer[i][1], self.__spectra_collection[self.__subindexer[i][0]], i)
self.__peakcache[i] = p
return p
[docs]
def get_integral_by_peakID(self, peakID:int) -> FitIntegralCollection:
"""
returns the FitIntegralCollection for a given peakID.
Each Fit contains a collection of integrals, regardless whether it was
created because of a peakfit of an integration. With a peakfit it is
needed for the background determination.
Parameters
----------
peakID : int
ID of the peak
(NOT the index in this Fitlist object, but the Peak.ID property)
"""
# the integral belonging to peakid is the one with the index stored in the first element in the subindexer belonging to the peakid
return self.__integral_collection[self.__subindexer[peakID][0]]
[docs]
def get_integral_list_by_type(self, int_type:str, include_ids:bool=False)->list:
"""
Returns all FitIntegrals of a given type as a list.
Parameters
----------
int_type : str
Selects which type of integrals to return.
Possible Types: sub, bg, tot
include_ids : bool
If True, populates list with tuples of
(FitIntegral, integralID:int)
"""
int_lst = []
for id, int_dict in enumerate(self.__integral_collection.to_list()):
if int_type in int_dict.keys():
if include_ids:
int_lst.append((int_dict[int_type], id))
else:
int_lst.append(int_dict[int_type])
return int_lst
[docs]
def get_all_integrals(self) -> list:
"""
Returns all FitIntegralCollection objects in a List, ordered by integralID.
"""
return self.__integral_collection.to_list()
[docs]
def closest(self, pos):
"""Returns the peak with the best matching position to the argument pos"""
closest_dist = [Peak.build_empty(), 10**10]
for elem in self:
vgl = np.abs(elem.pos - pos)
if vgl<closest_dist[1]:
closest_dist = [elem, vgl]
return closest_dist
[docs]
def debug(self):
print("Fitlist object summary:")
print("")
print(f"length: {self.__length}")
print(f"subindexer:")
for i, p in enumerate(self.__subindexer):
print(f"{i:>5}: {p}")
print(f"Integral Collection:")
for index, fint_dict in enumerate(self.__integral_collection):
print(f"Index {index}:")
for key in fint_dict:
print(f"\t {key}: {repr(fint_dict[key])}")
print("----------------------------------------------------------------------")
print(f"__subindexer")