""" Class ScenarioCategory

Creation date: 2018 10 30
Author(s): <NAME>

To do:
Add "comprises" method based on the "fall_into" method that is defined for Scenario.

Modifications:
2018 11 05: Make code PEP8 compliant.
2018 11 07: Change use of models.
2018 11 22: Enable instantiation using JSON code.
2018 11 29: Add functionality to return the derived Tags.
2018 12 06: Make it possible to return full JSON code (incl. attributes' JSON code).
2019 05 22: Make use of type_checking.py to shorten the initialization.
2019 10 11: Update of terminology.
2019 11 04: Add options to automatically assign unique ids to actor/activities.
2020 07 30: Update conversion of scenario category to a string.
2020 07 31: Add the includes method.
2020 08 15: Remove static_environment and add static_physical_things.
2020 08 16: Add dynamic_physical_thing_categories.
2020 08 23: Remove the update_uid options because uid is automatically generated by ScenarioElement.
2020 08 24: Enable instantiation of ScenarioCategory from json without needing full json code.
2020 08 25: Add comprises() function.
2020 10 04: Change way of creating object from JSON code.
2020 10 12: Remove Dynamic/StaticPhysicalThing and use PhysicalElement instead.
"""

from __future__ import annotations
from typing import List, Tuple, Union
import fnmatch
import numpy as np
from .activity import Activity
from .activity_category import ActivityCategory, _activity_category_from_json
from .actor import Actor
from .actor_category import ActorCategory, _actor_category_from_json
from .physical_element_category import PhysicalElementCategory, _physical_element_category_from_json
from .qualitative_element import QualitativeElement, _qualitative_element_props_from_json
from .scenario_element import DMObjects, _attributes_from_json, _object_from_json
from .type_checking import check_for_type, check_for_list, check_for_tuple


