Skip to content

CoBaIR

This module provides a class for a two-layer bayes net for context based intention recognition.

BayesNet

Source code in CoBaIR/bayes_net.py
class BayesNet():
    def __init__(self, config: dict = None, bn_verbosity: int = 0, validate: bool = True) -> None:
        '''
        Initializes the BayesNet with the given config.

        Args:
            config: A dict with a config following the config format.
            bn_verbosity: sets the verbose flag for bnlearn. See [bnlearn API](
                https://erdogant.github.io/bnlearn/pages/html/bnlearn.bnlearn.html?highlight=verbose
                #bnlearn.bnlearn.make_DAG) for more information
            validate: Flag if the given config should be validated or not. 
                This is necessary to load invalid configs
        '''
        self.log = logging.getLogger(self.__class__.__name__)

        self.valid = False
        self.bn_verbosity = bn_verbosity
        self.discretization_functions = {}

        if config is None:
            validate = False
        config = config_to_default_dict(config)

        # if not config:
        #     self.config = {'intentions': defaultdict(lambda: defaultdict(
        #         lambda: defaultdict(int))), 'contexts': defaultdict(lambda: defaultdict(float))}
        #     return

        self.config = deepcopy(config)
        self.decision_threshold = self.config['decision_threshold']

        if validate:
            self.validate_config()

        # Translation dicts for context to card number in bnlearn and vice versa
        self._create_value_to_card()
        self._create_card_to_value()
        # Translation dict for the std values to probabilities
        self.value_to_prob = {5: 0.95, 4: 0.75,
                              3: 0.5, 2: 0.25, 1: 0.05, 0: 0.0}

        # initialize the bayes net structure
        self.contexts = self.evidence = list(self.config['contexts'].keys())
        self.intentions = list(self.config['intentions'].keys())
        self.edges = list(itertools.product(self.contexts, self.intentions))
        self._create_evidence_card()

        # create CPTs for the bayes net
        self.cpts = []
        self._create_context_cpts()
        self._create_intention_cpts()
        if self.valid:
            self.DAG = bn.make_DAG(self.edges, CPD=self.cpts,
                                   verbose=self.bn_verbosity)

    def _create_value_to_card(self):
        '''
        Initializes the translation dict for the context values to card numbers for bnlearn
        '''
        self.value_to_card = defaultdict(dict)
        for context, probabilities in self.config['contexts'].items():
            count = 0
            for key, _ in probabilities.items():
                self.value_to_card[context][key] = count
                count += 1

    def _create_card_to_value(self):
        '''
        Initializes the backtranslation dict for the context values to card numbers for bnlearn
        '''
        self.card_to_value = defaultdict(dict)
        for context, values in self.value_to_card.items():
            for name, num in values.items():
                self.card_to_value[context][num] = name

    def _create_context_cpts(self):
        '''
        Create the Conditional Probability Tables for all context nodes in the DAG and 
            APPENDS them to self.cpts
        '''
        for context, probabilities in self.config['contexts'].items():
            values = [None] * len(probabilities)
            for value in probabilities:
                values[self.value_to_card[context][value]] = [
                    probabilities[value]]
            self.cpts.append(TabularCPD(variable=context,
                             variable_card=len(probabilities), values=values))

    def _create_intention_cpts(self):
        '''
        Create the Conditional Probability Tables for all intention nodes in the DAG and 
            APPENDS them to self.cpts
        '''
        for intention, context_influence in self.config['intentions'].items():
            values = self._calculate_probability_values(context_influence)
            # create a TabularCPD
            self.cpts.append(
                TabularCPD(variable=intention,
                           variable_card=2,  # intentions are always binary
                           values=values,
                           evidence=self.evidence,
                           evidence_card=self.evidence_card)
            )

    def _create_evidence_card(self):
        '''
        create the evidence_card for bnlearn
        '''
        self.evidence_card = []
        for evidence_variable in self.evidence:
            self.evidence_card.append(
                len(self.config['contexts'][evidence_variable]))

    def _create_combined_context(self, context_influence: dict) -> dict:
        """
        Creates a dict with the combined contexts in card index format from context_influence.
        TODO: example makes no sense - replace
        Args:
            context_influence:
                A dict with the influence values for contexts.
                Example: {'speech commands':
                            {'pickup': 5, 'handover': 0, 'other': 0},
                          'human holding object':
                            {True: 1, False: 4},
                          'human activity':
                            {'idle': 4, 'working': 3}
                          }

        Returns:
            dict: A dict with the combined contexts
            Example: {(0, 2): {('pickup', 'working'): 5}), (0, 1): {('pickup', True): 5})}

        """
        combined_context = {}
        for context in context_influence:
            if isinstance(context, tuple):
                combined_context[tuple(map(self.evidence.index, context))
                                 ] = context_influence[context]
        return combined_context

    def _alter_combined_context(self, count: Counter, context_influence: dict,
                                combined_context: dict) -> dict:
        """
        Overwrites the influence values for the cases of combined influence.

        Args:
            count: 
                A counter that indicates for which combination of context the average is calculated
            context_influence: 
                A dict with the influence values for contexts.
                Example: {'speech commands':
                            {'pickup': 5, 'handover': 0, 'other': 0},
                          'human holding object':
                            {True: 1, False: 4},
                          'human activity':
                            {'idle': 4, 'working': 3}
                          }
            combined_context:
                A dict with the combined contexts
                Example: {(0, 2): {('pickup', 'working'): 5}), (0, 1): {('pickup', True): 5})}

        Returns:
            dict:
                A dict with the adjusted influence values for contexts.
                Example: {'speech commands':
                            {'pickup': 5, 'handover': 0, 'other': 0},
                          'human holding object':
                            {True: 5, False: 4},
                          'human activity':
                            {'idle': 4, 'working': 3}
                          }

        """
        active_case = list(
            map(lambda tup: self.card_to_value[self.evidence[tup[0]]][tup[1]], enumerate(count)))

        altered_context_influence = deepcopy(context_influence)

        for context_tuple, values in combined_context.items():
            combined_case = True
            # There should always be only one key
            value_tuple = list(values.keys())[0]
            for i, context_index in enumerate(context_tuple):
                if active_case[context_index] != value_tuple[i]:
                    combined_case = False
                    break
            if combined_case:
                for i, index in enumerate(context_tuple):
                    altered_context_influence[self.evidence[index]][value_tuple[i]] = \
                        combined_context[context_tuple][value_tuple]
                break
        return altered_context_influence

    def _calculate_probability_values(self, context_influence: dict) -> list:
        '''
        Calculates the probability values with the given context_influence from the config.

        Influence on the positive case(intention is true) is calculated as the
        average over all influences for the given context.
        The influence mapping is given in
        self.value_to_prob = {5: 0.95, 4: 0.75,
            3: 0.5, 2: 0.25, 1: 0.05, 0: 0.0}
        Args:
            context_influence:
                A dict with the influence values for contexts.
                Example: {'speech commands':
                            {'pickup': 5, 'handover': 0, 'other': 0},
                        'human holding object':
                            {True: 1, False: 4},
                        'human activity':
                            {'idle': 4, 'working': 3}
                        }
        Returns:
            list:
            A list of lists containing the probability values for the negative and positive.
            Example:

            [[0.416, 0.5, 0.183, 0.266, 0.733, 0.816, 0.5, 0.583, 0.733, 0.816, 0.5, 0.583],

            [0.583, 0.5, 0.816, 0.733, 0.266, 0.183, 0.5, 0.416, 0.266, 0.183, 0.5, 0.416]]
        '''
        # For every intention calculate the average of their influencing contexts
        pos_values = []
        combined_context = self._create_combined_context(context_influence)

        for count in Counter(self.evidence_card):
            # Here I need to average over all the values that are in the config at position count
            average = 0

            # alternate context_influence
            altered_context_influence = self._alter_combined_context(
                count, context_influence, combined_context)

            ####

            for i in range(len(self.evidence_card)):
                value = self.card_to_value[self.evidence[i]][count[i]]
                influence = altered_context_influence[self.evidence[i]][value]
                prob = self.value_to_prob.get(influence, 0)
                average += prob
            if len(self.evidence) > 0:
                average /= len(self.evidence)
            else:
                average = 0
            pos_values.append(average)
        # create neg_values
        neg_values = [1-value for value in pos_values]
        return [neg_values, pos_values]

    def valid_evidence(self, context: str, instantiation) -> tuple[bool, str]:
        """
        Tests if evidence is a valid instantiation for the context.

        Returns a bool if evidence is valid or not and a string with a error message if not valid.
        Args:
            context: a context
            instantiation: an instantiation of the context
        Returns:
            tuple[bool, str]:
            A tuple of bool to indicate validity and str for error/warn message
        """

        if context not in self.config['contexts']:
            # If context not known to the config is given in evidence it will just ignore that context.
            return True, f'Context "{context}" not set in config - will be ignored'

        if not isinstance(instantiation, Hashable):
            # I not hasable, it can't be used!
            return False, f'Context instatiations must be hashable! Instantiation "{instantiation}" for context "{context}" is not hashable!'

        if instantiation is None:
            # instantiation None is possible - in this case the apriori values will be used.
            return True, f'No instantiation given for context "{context}" - A prori values will be used.'

        if not instantiation in self.config['contexts'][context].keys():
            invalid_msg = f'"{instantiation}" is not a valid instantiation for "{context}". Using None instead'
            valid_options = list(self.config["contexts"][context].keys())
            valid_options_msg = f' Valid options are {valid_options}'
            return True, invalid_msg + valid_options_msg

        return True, ''

    def bind_discretization_function(self, context, discretization_function):
        """
        binds a discretization_function to a specific context.


        Args:
            context: One of the possible contexts from the config
            discretization_function: A discretization function which has to take one parameter and 
                return one of the possible discrete context instantiations.
        """
        if context not in self.contexts:
            raise ValueError(
                f'Cannot bind discretization function to {context}. Context does not exist!')
        self.discretization_functions[context] = discretization_function

    def infer(self, evidence, normalized=True, decision_threshold=None) -> tuple:
        '''
        infers the probabilities for the intentions with given evidence.

        Args:
            evidence:
                Evidence to infer the probabilities of all intentions.
                Evidence can contain context which is not in the config; 
                    it must not contain all possible contexts.
                Example:
                    {'speech commands': 'pickup',
                     'human holding object': True,
                     'human activity': 'idle'}
            decision_threshold: a threshold for picking the most likely intention. 
                Must be between 0 and 1. 
                If not given the decision_threshold defined on initialization is taken. 
            normalized: Flag if the returned inference is normalized to sum up to 1.
        Returns:
            tuple:
            Returns the highest ranking intention (or None if decision_threshold is not reached), the decision threshold
            and a dictionary of intentions and the corresponding probabilities.
        '''
        # check if evidence values are in instantiations and create a card form of bnlearn
        if decision_threshold is None:
            decision_threshold = self.config['decision_threshold']
        card_evidence = {}
        errors = []
        warning_msgs = []
        for context, instantiation in evidence.items():
            valid, err_msg = self.valid_evidence(context, instantiation)
            if valid:
                if err_msg:
                    warning_msgs.append(err_msg)
                    continue
                card_evidence[context] = self.value_to_card[context][instantiation]
            elif context in self.discretization_functions and instantiation is not None:
                discrete_instantiation = self.discretization_functions[context](
                    instantiation)
                valid, err_msg = self.valid_evidence(
                    context, discrete_instantiation)
                if valid:
                    if err_msg:
                        warning_msgs.append(err_msg)
                        continue
                    card_evidence[context] = self.value_to_card[context][discrete_instantiation]
                else:
                    errors.append(err_msg)
            else:
                errors.append(err_msg)

        if warning_msgs:
            for warning in warning_msgs:
                warnings.warn(warning)

        if errors:
            raise ValueError(f"{errors}")

        if self.valid:
            inference = {}
            for intention in self.intentions:
                # only True values of binary intentions will be saved
                inference[intention] = bn.inference.fit(
                    self.DAG,
                    variables=[intention],
                    evidence=card_evidence,
                    verbose=self.bn_verbosity
                ).values[1]

            if normalized:
                inference = self.normalize_inference(inference)
            max_intention = max(inference, key=inference.get)
            max_intention = max_intention if inference[max_intention] > decision_threshold else None
            return max_intention, decision_threshold, inference
        else:
            raise ValueError('Invalid configuration')

    def normalize_inference(self, inference: dict) -> dict:
        '''
        Normalizes the inference to a proper probability distribution.

        Inference which is not normalized will be normalized for one intention being True or False,
        which leads to uninterpretable results for inference of multiple intentions.

        Args:
            inference: dictionary of intentions and the corresponding probabilities
        Returns:
            dict: dictionary of intentions and the corresponding normalized probabilities.

        '''
        normalized_inference = {}
        probability_sum = sum(inference.values())
        for intention, probability in inference.items():
            normalized_inference[intention] = probability / probability_sum
        return normalized_inference

    def validate_config(self):
        '''
        validate that the current config follows the correct format.

        Raises:
            Warnings: Warning is raised if the config is not valid.

        Returns: 
            bool: True if config is valid, False otherwise
        '''
        # We assume the config is valid - if not this will be set to False - this allows to raise multiple warnings
        self.valid = True
        if 'contexts' not in self.config:
            warnings.warn('Field "contexts" must be defined in the config')
            self.valid = False
        if 'intentions' not in self.config:
            warnings.warn('Field "intentions" must be defined in the config')
            self.valid = False
        if not len(self.config['contexts']):
            warnings.warn('No contexts defined')
            self.valid = False
        if not len(self.config['intentions']):
            warnings.warn('No intentions defined')
            self.valid = False
        if not isinstance(self.config['decision_threshold'], float) or \
                not (0 <= self.config['decision_threshold'] < 1):
            warnings.warn(
                'Decision threshold must be a number between 0 and 1')
            self.valid = False

        # Intentions need to have influence value for all contexts and their possible instantiations
        for intention, context_influences in self.config['intentions'].items():
            for context, influences in context_influences.items():

                if isinstance(context, str) and context not in self.config['contexts']:
                    warnings.warn(
                        f'Context influence {context} cannot be found in the defined contexts!')
                    self.valid = False

                for instantiation, influence in influences.items():
                    if not isinstance(instantiation, tuple):
                        if not (0 <= influence <= 5 and isinstance(influence, int)):
                            warnings.warn(
                                f'Influence Value for {intention}.{context}.{instantiation} must be an integer between 0 and 5! Is {influence}')
                            self.valid = False
                        if instantiation not in self.config['contexts'][context].keys():
                            warnings.warn(
                                f'An influence needs to be defined for all instantiations! {intention}.{context}.{instantiation} does not fit the defined instantiations for {context}')
                            self.valid = False

        # Probabilities need to sum up to 1
        for context, instantiations in self.config['contexts'].items():
            for instantiation, value in instantiations.items():
                if not isinstance(value, float):
                    warnings.warn(
                        f'Apriori probability of context "{context}.{instantiation}" is not a number')
                    self.valid = False
            if sum(instantiations.values()) != 1.0:
                warnings.warn(
                    f'The sum of probabilities for context instantiations must be 1 - For "{context}" it is {sum(instantiations.values())}!')
                self.valid = False

        # This is the config of the currently running BayesNet
        if self.valid:
            self.valid_config = deepcopy(self.config)
        return self.valid

    def _create_zero_influence_dict(self, context_with_instantiations: dict) -> defaultdict:
        """
        This uses the context dict from config['contexts'] to instantiate a dict that can be used in 
            config['intentions']['some_context']

        Args:
            context_with_instantiations: 
                a dict holding contexts, their instantiations and corresponding apriori probabilities
        Returns:
            defaultdict:
            A dictionary with zero-initialized influence values for every given context.
            Example:
            {some_context:{
                inst_1:0,
                inst_2:0,
                ...
                inst_3:0
                }
            }
        """
        zeros = defaultdict(lambda: defaultdict(dict))
        for context, instantiations in context_with_instantiations.items():
            for instantiation, _ in instantiations.items():
                zeros[context][instantiation] = 0
        return zeros

    def add_context(self, context: str, instantiations: dict):
        """
        This will add a new context to the config and updates the bayesNet.

        Args:
            context: a new context for the config
            instantiations:
                a dict of the instantiations and their corresponding apriori probabilities
                Example:
                    {True: 0.6, False:0.4}
        Raises:
            ValueError: Raises a ValueError if the context already exists in the config
        """
        # check if context exists already
        if context in self.config['contexts']:
            raise ValueError(
                'Cannot add existing context - use edit_context to edit an existing context')
        # fill in the new context
        self.config['contexts'][context] = instantiations
        # add this context in every intention with instantiations and values beeing zero.
        self._transport_context_into_intentions()
        # reinizialize
        self.__init__(self.config)

    def add_intention(self, intention: str):
        """
        This will add a new intention to the config and updates the bayesNet.

        Args:
            intention: Name of a new intention

        Raises:
            ValueError: Raises a ValueError if the intention already exists in the config
        """
        # check if intention exists already
        if intention in self.config['intentions']:
            raise ValueError(
                'Cannot add existing intention - use edit_intention to edit an existing intention')
        # add in the intention filled with zeros for all contexts
        self.config['intentions'][intention] = defaultdict(
            lambda: defaultdict(int))
        self._transport_context_into_intentions()
        # for context, instantiations_with_values in self.config['contexts'].items():
        #     zeros = self._create_zero_influence_dict(
        #         {context: instantiations_with_values})
        #     self.config['intentions'][intention][context] = zeros[context]
        # reinizialize
        self.__init__(self.config)

    def edit_context(self, context: str, instantiations: dict, new_name: str = None):
        """
        Edits an existing context - this can also be used to remove instantiations

        !!! note
            Changing the name of an instantiation will always set the influence value of this 
                instantiation to zero for all intentions!

        Args:
            context: Name of the context to edit
            instantiations:
                A Dict of instantiations and their corresponding apriori probabilities.
                Example: {True: 0.6, False: 0.4}
            new_name: A new name for the context


        Raises:
            ValueError: Raises a ValueError if the context does not exists in the config
        """
        # check if context exists already - only then I can edit
        if context not in self.config['contexts']:
            raise ValueError(
                'Cannot edit non existing context - use add_context to add a new context')
        if new_name:  # del old names context
            del self.config['contexts'][context]
            # rename all occurences in intentions
            for intention in self.config['intentions']:
                old_instantiations = deepcopy(
                    self.config['intentions'][intention][context])
                del self.config['intentions'][intention][context]
                self.config['intentions'][intention][new_name] = old_instantiations
            context = new_name

        self.config['contexts'][context] = instantiations
        self._remove_context_from_intentions()
        self._transport_context_into_intentions()
        # reinizialize
        self.__init__(self.config)

    def edit_intention(self, intention: str, new_name: str):
        """
        Edits an existing intention.

        Args:
            intention: Name of the intention to edit
            new_name: A new name for the intention

        Raises:
            ValueError: Raises a ValueError if the intention does not exists in the config
        """
        # check if context exists already - only then I can edit
        if intention not in self.config['intentions']:
            raise ValueError(
                'Cannot edit non existing intention - use add_intention to add a new intention')
        if new_name in self.config['intentions']:
            raise ValueError(
                f'{new_name} exists - cannot be given as the new name for {intention}')
        old_values = deepcopy(self.config['intentions'][intention])
        del self.config['intentions'][intention]
        self.config['intentions'][new_name] = old_values
        # reinizialize
        self.__init__(self.config)

    def del_context(self, context: str):
        """
        Removes a context.

        Args:
            context: Name of the context to delete

        Raises:
            ValueError: An ValueError is raised if the context is not in self.config.
        """
        # Check if context exists already - only then I can edit
        if context not in self.config['contexts']:
            raise ValueError(
                'Cannot delete non-existing context - use add_context to add a new context')

        del self.config['contexts'][context]
        self._remove_context_from_intentions()
        self._transport_context_into_intentions()

        self.__init__(self.config)

    def del_intention(self, intention):
        """
        remove an intention.

        Args:
            intention: Name of the intention to delete

        Raises:
            ValueError: An ValueError is raised if the intention is not in self.config.
        """
        if intention not in self.config['intentions']:
            raise ValueError(
                'Cannot delete non existing intention - use add_intention to add a new intention')
        del self.config['intentions'][intention]
        # reinizialize
        self.__init__(self.config)

    def save(self, path: str, save_invalid: bool = True):
        """
        saves the config of the bayesNet to a yml file.

        Args:
            path: path to the file the config will be saved in
            save_invalid: Flag to decide if invalid configs can be saved
        Raises:
            ValueError: 
                A ValueError is raised if `save_invalid` is `False` and the config is not valid
        """
        if not self.valid and not save_invalid:
            warnings.warn("Invalid configuration will not be saved.")
        else:
            with open(path, 'w', encoding='utf-8') as save_file:
                yaml.dump(default_to_regular(self.config), save_file)

    def load(self, path: str):
        """
        Loads a config from file and reinitializes the bayesNet.

        Args:
            path: path to the file the config is saved in
        """
        config = load_config(path)
        # reinitialize with config
        self.__init__(config)

    def change_context_apriori_value(self, context: str, instantiation, value: float):
        """
        Changes the apriori_value for a context instantiation.

        Args:
            context: Name of the context
            instantiation: The instantiation for which the apriori value needs to be changed
            value: the new apriori value
        Raises:
            ValueError: Raises a ValueError if the instantiation does not exists in the config
        """
        # check if this value already exists because I'm using defaultdict
        # otherwise you can just add values
        if instantiation in self.config['contexts'][context]:
            self.config['contexts'][context][instantiation] = value
            # reinizialize
            self.__init__(self.config)
        else:
            raise ValueError(
                'change_context_apriori_value can only change values that exist already')

    def change_influence_value(self, intention: str, context: str, instantiation, value: int):
        """
        Update the influence value of a specific intention for a particular context instance..

        Args:
            intention: Name of the intention
            context: name of the context
            instantiation: The instantiation for which the influence value should be changed
            value: the new influence value. Can be one out of [0, 1, 2, 3, 4, 5]
        Raises:
            ValueError: Raises a ValueError if the instantiation does not exists in the config
        """
        # check if this value already exists because I'm using defaultdict
        # otherwise you can just add values
        if instantiation in self.config['intentions'][intention][context]:
            self.config['intentions'][intention][context][instantiation] = value
            self.__init__(self.config)
        else:
            raise ValueError(
                'change_influence_value can only change values that exist already')

    def add_combined_influence(self, intention: str, contexts: tuple,
                               instantiations: tuple, value: int):
        """
        Adds an influence value for a combination of context instantiations.

        Args:
            intention: Name of the intention
            contexts: tuple containing the names of the contexts
            instantiations: Tuple of context instances to set influence value for.
            value: influence value. Can be one out of [0, 1, 2, 3, 4, 5]
        Raises:
            ValueError: Raises a ValueError if the instantiation does not exists in the config
        """
        if not contexts:
            raise ValueError('Contexts list cannot be empty.')
        if intention not in self.config['intentions']:
            raise ValueError(
                f'"{intention}" does not exist in the list of intentions')
        for i, instantiation in enumerate(instantiations):
            if instantiation not in self.config['intentions'][intention][contexts[i]]:
                raise ValueError(
                    'add_combined_influence can only combine context instantiations that already exist')
        self.config['intentions'][intention][contexts][instantiations] = value
        self.__init__(self.config)

    def del_combined_influence(self, intention: str, contexts: tuple, instantiations: tuple):
        """
        Adds an influence value for a combination of context instantiations.

        Args:
            intention: Name of the intention
            contexts: tuple containing the names of the contexts
            instantiations: tuple of context instantiations
        Raises:
            ValueError: Raises a ValueError if the instantiation does not exists in the config
        """
        if instantiations not in self.config['intentions'][intention][contexts]:
            raise ValueError(
                'Combined context instantiations must exist to be removed.')
        del self.config['intentions'][intention][contexts]

    def _transport_context_into_intentions(self):
        """
        Transports contexts and their instantiations defined in the config['contexts'] into 
            config['intentions'] as influencing context if not present.
        """
        for context in self.config['contexts']:
            for instantiation in self.config['contexts'][context]:
                for intention in self.config['intentions']:
                    if instantiation not in self.config['intentions'][intention][context]:
                        # This only works if it is a defaultdict
                        self.config['intentions'][intention][context][instantiation] = 0

    def _remove_context_from_intentions(self):
        """
        This removes context or instantiation after removing/changing instantiations and/or context.
        """
        # This is a hack because you can't edit while iterating a dict
        contexts_to_remove_from_intentions = []
        context_instantiations_to_remove_from_intentions = []
        for intention in self.config['intentions']:
            for context in self.config['intentions'][intention]:
                if context not in self.config['contexts']:
                    contexts_to_remove_from_intentions.append(
                        (intention, context))
                else:
                    for instantiation in self.config['intentions'][intention][context]:
                        if instantiation not in self.config['contexts'][context]:
                            context_instantiations_to_remove_from_intentions.append(
                                (intention, context, instantiation))
        for intention, context in contexts_to_remove_from_intentions:
            del self.config['intentions'][intention][context]
        for intention, context, instantiation in context_instantiations_to_remove_from_intentions:
            del self.config['intentions'][intention][context][instantiation]

    def change_decision_threshold(self, decision_threshold):
        """
        Changes the decision threshold in the config.
        Args:
            decision_threshold: The new decision threshold.
        """
        self.config['decision_threshold'] = decision_threshold
        self.__init__(self.config)

