def init_variable_values(self, entity, instance_object, instance_id):
        for variable_name, variable_values in instance_object.items():
            path_in_json = [entity.plural, instance_id, variable_name]
            try:
                entity.check_variable_defined_for_entity(variable_name)
            except ValueError as e:  # The variable is defined for another entity
                raise SituationParsingError(path_in_json, e.args[0])
            except VariableNotFound as e:  # The variable doesn't exist
                raise SituationParsingError(path_in_json, str(e), code = 404)

            instance_index = self.get_ids(entity.plural).index(instance_id)

            if not isinstance(variable_values, dict):
                if self.default_period is None:
                    raise SituationParsingError(path_in_json,
                        "Can't deal with type: expected object. Input variables should be set for specific periods. For instance: {'salary': {'2017-01': 2000, '2017-02': 2500}}, or {'birth_date': {'ETERNITY': '1980-01-01'}}.")
                variable_values = {self.default_period: variable_values}

            for period_str, value in variable_values.items():
                try:
                    period(period_str)
                except ValueError as e:
                    raise SituationParsingError(path_in_json, e.args[0])
                variable = entity.get_variable(variable_name)
                self.add_variable_value(entity, variable, instance_index, instance_id, period_str, value)
Ejemplo n.º 2
0
    def init_variable_values(self,
                             entity,
                             entity_object,
                             entity_id,
                             default_period=None):
        for variable_name, variable_values in entity_object.items():
            path_in_json = [entity.plural, entity_id, variable_name]
            try:
                entity.check_variable_defined_for_entity(variable_name)
            except ValueError as e:  # The variable is defined for another entity
                raise SituationParsingError(path_in_json, e.args[0])
            except VariableNotFound as e:  # The variable doesn't exist
                raise SituationParsingError(path_in_json, e.message, code=404)

            if not isinstance(variable_values, dict):

                if default_period is None:
                    raise SituationParsingError(
                        path_in_json,
                        "Can't deal with type: expected object. Input variables should be set for specific periods. For instance: {'salary': {'2017-01': 2000, '2017-02': 2500}}, or {'birth_date': {'ETERNITY': '1980-01-01'}}."
                    )
                variable_values = {default_period: variable_values}

            for period, value in variable_values.items():
                self.init_variable_value(entity, entity_id, variable_name,
                                         period, value)
Ejemplo n.º 3
0
    def build_from_entities(self,
                            tax_benefit_system,
                            input_dict,
                            default_period=None,
                            **kwargs):
        """
            Build a simulation from a Python dict ``input_dict`` fully specifying entities.

            Examples:

            >>> simulation_builder.build_from_entities({
                'persons': {'Javier': { 'salary': {'2018-11': 2000}}},
                'households': {'household': {'parents': ['Javier']}}
                })
        """
        simulation = kwargs.pop(
            'simulation', None
        )  # Only for backward compatibility with previous Simulation constructor
        if simulation is None:
            simulation = Simulation(tax_benefit_system, **kwargs)

        check_type(input_dict, dict, ['error'])
        unexpected_entities = [
            entity for entity in input_dict
            if entity not in tax_benefit_system.entities_plural()
        ]
        if unexpected_entities:
            unexpected_entity = unexpected_entities[0]
            raise SituationParsingError([unexpected_entity], ''.join([
                "Some entities in the situation are not defined in the loaded tax and benefit system.",
                "These entities are not found: {0}.",
                "The defined entities are: {1}."
            ]).format(', '.join(unexpected_entities),
                      ', '.join(tax_benefit_system.entities_plural())))
        persons_json = input_dict.get(tax_benefit_system.person_entity.plural,
                                      None)

        if not persons_json:
            raise SituationParsingError([
                tax_benefit_system.person_entity.plural
            ], 'No {0} found. At least one {0} must be defined to run a simulation.'
                                        .format(tax_benefit_system.
                                                person_entity.key))

        self.hydrate_entity(simulation.persons,
                            persons_json,
                            default_period=default_period)

        for entity_class in tax_benefit_system.group_entities:
            entities_json = input_dict.get(entity_class.plural)
            self.hydrate_entity(simulation.entities[entity_class.key],
                                entities_json,
                                default_period=default_period)

        return simulation
