"""Utility functions for pylinac."""
import decimal
import os
import os.path as osp
import struct
import subprocess
from collections.abc import Iterable
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Union, Sequence, Type, BinaryIO
import numpy as np
import pydicom
from .typing import NumberLike
from .. import __version__
[docs]def convert_to_enum(value: Union[str, Enum, None], enum: Type[Enum]) -> Enum:
"""Convert a value to an enum representation from an enum value if needed"""
if isinstance(value, enum):
return value
else:
return enum(value)
[docs]@dataclass
class ResultBase:
pylinac_version: str = field(init=False) #:
date_of_analysis: datetime = field(init=False) #:
def __post_init__(self):
self.pylinac_version = __version__
self.date_of_analysis = datetime.today()
[docs]def clear_data_files():
"""Delete all demo files, image classifiers, etc from the demo folder"""
demo_folder = osp.join(osp.dirname(osp.dirname(__file__)), 'demo_files')
if osp.isdir(demo_folder):
for file in os.listdir(demo_folder):
full_file = osp.join(demo_folder, file)
if osp.isfile(full_file):
os.remove(full_file)
print("Pylinac data files cleared.")
[docs]def assign2machine(source_file: str, machine_file: str):
"""Assign a DICOM RT Plan file to a specific machine. The source file is overwritten to contain
the machine of the machine file.
Parameters
----------
source_file : str
Path to the DICOM RTPlan file that contains the fields/plan desired
(e.g. a Winston Lutz set of fields or Varian's default PF files).
machine_file : str
Path to a DICOM RTPlan file that has the desired machine. This is easily obtained from pushing a plan from the TPS
for that specific machine. The file must contain at least one valid field.
"""
dcm_source = pydicom.dcmread(source_file)
dcm_machine = pydicom.dcmread(machine_file)
for beam in dcm_source.BeamSequence:
beam.TreatmentMachineName = dcm_machine.BeamSequence[0].TreatmentMachineName
dcm_source.save_as(source_file)
[docs]def is_close(val: NumberLike, target: Union[NumberLike, Sequence], delta: NumberLike=1):
"""Return whether the value is near the target value(s).
Parameters
----------
val : number
The value being compared against.
target : number, iterable
If a number, the values are simply evaluated.
If a sequence, each target is compared to ``val``.
If any values of ``target`` are close, the comparison is considered True.
Returns
-------
bool
"""
try:
targets = (value for value in target)
except (AttributeError, TypeError):
targets = [target]
for target in targets:
if target - delta < val < target + delta:
return True
return False
[docs]def simple_round(number: NumberLike, decimals: int=0) -> float:
"""Round a number to the given number of decimals. Fixes small floating number errors."""
num = int(round(number * 10 ** decimals))
num /= 10 ** decimals
return num
[docs]def isnumeric(object) -> bool:
"""Check whether the passed object is numeric in any sense."""
return isinstance(object, (int, float, decimal.Decimal, np.number))
def is_float_like(number) -> bool:
return isinstance(number, (float, np.float, np.float16, np.float32, np.float64))
def is_int_like(number) -> bool:
return isinstance(number, (int, np.int, np.int16, np.int32, np.int64, np.int8))
[docs]def is_iterable(object) -> bool:
"""Determine if an object is iterable."""
return isinstance(object, Iterable)
[docs]class Structure:
"""A simple structure that assigns the arguments to the object."""
def __init__(self, **kwargs):
self.__dict__.update(**kwargs)
def update(self, **kwargs):
self.__dict__.update(**kwargs)
[docs]def decode_binary(file: BinaryIO, dtype: Union[Type[int], Type[float], Type[str]], num_values: int = 1, cursor_shift: int = 0) -> \
Union[int, float, str, np.ndarray]:
"""Read in a raw binary file and convert it to given data types.
Parameters
----------
file
The open file object.
dtype
The expected data type to return. If int or float and num_values > 1, will return numpy array.
num_values
The expected number of dtype to return
.. note:: This is not the same as the number of bytes.
cursor_shift : int
The number of bytes to move the cursor forward after decoding. This is used if there is a
reserved section after the read-in segment.
"""
f = file
if dtype == str: # if string
output = f.read(num_values)
if type(f) is not str: # in py3 fc will be bytes
output = output.decode()
# strip the padding ("\x00")
output = output.strip('\x00')
elif dtype == int:
ssize = struct.calcsize('i') * num_values
output = np.asarray(struct.unpack('i' * num_values, f.read(ssize)))
if len(output) == 1:
output = int(output)
elif dtype == float:
ssize = struct.calcsize('f') * num_values
output = np.asarray(struct.unpack('f' * num_values, f.read(ssize)))
if len(output) == 1:
output = float(output)
else:
raise TypeError(f"datatype '{dtype}' was not valid")
# shift cursor if need be (e.g. if a reserved section follows)
if cursor_shift:
f.seek(cursor_shift, 1)
return output
[docs]def open_path(path: str) -> None:
"""Open the specified path in the system default viewer."""
if os.name == 'darwin':
launcher = "open"
elif os.name == 'posix':
launcher = "xdg-open"
elif os.name == 'nt':
launcher = "explorer"
subprocess.call([launcher, path])
[docs]def file_exists(filename: str) -> str:
"""Check if the file exists and if it does add a timestamp"""
if osp.exists(filename):
filename, ext = osp.splitext(filename)
mytime = datetime.now().strftime("%Y%m%d%H%M%S")
filename = filename + mytime + ext
return filename