__init__(self, config=None, bn_verbosity=0, validate=True) special

Initializes the BayesNet with the given config.

Parameters:

Name Type Description Default
config dict

A dict with a config following the config format.

None
bn_verbosity int

sets the verbose flag for bnlearn. See [bnlearn API]( https://erdogant.github.io/bnlearn/pages/html/bnlearn.bnlearn.html?highlight=verbose

bnlearn.bnlearn.make_DAG) for more information

0
validate bool

Flag if the given config should be validated or not. This is necessary to load invalid configs

True
Source code in CoBaIR/bayes_net.py
def __init__(self, config: dict = None, bn_verbosity: int = 0, validate: bool = True) -> None:
    '''
    Initializes the BayesNet with the given config.

    Args:
        config: A dict with a config following the config format.
        bn_verbosity: sets the verbose flag for bnlearn. See [bnlearn API](
            https://erdogant.github.io/bnlearn/pages/html/bnlearn.bnlearn.html?highlight=verbose
            #bnlearn.bnlearn.make_DAG) for more information
        validate: Flag if the given config should be validated or not. 
            This is necessary to load invalid configs
    '''
    self.log = logging.getLogger(self.__class__.__name__)

    self.valid = False
    self.bn_verbosity = bn_verbosity
    self.discretization_functions = {}

    if config is None:
        validate = False
    config = config_to_default_dict(config)

    # if not config:
    #     self.config = {'intentions': defaultdict(lambda: defaultdict(
    #         lambda: defaultdict(int))), 'contexts': defaultdict(lambda: defaultdict(float))}
    #     return

    self.config = deepcopy(config)
    self.decision_threshold = self.config['decision_threshold']

    if validate:
        self.validate_config()

    # Translation dicts for context to card number in bnlearn and vice versa
    self._create_value_to_card()
    self._create_card_to_value()
    # Translation dict for the std values to probabilities
    self.value_to_prob = {5: 0.95, 4: 0.75,
                          3: 0.5, 2: 0.25, 1: 0.05, 0: 0.0}

    # initialize the bayes net structure
    self.contexts = self.evidence = list(self.config['contexts'].keys())
    self.intentions = list(self.config['intentions'].keys())
    self.edges = list(itertools.product(self.contexts, self.intentions))
    self._create_evidence_card()

    # create CPTs for the bayes net
    self.cpts = []
    self._create_context_cpts()
    self._create_intention_cpts()
    if self.valid:
        self.DAG = bn.make_DAG(self.edges, CPD=self.cpts,
                               verbose=self.bn_verbosity)