Ejemplo n.º 4
0
    def init_variable_value(self, entity, entity_id, variable_name, period_str,
                            value):
        path_in_json = [entity.plural, entity_id, variable_name, period_str]
        entity_index = entity.ids.index(entity_id)
        holder = entity.get_holder(variable_name)
        try:
            period = make_period(period_str)
        except ValueError as e:
            raise SituationParsingError(path_in_json, e.args[0])
        if value is None:
            return
        array = holder.buffer.get(period)
        if array is None:
            array = holder.default_array()
        if holder.variable.value_type == Enum and isinstance(
                value, basestring_type):
            try:
                value = holder.variable.possible_values[value].index
            except KeyError:
                possible_values = [
                    item.name for item in holder.variable.possible_values
                ]
                raise SituationParsingError(
                    path_in_json,
                    "'{}' is not a known value for '{}'. Possible values are ['{}']."
                    .format(value, variable_name,
                            "', '".join(possible_values)))
        if holder.variable.value_type in (float, int) and isinstance(
                value, basestring_type):
            value = eval_expression(value)
        try:
            array[entity_index] = value
        except (OverflowError):
            error_message = "Can't deal with value: '{}', it's too large for type '{}'.".format(
                value, holder.variable.json_type)
            raise SituationParsingError(path_in_json, error_message)
        except (ValueError, TypeError):
            if holder.variable.value_type == date:
                error_message = "Can't deal with date: '{}'.".format(value)
            else:
                error_message = "Can't deal with value: expected type {}, received '{}'.".format(
                    holder.variable.json_type, value)
            raise SituationParsingError(path_in_json, error_message)

        if not variable_name in self.input:
            self.input[variable_name] = {}

        self.input[variable_name][period_str] = array

        holder.buffer[period] = array
 def check_persons_to_allocate(self, persons_plural, entity_plural,
                               persons_ids,
                               person_id, entity_id, role_id,
                               persons_to_allocate, index):
     check_type(person_id, str, [entity_plural, entity_id, role_id, str(index)])
     if person_id not in persons_ids:
         raise SituationParsingError([entity_plural, entity_id, role_id],
             "Unexpected value: {0}. {0} has been declared in {1} {2}, but has not been declared in {3}.".format(
                 person_id, entity_id, role_id, persons_plural)
             )
     if person_id not in persons_to_allocate:
         raise SituationParsingError([entity_plural, entity_id, role_id],
             "{} has been declared more than once in {}".format(
                 person_id, entity_plural)
             )
Ejemplo n.º 6
0
    def finalize_variables_init(self, entity, entities_json):
        for variable_name, holder in entity._holders.items():
            periods = holder.buffer.keys()
            # We need to handle small periods first for set_input to work
            sorted_periods = sorted(periods, key=key_period_size)
            for period in sorted_periods:
                array = holder.buffer[period]
                try:
                    holder.set_input(period, array)
                except PeriodMismatchError as e:
                    # This errors happens when we try to set a variable value for a period that doesn't match its definition period
                    # It is only raised when we consume the buffer. We thus don't know which exact key caused the error.
                    # We do a basic research to find the culprit path
                    culprit_path = next(
                        dpath.search(entities_json,
                                     "*/{}/{}".format(e.variable_name,
                                                      str(e.period)),
                                     yielded=True), None)
                    if culprit_path:
                        path = [entity.plural] + culprit_path[0].split('/')
                    else:
                        path = [
                            entity.plural
                        ]  # Fallback: if we can't find the culprit, just set the error at the entities level

                    raise SituationParsingError(path, e.message)
Ejemplo n.º 7
0
    def build_from_variables(self, tax_benefit_system, input_dict):
        """
            Build a simulation from a Python dict ``input_dict`` describing variables values without expliciting entities.

            This method uses :any:`build_default_simulation` to infer an entity structure

            Example:

            >>> simulation_builder.build_from_variables(
                {'salary': {'2016-10': 12000}}
                )
        """
        count = _get_person_count(input_dict)
        simulation = self.build_default_simulation(tax_benefit_system, count)
        for variable, value in input_dict.items():
            if not isinstance(value, dict):
                if self.default_period is None:
                    raise SituationParsingError([
                        variable
                    ], "Can't deal with type: expected object. Input variables should be set for specific periods. For instance: {'salary': {'2017-01': 2000, '2017-02': 2500}}, or {'birth_date': {'ETERNITY': '1980-01-01'}}."
                                                )
                simulation.set_input(variable, self.default_period, value)
            else:
                for period_str, dated_value in value.items():
                    simulation.set_input(variable, period_str, dated_value)
        return simulation
Ejemplo n.º 8
0
def check_type(input, input_type, path=[]):
    json_type_map = {
        dict: "Object",
        list: "Array",
        basestring_type: "String",
    }
    if not isinstance(input, input_type):
        raise SituationParsingError(
            path, "Invalid type: must be of type '{}'.".format(
                json_type_map[input_type]))
    def raise_period_mismatch(self, entity, json, e):
        # This error happens when we try to set a variable value for a period that doesn't match its definition period
        # It is only raised when we consume the buffer. We thus don't know which exact key caused the error.
        # We do a basic research to find the culprit path
        culprit_path = next(
            dpath.search(json, "*/{}/{}".format(e.variable_name, str(e.period)), yielded = True),
            None)
        if culprit_path:
            path = [entity.plural] + culprit_path[0].split('/')
        else:
            path = [entity.plural]  # Fallback: if we can't find the culprit, just set the error at the entities level

        raise SituationParsingError(path, e.message)