class ScenarioCategory(QualitativeElement):
    """ ScenarioCategory - A qualitative description

    Although a scenario is a quantitative description, there also exists a
    qualitative description of a scenario. We refer to the qualitative
    description of a scenario as a scenario category. The qualitative
    description can be regarded as an abstraction of the quantitative scenario.

    Scenario categories comprise scenarios. A scenario category may comprise
    multiple scenarios. On the other hand, multiple scenario categories may
    comprise the same scenario.

    A scenario category can include another scenario category.

    When instantiating the ScenarioCategory object, the name, description,
    image, unique id (uid), and tags are passed. To set the static physical
    things, activities, actors, and acts, use the corresponding methods, i.e.,
    set_physical_elements(), set_activities(), set_actors(), and set_acts(),
    respectively.

    Attributes:
        description (str): A description of the scenario class. The objective of
            the description is to make the scenario class human interpretable.
        image (str): Path to image that schematically shows the class.
        activities (List[ActivityCategory]): List of activities that
            are used for this ScenarioCategory.
        physical_elements (List[PhysicalElementCategory]): List of physical
            things that participate in the Scenario. Could be both static and
            dynamic.
        actors (List[ActorCategory]): List of actors that participate in the
            Scenario.
        acts (List[Tuple[ActorCategory, ActivityCategory]]): The acts describe
            which actors perform which activities.
        name (str): A name that serves as a short description of the scenario
            category.
        uid (int): A unique ID.
        tags (List[Tag]): A list of tags that formally defines the scenario
            category. These tags determine whether scenarios fall into this
            scenario category or not.
    """
    def __init__(self, image: str, description: str = "", **kwargs):
        # Check the types of the inputs
        check_for_type("image", image, str)

        # Assign the attributes
        QualitativeElement.__init__(self, description=description, **kwargs)
        self.image = image
        self.activities = []  # Type: List[ActivityCategory]
        self.physical_elements = []  # Type: List[PhysicalElementCategory]
        self.actors = []  # Type: List[ActorCategory]
        self.acts = []  # Type: List[Tuple[ActorCategory, ActivityCategory]]

        # Set attributes if provided by kwargs.
        if "physical_element_categories" in kwargs:
            self.set_physical_elements(kwargs["physical_element_categories"])
        if "actor_categories" in kwargs:
            self.set_actors(kwargs["actor_categories"])
        if "activity_categories" in kwargs:
            self.set_activities(kwargs["activity_categories"])
        if "acts" in kwargs:
            self.set_acts(kwargs["acts"])

        # Some parameters
        # Maximum number of characters that are used when printing the general description
        self.maxprintlength = 80

    def set_physical_elements(self, physical_elements: List[PhysicalElementCategory]) -> None:
        """ Set the physical elements

        Check whether the physical things are correctly defined.

        :param physical_elements: List of physical thing categories that define
            the static environment and part of the dynamic environment
            qualitatively.
        """
        # Check whether the static physical things are correctly defined.
        check_for_list("physical_elements", physical_elements, PhysicalElementCategory,
                       can_be_none=False)

        # Assign static physical thing categories to an attribute.
        self.physical_elements = physical_elements

    def set_activities(self, activity_categories: List[ActivityCategory]) -> None:
        """ Set the activities

        Check whether the activities are correctly defined. Activities should be
        a list with instantiations of ActivityCategory.

        :param activity_categories: List of activities that are used for this
            ScenarioCategory.
        """
        # Check whether the activities are correctly defined.
        check_for_list("activities", activity_categories, ActivityCategory, can_be_none=False)

        # Assign activity categories to an attribute.
        self.activities = activity_categories  # Type: List[ActivityCategory]

    def set_actors(self, actor_categories: List[ActorCategory]) -> None:
        """ Set the actors

        Check whether the actors are correctly defined. Actors should be a list
        with instantiations of ActorCategory.

        :param actor_categories: List of actors that participate in the
            Scenario.
        """
        # Check whether the actors are correctly defined.
        check_for_list("actors", actor_categories, ActorCategory, can_be_none=False)

        # Assign actor categories to an attribute.
        self.actors = actor_categories  # Type: List[ActorCategory]

    def set_acts(self, acts_scenario_category: List[Tuple[ActorCategory, ActivityCategory]],
                 verbose: bool = True) -> None:
        """ Set the acts

        Check whether the acts are correctly defined. Each act should be a tuple
        with an actor category and an activity category, i.e.,
        (ActorCategory, ActivityCategory). Acts is a list
        containing multiple tuples (ActorCategory, ActivityCategory).

        :param acts_scenario_category: The acts describe which actors perform
            which activities. The actors and activities that are used in acts
            should also be passed with the actors and activities arguments. If
            not, a warning will be shown and the corresponding actor/activity
            will be added to the list of actors/activities.
        :param verbose: Set to False if warning should be surpressed.
        """
        check_for_list("acts", acts_scenario_category, tuple)
        for act in acts_scenario_category:
            check_for_tuple("act", act, (ActorCategory, ActivityCategory))

        # Set the acts.
        self.acts = acts_scenario_category
        _check_acts(self.acts, self.actors, self.activities, verbose=verbose)

    def derived_tags(self) -> dict:
        """ Return all tags, including the tags of the attributes.

        The ScenarioCategory has tags, but also its attributes can have tags.
        More specifically, the each PhysicalElementCategory, ActorCategory, and
        ActivityCategory might have tags. A dictionary will be returned. Each
        item of the dictionary contains a list of tags corresponding to either
        the own object (i.e., ScenarioCategory), a PhysicalElementCategory, or
        an ActorCategory.

        The tags that might be associated with the ActivityCategory are returned
        with the ActorCategory if the corresponding ActorCategory is performing
        that ActivityCategory according to the defined acts.

        :return: List of tags.
        """
        # Instantiate the dictionary.
        tags = {}

        # Provide the tags of the very own object (ScenarioCategory).
        if self.tags:
            tags["{:s}::ScenarioCategory".format(self.name)] = self.tags

        # Provide the tags for each ActorCategory.
        tags = derive_actor_tags(self.actors, self.acts, tags=tags)

        # Provide the tags for each PhysicalElementCategory.
        for physical_element in self.physical_elements:
            if physical_element.tags:
                tags["{:s}::PhysicalElementCategory".format(physical_element.name)] = \
                    physical_element.tags

        # Return the tags.
        return tags

    def includes(self, scenario_category: ScenarioCategory) -> bool:
        """ Check if this scenario category includes the given scenario category

        It is checked whether the passed ScenarioCategory is included in this
        scenario category. To determine whether this is the case, the derived
        tags are used. The derived tags from this scenario category should be at
        least present (or subtags of the tags) in the provided ScenarioCategory.

        :param scenario_category: The potential ScenarioCategory that is
            included in this scenario category.
        :return: Whether or not the ScenarioCategory is included.
        """
        # Determine the derived tags of this and the other scenario category.
        own_tags = self.derived_tags()
        other_tags = scenario_category.derived_tags()

        # Check for tags directly related to the ScenarioCategory. These tags should be directly
        # present for the scenario.
        if not _check_tags(own_tags, other_tags, "ScenarioCategory", "ScenarioCategory"):
            return False

        # Check for the actors, dynamic physical things, and static physical things.
        if not _check_multiple_tags(own_tags, other_tags, "ActorCategory") or \
                not _check_multiple_tags(own_tags, other_tags, "PhysicalElementCategory"):
            return False
        return True

    def comprises(self, scenario) -> bool:
        """ Check if this scenario category comprises the given scenario

        It is checked whether the passed Scenario is comprised in this scenario
        category. To determine whether this is the case, the derived tags are
        used. The derived tags from this scenario category should be at
        least present (or subtags of the tags) in the provided Scenario.

        :param scenario: The potential ScenarioCategory that is
            included in this scenario category.
        :return: Whether or not the ScenarioCategory is included.
        """
        # Determine the derived tags of this and the other scenario category.
        own_tags = self.derived_tags()
        other_tags = scenario.derived_tags()

        # Check for tags directly related to the ScenarioCategory. These tags should be directly
        # present for the scenario.
        if not _check_tags(own_tags, other_tags, "ScenarioCategory", "Scenario"):
            return False

        # Check for the actors, dynamic physical things, and static physical things.
        if not _check_multiple_tags(own_tags, other_tags, "ActorCategory", "Actor") or \
                not _check_multiple_tags(own_tags, other_tags, "PhysicalElementCategory",
                                         "PhysicalElement"):
            return False
        return True

    def __str__(self) -> str:
        """ Method that will be called when printing the scenario category.

        :return: string to print.
        """

        # Show the name
        string = "Name: {:s}\n".format(self.name)

        # Show the description of the scenario class
        string += "Description:\n"
        words = self.description.split(' ')
        line = ""
        for word in words:
            if len(line) + len(word) <= self.maxprintlength:
                line += "  {:s}".format(word)
            else:
                string += "{:s}\n".format(line)
                line = "  {:s}".format(word)
        if line:
            string += "{:s}\n".format(line)

        # Show the tags
        string += _print_tags(self.derived_tags())
        return string

    def to_json(self) -> dict:
        scenario_category = QualitativeElement.to_json(self)
        scenario_category["image"] = self.image
        scenario_category["physical_element_categories"] = \
            [dict(name=element.name, uid=element.uid) for element in self.physical_elements]
        scenario_category["actor_categories"] = [dict(name=actor.name, uid=actor.uid)
                                                 for actor in self.actors]
        scenario_category["activity_categories"] = [dict(name=activity.name, uid=activity.uid)
                                                    for activity in self.activities]
        scenario_category["acts"] = []
        for actor, activity in self.acts:
            scenario_category["acts"].append({"actor": actor.uid,
                                              "activity": activity.uid})
        scenario_category["derived_tags"] = self.derived_tags()
        for key, tags in scenario_category["derived_tags"].items():
            scenario_category["derived_tags"][key] = [tag.to_json() for tag in tags]
        return scenario_category

    def to_json_full(self) -> dict:
        scenario_category = self.to_json()
        scenario_category["physical_element_categories"] = [element.to_json_full() for element in
                                                            self.physical_elements]
        scenario_category["actor_categories"] = [actor.to_json_full() for actor in
                                                 self.actors]
        scenario_category["activity_categories"] = [activity.to_json_full() for activity in
                                                    self.activities]
        return scenario_category


def _check_acts(acts: Union[List[Tuple[ActorCategory, ActivityCategory]],
                            List[Tuple[Actor, Activity]]],
                actors: Union[List[ActorCategory], List[Actor]],
                activities: Union[List[ActivityCategory], List[Activity]],
                verbose: bool = True):
    # Check whether the actors/activities defined with the acts are already listed. If not,
    # the corresponding actor/activity will be added and a warning will be shown.
    for thing, activity in acts:
        if thing not in actors:
            if verbose:
                print("Actor with name '{:s}' ".format(thing.name) +
                      "is used with acts but not defined in the list of actors.")
                print("Therefore, the actor is added to the list of actors.")
            actors.append(thing)
        if activity not in activities:
            if verbose:
                print("Activity with name '{:s}' is used with acts but".format(activity.name) +
                      " not defined in the list of activities.")
                print("Therefore, the activity is added to the list of activities.")
            activities.append(activity)