add_combined_influence(self, intention, contexts, instantiations, value)

Adds an influence value for a combination of context instantiations.

Parameters:

Name Type Description Default
intention str

Name of the intention

required
contexts tuple

tuple containing the names of the contexts

required
instantiations tuple

Tuple of context instances to set influence value for.

required
value int

influence value. Can be one out of [0, 1, 2, 3, 4, 5]

required

Exceptions:

Type Description
ValueError

Raises a ValueError if the instantiation does not exists in the config

Source code in CoBaIR/bayes_net.py
def add_combined_influence(self, intention: str, contexts: tuple,
                           instantiations: tuple, value: int):
    """
    Adds an influence value for a combination of context instantiations.

    Args:
        intention: Name of the intention
        contexts: tuple containing the names of the contexts
        instantiations: Tuple of context instances to set influence value for.
        value: influence value. Can be one out of [0, 1, 2, 3, 4, 5]
    Raises:
        ValueError: Raises a ValueError if the instantiation does not exists in the config
    """
    if not contexts:
        raise ValueError('Contexts list cannot be empty.')
    if intention not in self.config['intentions']:
        raise ValueError(
            f'"{intention}" does not exist in the list of intentions')
    for i, instantiation in enumerate(instantiations):
        if instantiation not in self.config['intentions'][intention][contexts[i]]:
            raise ValueError(
                'add_combined_influence can only combine context instantiations that already exist')
    self.config['intentions'][intention][contexts][instantiations] = value
    self.__init__(self.config)

