twbk_opener_ND2000/tbwk/Measurement.py
2025-03-27 14:51:37 +08:00

225 lines
7.0 KiB
Python

import numpy as np
from scipy.interpolate import interp1d
from tbwk.RawOpener import Block
from tbwk.Properties import PropertyBag
"""
Block structure
(all is little endian)
The header of each block consists of block type (4 bytes), block size (4 bytes) and 4 empty bytes.
A measurement block (151) contains 3 nested blocks. The blocks content start at 520.
- 152, Contains a description and the measurement name:
12 bytes (?)
1 int8 (length), usually 28
n char, usually "Thermo Scientific DataCarton"
8 bytes, usually 00 00 00 00 00 f0 3f. Might be float64 for "1.0"
1 int8 (length)
n char, contains the measurement name
12 bytes, usually 3 int32: [1, 1, 0]
- 920, a wrapper for another block.
12 bytes (?)
Then another subblock starts with FE FF FF FF, with the content starting at offset 520 again.
- 921
12 bytes
1 int8 (length), usually 29
n char, usually "Thermo Scientific UV Spectrum"
8 bytes, usually 00 00 00 00 00 f0 3f.
- 922
12 bytes
\n
4 bytes
- 930
12 bytes
Then another subblock starts with FE FF FF FF, offset at 520
- 931
12 bytes
1 int8 (length), usually 30
n char, usually "Thermo Scientific Data-Vector " (sic)
1 int8 (length), usually 29
n char, usually "SpectrumFileFormat.UVSpectrum"
1 int8 (length)
n char, measurement name again.
8 bytes, windows filetime, eg 97 dc af d7 2c 46 d6 01 = 2020-06-19 13:29:06.5140375 (+2)
- 932
y values.
y label at offset 59, containing axis label in long and short:
1 int8
n char
1 int8
n char
8 bytes
21 bytes
1 int32 (4 bytes) indicating number of floats
n float64 containing y values
- 932
x values
y label at offset 59, containing axis label in long and short:
1 int8
n char
1 int8 (usually 0)
n char (empty)
8 bytes
21 bytes
1 int32 (4 bytes) indicating number of floats
n float64 containing y values
- 990 (10X?)
- All contain XML data.
- 62, content starts commonly at 520 after the block header.
12 bytes (?)
n char, XML content
"""
class Measurement:
"""
Represents a single measurement on a NanoDrop 2000.
Use the methods of this object for the most often used parameters, or
access the PropertyBag to access the tabular values as set by the measurement method.
"""
title: str = None
x_values: np.ndarray = None
x_label: str = None
y_values: np.ndarray = None
y_label: str = None
properties: PropertyBag = None
def __init__(self,
title: str,
x_values: np.ndarray,
x_label: str,
y_values: np.ndarray,
y_label: str,
properties: PropertyBag = None,
):
"""
:param title: Title of the measurement
:param x_values: numpy array containing x values
:param x_label: label for x values
:param y_values: numpy array containing y values (must be equal size)
:param y_label: label for y values
:param properties: A property bag
"""
assert len(x_values) == len(y_values)
self.title = title
self.x_values = x_values
self.x_label = x_label
self.y_values = y_values
self.y_label = y_label
self.properties = properties
def __repr__(self) -> str:
return f"<Measurement[{self.title}], {self.properties.get_method_title()}>"
def get_title(self) -> str:
"""
Returns the given sample name (title) of the measurement
"""
return self.title
def get_method_title(self) -> str:
"""
Returns the title of the method
"""
return self.properties.get_method_title()
def get_method_description(self) -> str:
"""
Returns the description of the method.
"""
return self.properties.get_method_description()
def get_x(self) -> np.ndarray:
"""
Returns x values as a numpy array, float64
Should usually contain the measured wavelengths.
"""
return self.x_values
def get_x_label(self) -> str:
"""
Returns the saved label for the x-axis.
"""
return self.x_label
def get_y(self) -> np.ndarray:
"""
Returns y values as a numpy array, float64
Should usually contain the measured absorption.
:return:
"""
return self.y_values
def get_y_label(self) -> str:
"""
Returns the saved label for the y-axis.
"""
return self.y_label
def get_property_bag(self) -> PropertyBag:
"""
Returns the property bag.
"""
return self.properties
def get_absorption_at(self, wavelength: float, from_spectrum=False) -> float:
""" Returns the absorption at a given wavelength.
If from_spectrum is set to true, the value comes always from the spectrum. If set to False, the measured
values are tried first. """
if from_spectrum is False:
wavelength_id = f"A{wavelength:.0f}"
if self.properties.has_property(wavelength_id):
value = self.properties.get_property(wavelength_id)
return value.get_value().get_value()
# Try if we find the measured value exactly
f = np.isin(self.x_values, wavelength, assume_unique=True)
result = self.y_values[f]
if len(result) == 1:
return result.item()
# If this does not work, we need to intrapolate
f = interp1d(self.x_values, self.y_values, kind="cubic")
return f(wavelength)
@classmethod
def from_block(cls, block):
assert block.type == Block.Measurement
# Create a property bag
properties = PropertyBag.from_xml(block.parsed_content[2].parsed_content)
# Create new measurement classes
ret = cls(
title=block.parsed_content[0].parsed_content[1].decode("utf8"),
x_values=block.parsed_content[1].parsed_content[2].parsed_content[2].parsed_content[3],
x_label=block.parsed_content[1].parsed_content[2].parsed_content[2].parsed_content[0].decode("utf8"),
y_values=block.parsed_content[1].parsed_content[2].parsed_content[1].parsed_content[3],
y_label=block.parsed_content[1].parsed_content[2].parsed_content[1].parsed_content[0].decode("utf8"),
properties=properties,
)
return ret