def _scenario_category_props_from_json(json: dict, attribute_objects: DMObjects, **kwargs) -> dict:
    props = _qualitative_element_props_from_json(json)
    props["image"] = json["image"]
    props.update(_attributes_from_json(
        json, attribute_objects,
        dict(physical_element_categories=(_physical_element_category_from_json,
                                          "physical_element_category"),
             actor_categories=(_actor_category_from_json, "actor_category"),
             activity_categories=(_activity_category_from_json, "activity_category")),
        **kwargs))
    props["acts"] = _get_acts(json, props["actor_categories"], props["activity_categories"])
    return props


def _scenario_category_from_json(json: dict, attribute_objects: DMObjects, **kwargs) \
        -> ScenarioCategory:
    return ScenarioCategory(**_scenario_category_props_from_json(json, attribute_objects, **kwargs))


def scenario_category_from_json(json: dict, attribute_objects: DMObjects = None, **kwargs) -> \
        ScenarioCategory:
    """ Get ScenarioCategory object from JSON code.

    It is assumed that all the attributes are fully defined. Hence, all
    StaticPhysicalThingCategories, DynamicPhysicalThingCategories,
    ActorCategory, and ActivityCategory need to be defined, instead of only a
    reference to their IDs.
    Alternatively, the static physical thing categories, dynamic physical thing
    categories, actor categories, and activity categories can be passed as
    arguments.
    Further optional arguments (provided by kwargs) are:
    - physical_elements (List[PhysicalElementCategory]): The physical elements
        for defining the static environment.
    - actors (List[ActorCategory]): The actor categories.
    - activities (List[ActivityCategory]): The activity categories.
    For all these arguments: If given, it will not be based on the JSON code.

    :param json: JSON code of ScenarioCategory.
    :param attribute_objects: A structure for storing all objects (optional).
    :return: ScenarioCategory object.
    """
    return _object_from_json(json, _scenario_category_from_json, "scenario_category",
                             attribute_objects, **kwargs)


def derive_actor_tags(actors: List, acts: List, tags: dict = None) -> dict:
    """ Derive the tags that are associated with the actors.

    The tags of an Actor(Category) will be added to the dictionary "tags". The
    key equals <name of actor>::<class>, where class is supposed to be either
    Actor or ActorCategory, whereas the value will be the list of tags that are
    associated with the actor.

    :param actors: The actors of  the Scenario(Category).
    :param acts: The acts of the Scenario(Category).
    :param tags: Initial tags that will be amended with tags of the actors.
    :return: Dictionary with each actor as a key and the corresponding values
        denote the tags.
    """
    # By default, tags is an empty dictionary
    if tags is None:
        tags = {}

    for actor in actors:
        actor_tags = actor.get_tags()
        for act in acts:
            if act[0] == actor:
                actor_tags += act[1].get_tags()
        if actor_tags:
            if isinstance(actor, ActorCategory):
                class_name = "ActorCategory"
            elif isinstance(actor, Actor):
                class_name = "Actor"
            else:
                raise TypeError("Actor is of type '{}' while it should be ".format(type(actor)) +
                                "of type ActorCategory, or Actor.")
            key = "{:s}::{:s}".format(actor.name, class_name)
            i = 1
            while key in tags:  # Make sure that a unique key is used.
                i += 1
                key = "{:s}{:d}::{:s}".format(actor.name, i, class_name)
            tags[key] = list(set(actor_tags))  # list(set()) makes sure that tags are unique.
    return tags


def _check_tags(tags: dict, subtags: dict, tags_class: str = "ScenarioCategory",
                subtags_class: str = "ScenarioCategory") -> bool:
    """ Check whether (sub)tags of <tags> are present in <subtags>.

    The tags are provided as dictionaries, where each item corresponds to the
    tags of one object that is part of the scenario (category). This function
    checks whether all tags in <tags> of the class <tags_class> are present in
    the tags in <subtags> of the class <subtags_class> (or subtags of the
    <tags>).

    :param tags: Dictionary of the derived tags of the ScenarioCategory.
    :param subtags: Dictionary of the derived tags of the Scenario.
    :param tags_class: Specify attribute to be used of the ScenarioCategory.
    :param subtags_class: Specify attribute to be used of the Scenario.
    :return: Whether the tags of the ScenarioCategory are found in the tags
        of the Scenario.
    """
    sc_keys = fnmatch.filter(tags, "*::{:s}".format(tags_class))
    if sc_keys:  # In this case, there are tags in <tags> related to <tags_class>.
        s_keys = fnmatch.filter(subtags, "*::{:s}".format(subtags_class))
        if s_keys:  # There are tags in <subtags> related to <subtags_class>.
            for tag in tags[sc_keys[0]]:
                if not any(map(tag.is_supertag_of, subtags[s_keys[0]])):
                    return False  # A tag of <tags> is not found in the <subtags>.
        else:  # There are no tags in <subtags> related to <subtags_class>.
            return False
    return True


def _check_multiple_tags(own_tags: dict, other_tags: dict, tags_class: str,
                         subtags_class: str = None) -> bool:
    """ Check if all tags in `own_tags` are present in `other_tags`.

    This is done for a specific attribute (e.g., actor_categories). For example,
    with actor categories, there is a list made, where each item is a list
    itself with the tags of the corresponding actor category. For each the
    actor category in <own_tags>, there needs to be a (different) actor category
    in <other_tags> that has the same tags (or more tags, or corresponding
    subtags).

    :param own_tags: The tags of the own scenario category.
    :param other_tags: The tags of the scenario category that is potentially
        'included' in the former.
    :param tags_class: Check for which attribute we need to check.
    :param subtags_class: If specified, this class is used for the `other_tags`.
    :return: True if all tags in `own_tags` are present in `other_tags`.
    """
    if subtags_class is None:
        subtags_class = tags_class
    own_objects = fnmatch.filter(own_tags, "*::{:s}".format(tags_class))
    other_objects = fnmatch.filter(other_tags, "*::{:s}".format(subtags_class))
    if len(own_objects) > len(other_objects):  # There must be equal or more objects in other SC.
        return False

    # Create a boolean matrix, where the (i,j)-th element is True if the i-th object of the other
    # objects might correspond to the j-th object of our own objects.
    match = np.zeros((len(other_objects), len(own_objects)), dtype=np.bool)
    for i, other_object in enumerate(other_objects):
        for j, own_object in enumerate(own_objects):
            match[i, j] = all(any(map(tag.is_supertag_of, other_tags[other_object]))
                              for tag in own_tags[own_object])

    # Check if all of our own objects can be matched with the actos in the other objects.
    return _check_match_matrix(match)