add_context(self, context, instantiations)

This will add a new context to the config and updates the bayesNet.

Parameters:

Name Type Description Default
context str

a new context for the config

required
instantiations dict

a dict of the instantiations and their corresponding apriori probabilities Example: {True: 0.6, False:0.4}

required

Exceptions:

Type Description
ValueError

Raises a ValueError if the context already exists in the config

Source code in CoBaIR/bayes_net.py
def add_context(self, context: str, instantiations: dict):
    """
    This will add a new context to the config and updates the bayesNet.

    Args:
        context: a new context for the config
        instantiations:
            a dict of the instantiations and their corresponding apriori probabilities
            Example:
                {True: 0.6, False:0.4}
    Raises:
        ValueError: Raises a ValueError if the context already exists in the config
    """
    # check if context exists already
    if context in self.config['contexts']:
        raise ValueError(
            'Cannot add existing context - use edit_context to edit an existing context')
    # fill in the new context
    self.config['contexts'][context] = instantiations
    # add this context in every intention with instantiations and values beeing zero.
    self._transport_context_into_intentions()
    # reinizialize
    self.__init__(self.config)

add_intention(self, intention)

This will add a new intention to the config and updates the bayesNet.

Parameters:

Name Type Description Default
intention str

Name of a new intention

required

Exceptions:

Type Description
ValueError

Raises a ValueError if the intention already exists in the config

Source code in CoBaIR/bayes_net.py
def add_intention(self, intention: str):
    """
    This will add a new intention to the config and updates the bayesNet.

    Args:
        intention: Name of a new intention

    Raises:
        ValueError: Raises a ValueError if the intention already exists in the config
    """
    # check if intention exists already
    if intention in self.config['intentions']:
        raise ValueError(
            'Cannot add existing intention - use edit_intention to edit an existing intention')
    # add in the intention filled with zeros for all contexts
    self.config['intentions'][intention] = defaultdict(
        lambda: defaultdict(int))
    self._transport_context_into_intentions()
    # for context, instantiations_with_values in self.config['contexts'].items():
    #     zeros = self._create_zero_influence_dict(
    #         {context: instantiations_with_values})
    #     self.config['intentions'][intention][context] = zeros[context]
    # reinizialize
    self.__init__(self.config)

bind_discretization_function(self, context, discretization_function)

binds a discretization_function to a specific context.

Parameters:

Name Type Description Default
context

One of the possible contexts from the config

required
discretization_function

A discretization function which has to take one parameter and return one of the possible discrete context instantiations.

required
Source code in CoBaIR/bayes_net.py
def bind_discretization_function(self, context, discretization_function):
    """
    binds a discretization_function to a specific context.


    Args:
        context: One of the possible contexts from the config
        discretization_function: A discretization function which has to take one parameter and 
            return one of the possible discrete context instantiations.
    """
    if context not in self.contexts:
        raise ValueError(
            f'Cannot bind discretization function to {context}. Context does not exist!')
    self.discretization_functions[context] = discretization_function

change_context_apriori_value(self, context, instantiation, value)

Changes the apriori_value for a context instantiation.

Parameters:

Name Type Description Default
context str

Name of the context

required
instantiation

The instantiation for which the apriori value needs to be changed

required
value float

the new apriori value

required

Exceptions:

Type Description
ValueError

Raises a ValueError if the instantiation does not exists in the config

Source code in CoBaIR/bayes_net.py
def change_context_apriori_value(self, context: str, instantiation, value: float):
    """
    Changes the apriori_value for a context instantiation.

    Args:
        context: Name of the context
        instantiation: The instantiation for which the apriori value needs to be changed
        value: the new apriori value
    Raises:
        ValueError: Raises a ValueError if the instantiation does not exists in the config
    """
    # check if this value already exists because I'm using defaultdict
    # otherwise you can just add values
    if instantiation in self.config['contexts'][context]:
        self.config['contexts'][context][instantiation] = value
        # reinizialize
        self.__init__(self.config)
    else:
        raise ValueError(
            'change_context_apriori_value can only change values that exist already')

change_decision_threshold(self, decision_threshold)

Changes the decision threshold in the config.

Parameters:

Name Type Description Default
decision_threshold

The new decision threshold.