Ejemplo n.º 10
0
    def add_variable_value(self, entity, variable, instance_index, instance_id, period_str, value):
        path_in_json = [entity.plural, instance_id, variable.name, period_str]

        if value is None:
            return

        array = self.get_input(variable.name, str(period_str))

        if array is None:
            array_size = self.get_count(entity.plural)
            array = variable.default_array(array_size)

        try:
            value = variable.check_set_value(value)
        except ValueError as error:
            raise SituationParsingError(path_in_json, *error.args)

        array[instance_index] = value

        self.input_buffer[variable.name][str(period(period_str))] = array
Ejemplo n.º 11
0
    def build_from_entities(self, tax_benefit_system, input_dict):
        """
            Build a simulation from a Python dict ``input_dict`` fully specifying entities.

            Examples:

            >>> simulation_builder.build_from_entities({
                'persons': {'Javier': { 'salary': {'2018-11': 2000}}},
                'households': {'household': {'parents': ['Javier']}}
                })
        """
        input_dict = deepcopy(input_dict)

        simulation = Simulation(tax_benefit_system, tax_benefit_system.instantiate_entities())

        # Register variables so get_variable_entity can find them
        for (variable_name, _variable) in tax_benefit_system.variables.items():
            self.register_variable(variable_name, simulation.get_variable_population(variable_name).entity)

        check_type(input_dict, dict, ['error'])
        axes = input_dict.pop('axes', None)

        unexpected_entities = [entity for entity in input_dict if entity not in tax_benefit_system.entities_plural()]
        if unexpected_entities:
            unexpected_entity = unexpected_entities[0]
            raise SituationParsingError([unexpected_entity],
                ''.join([
                    "Some entities in the situation are not defined in the loaded tax and benefit system.",
                    "These entities are not found: {0}.",
                    "The defined entities are: {1}."]
                    )
                .format(
                ', '.join(unexpected_entities),
                ', '.join(tax_benefit_system.entities_plural())
                    )
                )
        persons_json = input_dict.get(tax_benefit_system.person_entity.plural, None)

        if not persons_json:
            raise SituationParsingError([tax_benefit_system.person_entity.plural],
                'No {0} found. At least one {0} must be defined to run a simulation.'.format(tax_benefit_system.person_entity.key))

        persons_ids = self.add_person_entity(simulation.persons.entity, persons_json)

        for entity_class in tax_benefit_system.group_entities:
            instances_json = input_dict.get(entity_class.plural)
            if instances_json is not None:
                self.add_group_entity(self.persons_plural, persons_ids, entity_class, instances_json)
            else:
                self.add_default_group_entity(persons_ids, entity_class)

        if axes:
            self.axes = axes
            self.expand_axes()

        try:
            self.finalize_variables_init(simulation.persons)
        except PeriodMismatchError as e:
            self.raise_period_mismatch(simulation.persons.entity, persons_json, e)

        for entity_class in tax_benefit_system.group_entities:
            try:
                population = simulation.populations[entity_class.key]
                self.finalize_variables_init(population)
            except PeriodMismatchError as e:
                self.raise_period_mismatch(population.entity, instances_json, e)

        return simulation