def _check_match_matrix(match: np.array) -> bool:
    # The matching of the actors need to be done. If a match is found, the corresponding
    # row and column will be removed from the match matrix.
    n_matches = 1  # Number of matches to look for.
    while match.size:
        # If there is at least one ActorCategory left that has no match, a False will be
        # returned.
        if not all(np.any(match, axis=1)):
            return False

        sum_match_actor = np.sum(match, axis=0)
        j = next((j for j in range(match.shape[1]) if sum_match_actor[j] == n_matches), -1)
        if j >= 0:  # We found an actor of our own scenario category with n matches.
            i = next(i for i in range(match.shape[0]) if match[i, j])
        else:
            sum_match_actor = np.sum(match, axis=1)
            i = next((i for i in range(match.shape[0]) if sum_match_actor[i] == n_matches), -1)
            if i >= 0:  # We found an actor of the ScenarioCategory with n matches
                j = next(j for j in range(match.shape[1]) if match[i, j])
            else:
                # Try again for higher n (number of matches)
                n_matches = n_matches + 1
                continue
        match = np.delete(np.delete(match, i, axis=0), j, axis=1)
        n_matches = 1
    return True


def _print_tags(derived_tags: dict) -> str:
    string = "Tags:\n"
    for i, (key, tags) in enumerate(derived_tags.items(), start=1):
        string += u"{}\u2500 {:s}\n".format(u"\u2514" if i == len(derived_tags) else u"\u251C", key)
        for j, tag in enumerate(tags, start=1):
            string += "{}  {}\u2500 {:s}\n".format(" " if i == len(derived_tags) else u"\u2502",
                                                   "\u2514" if j == len(tags) else "\u251C",
                                                   tag)
    return string


def _get_acts(json, actors, activities):
    actor_uids = [actor.uid for actor in actors]
    activity_uids = [activity.uid for activity in activities]
    acts = []
    for act in json["acts"]:
        acts.append((actors[actor_uids.index(act["actor"])],
                     activities[activity_uids.index(act["activity"])]))
    return acts

""" Class ScenarioCategory

Creation date: 2018 10 30
Author(s): <NAME>

To do:
Add "comprises" method based on the "fall_into" method that is defined for Scenario.

Modifications:
2018 11 05: Make code PEP8 compliant.
2018 11 07: Change use of models.
2018 11 22: Enable instantiation using JSON code.
2018 11 29: Add functionality to return the derived Tags.
2018 12 06: Make it possible to return full JSON code (incl. attributes' JSON code).
2019 05 22: Make use of type_checking.py to shorten the initialization.
2019 10 11: Update of terminology.
2019 11 04: Add options to automatically assign unique ids to actor/activities.
2020 07 30: Update conversion of scenario category to a string.
2020 07 31: Add the includes method.
2020 08 15: Remove static_environment and add static_physical_things.
2020 08 16: Add dynamic_physical_thing_categories.
2020 08 23: Remove the update_uid options because uid is automatically generated by ScenarioElement.
2020 08 24: Enable instantiation of ScenarioCategory from json without needing full json code.
2020 08 25: Add comprises() function.
2020 10 04: Change way of creating object from JSON code.
2020 10 12: Remove Dynamic/StaticPhysicalThing and use PhysicalElement instead.
"""

from __future__ import annotations
from typing import List, Tuple, Union
import fnmatch
import numpy as np
from .activity import Activity
from .activity_category import ActivityCategory, _activity_category_from_json
from .actor import Actor
from .actor_category import ActorCategory, _actor_category_from_json
from .physical_element_category import PhysicalElementCategory, _physical_element_category_from_json
from .qualitative_element import QualitativeElement, _qualitative_element_props_from_json
from .scenario_element import DMObjects, _attributes_from_json, _object_from_json
from .type_checking import check_for_type, check_for_list, check_for_tuple