required
Source code in CoBaIR/bayes_net.py
def change_decision_threshold(self, decision_threshold):
    """
    Changes the decision threshold in the config.
    Args:
        decision_threshold: The new decision threshold.
    """
    self.config['decision_threshold'] = decision_threshold
    self.__init__(self.config)

change_influence_value(self, intention, context, instantiation, value)

Update the influence value of a specific intention for a particular context instance..

Parameters:

Name Type Description Default
intention str

Name of the intention

required
context str

name of the context

required
instantiation

The instantiation for which the influence value should be changed

required
value int

the new influence value. Can be one out of [0, 1, 2, 3, 4, 5]

required

Exceptions:

Type Description
ValueError

Raises a ValueError if the instantiation does not exists in the config

Source code in CoBaIR/bayes_net.py
def change_influence_value(self, intention: str, context: str, instantiation, value: int):
    """
    Update the influence value of a specific intention for a particular context instance..

    Args:
        intention: Name of the intention
        context: name of the context
        instantiation: The instantiation for which the influence value should be changed
        value: the new influence value. Can be one out of [0, 1, 2, 3, 4, 5]
    Raises:
        ValueError: Raises a ValueError if the instantiation does not exists in the config
    """
    # check if this value already exists because I'm using defaultdict
    # otherwise you can just add values
    if instantiation in self.config['intentions'][intention][context]:
        self.config['intentions'][intention][context][instantiation] = value
        self.__init__(self.config)
    else:
        raise ValueError(
            'change_influence_value can only change values that exist already')

del_combined_influence(self, intention, contexts, instantiations)

Adds an influence value for a combination of context instantiations.

Parameters:

Name Type Description Default
intention str

Name of the intention

required
contexts tuple

tuple containing the names of the contexts

required
instantiations tuple

tuple of context instantiations

required

Exceptions:

Type Description
ValueError

Raises a ValueError if the instantiation does not exists in the config

Source code in CoBaIR/bayes_net.py
def del_combined_influence(self, intention: str, contexts: tuple, instantiations: tuple):
    """
    Adds an influence value for a combination of context instantiations.

    Args:
        intention: Name of the intention
        contexts: tuple containing the names of the contexts
        instantiations: tuple of context instantiations
    Raises:
        ValueError: Raises a ValueError if the instantiation does not exists in the config
    """
    if instantiations not in self.config['intentions'][intention][contexts]:
        raise ValueError(
            'Combined context instantiations must exist to be removed.')
    del self.config['intentions'][intention][contexts]

del_context(self, context)

Removes a context.

Parameters:

Name Type Description Default
context str

Name of the context to delete

required

Exceptions:

Type Description
ValueError

An ValueError is raised if the context is not in self.config.

Source code in CoBaIR/bayes_net.py
def del_context(self, context: str):
    """
    Removes a context.

    Args:
        context: Name of the context to delete

    Raises:
        ValueError: An ValueError is raised if the context is not in self.config.
    """
    # Check if context exists already - only then I can edit
    if context not in self.config['contexts']:
        raise ValueError(
            'Cannot delete non-existing context - use add_context to add a new context')

    del self.config['contexts'][context]
    self._remove_context_from_intentions()
    self._transport_context_into_intentions()

    self.__init__(self.config)

del_intention(self, intention)

remove an intention.

Parameters:

Name Type Description Default
intention

Name of the intention to delete

required

Exceptions:

Type Description
ValueError

An ValueError is raised if the intention is not in self.config.

Source code in CoBaIR/bayes_net.py
def del_intention(self, intention):
    """
    remove an intention.

    Args:
        intention: Name of the intention to delete

    Raises:
        ValueError: An ValueError is raised if the intention is not in self.config.
    """
    if intention not in self.config['intentions']:
        raise ValueError(
            'Cannot delete non existing intention - use add_intention to add a new intention')
    del self.config['intentions'][intention]
    # reinizialize
    self.__init__(self.config)

edit_context(self, context, instantiations, new_name=None)

Edits an existing context - this can also be used to remove instantiations

Note

Changing the name of an instantiation will always set the influence value of this instantiation to zero for all intentions!

Parameters:

Name Type Description Default
context str

Name of the context to edit

required
instantiations dict

A Dict of instantiations and their corresponding apriori probabilities. Example: {True: 0.6, False: 0.4}

required
new_name str

A new name for the context

None

Exceptions:

Type Description
ValueError

Raises a ValueError if the context does not exists in the config

Source code in CoBaIR/bayes_net.py
def edit_context(self, context: str, instantiations: dict, new_name: str = None):
    """
    Edits an existing context - this can also be used to remove instantiations

    !!! note
        Changing the name of an instantiation will always set the influence value of this 
            instantiation to zero for all intentions!

    Args:
        context: Name of the context to edit
        instantiations:
            A Dict of instantiations and their corresponding apriori probabilities.
            Example: {True: 0.6, False: 0.4}
        new_name: A new name for the context


    Raises:
        ValueError: Raises a ValueError if the context does not exists in the config
    """
    # check if context exists already - only then I can edit
    if context not in self.config['contexts']:
        raise ValueError(
            'Cannot edit non existing context - use add_context to add a new context')
    if new_name:  # del old names context
        del self.config['contexts'][context]
        # rename all occurences in intentions
        for intention in self.config['intentions']:
            old_instantiations = deepcopy(
                self.config['intentions'][intention][context])
            del self.config['intentions'][intention][context]
            self.config['intentions'][intention][new_name] = old_instantiations
        context = new_name

    self.config['contexts'][context] = instantiations
    self._remove_context_from_intentions()
    self._transport_context_into_intentions()
    # reinizialize
    self.__init__(self.config)

edit_intention(self, intention, new_name)

Edits an existing intention.

Parameters:

Name Type Description Default
intention str

Name of the intention to edit

required
new_name str

A new name for the intention

required

Exceptions:

Type Description
ValueError

Raises a ValueError if the intention does not exists in the config

Source code in CoBaIR/bayes_net.py
def edit_intention(self, intention: str, new_name: str):
    """
    Edits an existing intention.

    Args:
        intention: Name of the intention to edit
        new_name: A new name for the intention

    Raises:
        ValueError: Raises a ValueError if the intention does not exists in the config
    """
    # check if context exists already - only then I can edit
    if intention not in self.config['intentions']:
        raise ValueError(
            'Cannot edit non existing intention - use add_intention to add a new intention')
    if new_name in self.config['intentions']:
        raise ValueError(
            f'{new_name} exists - cannot be given as the new name for {intention}')
    old_values = deepcopy(self.config['intentions'][intention])
    del self.config['intentions'][intention]
    self.config['intentions'][new_name] = old_values
    # reinizialize
    self.__init__(self.config)

infer(self, evidence, normalized=True, decision_threshold=None)

infers the probabilities for the intentions with given evidence.

Parameters:

Name Type Description Default
evidence

