Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
run_details.py 9.48 KiB
from __future__ import (absolute_import, division, print_function)

from isis_powder.routines import common, yaml_parser
import os


def create_run_details_object(run_number_string, inst_settings, is_vanadium_run, empty_run_call=None,
                              grouping_file_name_call=None, vanadium_run_call=None,
                              splined_name_list=None, van_abs_file_name=None):
    """
    Creates and returns a run details object which holds various
    properties about the current run.
    :param run_number_string: The user string for the current run
    :param inst_settings: The current instrument object
    :param is_vanadium_run: Boolean of if the current run is a vanadium run
    :param empty_run_call: (Optional) Callable to setup custom empty run number(s) from mapping file
    :param grouping_file_name_call: (Optional) Callable to setup custom grouping file name
    :param vanadium_run_call: (Optional) Callable to setup custom van run number(s) from mapping file
    :param splined_name_list: (Optional) List of unique properties to generate a splined vanadium name from
    :param van_abs_file_name: (Optional) The name of the vanadium absorption file
    :return: RunDetails object with attributes set to applicable values
    """
    cal_map_dict = RunDetailsWrappedCommonFuncs.get_cal_mapping_dict(
        run_number_string=run_number_string, inst_settings=inst_settings)
    run_number = common.get_first_run_number(run_number_string=run_number_string)

    # Get names of files we will be using
    calibration_dir = os.path.normpath(os.path.expanduser(inst_settings.calibration_dir))
    label = common.cal_map_dictionary_key_helper(dictionary=cal_map_dict, key="label")
    offset_file_name = common.cal_map_dictionary_key_helper(dictionary=cal_map_dict, key="offset_file_name")

    # Always make sure the offset file name is included
    if splined_name_list:
        # Force Python to make a copy so we don't modify original
        new_splined_list = list(splined_name_list)
        new_splined_list.append(offset_file_name)
    else:
        new_splined_list = [offset_file_name]

    # These can either be generic or custom so defer to another method
    results_dict = _get_customisable_attributes(
        cal_dict=cal_map_dict, inst_settings=inst_settings, empty_run_call=empty_run_call,
        grouping_name_call=grouping_file_name_call, vanadium_run_call=vanadium_run_call,
        splined_name_list=new_splined_list)

    vanadium_run_string = results_dict["vanadium_runs"]

    if is_vanadium_run:
        # The run number should be the vanadium number in this case
        run_number = vanadium_run_string
        output_run_string = vanadium_run_string
    else:
        # Otherwise set it to the user input
        output_run_string = run_number_string

    # Get the file extension if set
    file_extension = getattr(inst_settings, "file_extension")
    if file_extension:
        # Prefix dot if user has forgotten to
        file_extension = file_extension if file_extension.startswith('.') else '.' + file_extension

    # Get the output name suffix if set
    suffix = getattr(inst_settings, "suffix")

    # Sample empty if there is one as this is instrument specific
    sample_empty = getattr(inst_settings, "sample_empty", None)

    # Generate the paths
    grouping_file_path = os.path.join(calibration_dir, results_dict["grouping_file_name"])
    # Offset  and splined vanadium is within the correct label folder
    offset_file_path = os.path.join(calibration_dir, label, offset_file_name)
    splined_van_path = os.path.join(calibration_dir, label, results_dict["splined_van_name"])
    unsplined_van_path = os.path.join(calibration_dir, label, results_dict["unsplined_van_name"])
    van_absorb_path = os.path.join(calibration_dir, van_abs_file_name) if van_abs_file_name else None

    return _RunDetails(empty_run_number=results_dict["empty_runs"], file_extension=file_extension,
                       run_number=run_number, output_run_string=output_run_string, label=label,
                       offset_file_path=offset_file_path, grouping_file_path=grouping_file_path,
                       splined_vanadium_path=splined_van_path, vanadium_run_number=vanadium_run_string,
                       sample_empty=sample_empty, vanadium_abs_path=van_absorb_path,
                       unsplined_vanadium_path=unsplined_van_path, output_suffix=suffix)


