docs(utils/opatio): added doxygen comments to python module

This commit is contained in:
2025-02-16 12:47:42 -05:00
parent 11b3811382
commit fde561cdef
2 changed files with 134 additions and 28 deletions

View File

@@ -349,7 +349,7 @@ OPTIMIZE_OUTPUT_SLICE = NO
# #
# Note see also the list of default file extension mappings. # Note see also the list of default file extension mappings.
EXTENSION_MAPPING = EXTENSION_MAPPING = py=C++
# If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments # If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments
# according to the Markdown format, which allows for more readable # according to the Markdown format, which allows for more readable
@@ -534,7 +534,7 @@ TIMESTAMP = NO
# normally produced when WARNINGS is set to YES. # normally produced when WARNINGS is set to YES.
# The default value is: NO. # The default value is: NO.
EXTRACT_ALL = NO EXTRACT_ALL = YES
# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will # If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
# be included in the documentation. # be included in the documentation.
@@ -991,7 +991,7 @@ WARN_LOGFILE =
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched. # Note: If this tag is empty the current directory is searched.
INPUT = src tests INPUT = src tests utils/opatio
# This tag can be used to specify the character encoding of the source files # This tag can be used to specify the character encoding of the source files
# that Doxygen parses. Internally Doxygen uses the UTF-8 encoding. Doxygen uses # that Doxygen parses. Internally Doxygen uses the UTF-8 encoding. Doxygen uses

View File