class ScenarioCategory(QualitativeElement):
    """ ScenarioCategory - A qualitative description

    Although a scenario is a quantitative description, there also exists a
    qualitative description of a scenario. We refer to the qualitative
    description of a scenario as a scenario category. The qualitative
    description can be regarded as an abstraction of the quantitative scenario.

    Scenario categories comprise scenarios. A scenario category may comprise
    multiple scenarios. On the other hand, multiple scenario categories may
    comprise the same scenario.

    A scenario category can include another scenario category.

    When instantiating the ScenarioCategory object, the name, description,
    image, unique id (uid), and tags are passed. To set the static physical
    things, activities, actors, and acts, use the corresponding methods, i.e.,
    set_physical_elements(), set_activities(), set_actors(), and set_acts(),
    respectively.

    Attributes:
        description (str): A description of the scenario class. The objective of
            the description is to make the scenario class human interpretable.
        image (str): Path to image that schematically shows the class.
        activities (List[ActivityCategory]): List of activities that
            are used for this ScenarioCategory.
        physical_elements (List[PhysicalElementCategory]): List of physical
            things that participate in the Scenario. Could be both static and
            dynamic.
        actors (List[ActorCategory]): List of actors that participate in the
            Scenario.
        acts (List[Tuple[ActorCategory, ActivityCategory]]): The acts describe
            which actors perform which activities.
        name (str): A name that serves as a short description of the scenario
            category.
        uid (int): A unique ID.
        tags (List[Tag]): A list of tags that formally defines the scenario
            category. These tags determine whether scenarios fall into this
            scenario category or not.
    """
    def __init__(self, image: str, description: str = "", **kwargs):
        # Check the types of the inputs
        check_for_type("image", image, str)

        # Assign the attributes
        QualitativeElement.__init__(self, description=description, **kwargs)
        self.image = image
        self.activities = []  # Type: List[ActivityCategory]
        self.physical_elements = []  # Type: List[PhysicalElementCategory]
        self.actors = []  # Type: List[ActorCategory]
        self.acts = []  # Type: List[Tuple[ActorCategory, ActivityCategory]]

        # Set attributes if provided by kwargs.
        if "physical_element_categories" in kwargs:
            self.set_physical_elements(kwargs["physical_element_categories"])
        if "actor_categories" in kwargs:
            self.set_actors(kwargs["actor_categories"])
        if "activity_categories" in kwargs:
            self.set_activities(kwargs["activity_categories"])
        if "acts" in kwargs:
            self.set_acts(kwargs["acts"])

        # Some parameters
        # Maximum number of characters that are used when printing the general description
        self.maxprintlength = 80

    def set_physical_elements(self, physical_elements: List[PhysicalElementCategory]) -> None:
        """ Set the physical elements

        Check whether the physical things are correctly defined.

        :param physical_elements: List of physical thing categories that define
            the static environment and part of the dynamic environment
            qualitatively.
        """
        # Check whether the static physical things are correctly defined.
        check_for_list("physical_elements", physical_elements, PhysicalElementCategory,
                       can_be_none=False)

        # Assign static physical thing categories to an attribute.
        self.physical_elements = physical_elements

    def set_activities(self, activity_categories: List[ActivityCategory]) -> None:
        """ Set the activities

        Check whether the activities are correctly defined. Activities should be
        a list with instantiations of ActivityCategory.

        :param activity_categories: List of activities that are used for this
            ScenarioCategory.
        """
        # Check whether the activities are correctly defined.
        check_for_list("activities", activity_categories, ActivityCategory, can_be_none=False)

        # Assign activity categories to an attribute.
        self.activities = activity_categories  # Type: List[ActivityCategory]

    def set_actors(self, actor_categories: List[ActorCategory]) -> None:
        """ Set the actors

        Check whether the actors are correctly defined. Actors should be a list
        with instantiations of ActorCategory.

        :param actor_categories: List of actors that participate in the
            Scenario.
        """
        # Check whether the actors are correctly defined.
        check_for_list("actors", actor_categories, ActorCategory, can_be_none=False)

        # Assign actor categories to an attribute.
        self.actors = actor_categories  # Type: List[ActorCategory]

    def set_acts(self, acts_scenario_category: List[Tuple[ActorCategory, ActivityCategory]],
                 verbose: bool = True) -> None:
        """ Set the acts

        Check whether the acts are correctly defined. Each act should be a tuple
        with an actor category and an activity category, i.e.,
        (ActorCategory, ActivityCategory). Acts is a list
        containing multiple tuples (ActorCategory, ActivityCategory).

        :param acts_scenario_category: The acts describe which actors perform
            which activities. The actors and activities that are used in acts
            should also be passed with the actors and activities arguments. If
            not, a warning will be shown and the corresponding actor/activity
            will be added to the list of actors/activities.
        :param verbose: Set to False if warning should be surpressed.
        """
        check_for_list("acts", acts_scenario_category, tuple)
        for act in acts_scenario_category:
            check_for_tuple("act", act, (ActorCategory, ActivityCategory))

        # Set the acts.
        self.acts = acts_scenario_category
        _check_acts(self.acts, self.actors, self.activities, verbose=verbose)

    def derived_tags(self) -> dict:
        """ Return all tags, including the tags of the attributes.

        The ScenarioCategory has tags, but also its attributes can have tags.
        More specifically, the each PhysicalElementCategory, ActorCategory, and
        ActivityCategory might have tags. A dictionary will be returned. Each
        item of the dictionary contains a list of tags corresponding to either
        the own object (i.e., ScenarioCategory), a PhysicalElementCategory, or
        an ActorCategory.

        The tags that might be associated with the ActivityCategory are returned
        with the ActorCategory if the corresponding ActorCategory is performing
        that ActivityCategory according to the defined acts.

        :return: List of tags.
        """
        # Instantiate the dictionary.
        tags = {}

        # Provide the tags of the very own object (ScenarioCategory).
        if self.tags:
            tags["{:s}::ScenarioCategory".format(self.name)] = self.tags

        # Provide the tags for each ActorCategory.
        tags = derive_actor_tags(self.actors, self.acts, tags=tags)

        # Provide the tags for each PhysicalElementCategory.
        for physical_element in self.physical_elements:
            if physical_element.tags:
                tags["{:s}::PhysicalElementCategory".format(physical_element.name)] = \
                    physical_element.tags

        # Return the tags.
        return tags

    def includes(self, scenario_category: ScenarioCategory) -> bool:
        """ Check if this scenario category includes the given scenario category

        It is checked whether the passed ScenarioCategory is included in this
        scenario category. To determine whether this is the case, the derived
        tags are used. The derived tags from this scenario category should be at
        least present (or subtags of the tags) in the provided ScenarioCategory.

        :param scenario_category: The potential ScenarioCategory that is
            included in this scenario category.
        :return: Whether or not the ScenarioCategory is included.
        """
        # Determine the derived tags of this and the other scenario category.
        own_tags = self.derived_tags()
        other_tags = scenario_category.derived_tags()

        # Check for tags directly related to the ScenarioCategory. These tags should be directly
        # present for the scenario.
        if not _check_tags(own_tags, other_tags, "ScenarioCategory", "ScenarioCategory"):
            return False

        # Check for the actors, dynamic physical things, and static physical things.
        if not _check_multiple_tags(own_tags, other_tags, "ActorCategory") or \
                not _check_multiple_tags(own_tags, other_tags, "PhysicalElementCategory"):
            return False
        return True

    def comprises(self, scenario) -> bool:
        """ Check if this scenario category comprises the given scenario

        It is checked whether the passed Scenario is comprised in this scenario
        category. To determine whether this is the case, the derived tags are
        used. The derived tags from this scenario category should be at
        least present (or subtags of the tags) in the provided Scenario.

        :param scenario: The potential ScenarioCategory that is
            included in this scenario category.
        :return: Whether or not the ScenarioCategory is included.
        """
        # Determine the derived tags of this and the other scenario category.
        own_tags = self.derived_tags()
        other_tags = scenario.derived_tags()

        # Check for tags directly related to the ScenarioCategory. These tags should be directly
        # present for the scenario.
        if not _check_tags(own_tags, other_tags, "ScenarioCategory", "Scenario"):
            return False

        # Check for the actors, dynamic physical things, and static physical things.
        if not _check_multiple_tags(own_tags, other_tags, "ActorCategory", "Actor") or \
                not _check_multiple_tags(own_tags, other_tags, "PhysicalElementCategory",
                                         "PhysicalElement"):
            return False
        return True

    def __str__(self) -> str:
        """ Method that will be called when printing the scenario category.

        :return: string to print.
        """

        # Show the name
        string = "Name: {:s}\n".format(self.name)

        # Show the description of the scenario class
        string += "Description:\n"
        words = self.description.split(' ')
        line = ""
        for word in words:
            if len(line) + len(word) <= self.maxprintlength:
                line += "  {:s}".format(word)
            else:
                string += "{:s}\n".format(line)
                line = "  {:s}".format(word)
        if line:
            string += "{:s}\n".format(line)

        # Show the tags
        string += _print_tags(self.derived_tags())
        return string

    def to_json(self) -> dict:
        scenario_category = QualitativeElement.to_json(self)
        scenario_category["image"] = self.image
        scenario_category["physical_element_categories"] = \
            [dict(name=element.name, uid=element.uid) for element in self.physical_elements]
        scenario_category["actor_categories"] = [dict(name=actor.name, uid=actor.uid)
                                                 for actor in self.actors]
        scenario_category["activity_categories"] = [dict(name=activity.name, uid=activity.uid)
                                                    for activity in self.activities]
        scenario_category["acts"] = []
        for actor, activity in self.acts:
            scenario_category["acts"].append({"actor": actor.uid,
                                              "activity": activity.uid})
        scenario_category["derived_tags"] = self.derived_tags()
        for key, tags in scenario_category["derived_tags"].items():
            scenario_category["derived_tags"][key] = [tag.to_json() for tag in tags]
        return scenario_category

    def to_json_full(self) -> dict:
        scenario_category = self.to_json()
        scenario_category["physical_element_categories"] = [element.to_json_full() for element in
                                                            self.physical_elements]
        scenario_category["actor_categories"] = [actor.to_json_full() for actor in
                                                 self.actors]
        scenario_category["activity_categories"] = [activity.to_json_full() for activity in
                                                    self.activities]
        return scenario_category


