Initial commit

This commit is contained in:
frederic committed 2026-10-05 17:10:56 +02:00
commit 2ae821883c
10 files changed
+1841

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
from importlib.metadata import PackageNotFoundError, version
from .spexical import SpeXICAL
from .spexiraw import SpeXIRAW
try:
__version__ = version("spexipy")
except PackageNotFoundError: # Running from source tree?
__version__ = "0.0.0"
__all__ = ["SpeXICAL", "SpeXIRAW", "__version__"]
+123
View File
@@ -0,0 +1,123 @@
import h5py
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
from .spexical import SpeXICAL
def cube_inspect(file: str | h5py.File, e_range: None | tuple[float, float] = None,
pixels: None | list[tuple[int, int]] = [(10, 10), (69, 69), (10, 69), (69, 10)]):
if isinstance(file, str):
data = h5py.File(file, 'r')
elif isinstance(file, h5py.File):
data = file
else:
raise ValueError(f"Invalid type for file parameter: {type(file)}")
all_px = np.sum(data['cube/cube'][:, :, :], axis=(1, 2))
energies = data['cube/energies'][:]
fig, ax = plt.subplots()
ax.plot(energies, all_px, label='All pixels')
for px in pixels:
ax.plot(energies, data['cube/cube'][:, px[1], px[0]], label=f"({px[0]},{px[1]})")
ax.set_yscale('log')
if e_range:
ax.set_xlim(*e_range)
ax.legend()
plt.show()
if isinstance(file, str):
data.close()
del data
def cube_range_to_bins(data: h5py.File, e_lower: float, e_upper: float) -> tuple[int, int]:
bin_count = len(data['cube/energies'])
bin_size = data['cube/energies'][1] - data['cube/energies'][0]
offset = data['cube/energies'][0] - (bin_size / 2.)
return (
min(max(0, int(round((e_lower - offset) / bin_size))), bin_count),
min(max(0, int(round((e_upper - offset) / bin_size))), bin_count)
)
def cube_mean_in_range(file: str | h5py.File, e_range: tuple[float, float]) -> float:
if isinstance(file, str):
data = h5py.File(file, 'r')
elif isinstance(file, h5py.File):
data = file
else:
raise ValueError(f"Invalid type for file parameter: {type(file)}")
bins = cube_range_to_bins(data, *e_range)
energies = data['cube/energies'][bins[0]:bins[1]]
weighted_sum = np.tensordot(energies, data['cube/cube'][bins[0]:bins[1], :, :], axes=(0, 0))
total_counts = np.sum(data['cube/cube'][bins[0]:bins[1], :, :], axis=0)
with np.errstate(divide='ignore', invalid='ignore'):
mean_energy = np.where(total_counts > 0,
weighted_sum / total_counts,
np.nan)
mean_energy[total_counts == 0] = np.nanmean(mean_energy)
if isinstance(file, str):
data.close()
del data
return mean_energy
def cal_adjust(cal: SpeXICAL, lower: np.array, upper: np.array):
old_gain = cal.pixel_gain.copy()
old_offset = cal.pixel_offset.copy()
lower_mean = np.nanmean(lower)
upper_mean = np.nanmean(upper)
pixel_gain = (lower - upper) / (lower_mean - upper_mean)
pixel_offset = (lower_mean * upper - upper_mean * lower) / (lower_mean - upper_mean)
pixel_gain[np.where(np.isnan(pixel_gain))] = np.nanmean(pixel_gain)
pixel_offset[np.where(np.isnan(pixel_offset))] = np.nanmean(pixel_offset)
# Merge with the old gains and offsets, because the new data has been obtained with those already in place
cal.pixel_gain = (pixel_gain * old_gain).astype(np.float32)
cal.pixel_offset = ((pixel_offset * old_gain) + old_offset).astype(np.float32)
def cal_adjust_scale(cal: SpeXICAL, e_real: tuple[float, float], e_measured: tuple[float, float]):
old_gain = cal.global_gain
old_offset = cal.global_offset
global_gain = (e_measured[0] - e_measured[1]) / (e_real[0] - e_real[1]) * old_gain
global_offset = (e_real[0] * e_measured[1] - e_real[1] * e_measured[0]) / (
e_real[0] - e_real[1]) * old_gain + old_offset
cal.global_gain = global_gain
cal.global_offset = global_offset
def cal_adjust_per_size(cal: SpeXICAL, peaks: list[tuple[float, float]]):
base = peaks[0]
out = []
for peak in peaks[1:]:
gain = (peak[0] - peak[1]) / (base[0] - base[1])
offset = (base[0] * peak[1] - base[1] * peak[0]) / (base[0] - base[1])
i = len(out)
if len(cal.cluster_calibrations) > i:
old_gain = cal.cluster_calibrations[i][1]
old_offset = cal.cluster_calibrations[i][0]
gain *= old_gain
offset *= old_gain
offset += old_offset
out.append((offset, gain))
cal.cluster_calibrations = out
+130
View File
@@ -0,0 +1,130 @@
import numpy as np
import struct
class SpeXICAL:
def __init__(self, path):
self.is_open = False
self._supported_versions = [2, 3]
self._tag = b'SPEXICAL'
self._header_struct = struct.Struct('<8sBI36sxHHBffB')
self._cluster_cal_struct = struct.Struct('<ff')
self._path = None
self._file = None
self._open(path)
def _open(self, path):
self._path = path
self._file = open(path, mode='rb')
self._file.seek(0, 2) # move cursor to end
self.file_size = self._file.tell()
self._file.seek(0, 0) # move back to start
tag = self._file.read(len(self._tag))
if tag != self._tag:
raise RuntimeError("File does not look like a SpeXIDAQ calibration file")
version = struct.unpack('<B', self._file.read(1))[0]
if version not in self._supported_versions:
raise RuntimeError(f"File uses format version {version} but only versions {self._supported_versions} are supported")
if self.file_size < self._header_struct.size:
raise RuntimeError("File too short to be a SpeXIDAQ calibration file")
self._file.seek(0, 0) # back to the start
tag, self.format_version, self.version, self.uuid, self.width, \
self.height, self.detector_gain, self.global_gain, \
self.global_offset, self._cluster_cal_count = self._header_struct.unpack_from(self._file.read(self._header_struct.size))
self._px_count = self.width * self.height
self._calc_min_size()
if self.file_size < self._min_size:
raise RuntimeError("File too short to be a SpeXIDAQ calibration file")
self.cluster_calibrations = []
if self.format_version >= 3:
for i in range(self._cluster_cal_count):
offset, gain = self._cluster_cal_struct.unpack_from(self._file.read(self._cluster_cal_struct.size))
self.cluster_calibrations.append((offset, gain))
else:
cluster_cal_struct = struct.Struct('<f')
for i in range(self._cluster_cal_count):
offset = cluster_cal_struct.unpack_from(self._file.read(cluster_cal_struct.size))
self.cluster_calibrations.append(offset)
self.dark_offset = self._load_frame(np.float32)
self.thresholds = self._load_frame(np.float32)
self.pixel_gain = self._load_frame(np.float32)
self.pixel_offset = self._load_frame(np.float32)
if self.format_version < 3:
self.fine_gain = self._load_frame(np.float32)
self.fine_offset = self._load_frame(np.float32)
self.pixel_mask = self._load_frame(np.uint16)
self.comment = self._file.read(self.file_size - self._min_size)
self._file.close()
self._file = None
self.is_open = True
def _calc_min_size(self):
self._min_size = self._header_struct.size + self._cluster_cal_struct.size * self._cluster_cal_count
self._min_size += self._px_count * (4 + 4 + 4 + 4 + 2) + 4 * 5
self._min_size += 1 # null-termination of comment string
def _load_frame(self, dtype):
w, h = struct.unpack('<HH', self._file.read(4))
if w != self.width or h != self.height:
raise RuntimeError(f"Unexpected frame size {w}x{h} in calibration, expected {self.width}x{self.height}")
frame = np.frombuffer(self._file.read(self._px_count * np.dtype(dtype).itemsize), dtype=dtype).copy()
frame.shape = (self.width, self.height)
return frame
def _store_frame(self, frame):
if frame.shape != (self.width, self.height):
raise RuntimeError("Trying to store unsupported frame shape")
self._file.write(struct.pack('<HH', self.width, self.height))
self._file.write(frame.tobytes())
def save_as(self, path):
self._path = path
self.save()
def save(self):
self._file = open(self._path, mode='wb')
header_buf = self._header_struct.pack(self._tag, self.format_version, self.version, self.uuid, self.width, self.height, self.detector_gain, self.global_gain, self.global_offset, len(self.cluster_calibrations))
self._file.write(header_buf)
if self.format_version >= 3:
for cal in self.cluster_calibrations:
self._file.write(self._cluster_cal_struct.pack(cal[0], cal[1]))
else:
cluster_cal_struct = struct.Struct('<f')
for cal in self.cluster_calibrations:
self._file.write(cluster_cal_struct.pack(cal))
self._store_frame(self.dark_offset)
self._store_frame(self.thresholds)
self._store_frame(self.pixel_gain)
self._store_frame(self.pixel_offset)
if self.format_version < 3:
self._store_frame(self.fine_gain)
self._store_frame(self.fine_offset)
self._store_frame(self.pixel_mask)
self._file.write(self.comment)
self._file.write(b'\x00')
self._file.close()
self._file = None
+83
View File
@@ -0,0 +1,83 @@
import numpy as np
import struct
class SpeXIRAW:
def __init__(self, path):
# initialise some defaults
self.is_open = False
self._supported_version = 1
# define some helper values and structs for reading
self._tag = b'SPEXIRAW'
self._header_struct = struct.Struct('<8sBHHfIB36s8s')
self._frame_id_struct = struct.Struct('<Id')
self._types = {1: 'uint8',
2: 'uint16',
3: 'uint32',
4: 'uint64'}
self._open(path)
def _open(self, path):
self._file = open(path, mode='rb')
self._file.seek(0, 2) # move cursor to end
self.file_size = self._file.tell()
self._file.seek(0, 0) # move back to start
tag1 = self._file.read(8)
if tag1 != self._tag:
raise RuntimeError("File does not look like a SpeXIRAW file")
version = struct.unpack('<B', self._file.read(1))[0]
if version != self._supported_version:
raise RuntimeError(f"File uses format version {version} but only version {self._supported_version} is supported")
if self.file_size < self._header_struct.size:
raise RuntimeError("File too short to be a SpeXIRAW file")
self._file.seek(0, 0) # back to the start
tag1, self.format_version, self.width, self.height, self.pixel_pitch, \
self.fps, self.type, self.detector_uuid, tag2 = self._header_struct.unpack_from(self._file.read(self._header_struct.size))
if tag2 != self._tag:
raise RuntimeError("File is probably corrupted")
self._dtype = np.dtype(self._types[self.type])
self._frame_size = self.width * self.height * self._dtype.itemsize
self.frame_count = int(np.floor((self.file_size - self._header_struct.size) / (self._frame_size + self._frame_id_struct.size)))
self.is_open = True
def frames(self):
if not self.is_open:
return
self._file.seek(self._header_struct.size, 0) # move to start of frame data
for n in range(self.frame_count):
#self._file.seek(self._frame_id_struct.size, 1) # skip header, unused in iterator
index, timestamp = self._frame_id_struct.unpack(self._file.read(self._frame_id_struct.size))
frame = np.fromfile(self._file, dtype=self._dtype, count=self.width * self.height)
frame.shape = (self.width, self.height)
yield index, timestamp, frame
def frame(self, index):
if not self.is_open:
raise RuntimeError("No open file")
if not isinstance(index, int):
raise ValueError("Numeric frame index required")
if index >= self.frame_count:
raise ValueError("Requested frame beyond end of file")
if index < 0:
index += self.frame_count
if index < 0:
raise ValueError("Requested frame before start of file")
self._file.seek(self._header_struct.size + (self._frame_id_struct.size + self._frame_size) * index + self._frame_id_struct.size)
frame = np.fromfile(self._file, dtype=self._dtype, count=self.width * self.height)
frame.shape = (self.width, self.height)
return frame
+45
View File
@@ -0,0 +1,45 @@
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
def plot_frame(frame, title=None, out=None):
if out is None:
out = plt.figure()
if isinstance(out, plt.Figure):
ax = out.add_subplot(1, 1, 1)
else:
ax = out
im = ax.imshow(frame)
ax.set_xlabel('X')
ax.set_ylabel('Y')
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.05)
plt.colorbar(im, cax=cax)
if title is not None:
if out is plt:
ax.title(title)
else:
ax.set_title(title)
def plot_frames(frames):
if isinstance(frames, np.ndarray):
plot_frame(frames)
elif len(frames) == 1:
plot_frame(frames[0])
else:
rows = 1
cols = len(frames)
if len(frames) > 3:
rows = ceil(len(frames) / 2)
cols = 2
fig, sub = plt.subplots(rows, cols, sharey=True, sharex=True, figsize=(14, 5 * rows))
for i in range(len(frames)):
plot_frame(frames[i], out=sub[i])
plt.tight_layout()