@@ -13,31 +13,40 @@ import os
@dataclass @dataclass
class Header: class Header:
magic: str """
version: int @brief Structure to hold the header information of an OPAT file.
numTables: int """
headerSize: int magic: str #< Magic number to identify the file type
indexOffset: int version: int #< Version of the OPAT file format
creationDate: str numTables: int #< Number of tables in the file
sourceInfo: str headerSize: int #< Size of the header
comment: str indexOffset: int #< Offset to the index section
reserved: bytes creationDate: str #< Creation date of the file
sourceInfo: str #< Source information
comment: str #< Comment section
reserved: bytes #< Reserved for future use
@dataclass @dataclass
class TableIndex: class TableIndex:
X: float """
Z: float @brief Structure to hold the index information of a table in an OPAT file.
byteStart: int """
byteEnd: int X: float #< X composition value
sha256: bytes Z: float #< Z composition value
byteStart: int #< Byte start position of the table
byteEnd: int #< Byte end position of the table
sha256: bytes #< SHA-256 hash of the table data
@dataclass @dataclass
class OPATTable: class OPATTable:
N_R: int """
N_T: int @brief Structure to hold the data of an OPAT table.
logR: Iterable[float] """
logT: Iterable[float] N_R: int #< Number of R values
logKappa: Iterable[Iterable[float]] N_T: int #< Number of T values
logR: Iterable[float] #< Logarithm of R values
logT: Iterable[float] #< Logarithm of T values
logKappa: Iterable[Iterable[float]] #< Logarithm of Kappa values
defaultHeader = Header( defaultHeader = Header(
magic="OPAT", magic="OPAT",
@@ -58,19 +67,31 @@ class OpatIO:
@staticmethod @staticmethod
def validate_char_array_size(s: str, nmax: int) -> bool: def validate_char_array_size(s: str, nmax: int) -> bool:
"""
@brief Validate the size of a character array.
@param s The string to validate.
@param nmax The maximum allowed size.
@return True if the string size is valid, False otherwise.
"""
if len(s) > nmax: if len(s) > nmax:
return False return False
return True return True
@staticmethod @staticmethod
def validate_logKappa(logKappa): def validate_logKappa(logKappa):
"""
@brief Validate the logKappa array.
@param logKappa The logKappa array to validate.
@throws ValueError if logKappa is not a non-empty 2D array.
@throws TypeError if logKappa is not a 2D array or iterable.
"""
if isinstance(logKappa, np.ndarray): if isinstance(logKappa, np.ndarray):
if logKappa.ndim == 2: if logKappa.ndim == 2:
return return
else: else:
raise ValueError("logKappa must be a non-empty 2D array") raise ValueError("logKappa must be a non-empty 2D array")
if isintance(logKappa, collectionIterable) and all(isinstance(row, collectionIterable) for row in logKappa): if isinstance(logKappa, collectionIterable) and all(isinstance(row, collectionIterable) for row in logKappa):
try: try:
first_row = next(iter(logKappa)) first_row = next(iter(logKappa))
if all(isinstance(x, (int, float)) for x in first_row): if all(isinstance(x, (int, float)) for x in first_row):
@@ -84,6 +105,13 @@ class OpatIO:
@staticmethod @staticmethod
def validate_1D(arr, name: str): def validate_1D(arr, name: str):
"""
@brief Validate a 1D array.
@param arr The array to validate.
@param name The name of the array.
@throws ValueError if the array is not 1D or not fully numeric.
@throws TypeError if the array is not a non-empty 1D array or iterable.
"""
if isinstance(arr, np.ndarray): if isinstance(arr, np.ndarray):
if arr.ndim == 1: if arr.ndim == 1:
return return
@@ -95,29 +123,60 @@ class OpatIO:
else: else:
raise ValueError(f"{name} must be fully numeric") raise ValueError(f"{name} must be fully numeric")
else: else:
raise TypeError(f"{name} must be a non-empty 2D array or iterable") raise TypeError(f"{name} must be a non-empty 1D array or iterable")
@staticmethod @staticmethod
def compute_checksum(data: bytes) -> bytes: def compute_checksum(data: bytes) -> bytes:
"""
@brief Compute the SHA-256 checksum of the given data.
@param data The data to compute the checksum for.
@return The SHA-256 checksum.
"""
return hashlib.sha256(data).digest() return hashlib.sha256(data).digest()
def set_version(self, version: int) -> int: def set_version(self, version: int) -> int:
"""
@brief Set the version of the OPAT file.
@param version The version to set.
@return The set version.
"""
self.header.version = version self.header.version = version
return self.header.version return self.header.version
def set_source(self, source: str) -> str: def set_source(self, source: str) -> str:
"""
@brief Set the source information of the OPAT file.
@param source The source information to set.
@return The set source information.
@throws TypeError if the source string is too long.
"""
if not self.validate_char_array_size(source, 64): if not self.validate_char_array_size(source, 64):
raise TypeError(f"sourceInfo string ({source}) is too long ({len(source)}). Max length is 64") raise TypeError(f"sourceInfo string ({source}) is too long ({len(source)}). Max length is 64")
self.header.sourceInfo = source self.header.sourceInfo = source
return self.header.sourceInfo return self.header.sourceInfo
def set_comment(self, comment: str) -> str: def set_comment(self, comment: str) -> str:
"""
@brief Set the comment of the OPAT file.
@param comment The comment to set.
@return The set comment.
@throws TypeError if the comment string is too long.
"""
if not self.validate_char_array_size(comment, 128): if not self.validate_char_array_size(comment, 128):
raise TypeError(f"comment string ({comment}) is too long ({len(comment)}). Max length is 128") raise TypeError(f"comment string ({comment}) is too long ({len(comment)}). Max length is 128")
self.header.comment = comment self.header.comment = comment
return self.header.comment return self.header.comment
def add_table(self, X: float, Z: float, logR: Iterable[float], logT: Iterable[float], logKappa: Iterable[Iterable[float]]): def add_table(self, X: float, Z: float, logR: Iterable[float], logT: Iterable[float], logKappa: Iterable[Iterable[float]]):
"""
@brief Add a table to the OPAT file.
@param X The X composition value.
@param Z The Z composition value.
@param logR The logR values.
@param logT The logT values.
@param logKappa The logKappa values.
@throws ValueError if logKappa is not a non-empty 2D array or if logR and logT are not 1D arrays.
"""
self.validate_logKappa(logKappa) self.validate_logKappa(logKappa)
self.validate_1D(logR, "logR") self.validate_1D(logR, "logR")
self.validate_1D(logT, "logT") self.validate_1D(logT, "logT")
@@ -142,6 +201,10 @@ class OpatIO:
def _header_bytes(self) -> bytes: def _header_bytes(self) -> bytes:
"""
@brief Convert the header to bytes.
@return The header as bytes.
"""
headerBytes = struct.pack( headerBytes = struct.pack(
"<4s H I I Q 16s 64s 128s 26s", "<4s H I I Q 16s 64s 128s 26s",
self.header.magic.encode('utf-8'), self.header.magic.encode('utf-8'),
@@ -157,6 +220,11 @@ class OpatIO:
return headerBytes return headerBytes
def _table_bytes(self, table: OPATTable) -> Tuple[bytes, bytes]: def _table_bytes(self, table: OPATTable) -> Tuple[bytes, bytes]:
"""
@brief Convert a table to bytes.
@param table The OPAT table.
@return A tuple containing the checksum and the table as bytes.
"""
logR = table.logR.flatten() logR = table.logR.flatten()
logT = table.logT.flatten() logT = table.logT.flatten()
logKappa = table.logKappa.flatten() logKappa = table.logKappa.flatten()
@@ -172,6 +240,12 @@ class OpatIO:
return (checksum, tableBytes) return (checksum, tableBytes)
def _tableIndex_bytes(self, tableIndex: TableIndex) -> bytes: def _tableIndex_bytes(self, tableIndex: TableIndex) -> bytes:
"""
@brief Convert a table index to bytes.
@param tableIndex The table index.
@return The table index as bytes.
@throws RuntimeError if the table index entry does not have 64 bytes.
"""
tableIndexBytes = struct.pack( tableIndexBytes = struct.pack(
'<ddQQ', '<ddQQ',
tableIndex.X, tableIndex.X,
@@ -187,8 +261,12 @@ class OpatIO:
return tableIndexBytes return tableIndexBytes
def __repr__(self) -> str: def __repr__(self) -> str:
"""
@brief Get the string representation of the OpatIO object.
@return The string representation.
"""
reprString = f"""OpatIO( reprString = f"""OpatIO(
versoion: {self.header.version} version: {self.header.version}
numTables: {self.header.numTables} numTables: {self.header.numTables}
headerSize: {self.header.headerSize} headerSize: {self.header.headerSize}
indexOffset: {self.header.indexOffset} indexOffset: {self.header.indexOffset}
@@ -200,12 +278,19 @@ class OpatIO:
return reprString return reprString
def _format_table_as_string(self, table: OPATTable, X: float, Z: float) -> str: def _format_table_as_string(self, table: OPATTable, X: float, Z: float) -> str:
"""
@brief Format a table as a string.
@param table The OPAT table.
@param X The X composition value.
@param Z The Z composition value.
@return The formatted table as a string.
"""
tableString: List[str] = [] tableString: List[str] = []
# fixed width X and Z header per table # fixed width X and Z header per table
tableString.append(f"X: {X:<10.4f} Z: {Z:<10.4f}") tableString.append(f"X: {X:<10.4f} Z: {Z:<10.4f}")
tableString.append("-" * 80) tableString.append("-" * 80)
# write logR accross the top (reserving one col for where logT will be) # write logR across the top (reserving one col for where logT will be)
logRRow = f"{"":<10}" logRRow = f"{'':<10}"
logRRowTrue = "".join(f"{r:<10.4f}" for r in table.logR) logRRowTrue = "".join(f"{r:<10.4f}" for r in table.logR)
tableString.append(logRRow + logRRowTrue) tableString.append(logRRow + logRRowTrue)
for i, logT in enumerate(table.logT): for i, logT in enumerate(table.logT):
@@ -218,6 +303,11 @@ class OpatIO:
@staticmethod @staticmethod
def print_table_indexes(table_indexes: List[TableIndex]) -> str: def print_table_indexes(table_indexes: List[TableIndex]) -> str:
"""
@brief Print the table indexes.
@param table_indexes The list of table indexes.
@return The formatted table indexes as a string.
"""
if not table_indexes: if not table_indexes:
print("No table indexes found.") print("No table indexes found.")
return return
@@ -231,6 +321,11 @@ class OpatIO:
return '\n'.join(tableRows) return '\n'.join(tableRows)
def save_as_ascii(self, filename: str) -> str: def save_as_ascii(self, filename: str) -> str:
"""
@brief Save the OPAT file as an ASCII file.
@param filename The name of the file.
@return The name of the saved file.
"""
numericFMT = "{:.18e}" numericFMT = "{:.18e}"
currentStartByte: int = 256 currentStartByte: int = 256
tableIndexs: List[bytes] = [] tableIndexs: List[bytes] = []
@@ -268,6 +363,12 @@ class OpatIO:
f.write(self.print_table_indexes(tableIndexs)) f.write(self.print_table_indexes(tableIndexs))
def save(self, filename: str) -> str: def save(self, filename: str) -> str:
"""
@brief Save the OPAT file as a binary file.
@param filename The name of the file.
@return The name of the saved file.
@throws RuntimeError if the header does not have 256 bytes.
"""
tempHeaderBytes = self._header_bytes() tempHeaderBytes = self._header_bytes()
if len(tempHeaderBytes) != 256: if len(tempHeaderBytes) != 256:
@@ -305,6 +406,12 @@ class OpatIO:
def loadOpat(filename: str) -> OpatIO: def loadOpat(filename: str) -> OpatIO:
"""
@brief Load an OPAT file.
@param filename The name of the file.
@return The loaded OpatIO object.
@throws RuntimeError if the header does not have 256 bytes.
"""
opat = OpatIO() opat = OpatIO()
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
headerBytes: bytes = f.read(256) headerBytes: bytes = f.read(256)
@@ -355,4 +462,3 @@ def loadOpat(filename: str) -> OpatIO:
opat.add_table(tableIndex.X, tableIndex.Z, logR, logT, logKappa) opat.add_table(tableIndex.X, tableIndex.Z, logR, logT, logKappa)
return opat return opat