Source code for mantidimaging.core.io.instrument_log
# Copyright (C) 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum, auto
from pathlib import Path
from typing import Any, ClassVar
import numpy as np
from mantidimaging.core.utility.data_containers import ProjectionAngles, Counts
[docs]
class LogColumn(Enum):
TIMESTAMP = auto()
IMAGE_TYPE_IMAGE_COUNTER = auto()
PROJECTION_NUMBER = auto()
PROJECTION_ANGLE = auto()
COUNTS_BEFORE = auto()
COUNTS_AFTER = auto()
TIME_OF_FLIGHT = auto() # in seconds
SPECTRUM_COUNTS = auto()
[docs]
class ShutterCountColumn(Enum):
PULSE = auto()
SHUTTER_COUNT = auto()
LogDataType = dict[LogColumn, list[float | int]]
ShutterCountType = dict[ShutterCountColumn, list[float | int]]
[docs]
class NoParserFound(RuntimeError):
pass
[docs]
class InvalidLog(RuntimeError):
pass
[docs]
class BaseParser(ABC):
"""
Base class for parsers
"""
def __init__(self, lines: list[str]):
self.lines = lines
[docs]
@classmethod
@abstractmethod
def match(cls, lines: list[str], filename: str) -> bool:
"""Check if the name and content of the file is likely to be readable by this parser."""
...
[docs]
@abstractmethod
def parse(self):
"""Parse the log file"""
...
[docs]
def cleaned_lines(self) -> list[str]:
return [line for line in self.lines if line.strip() != ""]
[docs]
class InstrumentLogParser(BaseParser):
"""
A base class for parsing instrument log files.
This class provides a template for parsing instrument log files. Subclasses should
implement the `parse` method to define the specific parsing logic.
Attributes:
None
Methods:
parse: Parse the log file and return the parsed data.
"""
def __init_subclass__(cls) -> None:
"""Automatically register subclasses"""
InstrumentLog.register_parser(cls)
[docs]
@abstractmethod
def parse(self) -> LogDataType:
"""Parse the log file"""
...
[docs]
class InstrumentShutterCountParser(BaseParser):
"""
A parser for instrument shutter count logs.
This class is responsible for parsing instrument shutter count log files
and returning the parsed shutter count data.
Attributes:
None
Methods:
parse: Parse the log file and return the shutter count data.
"""
def __init_subclass__(cls) -> None:
"""Automatically register subclasses"""
ShutterCount.register_parser(cls)
[docs]
@abstractmethod
def parse(self) -> ShutterCountType:
"""Parse the log file and return the shutter count data"""
...
[docs]
class InstrumentLog:
"""Multiformat instrument log reader
New parsers can be implemented by subclassing InstrumentLogParser
"""
parsers: ClassVar[list[type[InstrumentLogParser]]] = []
parser: type[InstrumentLogParser]
data: LogDataType
length: int
def __init__(self, lines: list[str], source_file: Path):
self.lines = lines
self.source_file = source_file
self._find_parser()
self.parse()
def _find_parser(self) -> None:
for parser in self.parsers:
if parser.match(self.lines, self.source_file.name):
self.parser = parser
return
raise NoParserFound(
f"File format not recognised for log file: '{self.source_file.name}'. "
"See documentation for details of supported formats: "
"https://mantidproject.github.io/mantidimaging/user_docs/explanations/gui/loading_saving.html#load-log-for-stack"
)
[docs]
def parse(self) -> None:
self.data = self.parser(self.lines).parse()
lengths = [len(val) for val in self.data.values()]
if len(set(lengths)) != 1:
raise InvalidLog(f"Mismatch in column lengths: {lengths}")
self.length = lengths[0]
[docs]
@classmethod
def register_parser(cls, parser: type[InstrumentLogParser]) -> None:
cls.parsers.append(parser)
[docs]
def get_column(self, key: LogColumn) -> list[float]:
return self.data[key]
[docs]
def projection_numbers(self) -> np.ndarray:
return np.array(self.get_column(LogColumn.PROJECTION_NUMBER), dtype=np.uint32)
[docs]
def has_projection_angles(self) -> bool:
return LogColumn.PROJECTION_ANGLE in self.data
[docs]
def projection_angles(self) -> ProjectionAngles:
angles = np.array(self.get_column(LogColumn.PROJECTION_ANGLE), dtype=np.float64)
return ProjectionAngles(np.deg2rad(angles))
[docs]
def raise_if_angle_missing(self, image_filenames: list[str]) -> None:
image_numbers = [ifile[ifile.rfind("_") + 1:] for ifile in image_filenames]
if self.length != len(image_numbers):
RuntimeError(f"Log size mismatch. Found {self.length} log entries,"
f"but {len(image_numbers)} images")
if LogColumn.PROJECTION_NUMBER in self.data:
for projection_num, image_num in zip(self.projection_numbers(), image_numbers, strict=True):
if str(projection_num) not in image_num:
raise RuntimeError(f"Mismatching angle for projection {projection_num} "
f"was going to be used for image file {image_num}")
[docs]
def counts(self) -> Counts:
if not (LogColumn.COUNTS_BEFORE in self.data and LogColumn.COUNTS_AFTER in self.data):
raise ValueError("Log does not have counts")
counts_before = np.array(self.get_column(LogColumn.COUNTS_BEFORE))
counts_after = np.array(self.get_column(LogColumn.COUNTS_AFTER))
return Counts(counts_after - counts_before)
[docs]
class ShutterCount:
"""
Represents a shutter count log.
Attributes:
parsers (ClassVar[list[type[InstrumentShutterCountParser]]]): List of registered parsers.
parser (type[InstrumentShutterCountParser]): The parser used to parse the log.
data (ShutterCountType): The parsed data from the log.
length (int): The length of the log data.
Methods:
__init__(self, lines: list[str], source_file: Path): Initializes a new instance of the ShutterCount class.
_find_parser(self) -> None: Finds the appropriate parser for the log.
parse(self) -> None: Parses the log using the selected parser.
register_parser(cls, parser: type[InstrumentShutterCountParser]) -> None: Registers a parser for the log.
get_column(self, key: ShutterCountColumn) -> list[float | int]: Returns the specified column from the log data.
pulse_per_shutter_range_numbers(self) -> np.array: Returns an array of pulse per shutter range numbers.
has_Pulse(self) -> bool: Checks if the log contains the 'PULSE' column.
raise_if_counts_missing(self): Raises an exception if the counts are missing in the log.
"""
parsers: ClassVar[list[type[InstrumentShutterCountParser]]] = []
parser: type[InstrumentShutterCountParser]
data: ShutterCountType
length: int
def __init__(self, lines: list[str], source_file: Path):
self.lines = lines
self.source_file = source_file
self._find_parser()
self.parse()
def _find_parser(self) -> None:
for parser in self.parsers:
if parser.match(self.lines, self.source_file.name):
self.parser = parser
return
raise NoParserFound
[docs]
def parse(self) -> None:
self.data = self.parser(self.lines).parse()
lengths = [len(val) for val in self.data.values()]
if len(set(lengths)) != 1:
raise InvalidLog(f"Mismatch in column lengths: {lengths}")
self.length = lengths[0]
[docs]
@classmethod
def register_parser(cls, parser: type[InstrumentShutterCountParser]) -> None:
cls.parsers.append(parser)
[docs]
def get_column(self, key: ShutterCountColumn) -> list[float | int]:
return self.data[key]
[docs]
def pulse_per_shutter_range_numbers(self) -> np.ndarray[Any, Any]:
column_data = self.get_column(ShutterCountColumn.SHUTTER_COUNT)
return np.array(column_data, dtype=np.uint32)
[docs]
def has_Pulse(self) -> bool:
return ShutterCountColumn.PULSE in self.data