def _check_acts(acts: Union[List[Tuple[ActorCategory, ActivityCategory]],
                            List[Tuple[Actor, Activity]]],
                actors: Union[List[ActorCategory], List[Actor]],
                activities: Union[List[ActivityCategory], List[Activity]],
                verbose: bool = True):
    # Check whether the actors/activities defined with the acts are already listed. If not,
    # the corresponding actor/activity will be added and a warning will be shown.
    for thing, activity in acts:
        if thing not in actors:
            if verbose:
                print("Actor with name '{:s}' ".format(thing.name) +
                      "is used with acts but not defined in the list of actors.")
                print("Therefore, the actor is added to the list of actors.")
            actors.append(thing)
        if activity not in activities:
            if verbose:
                print("Activity with name '{:s}' is used with acts but".format(activity.name) +
                      " not defined in the list of activities.")
                print("Therefore, the activity is added to the list of activities.")
            activities.append(activity)


def _scenario_category_props_from_json(json: dict, attribute_objects: DMObjects, **kwargs) -> dict:
    props = _qualitative_element_props_from_json(json)
    props["image"] = json["image"]
    props.update(_attributes_from_json(
        json, attribute_objects,
        dict(physical_element_categories=(_physical_element_category_from_json,
                                          "physical_element_category"),
             actor_categories=(_actor_category_from_json, "actor_category"),
             activity_categories=(_activity_category_from_json, "activity_category")),
        **kwargs))
    props["acts"] = _get_acts(json, props["actor_categories"], props["activity_categories"])
    return props


def _scenario_category_from_json(json: dict, attribute_objects: DMObjects, **kwargs) \
        -> ScenarioCategory:
    return ScenarioCategory(**_scenario_category_props_from_json(json, attribute_objects, **kwargs))


def scenario_category_from_json(json: dict, attribute_objects: DMObjects = None, **kwargs) -> \
        ScenarioCategory:
    """ Get ScenarioCategory object from JSON code.

    It is assumed that all the attributes are fully defined. Hence, all
    StaticPhysicalThingCategories, DynamicPhysicalThingCategories,
    ActorCategory, and ActivityCategory need to be defined, instead of only a
    reference to their IDs.
    Alternatively, the static physical thing categories, dynamic physical thing
    categories, actor categories, and activity categories can be passed as
    arguments.
    Further optional arguments (provided by kwargs) are:
    - physical_elements (List[PhysicalElementCategory]): The physical elements
        for defining the static environment.
    - actors (List[ActorCategory]): The actor categories.
    - activities (List[ActivityCategory]): The activity categories.
    For all these arguments: If given, it will not be based on the JSON code.

    :param json: JSON code of ScenarioCategory.
    :param attribute_objects: A structure for storing all objects (optional).
    :return: ScenarioCategory object.
    """
    return _object_from_json(json, _scenario_category_from_json, "scenario_category",
                             attribute_objects, **kwargs)


def derive_actor_tags(actors: List, acts: List, tags: dict = None) -> dict:
    """ Derive the tags that are associated with the actors.

    The tags of an Actor(Category) will be added to the dictionary "tags". The
    key equals <name of actor>::<class>, where class is supposed to be either
    Actor or ActorCategory, whereas the value will be the list of tags that are
    associated with the actor.

    :param actors: The actors of  the Scenario(Category).
    :param acts: The acts of the Scenario(Category).
    :param tags: Initial tags that will be amended with tags of the actors.
    :return: Dictionary with each actor as a key and the corresponding values
        denote the tags.
    """
    # By default, tags is an empty dictionary
    if tags is None:
        tags = {}

    for actor in actors:
        actor_tags = actor.get_tags()
        for act in acts:
            if act[0] == actor:
                actor_tags += act[1].get_tags()
        if actor_tags:
            if isinstance(actor, ActorCategory):
                class_name = "ActorCategory"
            elif isinstance(actor, Actor):
                class_name = "Actor"
            else:
                raise TypeError("Actor is of type '{}' while it should be ".format(type(actor)) +
                                "of type ActorCategory, or Actor.")
            key = "{:s}::{:s}".format(actor.name, class_name)
            i = 1
            while key in tags:  # Make sure that a unique key is used.
                i += 1
                key = "{:s}{:d}::{:s}".format(actor.name, i, class_name)
            tags[key] = list(set(actor_tags))  # list(set()) makes sure that tags are unique.
    return tags


def _check_tags(tags: dict, subtags: dict, tags_class: str = "ScenarioCategory",
                subtags_class: str = "ScenarioCategory") -> bool:
    """ Check whether (sub)tags of <tags> are present in <subtags>.

    The tags are provided as dictionaries, where each item corresponds to the
    tags of one object that is part of the scenario (category). This function
    checks whether all tags in <tags> of the class <tags_class> are present in
    the tags in <subtags> of the class <subtags_class> (or subtags of the
    <tags>).

    :param tags: Dictionary of the derived tags of the ScenarioCategory.
    :param subtags: Dictionary of the derived tags of the Scenario.
    :param tags_class: Specify attribute to be used of the ScenarioCategory.
    :param subtags_class: Specify attribute to be used of the Scenario.
    :return: Whether the tags of the ScenarioCategory are found in the tags
        of the Scenario.
    """
    sc_keys = fnmatch.filter(tags, "*::{:s}".format(tags_class))
    if sc_keys:  # In this case, there are tags in <tags> related to <tags_class>.
        s_keys = fnmatch.filter(subtags, "*::{:s}".format(subtags_class))
        if s_keys:  # There are tags in <subtags> related to <subtags_class>.
            for tag in tags[sc_keys[0]]:
                if not any(map(tag.is_supertag_of, subtags[s_keys[0]])):
                    return False  # A tag of <tags> is not found in the <subtags>.
        else:  # There are no tags in <subtags> related to <subtags_class>.
            return False
    return True