Ejemplo n.º 12
0
    def add_group_entity(self, persons_plural, persons_ids, entity, instances_json):
        """
            Add all instances of one of the model's entities as described in ``instances_json``.
        """
        check_type(instances_json, dict, [entity.plural])
        entity_ids = list(map(str, instances_json.keys()))

        self.entity_ids[entity.plural] = entity_ids
        self.entity_counts[entity.plural] = len(entity_ids)

        persons_count = len(persons_ids)
        persons_to_allocate = set(persons_ids)
        self.memberships[entity.plural] = np.empty(persons_count, dtype = np.int32)
        self.roles[entity.plural] = np.empty(persons_count, dtype = object)

        self.entity_ids[entity.plural] = entity_ids
        self.entity_counts[entity.plural] = len(entity_ids)

        for instance_id, instance_object in instances_json.items():
            check_type(instance_object, dict, [entity.plural, instance_id])

            variables_json = instance_object.copy()  # Don't mutate function input

            roles_json = {
                role.plural or role.key: transform_to_strict_syntax(variables_json.pop(role.plural or role.key, []))
                for role in entity.roles
                }

            for role_id, role_definition in roles_json.items():
                check_type(role_definition, list, [entity.plural, instance_id, role_id])
                for index, person_id in enumerate(role_definition):
                    entity_plural = entity.plural
                    self.check_persons_to_allocate(persons_plural, entity_plural,
                                                   persons_ids,
                                                   person_id, instance_id, role_id,
                                                   persons_to_allocate, index)

                    persons_to_allocate.discard(person_id)

            entity_index = entity_ids.index(instance_id)
            role_by_plural = {role.plural or role.key: role for role in entity.roles}

            for role_plural, persons_with_role in roles_json.items():
                role = role_by_plural[role_plural]

                if role.max is not None and len(persons_with_role) > role.max:
                    raise SituationParsingError([entity.plural, instance_id, role_plural], f"There can be at most {role.max} {role_plural} in a {entity.key}. {len(persons_with_role)} were declared in '{instance_id}'.")

                for index_within_role, person_id in enumerate(persons_with_role):
                    person_index = persons_ids.index(person_id)
                    self.memberships[entity.plural][person_index] = entity_index
                    person_role = role.subroles[index_within_role] if role.subroles else role
                    self.roles[entity.plural][person_index] = person_role

            self.init_variable_values(entity, variables_json, instance_id)

        if persons_to_allocate:
            entity_ids = entity_ids + list(persons_to_allocate)
            for person_id in persons_to_allocate:
                person_index = persons_ids.index(person_id)
                self.memberships[entity.plural][person_index] = entity_ids.index(person_id)
                self.roles[entity.plural][person_index] = entity.flattened_roles[0]
            # Adjust previously computed ids and counts
            self.entity_ids[entity.plural] = entity_ids
            self.entity_counts[entity.plural] = len(entity_ids)

        # Convert back to Python array
        self.roles[entity.plural] = self.roles[entity.plural].tolist()
        self.memberships[entity.plural] = self.memberships[entity.plural].tolist()
Ejemplo n.º 13
0
    def hydrate_entity(self, entity, entities_json, default_period=None):
        """
            Hydrate an entity from a JSON dictionnary ``entities_json``.
        """
        check_type(entities_json, dict, [entity.plural])
        entities_json = OrderedDict(
            (str(key), value) for key, value in entities_json.items()
        )  # Stringify potential numeric keys, but keep the order
        entity.count = len(entities_json)
        entity.step_size = entity.count  # Related to axes.
        entity.ids = list(entities_json.keys())

        if not entity.is_person:
            persons = entity.simulation.persons
            entity.members_entity_id = np.empty(persons.count, dtype=np.int32)
            entity.members_role = np.empty(persons.count, dtype=object)
            entity.members_legacy_role = np.empty(persons.count,
                                                  dtype=np.int32)
            persons_to_allocate = set(persons.ids)

        for entity_id, entity_object in entities_json.items():
            check_type(entity_object, dict, [entity.plural, entity_id])
            if not entity.is_person:

                variables_json = entity_object.copy(
                )  # Don't mutate function input

                roles_json = {
                    role.plural or role.key: clean_person_list(
                        variables_json.pop(role.plural or role.key, []))
                    for role in entity.roles
                }

                persons = entity.simulation.persons

                for role_id, role_definition in roles_json.items():
                    check_type(role_definition, list,
                               [entity.plural, entity_id, role_id])
                    for index, person_id in enumerate(role_definition):
                        entity_plural = entity.plural
                        persons_plural = persons.plural
                        persons_ids = persons.ids
                        self.check_persons_to_allocate(persons_plural,
                                                       entity_plural,
                                                       persons_ids, person_id,
                                                       entity_id, role_id,
                                                       persons_to_allocate,
                                                       index)

                        persons_to_allocate.discard(person_id)

                entity_index = entity.ids.index(entity_id)
                for person_role, person_legacy_role, person_id in iter_over_entity_members(
                        entity, roles_json):
                    person_index = persons.ids.index(person_id)
                    entity.members_entity_id[person_index] = entity_index
                    entity.members_role[person_index] = person_role
                    entity.members_legacy_role[
                        person_index] = person_legacy_role

            else:
                variables_json = entity_object
            self.init_variable_values(entity,
                                      variables_json,
                                      entity_id,
                                      default_period=default_period)

        if not entity.is_person and persons_to_allocate:
            raise SituationParsingError([
                entity.plural
            ], '{0} have been declared in {1}, but are not members of any {2}. All {1} must be allocated to a {2}.'
                                        .format(
                                            persons_to_allocate,
                                            entity.simulation.persons.plural,
                                            entity.key))

        # Due to set_input mechanism, we must bufferize all inputs, then actually set them, so that the months are set first and the years last.
        self.finalize_variables_init(entity, entities_json)