def _get_customisable_attributes(cal_dict, inst_settings, empty_run_call, grouping_name_call, vanadium_run_call,
                                 splined_name_list):
    dict_to_return = {}
    if empty_run_call:
        empty_runs = empty_run_call.get_result()
    else:
        empty_runs = common.cal_map_dictionary_key_helper(dictionary=cal_dict, key="empty_run_numbers")
    dict_to_return["empty_runs"] = empty_runs

    if vanadium_run_call:
        vanadium_runs = vanadium_run_call.get_result()
    else:
        vanadium_runs = common.cal_map_dictionary_key_helper(dictionary=cal_dict, key="vanadium_run_numbers")
    dict_to_return["vanadium_runs"] = vanadium_runs

    if grouping_name_call:
        grouping_name = grouping_name_call.get_result()
    else:
        grouping_name = inst_settings.grouping_file_name
    dict_to_return["grouping_file_name"] = grouping_name

    dict_to_return["splined_van_name"] = common.generate_splined_name(vanadium_runs, splined_name_list)
    dict_to_return["unsplined_van_name"] = common.generate_unsplined_name(vanadium_runs, splined_name_list)

    return dict_to_return


class RunDetailsWrappedCommonFuncs(object):
    """
    Creates a compatible signature when using custom functions when
    constructing RunDetails objects and calls the common methods underneath.
    """
    @staticmethod
    def get_cal_mapping_dict(run_number_string, inst_settings):
        # Get the python dictionary from the YAML mapping
        run_number = common.get_first_run_number(run_number_string=run_number_string)
        cal_mapping_dict = yaml_parser.get_run_dictionary(run_number_string=run_number,
                                                          file_path=inst_settings.cal_mapping_path)
        return cal_mapping_dict

    @staticmethod
    def cal_dictionary_key_helper(key, append_to_error_message=None, **kwargs):
        dictionary = kwargs.pop("forwarded_value")
        return common.cal_map_dictionary_key_helper(dictionary=dictionary, key=key,
                                                    append_to_error_message=append_to_error_message)


class CustomFuncForRunDetails(object):
    # Holds a callable method, associated args and return value so we can pass it in
    # as a single method

    def __init__(self, user_function=None, *args, **func_kwargs):
        if args:
            # If we allow args with position Python gets confused as the forwarded value can be in the first place too
            raise RuntimeError("Cannot use un-named arguments with callable methods")

        self.user_function = user_function
        self.function_kwargs = func_kwargs

        self._previous_callable = None
        self._returned_value = None
        self._function_is_executed = False

    def _exec_func(self):
        forwarded_value = self._previous_callable.get_result() if self._previous_callable else None

        if not self.user_function:
            # We maybe are the 0th case just return any values we hold
            return forwarded_value

        if forwarded_value:
            self.function_kwargs["forwarded_value"] = forwarded_value
            self._returned_value = self.user_function(**self.function_kwargs)
        else:
            self._returned_value = self.user_function(**self.function_kwargs)

        self._function_is_executed = True

    def _set_previous_callable(self, previous_callable):
        if not previous_callable:
            return None
        elif not isinstance(previous_callable, CustomFuncForRunDetails):
            raise ValueError("Previous callable is not a CustomFuncForRunDetails type")

        self._previous_callable = previous_callable

    def add_to_func_chain(self, user_function, *args, **func_kwargs):
        # Construct a new object that will be the next in line
        next_in_chain = CustomFuncForRunDetails(user_function=user_function, *args, **func_kwargs)
        next_in_chain._set_previous_callable(self)
        return next_in_chain

    def get_result(self):
        if not self._function_is_executed:
            self._exec_func()
        return self._returned_value


class _RunDetails(object):
    """
    This class holds the full file paths associated with each run and various other useful attributes
    """

    def __init__(self, empty_run_number, file_extension, run_number, output_run_string, label,
                 offset_file_path, grouping_file_path, splined_vanadium_path, vanadium_run_number,
                 sample_empty, vanadium_abs_path, unsplined_vanadium_path, output_suffix):

        # Essential attribute
        self.empty_runs = empty_run_number
        self.run_number = run_number
        self.output_run_string = output_run_string

        self.label = label

        self.offset_file_path = offset_file_path
        self.grouping_file_path = grouping_file_path

        self.splined_vanadium_file_path = splined_vanadium_path
        self.unsplined_vanadium_file_path = unsplined_vanadium_path
        self.vanadium_run_numbers = vanadium_run_number

        # Optional
        self.file_extension = str(file_extension) if file_extension else None
        self.sample_empty = sample_empty
        self.vanadium_absorption_path = vanadium_abs_path
        self.output_suffix = output_suffix