def _check_multiple_tags(own_tags: dict, other_tags: dict, tags_class: str,
                         subtags_class: str = None) -> bool:
    """ Check if all tags in `own_tags` are present in `other_tags`.

    This is done for a specific attribute (e.g., actor_categories). For example,
    with actor categories, there is a list made, where each item is a list
    itself with the tags of the corresponding actor category. For each the
    actor category in <own_tags>, there needs to be a (different) actor category
    in <other_tags> that has the same tags (or more tags, or corresponding
    subtags).

    :param own_tags: The tags of the own scenario category.
    :param other_tags: The tags of the scenario category that is potentially
        'included' in the former.
    :param tags_class: Check for which attribute we need to check.
    :param subtags_class: If specified, this class is used for the `other_tags`.
    :return: True if all tags in `own_tags` are present in `other_tags`.
    """
    if subtags_class is None:
        subtags_class = tags_class
    own_objects = fnmatch.filter(own_tags, "*::{:s}".format(tags_class))
    other_objects = fnmatch.filter(other_tags, "*::{:s}".format(subtags_class))
    if len(own_objects) > len(other_objects):  # There must be equal or more objects in other SC.
        return False

    # Create a boolean matrix, where the (i,j)-th element is True if the i-th object of the other
    # objects might correspond to the j-th object of our own objects.
    match = np.zeros((len(other_objects), len(own_objects)), dtype=np.bool)
    for i, other_object in enumerate(other_objects):
        for j, own_object in enumerate(own_objects):
            match[i, j] = all(any(map(tag.is_supertag_of, other_tags[other_object]))
                              for tag in own_tags[own_object])

    # Check if all of our own objects can be matched with the actos in the other objects.
    return _check_match_matrix(match)


def _check_match_matrix(match: np.array) -> bool:
    # The matching of the actors need to be done. If a match is found, the corresponding
    # row and column will be removed from the match matrix.
    n_matches = 1  # Number of matches to look for.
    while match.size:
        # If there is at least one ActorCategory left that has no match, a False will be
        # returned.
        if not all(np.any(match, axis=1)):
            return False

        sum_match_actor = np.sum(match, axis=0)
        j = next((j for j in range(match.shape[1]) if sum_match_actor[j] == n_matches), -1)
        if j >= 0:  # We found an actor of our own scenario category with n matches.
            i = next(i for i in range(match.shape[0]) if match[i, j])
        else:
            sum_match_actor = np.sum(match, axis=1)
            i = next((i for i in range(match.shape[0]) if sum_match_actor[i] == n_matches), -1)
            if i >= 0:  # We found an actor of the ScenarioCategory with n matches
                j = next(j for j in range(match.shape[1]) if match[i, j])
            else:
                # Try again for higher n (number of matches)
                n_matches = n_matches + 1
                continue
        match = np.delete(np.delete(match, i, axis=0), j, axis=1)
        n_matches = 1
    return True


def _print_tags(derived_tags: dict) -> str:
    string = "Tags:\n"
    for i, (key, tags) in enumerate(derived_tags.items(), start=1):
        string += u"{}\u2500 {:s}\n".format(u"\u2514" if i == len(derived_tags) else u"\u251C", key)
        for j, tag in enumerate(tags, start=1):
            string += "{}  {}\u2500 {:s}\n".format(" " if i == len(derived_tags) else u"\u2502",
                                                   "\u2514" if j == len(tags) else "\u251C",
                                                   tag)
    return string


def _get_acts(json, actors, activities):
    actor_uids = [actor.uid for actor in actors]
    activity_uids = [activity.uid for activity in activities]
    acts = []
    for act in json["acts"]:
        acts.append((actors[actor_uids.index(act["actor"])],
                     activities[activity_uids.index(act["activity"])]))
    return acts