Evidence to infer the probabilities of all intentions. Evidence can contain context which is not in the config; it must not contain all possible contexts. Example: {'speech commands': 'pickup', 'human holding object': True, 'human activity': 'idle'}

required
decision_threshold

a threshold for picking the most likely intention. Must be between 0 and 1. If not given the decision_threshold defined on initialization is taken.

None
normalized

Flag if the returned inference is normalized to sum up to 1.

True

Returns:

Type Description
tuple

Returns the highest ranking intention (or None if decision_threshold is not reached), the decision threshold and a dictionary of intentions and the corresponding probabilities.

Source code in CoBaIR/bayes_net.py
def infer(self, evidence, normalized=True, decision_threshold=None) -> tuple:
    '''
    infers the probabilities for the intentions with given evidence.

    Args:
        evidence:
            Evidence to infer the probabilities of all intentions.
            Evidence can contain context which is not in the config; 
                it must not contain all possible contexts.
            Example:
                {'speech commands': 'pickup',
                 'human holding object': True,
                 'human activity': 'idle'}
        decision_threshold: a threshold for picking the most likely intention. 
            Must be between 0 and 1. 
            If not given the decision_threshold defined on initialization is taken. 
        normalized: Flag if the returned inference is normalized to sum up to 1.
    Returns:
        tuple:
        Returns the highest ranking intention (or None if decision_threshold is not reached), the decision threshold
        and a dictionary of intentions and the corresponding probabilities.
    '''
    # check if evidence values are in instantiations and create a card form of bnlearn
    if decision_threshold is None:
        decision_threshold = self.config['decision_threshold']
    card_evidence = {}
    errors = []
    warning_msgs = []
    for context, instantiation in evidence.items():
        valid, err_msg = self.valid_evidence(context, instantiation)
        if valid:
            if err_msg:
                warning_msgs.append(err_msg)
                continue
            card_evidence[context] = self.value_to_card[context][instantiation]
        elif context in self.discretization_functions and instantiation is not None:
            discrete_instantiation = self.discretization_functions[context](
                instantiation)
            valid, err_msg = self.valid_evidence(
                context, discrete_instantiation)
            if valid:
                if err_msg:
                    warning_msgs.append(err_msg)
                    continue
                card_evidence[context] = self.value_to_card[context][discrete_instantiation]
            else:
                errors.append(err_msg)
        else:
            errors.append(err_msg)

    if warning_msgs:
        for warning in warning_msgs:
            warnings.warn(warning)

    if errors:
        raise ValueError(f"{errors}")

    if self.valid:
        inference = {}
        for intention in self.intentions:
            # only True values of binary intentions will be saved
            inference[intention] = bn.inference.fit(
                self.DAG,
                variables=[intention],
                evidence=card_evidence,
                verbose=self.bn_verbosity
            ).values[1]

        if normalized:
            inference = self.normalize_inference(inference)
        max_intention = max(inference, key=inference.get)
        max_intention = max_intention if inference[max_intention] > decision_threshold else None
        return max_intention, decision_threshold, inference
    else:
        raise ValueError('Invalid configuration')

load(self, path)

Loads a config from file and reinitializes the bayesNet.

Parameters:

Name Type Description Default
path str

path to the file the config is saved in

required
Source code in CoBaIR/bayes_net.py
def load(self, path: str):
    """
    Loads a config from file and reinitializes the bayesNet.

    Args:
        path: path to the file the config is saved in
    """
    config = load_config(path)
    # reinitialize with config
    self.__init__(config)

normalize_inference(self, inference)

Normalizes the inference to a proper probability distribution.

Inference which is not normalized will be normalized for one intention being True or False, which leads to uninterpretable results for inference of multiple intentions.

Parameters:

Name Type Description Default
inference dict

dictionary of intentions and the corresponding probabilities

required

Returns:

Type Description
dict

dictionary of intentions and the corresponding normalized probabilities.

Source code in CoBaIR/bayes_net.py
def normalize_inference(self, inference: dict) -> dict:
    '''
    Normalizes the inference to a proper probability distribution.

    Inference which is not normalized will be normalized for one intention being True or False,
    which leads to uninterpretable results for inference of multiple intentions.

    Args:
        inference: dictionary of intentions and the corresponding probabilities
    Returns:
        dict: dictionary of intentions and the corresponding normalized probabilities.

    '''
    normalized_inference = {}
    probability_sum = sum(inference.values())
    for intention, probability in inference.items():
        normalized_inference[intention] = probability / probability_sum
    return normalized_inference

save(self, path, save_invalid=True)

saves the config of the bayesNet to a yml file.

Parameters:

Name Type Description Default
path str

path to the file the config will be saved in

required
save_invalid bool

Flag to decide if invalid configs can be saved

True

Exceptions:

Type Description
ValueError

A ValueError is raised if save_invalid is False and the config is not valid

Source code in CoBaIR/bayes_net.py
def save(self, path: str, save_invalid: bool = True):
    """
    saves the config of the bayesNet to a yml file.

    Args:
        path: path to the file the config will be saved in
        save_invalid: Flag to decide if invalid configs can be saved
    Raises:
        ValueError: 
            A ValueError is raised if `save_invalid` is `False` and the config is not valid
    """
    if not self.valid and not save_invalid:
        warnings.warn("Invalid configuration will not be saved.")
    else:
        with open(path, 'w', encoding='utf-8') as save_file:
            yaml.dump(default_to_regular(self.config), save_file)

valid_evidence(self, context, instantiation)

Tests if evidence is a valid instantiation for the context.

Returns a bool if evidence is valid or not and a string with a error message if not valid.

Parameters:

Name Type Description Default
context str

a context

required
instantiation

an instantiation of the context

required

Returns:

Type Description
tuple[bool, str]

A tuple of bool to indicate validity and str for error/warn message

Source code in CoBaIR/bayes_net.py
def valid_evidence(self, context: str, instantiation) -> tuple[bool, str]:
    """
    Tests if evidence is a valid instantiation for the context.

    Returns a bool if evidence is valid or not and a string with a error message if not valid.
    Args:
        context: a context
        instantiation: an instantiation of the context
    Returns:
        tuple[bool, str]:
        A tuple of bool to indicate validity and str for error/warn message
    """

    if context not in self.config['contexts']:
        # If context not known to the config is given in evidence it will just ignore that context.
        return True, f'Context "{context}" not set in config - will be ignored'

    if not isinstance(instantiation, Hashable):
        # I not hasable, it can't be used!
        return False, f'Context instatiations must be hashable! Instantiation "{instantiation}" for context "{context}" is not hashable!'

    if instantiation is None:
        # instantiation None is possible - in this case the apriori values will be used.
        return True, f'No instantiation given for context "{context}" - A prori values will be used.'

    if not instantiation in self.config['contexts'][context].keys():
        invalid_msg = f'"{instantiation}" is not a valid instantiation for "{context}". Using None instead'
        valid_options = list(self.config["contexts"][context].keys())
        valid_options_msg = f' Valid options are {valid_options}'
        return True, invalid_msg + valid_options_msg

    return True, ''

