"""Utility functions for pylinac."""from__future__importannotationsimportosimportos.pathasospimportstructfromabcimportabstractmethodfromcollections.abcimportIterablefromdatetimeimportdatetimefromenumimportEnumfromtypingimportBinaryIO,Generic,Sequence,TypeVarimportnumpyasnpimportpydicomfrompydanticimportBaseModel,ConfigDict,Fieldfrom..import__version__
[docs]defconvert_to_enum(value:str|Enum|None,enum:type[Enum])->Enum:"""Convert a value to an enum representation from an enum value if needed"""ifisinstance(value,enum):returnvalueelse:returnenum(value)
[docs]classOptionListMixin:"""A mixin class that will create a list of the class attributes. Used for enum-like classes"""@classmethoddefoptions(cls)->list[str]:return[optionforattr,optionincls.__dict__.items()ifnotcallable(option)andnotattr.startswith("__")]
[docs]classResultsDataMixin(Generic[T]):"""A mixin for classes that generate results data. This mixin is used to generate the results data and present it in different formats. The generic types allow correct type hinting of the results data."""@abstractmethoddef_generate_results_data(self)->T:pass
[docs]defresults_data(self,as_dict:bool=False,as_json:bool=False)->T|dict|str:"""Present the results data and metadata as a dataclass, dict, or tuple. The default return type is a dataclass. Parameters ---------- as_dict : bool If True, return the results as a dictionary. as_json : bool If True, return the results as a JSON string. Cannot be True if as_dict is True. """ifas_dictandas_json:raiseValueError("Cannot return as both dict and JSON. Pick one.")data=self._generate_results_data()ifas_dict:returndata.model_dump()ifas_json:returndata.model_dump_json()returndata
[docs]defclear_data_files():"""Delete all demo files, image classifiers, etc from the demo folder"""demo_folder=osp.join(osp.dirname(osp.dirname(__file__)),"demo_files")ifosp.isdir(demo_folder):forfileinos.listdir(demo_folder):full_file=osp.join(demo_folder,file)ifosp.isfile(full_file):os.remove(full_file)print("Pylinac data files cleared.")
[docs]defassign2machine(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)forbeamindcm_source.BeamSequence:beam.TreatmentMachineName=dcm_machine.BeamSequence[0].TreatmentMachineNamedcm_source.save_as(source_file)
[docs]defis_close(val:float,target:float|Sequence,delta:float=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=(valueforvalueintarget)except(AttributeError,TypeError):targets=[target]fortargetintargets:iftarget-delta<val<target+delta:returnTruereturnFalse
[docs]defsimple_round(number:float|int,decimals:int|None=0)->float|int:"""Round a number to the given number of decimals. Fixes small floating number errors. If decimals is None, no rounding is performed"""ifdecimalsisNone:returnnumbernum=int(round(number*10**decimals))ifdecimals>=1:num/=10**decimalsreturnnum
[docs]defis_iterable(object)->bool:"""Determine if an object is iterable."""returnisinstance(object,Iterable)
[docs]classStructure:"""A simple structure that assigns the arguments to the object."""def__init__(self,**kwargs):self.__dict__.update(**kwargs)defupdate(self,**kwargs):self.__dict__.update(**kwargs)
[docs]defdecode_binary(file:BinaryIO,dtype:type[int]|type[float]|type[str]|str|np.dtype,num_values:int=1,cursor_shift:int=0,strip_empty:bool=True,)->int|float|str|np.ndarray|list:"""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. strip_empty : bool Whether to strip trailing empty/null values for strings. """f=fileifisinstance(dtype,str):s=struct.calcsize(dtype)*num_valuesoutput=struct.unpack(dtype*num_values,f.read(s))iflen(output)==1:output=output[0]elifdtype==str:# if stringssize=struct.calcsize("c")*num_valuesoutput=struct.unpack("c"*num_values,f.read(ssize))ifstrip_empty:output="".join(o.decode()foroinoutputifo!=b"\x00")else:output="".join(o.decode()foroinoutput)elifdtype==int:ssize=struct.calcsize("i")*num_valuesoutput=np.asarray(struct.unpack("i"*num_values,f.read(ssize)))iflen(output)==1:output=int(np.squeeze(output))elifdtype==float:ssize=struct.calcsize("f")*num_valuesoutput=np.asarray(struct.unpack("f"*num_values,f.read(ssize)))iflen(output)==1:output=float(np.squeeze(output))else:raiseTypeError(f"datatype '{dtype}' was not valid")# shift cursor if need be (e.g. if a reserved section follows)ifcursor_shift:f.seek(cursor_shift,1)returnoutput