Class ScenarioCategory  Creation date: 2018 10 30 Author(s): <NAME>  To do: Add "comprises" method based on the "fall_into" method that is defined for Scenario.  Modifications: 2018 11 05: Make code PEP8 compliant. 2018 11 07: Change use of models. 2018 11 22: Enable instantiation using JSON code. 2018 11 29: Add functionality to return the derived Tags. 2018 12 06: Make it possible to return full JSON code (incl. attributes' JSON code). 2019 05 22: Make use of type_checking.py to shorten the initialization. 2019 10 11: Update of terminology. 2019 11 04: Add options to automatically assign unique ids to actor/activities. 2020 07 30: Update conversion of scenario category to a string. 2020 07 31: Add the includes method. 2020 08 15: Remove static_environment and add static_physical_things. 2020 08 16: Add dynamic_physical_thing_categories. 2020 08 23: Remove the update_uid options because uid is automatically generated by ScenarioElement. 2020 08 24: Enable instantiation of ScenarioCategory from json without needing full json code. 2020 08 25: Add comprises() function. 2020 10 04: Change way of creating object from JSON code. 2020 10 12: Remove Dynamic/StaticPhysicalThing and use PhysicalElement instead. ScenarioCategory - A qualitative description      Although a scenario is a quantitative description, there also exists a     qualitative description of a scenario. We refer to the qualitative     description of a scenario as a scenario category. The qualitative     description can be regarded as an abstraction of the quantitative scenario.      Scenario categories comprise scenarios. A scenario category may comprise     multiple scenarios. On the other hand, multiple scenario categories may     comprise the same scenario.      A scenario category can include another scenario category.      When instantiating the ScenarioCategory object, the name, description,     image, unique id (uid), and tags are passed. To set the static physical     things, activities, actors, and acts, use the corresponding methods, i.e.,     set_physical_elements(), set_activities(), set_actors(), and set_acts(),     respectively.      Attributes:         description (str): A description of the scenario class. The objective of             the description is to make the scenario class human interpretable.         image (str): Path to image that schematically shows the class.         activities (List[ActivityCategory]): List of activities that             are used for this ScenarioCategory.         physical_elements (List[PhysicalElementCategory]): List of physical             things that participate in the Scenario. Could be both static and             dynamic.         actors (List[ActorCategory]): List of actors that participate in the             Scenario.         acts (List[Tuple[ActorCategory, ActivityCategory]]): The acts describe             which actors perform which activities.         name (str): A name that serves as a short description of the scenario             category.         uid (int): A unique ID.         tags (List[Tag]): A list of tags that formally defines the scenario             category. These tags determine whether scenarios fall into this             scenario category or not. # Check the types of the inputs # Assign the attributes # Type: List[ActivityCategory] # Type: List[PhysicalElementCategory] # Type: List[ActorCategory] # Type: List[Tuple[ActorCategory, ActivityCategory]] # Set attributes if provided by kwargs. # Some parameters # Maximum number of characters that are used when printing the general description Set the physical elements          Check whether the physical things are correctly defined.          :param physical_elements: List of physical thing categories that define             the static environment and part of the dynamic environment             qualitatively. # Check whether the static physical things are correctly defined. # Assign static physical thing categories to an attribute. Set the activities          Check whether the activities are correctly defined. Activities should be         a list with instantiations of ActivityCategory.          :param activity_categories: List of activities that are used for this             ScenarioCategory. # Check whether the activities are correctly defined. # Assign activity categories to an attribute. # Type: List[ActivityCategory] Set the actors          Check whether the actors are correctly defined. Actors should be a list         with instantiations of ActorCategory.          :param actor_categories: List of actors that participate in the             Scenario. # Check whether the actors are correctly defined. # Assign actor categories to an attribute. # Type: List[ActorCategory] Set the acts          Check whether the acts are correctly defined. Each act should be a tuple         with an actor category and an activity category, i.e.,         (ActorCategory, ActivityCategory). Acts is a list         containing multiple tuples (ActorCategory, ActivityCategory).          :param acts_scenario_category: The acts describe which actors perform             which activities. The actors and activities that are used in acts             should also be passed with the actors and activities arguments. If             not, a warning will be shown and the corresponding actor/activity             will be added to the list of actors/activities.         :param verbose: Set to False if warning should be surpressed. # Set the acts. Return all tags, including the tags of the attributes.          The ScenarioCategory has tags, but also its attributes can have tags.         More specifically, the each PhysicalElementCategory, ActorCategory, and         ActivityCategory might have tags. A dictionary will be returned. Each         item of the dictionary contains a list of tags corresponding to either         the own object (i.e., ScenarioCategory), a PhysicalElementCategory, or         an ActorCategory.          The tags that might be associated with the ActivityCategory are returned         with the ActorCategory if the corresponding ActorCategory is performing         that ActivityCategory according to the defined acts.          :return: List of tags. # Instantiate the dictionary. # Provide the tags of the very own object (ScenarioCategory). # Provide the tags for each ActorCategory. # Provide the tags for each PhysicalElementCategory. # Return the tags. Check if this scenario category includes the given scenario category          It is checked whether the passed ScenarioCategory is included in this         scenario category. To determine whether this is the case, the derived         tags are used. The derived tags from this scenario category should be at         least present (or subtags of the tags) in the provided ScenarioCategory.          :param scenario_category: The potential ScenarioCategory that is             included in this scenario category.         :return: Whether or not the ScenarioCategory is included. # Determine the derived tags of this and the other scenario category. # Check for tags directly related to the ScenarioCategory. These tags should be directly # present for the scenario. # Check for the actors, dynamic physical things, and static physical things. Check if this scenario category comprises the given scenario          It is checked whether the passed Scenario is comprised in this scenario         category. To determine whether this is the case, the derived tags are         used. The derived tags from this scenario category should be at         least present (or subtags of the tags) in the provided Scenario.          :param scenario: The potential ScenarioCategory that is             included in this scenario category.         :return: Whether or not the ScenarioCategory is included. # Determine the derived tags of this and the other scenario category. # Check for tags directly related to the ScenarioCategory. These tags should be directly # present for the scenario. # Check for the actors, dynamic physical things, and static physical things. Method that will be called when printing the scenario category.          :return: string to print. # Show the name # Show the description of the scenario class # Show the tags # Check whether the actors/activities defined with the acts are already listed. If not, # the corresponding actor/activity will be added and a warning will be shown. Get ScenarioCategory object from JSON code.      It is assumed that all the attributes are fully defined. Hence, all     StaticPhysicalThingCategories, DynamicPhysicalThingCategories,     ActorCategory, and ActivityCategory need to be defined, instead of only a     reference to their IDs.     Alternatively, the static physical thing categories, dynamic physical thing     categories, actor categories, and activity categories can be passed as     arguments.     Further optional arguments (provided by kwargs) are:     - physical_elements (List[PhysicalElementCategory]): The physical elements         for defining the static environment.     - actors (List[ActorCategory]): The actor categories.     - activities (List[ActivityCategory]): The activity categories.     For all these arguments: If given, it will not be based on the JSON code.      :param json: JSON code of ScenarioCategory.     :param attribute_objects: A structure for storing all objects (optional).     :return: ScenarioCategory object. Derive the tags that are associated with the actors.      The tags of an Actor(Category) will be added to the dictionary "tags". The     key equals <name of actor>::<class>, where class is supposed to be either     Actor or ActorCategory, whereas the value will be the list of tags that are     associated with the actor.      :param actors: The actors of  the Scenario(Category).     :param acts: The acts of the Scenario(Category).     :param tags: Initial tags that will be amended with tags of the actors.     :return: Dictionary with each actor as a key and the corresponding values         denote the tags. # By default, tags is an empty dictionary # Make sure that a unique key is used. # list(set()) makes sure that tags are unique. Check whether (sub)tags of <tags> are present in <subtags>.      The tags are provided as dictionaries, where each item corresponds to the     tags of one object that is part of the scenario (category). This function     checks whether all tags in <tags> of the class <tags_class> are present in     the tags in <subtags> of the class <subtags_class> (or subtags of the     <tags>).      :param tags: Dictionary of the derived tags of the ScenarioCategory.     :param subtags: Dictionary of the derived tags of the Scenario.     :param tags_class: Specify attribute to be used of the ScenarioCategory.     :param subtags_class: Specify attribute to be used of the Scenario.     :return: Whether the tags of the ScenarioCategory are found in the tags         of the Scenario. # In this case, there are tags in <tags> related to <tags_class>. # There are tags in <subtags> related to <subtags_class>. # A tag of <tags> is not found in the <subtags>. # There are no tags in <subtags> related to <subtags_class>. Check if all tags in `own_tags` are present in `other_tags`.      This is done for a specific attribute (e.g., actor_categories). For example,     with actor categories, there is a list made, where each item is a list     itself with the tags of the corresponding actor category. For each the     actor category in <own_tags>, there needs to be a (different) actor category     in <other_tags> that has the same tags (or more tags, or corresponding     subtags).      :param own_tags: The tags of the own scenario category.     :param other_tags: The tags of the scenario category that is potentially         'included' in the former.     :param tags_class: Check for which attribute we need to check.     :param subtags_class: If specified, this class is used for the `other_tags`.     :return: True if all tags in `own_tags` are present in `other_tags`. # There must be equal or more objects in other SC. # Create a boolean matrix, where the (i,j)-th element is True if the i-th object of the other # objects might correspond to the j-th object of our own objects. # Check if all of our own objects can be matched with the actos in the other objects. # The matching of the actors need to be done. If a match is found, the corresponding # row and column will be removed from the match matrix. # Number of matches to look for. # If there is at least one ActorCategory left that has no match, a False will be # returned. # We found an actor of our own scenario category with n matches. # We found an actor of the ScenarioCategory with n matches # Try again for higher n (number of matches)