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)
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)
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
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) )
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)
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
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)
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
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
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()
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)