validate_config(self)

validate that the current config follows the correct format.

Exceptions:

Type Description
Warnings

Warning is raised if the config is not valid.

Returns: bool: True if config is valid, False otherwise

Source code in CoBaIR/bayes_net.py
def validate_config(self):
    '''
    validate that the current config follows the correct format.

    Raises:
        Warnings: Warning is raised if the config is not valid.

    Returns: 
        bool: True if config is valid, False otherwise
    '''
    # We assume the config is valid - if not this will be set to False - this allows to raise multiple warnings
    self.valid = True
    if 'contexts' not in self.config:
        warnings.warn('Field "contexts" must be defined in the config')
        self.valid = False
    if 'intentions' not in self.config:
        warnings.warn('Field "intentions" must be defined in the config')
        self.valid = False
    if not len(self.config['contexts']):
        warnings.warn('No contexts defined')
        self.valid = False
    if not len(self.config['intentions']):
        warnings.warn('No intentions defined')
        self.valid = False
    if not isinstance(self.config['decision_threshold'], float) or \
            not (0 <= self.config['decision_threshold'] < 1):
        warnings.warn(
            'Decision threshold must be a number between 0 and 1')
        self.valid = False

    # Intentions need to have influence value for all contexts and their possible instantiations
    for intention, context_influences in self.config['intentions'].items():
        for context, influences in context_influences.items():

            if isinstance(context, str) and context not in self.config['contexts']:
                warnings.warn(
                    f'Context influence {context} cannot be found in the defined contexts!')
                self.valid = False

            for instantiation, influence in influences.items():
                if not isinstance(instantiation, tuple):
                    if not (0 <= influence <= 5 and isinstance(influence, int)):
                        warnings.warn(
                            f'Influence Value for {intention}.{context}.{instantiation} must be an integer between 0 and 5! Is {influence}')
                        self.valid = False
                    if instantiation not in self.config['contexts'][context].keys():
                        warnings.warn(
                            f'An influence needs to be defined for all instantiations! {intention}.{context}.{instantiation} does not fit the defined instantiations for {context}')
                        self.valid = False

    # Probabilities need to sum up to 1
    for context, instantiations in self.config['contexts'].items():
        for instantiation, value in instantiations.items():
            if not isinstance(value, float):
                warnings.warn(
                    f'Apriori probability of context "{context}.{instantiation}" is not a number')
                self.valid = False
        if sum(instantiations.values()) != 1.0:
            warnings.warn(
                f'The sum of probabilities for context instantiations must be 1 - For "{context}" it is {sum(instantiations.values())}!')
            self.valid = False

    # This is the config of the currently running BayesNet
    if self.valid:
        self.valid_config = deepcopy(self.config)
    return self.valid

PrettySafeLoader (SafeLoader)

A YAML loader that constructs Python tuples from YAML sequences.

Source code in CoBaIR/bayes_net.py
class PrettySafeLoader(yaml.SafeLoader):
    """A YAML loader that constructs Python tuples from YAML sequences."""

    def construct_python_tuple(self, node):
        """
        Construct a Python tuple from a YAML sequence node.

        Args:
            node (Any): The YAML sequence node to construct a tuple from.

        Returns:
            tuple: The constructed tuple.
        """
        return tuple(self.construct_sequence(node))

construct_python_tuple(self, node)

Construct a Python tuple from a YAML sequence node.

Parameters:

Name Type Description Default
node Any

The YAML sequence node to construct a tuple from.

required

Returns:

Type Description
tuple

The constructed tuple.

Source code in CoBaIR/bayes_net.py
def construct_python_tuple(self, node):
    """
    Construct a Python tuple from a YAML sequence node.

    Args:
        node (Any): The YAML sequence node to construct a tuple from.

    Returns:
        tuple: The constructed tuple.
    """
    return tuple(self.construct_sequence(node))

config_to_default_dict(config=None)

This casts a config given as dict into a defaultdict.

Parameters:

Name Type Description Default
config dict

A dict with a config following the config format.

None

Returns:

Type Description
defaultdict

a defaultdict containing the config

Source code in CoBaIR/bayes_net.py
def config_to_default_dict(config: dict = None):
    """
    This casts a config given as dict into a defaultdict.

    Args:
        config: A dict with a config following the config format.
    Returns:
        defaultdict:
            a defaultdict containing the config
    """
    if not config:
        config = {}
    new_config = {'intentions': defaultdict(lambda: defaultdict(
        lambda: defaultdict(int))), 'contexts': defaultdict(lambda: defaultdict(float))}
    if 'contexts' in config:
        for context in config['contexts']:
            for instantiation, value in config['contexts'][context].items():
                new_config['contexts'][context][instantiation] = value
    if 'intentions' in config:
        for intention in config['intentions']:
            # HERE: there is the chance that there is no context yet - write intention once
            new_config['intentions'][intention] = defaultdict(
                lambda: defaultdict(int))
            for context in config['intentions'][intention]:
                for instantiation, value in config['intentions'][intention][context].items():
                    new_config['intentions'][intention][context][instantiation] = value
    if 'decision_threshold' in config:
        new_config['decision_threshold'] = config['decision_threshold']
    else:
        new_config['decision_threshold'] = 0.0

    return new_config

default_to_regular(d)

This casts a defaultdict to a regular dict which is needed for saving as yml file.

Parameters:

Name Type Description Default
d

the dict which should be casted

required

Returns:

Type Description
dict

a regular dict casted from the defaultdict

Source code in CoBaIR/bayes_net.py
def default_to_regular(d):
    """
    This casts a defaultdict to a regular dict which is needed for saving as yml file.

    Args:
        d: the dict which should be casted
    Returns:
        dict:
            a regular dict casted from the defaultdict
    """
    # casts dicts or default dicts because otherwise it will stop at the first dict and
    # if that has another defaultdict in it - that won't cast
    if isinstance(d, defaultdict) or isinstance(d, dict):
        d = {
            k: default_to_regular(v)
            for k, v in d.items()
            if not isinstance(v, dict) or v or isinstance(v, defaultdict)
        }
    return d

load_config(path)

Helper function to load a config.

Parameters:

Name Type Description Default
path

path to the file the config is saved in

required

Returns:

Type Description
defaultdict

a defaultdict containing the config

Source code in CoBaIR/bayes_net.py
def load_config(path):
    """
    Helper function to load a config.

    Args:
        path: path to the file the config is saved in
    Returns:
        defaultdict:
            a defaultdict containing the config
    """

    # if os.path.splitext(path)[-1] != ".yml":
    #     raise TypeError(
    #         'Invalid format file - only supporting yml files')
    with open(path, encoding='utf-8') as stream:
        return config_to_default_dict(yaml.load(stream, Loader=PrettySafeLoader))