PKFENZ Z openfisca_core/tools.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import numpy as np __all__ = [ 'assert_near', 'empty_clone', 'stringify_array', ] class Dummy(object): """A class that does nothing Used by function ``empty_clone`` to create an empty instance from an existing object. """ pass def assert_near(value, target_value, absolute_error_margin = 0, message = '', relative_error_margin = None): assert absolute_error_margin is not None or relative_error_margin is not None if isinstance(value, (list, tuple)): value = np.array(value) if isinstance(target_value, (list, tuple)): target_value = np.array(target_value) if isinstance(message, unicode): message = message.encode('utf-8') if isinstance(value, np.ndarray): if absolute_error_margin is not None: assert (abs(target_value - value) <= absolute_error_margin).all(), \ '{}{} differs from {} with an absolute margin {} > {}'.format(message, value, target_value, abs(target_value - value), absolute_error_margin) if relative_error_margin is not None: assert (abs(target_value - value) <= abs(relative_error_margin * target_value)).all(), \ '{}{} differs from {} with a relative margin {} > {}'.format(message, value, target_value, abs(target_value - value), abs(relative_error_margin * target_value)) else: if absolute_error_margin is not None: assert abs(target_value - value) <= absolute_error_margin, \ '{}{} differs from {} with an absolute margin {} > {}'.format(message, value, target_value, abs(target_value - value), absolute_error_margin) if relative_error_margin is not None: assert abs(target_value - value) <= abs(relative_error_margin * target_value), \ '{}{} differs from {} with a relative margin {} > {}'.format(message, value, target_value, abs(target_value - value), abs(relative_error_margin * target_value)) def empty_clone(original): """Create a new empty instance of the same class of the original object.""" new = Dummy() new.__class__ = original.__class__ return new def stringify_array(array): """Generate a clean string representation of a NumPY array. This function exists, because str(array) sucks for logs, etc. """ return u'[{}]'.format(u', '.join( unicode(cell) for cell in array )) if array is not None else u'None' PKFtEnoo#openfisca_core/decompositionsxml.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Handle decompositions in XML format (and convert then to JSON).""" import collections from . import conv N_ = lambda message: message def transform_node_xml_json_to_json(node_xml_json, root = True): comments = [] node_json = collections.OrderedDict() if root: node_json['@context'] = u'http://openfisca.fr/contexts/decomposition.jsonld' node_json['@type'] = 'Node' children_json = [] for key, value in node_xml_json.iteritems(): if key == 'color': node_json['color'] = [ int(color) for color in value.split(u',') ] elif key == 'desc': node_json['name'] = value elif key == 'shortname': node_json['short_name'] = value elif key == 'NODE': for child_xml_json in value: children_json.append(transform_node_xml_json_to_json(child_xml_json, root = False)) elif key in ('tail', 'text'): comments.append(value) elif key == 'typevar': node_json['type'] = value else: node_json[key] = value if children_json: node_json['children'] = children_json if comments: node_json['comment'] = u'\n\n'.join(comments) return node_json def translate_xml_element_to_json_item(xml_element): json_element = collections.OrderedDict() text = xml_element.text if text is not None: text = text.strip().strip('#').strip() or None if text is not None: json_element['text'] = text json_element.update(xml_element.attrib) for xml_child in xml_element: json_child_key, json_child = translate_xml_element_to_json_item(xml_child) json_element.setdefault(json_child_key, []).append(json_child) tail = xml_element.tail if tail is not None: tail = tail.strip().strip('#').strip() or None if tail is not None: json_element['tail'] = tail return xml_element.tag, json_element def make_validate_node_xml_json(tax_benefit_system): def validate_node_xml_json(node, state = None): validated_node, errors = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( code = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.not_none, ), color = conv.pipe( conv.test_isinstance(basestring), conv.function(lambda colors: colors.split(u',')), conv.uniform_sequence( conv.pipe( conv.input_to_int, conv.test_between(0, 255), conv.not_none, ), ), conv.test(lambda colors: len(colors) == 3, error = N_(u'Wrong number of colors in triplet.')), conv.function(lambda colors: u','.join(unicode(color) for color in colors)), ), desc = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.not_none, ), NODE = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_node_xml_json, drop_none_items = True, ), conv.empty_to_none, ), shortname = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.not_none, ), tail = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), text = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), typevar = conv.pipe( conv.test_isinstance(basestring), conv.input_to_int, conv.test_equals(2), ), ), constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, ), )(node, state = state or conv.default_state) if errors is not None: return validated_node, errors if not validated_node.get('NODE'): validated_node, errors = conv.struct( dict( code = conv.test_in(tax_benefit_system.column_by_name), ), default = conv.noop, )(validated_node, state = state) return validated_node, errors return validate_node_xml_json def xml_decomposition_to_json(xml_element, state = None): if xml_element is None: return None, None json_key, json_element = translate_xml_element_to_json_item(xml_element) if json_key != 'NODE': if state is None: state = conv.default_state return json_element, state._(u'Invalid root element in XML: "{}" instead of "NODE"').format(xml_element.tag) return json_element, None PKF0,openfisca_core/rates.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from __future__ import division def average_rate(target = None, varying = None): # target: numerator, varying: denominator return 1 - target / (varying * (varying != 0) + (varying == 0)) def marginal_rate(target = None, varying = None): # target: numerator, varying: denominator return 1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:]) PK 'GzAzAopenfisca_core/holders.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from __future__ import division import numpy as np from . import periods from .tools import empty_clone class DatedHolder(object): """A view of an holder, for a given period""" holder = None period = None def __init__(self, holder, period): self.holder = holder self.period = period @property def array(self): return self.holder.get_array(self.period) @array.deleter def array(self): self.holder.delete_array(self.period) @array.setter def array(self, array): self.holder.set_array(self.period, array) @property def column(self): return self.holder.column @property def entity(self): return self.holder.entity def to_value_json(self, use_label = False): transform_dated_value_to_json = self.holder.column.transform_dated_value_to_json return [ transform_dated_value_to_json(cell, use_label = use_label) for cell in self.array.tolist() ] class Holder(object): _array = None # Only used when column.is_permanent _array_by_period = None # Only used when not column.is_permanent column = None entity = None formula = None formula_output_period_by_requested_period = None def __init__(self, column = None, entity = None): assert column is not None assert self.column is None self.column = column assert entity is not None self.entity = entity @property def array(self): if not self.column.is_permanent: return self.get_array(self.entity.simulation.period) return self._array @array.setter def array(self, array): simulation = self.entity.simulation if not self.column.is_permanent: return self.set_array(simulation.period, array) if simulation.debug or simulation.trace: variable_infos = (self.column.name, None) step = simulation.traceback.get(variable_infos) if step is None: simulation.traceback[variable_infos] = dict( holder = self, ) self._array = array def at_period(self, period): return self if self.column.is_permanent else DatedHolder(self, period) def calculate(self, period = None, accept_other_period = False, requested_formulas_by_period = None): dated_holder = self.compute(period = period, accept_other_period = accept_other_period, requested_formulas_by_period = requested_formulas_by_period) return dated_holder.array def calculate_output(self, period): return self.formula.calculate_output(period) def clone(self, entity): """Copy the holder just enough to be able to run a new simulation without modifying the original simulation.""" new = empty_clone(self) new_dict = new.__dict__ for key, value in self.__dict__.iteritems(): if key in ('_array_by_period',): if value is not None: # There is no need to copy the arrays, because the formulas don't modify them. new_dict[key] = value.copy() elif key not in ('entity', 'formula'): new_dict[key] = value new_dict['entity'] = entity # Caution: formula must be cloned after the entity has been set into new. formula = self.formula if formula is not None: new_dict['formula'] = formula.clone(new) return new def compute(self, period = None, accept_other_period = False, requested_formulas_by_period = None): """Compute array if needed and/or convert it to requested period and return a dated holder containig it. The returned dated holder is always of the requested period and this method never returns None. """ entity = self.entity simulation = entity.simulation if period is None: period = simulation.period column = self.column # First look for dated_holders covering the whole period (without hole). dated_holder = self.at_period(period) if dated_holder.array is not None: return dated_holder assert self._array is None # self._array should always be None when dated_holder.array is None. column_start_instant = periods.instant(column.start) column_stop_instant = periods.instant(column.end) if (column_start_instant is None or column_start_instant <= period.start) \ and (column_stop_instant is None or period.start <= column_stop_instant): formula_dated_holder = self.formula.compute(period = period, requested_formulas_by_period = requested_formulas_by_period) assert formula_dated_holder is not None if not column.is_permanent: assert accept_other_period or formula_dated_holder.period == period, \ u"Requested period {} differs from {} returned by variable {}".format(period, formula_dated_holder.period, column.name) return formula_dated_holder array = np.empty(entity.count, dtype = column.dtype) array.fill(column.default) dated_holder.array = array return dated_holder def compute_add(self, period = None, requested_formulas_by_period = None): dated_holder = self.at_period(period) if dated_holder.array is not None: return dated_holder array = None unit = period.unit if unit == u'month': remaining_period_months = period.size else: assert unit == u'year', unit remaining_period_months = period.size * 12 requested_period = period.start.period(unit) while True: dated_holder = self.compute(accept_other_period = True, period = requested_period, requested_formulas_by_period = requested_formulas_by_period) requested_start = requested_period.start returned_period = dated_holder.period returned_start = returned_period.start assert returned_start.day == 1 # Note: A dated formula may start after requested period => returned_start is not always equal to # requested_start. assert returned_start >= requested_start, \ "Period {} returned by variable {} doesn't have the same start as requested period {}.".format( returned_period, self.column.name, requested_period) if returned_period.unit == u'month': returned_period_months = returned_period.size else: assert returned_period.unit == u'year', \ "Requested a monthly or yearly period. Got {} returned by variable {}.".format( returned_period, self.column.name) returned_period_months = returned_period.size * 12 requested_start_months = requested_start.year * 12 + requested_start.month returned_start_months = returned_start.year * 12 + returned_start.month returned_period_months = returned_start_months + returned_period_months - requested_start_months remaining_period_months -= returned_period_months assert remaining_period_months >= 0, \ "Period {} returned by variable {} is larger than the requested_period {}.".format( returned_period, self.column.name, requested_period) if array is None: array = dated_holder.array.copy() else: array += dated_holder.array if remaining_period_months <= 0: dated_holder = self.at_period(period) dated_holder.array = array return dated_holder if remaining_period_months % 12 == 0: requested_period = requested_start.offset(returned_period_months, u'month').period(u'year') else: requested_period = requested_start.offset(returned_period_months, u'month').period(u'month') def compute_add_divide(self, period = None, requested_formulas_by_period = None): dated_holder = self.at_period(period) if dated_holder.array is not None: return dated_holder array = None unit = period.unit if unit == u'month': remaining_period_months = period.size else: assert unit == u'year', unit remaining_period_months = period.size * 12 requested_period = period.start.period(unit) while True: dated_holder = self.compute(accept_other_period = True, period = requested_period, requested_formulas_by_period = requested_formulas_by_period) requested_start = requested_period.start returned_period = dated_holder.period returned_start = returned_period.start assert returned_start.day == 1 # Note: A dated formula may start after requested period. # assert returned_start <= requested_start <= returned_period.stop, \ # "Period {} returned by variable {} doesn't include start of requested period {}.".format( # returned_period, self.column.name, requested_period) requested_start_months = requested_start.year * 12 + requested_start.month returned_start_months = returned_start.year * 12 + returned_start.month if returned_period.unit == u'month': intersection_months = min(requested_start_months + requested_period.size, returned_start_months + returned_period.size) - requested_start_months intersection_array = dated_holder.array * intersection_months / returned_period.size else: assert returned_period.unit == u'year', \ "Requested a monthly or yearly period. Got {} returned by variable {}.".format( returned_period, self.column.name) intersection_months = min(requested_start_months + requested_period.size, returned_start_months + returned_period.size * 12) - requested_start_months intersection_array = dated_holder.array * intersection_months / (returned_period.size * 12) if array is None: array = intersection_array.copy() else: array += intersection_array remaining_period_months -= intersection_months if remaining_period_months <= 0: dated_holder = self.at_period(period) dated_holder.array = array return dated_holder if remaining_period_months % 12 == 0: requested_period = requested_start.offset(intersection_months, u'month').period(u'year') else: requested_period = requested_start.offset(intersection_months, u'month').period(u'month') def compute_divide(self, period = None, requested_formulas_by_period = None): dated_holder = self.at_period(period) if dated_holder.array is not None: return dated_holder array = None unit = period[0] year, month, day = period.start if unit == u'month': dated_holder = self.compute(accept_other_period = True, period = period, requested_formulas_by_period = requested_formulas_by_period) assert dated_holder.period.start <= period.start and period.stop <= dated_holder.period.stop, \ "Period {} returned by variable {} doesn't include requested period {}.".format( dated_holder.period, self.column.name, period) if dated_holder.period.unit == u'month': array = dated_holder.array * period.size / dated_holder.period.size else: assert dated_holder.period.unit == u'year', \ "Requested a monthly or yearly period. Got {} returned by variable {}.".format( dated_holder.period, self.column.name) array = dated_holder.array * period.size / (12 * dated_holder.period.size) dated_holder = self.at_period(period) dated_holder.array = array return dated_holder else: assert unit == u'year', unit return self.compute(period = period, requested_formulas_by_period = requested_formulas_by_period) def delete_arrays(self): if self._array is not None: del self._array if self._array_by_period is not None: del self._array_by_period def get_array(self, period): if self.column.is_permanent: return self.array assert period is not None array_by_period = self._array_by_period if array_by_period is not None: array = array_by_period.get(period) if array is not None: return array return None def graph(self, edges, get_input_variables_and_parameters, nodes, visited): column = self.column if self in visited: return visited.add(self) nodes.append(dict( id = column.name, group = self.entity.key_plural, label = column.name, title = column.label, )) period = self.entity.simulation.period formula = self.formula if formula is None or column.start is not None and column.start > period.stop.date or column.end is not None \ and column.end < period.start.date: return formula.graph_parameters(edges, get_input_variables_and_parameters, nodes, visited) def new_test_case_array(self, period): array = self.get_array(period) if array is None: return None entity = self.entity return array.reshape([entity.simulation.steps_count, entity.step_size]).sum(1) @property def real_formula(self): formula = self.formula if formula is None: return None return formula.real_formula def set_array(self, period, array): if self.column.is_permanent: self.array = array return assert period is not None simulation = self.entity.simulation if simulation.debug or simulation.trace: variable_infos = (self.column.name, period) step = simulation.traceback.get(variable_infos) if step is None: simulation.traceback[variable_infos] = dict( holder = self, ) array_by_period = self._array_by_period if array_by_period is None: self._array_by_period = array_by_period = {} array_by_period[period] = array def set_input(self, period, array): self.formula.set_input(period, array) def to_value_json(self, use_label = False): column = self.column transform_dated_value_to_json = column.transform_dated_value_to_json if column.is_permanent: array = self._array if array is None: return None return [ transform_dated_value_to_json(cell, use_label = use_label) for cell in array.tolist() ] value_json = {} if self._array_by_period is not None: for period, array in self._array_by_period.iteritems(): value_json[str(period)] = [ transform_dated_value_to_json(cell, use_label = use_label) for cell in array.tolist() ] return value_json PK3yIEopenfisca_core/__init__.pyPK 'GS9494openfisca_core/simulations.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import collections from . import periods from .tools import empty_clone, stringify_array class Simulation(object): compact_legislation_by_instant_cache = None debug = False debug_all = False # When False, log only formula calls with non-default parameters. entity_by_column_name = None entity_by_key_plural = None entity_by_key_singular = None period = None persons = None reference_compact_legislation_by_instant_cache = None stack_trace = None steps_count = 1 tax_benefit_system = None trace = False traceback = None def __init__(self, debug = False, debug_all = False, period = None, tax_benefit_system = None, trace = False): assert isinstance(period, periods.Period) self.period = period if debug: self.debug = True if debug_all: assert debug self.debug_all = True assert tax_benefit_system is not None self.tax_benefit_system = tax_benefit_system if trace: self.trace = True if debug or trace: self.stack_trace = collections.deque() self.traceback = collections.OrderedDict() # Note: Since simulations are short-lived and must be fast, don't use weakrefs for cache. self.compact_legislation_by_instant_cache = {} self.reference_compact_legislation_by_instant_cache = {} entity_class_by_key_plural = tax_benefit_system.entity_class_by_key_plural self.entity_by_key_plural = entity_by_key_plural = dict( (key_plural, entity_class(simulation = self)) for key_plural, entity_class in entity_class_by_key_plural.iteritems() ) self.entity_by_column_name = dict( (column_name, entity) for entity in entity_by_key_plural.itervalues() for column_name in entity.column_by_name.iterkeys() ) self.entity_by_key_singular = dict( (entity.key_singular, entity) for entity in entity_by_key_plural.itervalues() ) for entity in entity_by_key_plural.itervalues(): if entity.is_persons_entity: self.persons = entity break def calculate(self, column_name, period = None, accept_other_period = False, requested_formulas_by_period = None): if period is None: period = self.period return self.compute(column_name, period = period, accept_other_period = accept_other_period, requested_formulas_by_period = requested_formulas_by_period).array def calculate_add(self, column_name, period = None, requested_formulas_by_period = None): if period is None: period = self.period return self.compute_add(column_name, period = period, requested_formulas_by_period = requested_formulas_by_period).array def calculate_add_divide(self, column_name, period = None, requested_formulas_by_period = None): if period is None: period = self.period return self.compute_add_divide(column_name, period = period, requested_formulas_by_period = requested_formulas_by_period).array def calculate_divide(self, column_name, period = None, requested_formulas_by_period = None): if period is None: period = self.period return self.compute_divide(column_name, period = period, requested_formulas_by_period = requested_formulas_by_period).array def calculate_output(self, column_name, period = None): """Calculate the value using calculate_output hooks in formula classes.""" if period is None: period = self.period elif not isinstance(period, periods.Period): period = periods.period(period) entity = self.entity_by_column_name[column_name] holder = entity.get_or_new_holder(column_name) return holder.calculate_output(period) def clone(self, debug = False, debug_all = False, trace = False): """Copy the simulation just enough to be able to run the copy without modifying the original simulation.""" new = empty_clone(self) new_dict = new.__dict__ for key, value in self.__dict__.iteritems(): if key not in ('debug', 'debug_all', 'entity_by_key_plural', 'persons', 'trace'): new_dict[key] = value if debug: new_dict['debug'] = True if debug_all: new_dict['debug_all'] = True if trace: new_dict['trace'] = True if debug or trace: new_dict['stack_trace'] = collections.deque() new_dict['traceback'] = collections.OrderedDict() new_dict['entity_by_key_plural'] = entity_by_key_plural = dict( (key_plural, entity.clone(simulation = new)) for key_plural, entity in self.entity_by_key_plural.iteritems() ) new_dict['entity_by_column_name'] = dict( (column_name, entity) for entity in entity_by_key_plural.itervalues() for column_name in entity.column_by_name.iterkeys() ) new_dict['entity_by_key_singular'] = dict( (entity.key_singular, entity) for entity in entity_by_key_plural.itervalues() ) for entity in entity_by_key_plural.itervalues(): if entity.is_persons_entity: new_dict['persons'] = entity break return new def compute(self, column_name, period = None, accept_other_period = False, requested_formulas_by_period = None): if period is None: period = self.period elif not isinstance(period, periods.Period): period = periods.period(period) if (self.debug or self.trace) and self.stack_trace: variable_infos = (column_name, period) calling_frame = self.stack_trace[-1] caller_input_variables_infos = calling_frame['input_variables_infos'] if variable_infos not in caller_input_variables_infos: caller_input_variables_infos.append(variable_infos) return self.entity_by_column_name[column_name].compute(column_name, period = period, accept_other_period = accept_other_period, requested_formulas_by_period = requested_formulas_by_period) def compute_add(self, column_name, period = None, requested_formulas_by_period = None): if period is None: period = self.period elif not isinstance(period, periods.Period): period = periods.period(period) if (self.debug or self.trace) and self.stack_trace: variable_infos = (column_name, period) calling_frame = self.stack_trace[-1] caller_input_variables_infos = calling_frame['input_variables_infos'] if variable_infos not in caller_input_variables_infos: caller_input_variables_infos.append(variable_infos) return self.entity_by_column_name[column_name].compute_add(column_name, period = period, requested_formulas_by_period = requested_formulas_by_period) def compute_add_divide(self, column_name, period = None, requested_formulas_by_period = None): if period is None: period = self.period elif not isinstance(period, periods.Period): period = periods.period(period) if (self.debug or self.trace) and self.stack_trace: variable_infos = (column_name, period) calling_frame = self.stack_trace[-1] caller_input_variables_infos = calling_frame['input_variables_infos'] if variable_infos not in caller_input_variables_infos: caller_input_variables_infos.append(variable_infos) return self.entity_by_column_name[column_name].compute_add_divide(column_name, period = period, requested_formulas_by_period = requested_formulas_by_period) def compute_divide(self, column_name, period = None, requested_formulas_by_period = None): if period is None: period = self.period elif not isinstance(period, periods.Period): period = periods.period(period) if (self.debug or self.trace) and self.stack_trace: variable_infos = (column_name, period) calling_frame = self.stack_trace[-1] caller_input_variables_infos = calling_frame['input_variables_infos'] if variable_infos not in caller_input_variables_infos: caller_input_variables_infos.append(variable_infos) return self.entity_by_column_name[column_name].compute_divide(column_name, period = period, requested_formulas_by_period = requested_formulas_by_period) def get_array(self, column_name, period = None): if period is None: period = self.period elif not isinstance(period, periods.Period): period = periods.period(period) if (self.debug or self.trace) and self.stack_trace: variable_infos = (column_name, period) calling_frame = self.stack_trace[-1] caller_input_variables_infos = calling_frame['input_variables_infos'] if variable_infos not in caller_input_variables_infos: caller_input_variables_infos.append(variable_infos) return self.entity_by_column_name[column_name].get_array(column_name, period) def get_compact_legislation(self, instant): compact_legislation = self.compact_legislation_by_instant_cache.get(instant) if compact_legislation is None: compact_legislation = self.tax_benefit_system.get_compact_legislation( instant = instant, traced_simulation = self if self.trace else None, ) self.compact_legislation_by_instant_cache[instant] = compact_legislation return compact_legislation def get_holder(self, column_name, default = UnboundLocalError): entity = self.entity_by_column_name[column_name] if default is UnboundLocalError: return entity.holder_by_name[column_name] return entity.holder_by_name.get(column_name, default) def get_or_new_holder(self, column_name): entity = self.entity_by_column_name[column_name] return entity.get_or_new_holder(column_name) def get_reference_compact_legislation(self, instant): reference_compact_legislation = self.reference_compact_legislation_by_instant_cache.get(instant) if reference_compact_legislation is None: reference_compact_legislation = self.tax_benefit_system.get_reference_compact_legislation( instant = instant, traced_simulation = self if self.trace else None, ) self.reference_compact_legislation_by_instant_cache[instant] = reference_compact_legislation return reference_compact_legislation def graph(self, column_name, edges, get_input_variables_and_parameters, nodes, visited): self.entity_by_column_name[column_name].graph(column_name, edges, get_input_variables_and_parameters, nodes, visited) def legislation_at(self, instant, reference = False): assert isinstance(instant, periods.Instant), "Expected an instant. Got: {}".format(instant) if reference: return self.get_reference_compact_legislation(instant) return self.get_compact_legislation(instant) def stringify_input_variables_infos(self, input_variables_infos): return u', '.join( u'{}@{}<{}>{}'.format( input_holder.column.name, input_holder.entity.key_plural, str(input_variable_period), stringify_array(input_holder.get_array(input_variable_period)), ) for input_holder, input_variable_period in ( (self.get_holder(input_variable_name), input_variable_period1) for input_variable_name, input_variable_period1 in input_variables_infos ) ) def to_input_variables_json(self): return { column_name: self.get_holder(column_name).to_value_json() for entity in self.entity_by_key_plural.itervalues() for column_name in entity.column_by_name.iterkeys() if column_name in entity.holder_by_name } PK 'GL0}}openfisca_core/scenarios.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from __future__ import division import collections import itertools import numpy as np from . import conv, periods, simulations N_ = lambda message: message class AbstractScenario(object): axes = None input_variables = None period = None tax_benefit_system = None test_case = None @staticmethod def cleanup_period_in_json_or_python(value, state = None): if value is None: return None, None value = value.copy() if 'date' not in value: year = value.pop('year', None) if year is not None: value['date'] = year if 'period' not in value: date = value.pop('date', None) if date is not None: value['period'] = dict( unit = u'year', start = date, ) return value, None def fill_simulation(self, simulation, use_set_input_hooks = True, variables_name_to_skip = None): assert isinstance(simulation, simulations.Simulation) if variables_name_to_skip is None: variables_name_to_skip = set() column_by_name = self.tax_benefit_system.column_by_name entity_by_key_plural = simulation.entity_by_key_plural simulation_period = simulation.period test_case = self.test_case persons = None for entity in entity_by_key_plural.itervalues(): if entity.is_persons_entity: assert persons is None persons = entity assert persons is not None if test_case is None: if self.input_variables is not None: # Note: For set_input to work, handle days, before months, before years => use sorted(). for variable_name, array_by_period in sorted(self.input_variables.iteritems()): holder = simulation.get_or_new_holder(variable_name) entity = holder.entity for period, array in array_by_period.iteritems(): if entity.count == 0: entity.count = len(array) if use_set_input_hooks: holder.set_input(period, array) else: holder.set_array(period, array) if persons.count == 0: persons.count = 1 for entity in simulation.entity_by_key_plural.itervalues(): if entity is persons: continue index_holder = persons.get_or_new_holder(entity.index_for_person_variable_name) index_array = index_holder.array if index_array is None: index_holder.array = np.arange(persons.count, dtype = index_holder.column.dtype) role_holder = persons.get_or_new_holder(entity.role_for_person_variable_name) role_array = role_holder.array if role_array is None: role_holder.array = np.zeros(persons.count, role_holder.column.dtype) entity.roles_count = 1 if entity.count == 0: entity.count = max(index_holder.array) + 1 else: assert entity.count == max(index_holder.array) + 1 else: steps_count = 1 if self.axes is not None: for parallel_axes in self.axes: # All parallel axes have the same count, entity and period. axis = parallel_axes[0] steps_count *= axis['count'] simulation.steps_count = steps_count for entity in entity_by_key_plural.itervalues(): entity.step_size = entity_step_size = len(test_case[entity.key_plural]) entity.count = steps_count * entity_step_size persons_step_size = persons.step_size person_index_by_id = dict( (person[u'id'], person_index) for person_index, person in enumerate(test_case[persons.key_plural]) ) for entity_key_plural, entity in entity_by_key_plural.iteritems(): if entity.is_persons_entity: continue entity_step_size = entity.step_size persons.get_or_new_holder(entity.index_for_person_variable_name).array = person_entity_id_array = \ np.empty(steps_count * persons.step_size, dtype = column_by_name[entity.index_for_person_variable_name].dtype) persons.get_or_new_holder(entity.role_for_person_variable_name).array = person_entity_role_array = \ np.empty(steps_count * persons.step_size, dtype = column_by_name[entity.role_for_person_variable_name].dtype) for member_index, member in enumerate(test_case[entity_key_plural]): for person_role, person_id in entity.iter_member_persons_role_and_id(member): person_index = person_index_by_id[person_id] for step_index in range(steps_count): person_entity_id_array[step_index * persons_step_size + person_index] \ = step_index * entity_step_size + member_index person_entity_role_array[step_index * persons_step_size + person_index] = person_role entity.roles_count = person_entity_role_array.max() + 1 for entity_key_plural, entity in entity_by_key_plural.iteritems(): used_columns_name = set( key for entity_member in test_case[entity_key_plural] for key, value in entity_member.iteritems() if value is not None and key not in ( entity.index_for_person_variable_name, entity.role_for_person_variable_name, ) and key not in variables_name_to_skip ) for variable_name, column in column_by_name.iteritems(): if column.entity == entity.symbol and variable_name in used_columns_name: variable_periods = set() for cell in ( entity_member.get(variable_name) for entity_member in test_case[entity_key_plural] ): if isinstance(cell, dict): if any(value is not None for value in cell.itervalues()): variable_periods.update(cell.iterkeys()) elif cell is not None: variable_periods.add(simulation_period) holder = entity.get_or_new_holder(variable_name) variable_default_value = column.default # Note: For set_input to work, handle days, before months, before years => use sorted(). for variable_period in sorted(variable_periods): variable_values = [ variable_default_value if dated_cell is None else dated_cell for dated_cell in ( cell.get(variable_period) if isinstance(cell, dict) else (cell if variable_period == simulation_period else None) for cell in ( entity_member.get(variable_name) for entity_member in test_case[entity_key_plural] ) ) ] variable_values_iter = ( variable_value for step_index in range(steps_count) for variable_value in variable_values ) array = np.fromiter(variable_values_iter, dtype = column.dtype) \ if column.dtype is not object \ else np.array(list(variable_values_iter), dtype = column.dtype) if use_set_input_hooks: holder.set_input(variable_period, array) else: holder.set_array(variable_period, array) if self.axes is not None: if len(self.axes) == 1: parallel_axes = self.axes[0] # All parallel axes have the same count and entity. first_axis = parallel_axes[0] axis_count = first_axis['count'] axis_entity = simulation.entity_by_column_name[first_axis['name']] for axis in parallel_axes: axis_period = axis['period'] or simulation_period holder = simulation.get_or_new_holder(axis['name']) column = holder.column array = holder.get_array(axis_period) if array is None: array = np.empty(axis_entity.count, dtype = column.dtype) array.fill(column.default) array[axis['index']:: axis_entity.step_size] = np.linspace(axis['min'], axis['max'], axis_count) if use_set_input_hooks: holder.set_input(axis_period, array) else: holder.set_array(axis_period, array) else: axes_linspaces = [ np.linspace(0, first_axis['count'] - 1, first_axis['count']) for first_axis in ( parallel_axes[0] for parallel_axes in self.axes ) ] axes_meshes = np.meshgrid(*axes_linspaces) for parallel_axes, mesh in zip(self.axes, axes_meshes): # All parallel axes have the same count and entity. first_axis = parallel_axes[0] axis_count = first_axis['count'] axis_entity = simulation.entity_by_column_name[first_axis['name']] for axis in parallel_axes: axis_period = axis['period'] or simulation_period holder = simulation.get_or_new_holder(axis['name']) column = holder.column array = holder.get_array(axis_period) if array is None: array = np.empty(axis_entity.count, dtype = column.dtype) array.fill(column.default) array[axis['index']:: axis_entity.step_size] = axis['min'] \ + mesh.reshape(steps_count) * (axis['max'] - axis['min']) / (axis_count - 1) if use_set_input_hooks: holder.set_input(axis_period, array) else: holder.set_array(axis_period, array) def init_from_attributes(self, repair = False, **attributes): conv.check(self.make_json_or_python_to_attributes(repair = repair))(attributes) return self def make_json_or_python_to_attributes(self, repair = False): column_by_name = self.tax_benefit_system.column_by_name def json_or_python_to_attributes(value, state = None): if value is None: return value, None if state is None: state = conv.default_state # First validation and conversion step data, error = conv.pipe( conv.test_isinstance(dict), # TODO: Remove condition below, once every calls uses "period" instead of "date" & "year". self.cleanup_period_in_json_or_python, conv.struct( dict( axes = make_json_or_python_to_axes(self.tax_benefit_system), input_variables = conv.test_isinstance(dict), # Real test is done below, once period is known. period = conv.pipe( periods.json_or_python_to_period, # TODO: Check that period is valid in params. conv.not_none, ), test_case = conv.test_isinstance(dict), # Real test is done below, once period is known. ), ), )(value, state = state) if error is not None: return data, error # Second validation and conversion step data, error = conv.struct( dict( input_variables = make_json_or_python_to_input_variables(self.tax_benefit_system, data['period']), test_case = self.make_json_or_python_to_test_case(period = data['period'], repair = repair), ), default = conv.noop, )(data, state = state) if error is not None: return data, error # Third validation and conversion step errors = {} if data['input_variables'] is not None and data['test_case'] is not None: errors['input_variables'] = state._(u"Items input_variables and test_case can't both exist") errors['test_case'] = state._(u"Items input_variables and test_case can't both exist") elif data['axes'] is not None and data["test_case"] is None: errors['axes'] = state._(u"Axes can't be used with input_variables.") if errors: return data, errors if data['axes'] is not None: for parallel_axes_index, parallel_axes in enumerate(data['axes']): first_axis = parallel_axes[0] axis_count = first_axis['count'] axis_entity_key_plural = column_by_name[first_axis['name']].entity_key_plural first_axis_period = first_axis['period'] or data['period'] for axis_index, axis in enumerate(parallel_axes): if axis['min'] >= axis['max']: errors.setdefault('axes', {}).setdefault(parallel_axes_index, {}).setdefault( axis_index, {})['max'] = state._(u"Max value must be greater than min value") column = column_by_name[axis['name']] if axis['index'] >= len(data['test_case'][column.entity_key_plural]): errors.setdefault('axes', {}).setdefault(parallel_axes_index, {}).setdefault( axis_index, {})['index'] = state._(u"Index must be lower than {}").format( len(data['test_case'][column.entity_key_plural])) if axis_index > 0: if axis['count'] != axis_count: errors.setdefault('axes', {}).setdefault(parallel_axes_index, {}).setdefault( axis_index, {})['count'] = state._(u"Parallel indexes must have the same count") if column.entity_key_plural != axis_entity_key_plural: errors.setdefault('axes', {}).setdefault(parallel_axes_index, {}).setdefault( axis_index, {})['period'] = state._( u"Parallel indexes must belong to the same entity") axis_period = axis['period'] or data['period'] if axis_period.unit != first_axis_period.unit: errors.setdefault('axes', {}).setdefault(parallel_axes_index, {}).setdefault( axis_index, {})['period'] = state._( u"Parallel indexes must have the same period unit") elif axis_period.size != first_axis_period.size: errors.setdefault('axes', {}).setdefault(parallel_axes_index, {}).setdefault( axis_index, {})['period'] = state._( u"Parallel indexes must have the same period size") if errors: return data, errors self.axes = data['axes'] if data['input_variables'] is not None: self.input_variables = data['input_variables'] self.period = data['period'] if data['test_case'] is not None: self.test_case = data['test_case'] return self, None return json_or_python_to_attributes @classmethod def make_json_to_instance(cls, repair = False, tax_benefit_system = None): def json_to_instance(value, state = None): if value is None: return None, None self = cls() self.tax_benefit_system = tax_benefit_system return self.make_json_or_python_to_attributes(repair = repair)( value = value, state = state or conv.default_state) return json_to_instance def new_simulation(self, debug = False, debug_all = False, reference = False, trace = False, use_set_input_hooks = True): assert isinstance(reference, (bool, int)), \ 'Parameter reference must be a boolean. When True, the reference tax-benefit system is used.' tax_benefit_system = self.tax_benefit_system if reference: while True: reference_tax_benefit_system = tax_benefit_system.reference if reference_tax_benefit_system is None: break tax_benefit_system = reference_tax_benefit_system simulation = simulations.Simulation( debug = debug, debug_all = debug_all, period = self.period, tax_benefit_system = tax_benefit_system, trace = trace, ) self.fill_simulation(simulation, use_set_input_hooks = use_set_input_hooks) return simulation def to_json(self): return collections.OrderedDict( (key, value) for key, value in ( (key, getattr(self, key)) for key in ( 'period', 'input_variables', 'test_case', 'axes', ) ) if value is not None ) def extract_output_variables_name_to_ignore(output_variables_name_to_ignore): def extract_output_variables_name_to_ignore_converter(value, state = None): if value is None: return value, None new_value = collections.OrderedDict() for variable_name, variable_value in value.iteritems(): if variable_name.startswith(u'IGNORE_'): variable_name = variable_name[len(u'IGNORE_'):] output_variables_name_to_ignore.add(variable_name) new_value[variable_name] = variable_value return new_value, None return extract_output_variables_name_to_ignore_converter def make_json_or_python_to_array_by_period_by_variable_name(tax_benefit_system, period): def json_or_python_to_array_by_period_by_variable_name(value, state = None): if value is None: return value, None if state is None: state = conv.default_state error_by_variable_name = {} array_by_period_by_variable_name = collections.OrderedDict() for variable_name, variable_value in value.iteritems(): column = tax_benefit_system.column_by_name[variable_name] variable_array_by_period, error = column.make_json_to_array_by_period(period)(variable_value, state = state) if variable_array_by_period is not None: array_by_period_by_variable_name[variable_name] = variable_array_by_period if error is not None: error_by_variable_name[variable_name] = error return array_by_period_by_variable_name, error_by_variable_name or None return json_or_python_to_array_by_period_by_variable_name def make_json_or_python_to_axes(tax_benefit_system): column_by_name = tax_benefit_system.column_by_name return conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( conv.pipe( conv.item_or_sequence( conv.pipe( conv.test_isinstance(dict), conv.struct( dict( count = conv.pipe( conv.test_isinstance(int), conv.test_greater_or_equal(1), conv.not_none, ), index = conv.pipe( conv.test_isinstance(int), conv.test_greater_or_equal(0), conv.default(0), ), max = conv.pipe( conv.test_isinstance((float, int)), conv.not_none, ), min = conv.pipe( conv.test_isinstance((float, int)), conv.not_none, ), name = conv.pipe( conv.test_isinstance(basestring), conv.test_in(column_by_name), conv.test(lambda column_name: column_by_name[column_name].dtype in ( np.float32, np.int16, np.int32), error = N_(u'Invalid type for axe: integer or float expected')), conv.not_none, ), # TODO: Check that period is valid in params. period = periods.json_or_python_to_period, ), ), ), drop_none_items = True, ), conv.make_item_to_singleton(), ), drop_none_items = True, ), conv.empty_to_none, ) def make_json_or_python_to_input_variables(tax_benefit_system, period): column_by_name = tax_benefit_system.column_by_name variables_name = set(column_by_name) def json_or_python_to_input_variables(value, state = None): if value is None: return value, None if state is None: state = conv.default_state input_variables, errors = conv.pipe( conv.test_isinstance(dict), conv.uniform_mapping( conv.pipe( conv.test_isinstance(basestring), conv.test_in(variables_name), conv.not_none, ), conv.noop, ), make_json_or_python_to_array_by_period_by_variable_name(tax_benefit_system, period), conv.empty_to_none, )(value, state = state) if errors is not None: return input_variables, errors count_by_entity_key_plural = {} errors = {} for variable_name, array_by_period in input_variables.iteritems(): column = column_by_name[variable_name] entity_key_plural = column.entity_key_plural entity_count = count_by_entity_key_plural.get(entity_key_plural, 0) for variable_period, variable_array in array_by_period.iteritems(): if entity_count == 0: count_by_entity_key_plural[entity_key_plural] = entity_count = len(variable_array) elif len(variable_array) != entity_count: errors[column.name] = state._( u"Array has not the same length as other variables of entity {}: {} instead of {}").format( column.name, len(variable_array), entity_count) return input_variables, errors or None return json_or_python_to_input_variables def make_json_or_python_to_test(tax_benefit_system, default_absolute_error_margin = None, default_relative_error_margin = None): column_by_name = tax_benefit_system.column_by_name variables_name = set(column_by_name) validate = conv.struct( dict(itertools.chain( dict( absolute_error_margin = conv.pipe( conv.test_isinstance((float, int)), conv.test_greater_or_equal(0), ), axes = make_json_or_python_to_axes(tax_benefit_system), description = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, ), ignore = conv.pipe( conv.test_isinstance((bool, int)), conv.anything_to_bool, ), input_variables = conv.pipe( conv.test_isinstance(dict), conv.uniform_mapping( conv.pipe( conv.test_isinstance(basestring), conv.test_in(variables_name), conv.not_none, ), conv.noop, ), conv.empty_to_none, ), keywords = conv.pipe( conv.make_item_to_singleton(), conv.test_isinstance(list), conv.uniform_sequence( conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, ), drop_none_items = True, ), conv.empty_to_none, ), name = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, ), output_variables = conv.test_isinstance(dict), period = conv.pipe( periods.json_or_python_to_period, conv.not_none, ), relative_error_margin = conv.pipe( conv.test_isinstance((float, int)), conv.test_greater_or_equal(0), ), ).iteritems(), ( (entity_class.key_plural, conv.pipe( conv.make_item_to_singleton(), conv.test_isinstance(list), )) for entity_class in tax_benefit_system.entity_class_by_key_plural.itervalues() ), )), ) def json_or_python_to_test(value, state = None): if value is None: return value, None if state is None: state = conv.default_state output_variables_name_to_ignore = set() value, error = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( output_variables = extract_output_variables_name_to_ignore(output_variables_name_to_ignore), ), default = conv.noop, ), validate, )(value, state = state) if error is not None: return value, error value, error = conv.struct( dict( output_variables = make_json_or_python_to_input_variables(tax_benefit_system, value['period']), ), default = conv.noop, )(value, state = state) if error is not None: return value, error test_case = value.copy() absolute_error_margin = test_case.pop(u'absolute_error_margin') axes = test_case.pop(u'axes') description = test_case.pop(u'description') ignore = test_case.pop(u'ignore') input_variables = test_case.pop(u'input_variables') keywords = test_case.pop(u'keywords') name = test_case.pop(u'name') output_variables = test_case.pop(u'output_variables') period = test_case.pop(u'period') relative_error_margin = test_case.pop(u'relative_error_margin') if absolute_error_margin is None and relative_error_margin is None: absolute_error_margin = default_absolute_error_margin relative_error_margin = default_relative_error_margin if test_case is not None and all(entity_members is None for entity_members in test_case.itervalues()): test_case = None scenario, error = tax_benefit_system.Scenario.make_json_to_instance(repair = True, tax_benefit_system = tax_benefit_system)(dict( axes = axes, input_variables = input_variables, period = period, test_case = test_case, ), state = state) if error is not None: return scenario, error return { key: value for key, value in dict( absolute_error_margin = absolute_error_margin, description = description, ignore = ignore, keywords = keywords, name = name, output_variables = output_variables, output_variables_name_to_ignore = output_variables_name_to_ignore, relative_error_margin = relative_error_margin, scenario = scenario, ).iteritems() if value is not None }, None return json_or_python_to_test def set_entities_json_id(entities_json): for index, entity_json in enumerate(entities_json): if 'id' not in entity_json: entity_json['id'] = index return entities_json PK 'G #openfisca_core/taxbenefitsystems.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import collections # import weakref from . import conv, legislations, legislationsxml __all__ = [ 'AbstractTaxBenefitSystem', 'LegacyTaxBenefitSystem', 'LegislationLessTaxBenefitSystem', 'XmlBasedTaxBenefitSystem', ] class AbstractTaxBenefitSystem(object): _base_tax_benefit_system = None column_by_name = None # computed at instance initialization from entities column_by_name compact_legislation_by_instant_cache = None entity_class_by_key_plural = None legislation_json = None person_key_plural = None json_to_attributes = staticmethod(conv.pipe( conv.test_isinstance(dict), conv.struct({}), )) reference = None # Reference tax-benefit system. Used only by reforms. Note: Reforms can be chained. Scenario = None def __init__(self, entity_class_by_key_plural = None, legislation_json = None): # TODO: Currently: Don't use a weakref, because they are cleared by Paste (at least) at each call. self.compact_legislation_by_instant_cache = {} # weakref.WeakValueDictionary() if entity_class_by_key_plural is not None: self.entity_class_by_key_plural = entity_class_by_key_plural assert self.entity_class_by_key_plural is not None if legislation_json is not None: self.legislation_json = legislation_json # Note: self.legislation_json may be None for simulators without legislation parameters. # Now that classes of entities are defined, build a column_by_name by aggregating the column_by_name of each # entity class. assert self.column_by_name is None self.column_by_name = column_by_name = collections.OrderedDict() for entity_class in self.entity_class_by_key_plural.itervalues(): column_by_name.update(entity_class.column_by_name) if entity_class.is_persons_entity: self.person_key_plural = entity_class.key_plural @property def base_tax_benefit_system(self): base_tax_benefit_system = self._base_tax_benefit_system if base_tax_benefit_system is None: reference = self.reference if reference is None: return self self._base_tax_benefit_system = base_tax_benefit_system = reference.base_tax_benefit_system return base_tax_benefit_system def get_compact_legislation(self, instant, traced_simulation = None): if traced_simulation is None: compact_legislation = self.compact_legislation_by_instant_cache.get(instant) if compact_legislation is None and self.legislation_json is not None: dated_legislation_json = legislations.generate_dated_legislation_json(self.legislation_json, instant) compact_legislation = legislations.compact_dated_node_json(dated_legislation_json) self.compact_legislation_by_instant_cache[instant] = compact_legislation else: dated_legislation_json = legislations.generate_dated_legislation_json(self.legislation_json, instant) compact_legislation = legislations.compact_dated_node_json( dated_legislation_json, traced_simulation = traced_simulation, ) return compact_legislation def get_reference_compact_legislation(self, instant, traced_simulation = None): reference = self.reference if reference is None: return self.get_compact_legislation(instant, traced_simulation = traced_simulation) return reference.get_reference_compact_legislation(instant, traced_simulation = traced_simulation) @classmethod def json_to_instance(cls, value, state = None): attributes, error = conv.pipe( cls.json_to_attributes, conv.default({}), )(value, state = state or conv.default_state) if error is not None: return attributes, error return cls(**attributes), None def new_scenario(self): scenario = self.Scenario() scenario.tax_benefit_system = self return scenario def prefill_cache(self): pass class LegislationLessTaxBenefitSystem(AbstractTaxBenefitSystem): pass class XmlBasedTaxBenefitSystem(AbstractTaxBenefitSystem): """A tax-benefit sytem with legislation stored in a XML file.""" legislation_xml_file_path = None # class attribute or must be set before calling this __init__ method. preprocess_legislation = None def __init__(self, entity_class_by_key_plural = None): state = conv.State() legislation_json = conv.check(legislationsxml.xml_legislation_file_path_to_json)( self.legislation_xml_file_path, state = state) if self.preprocess_legislation is not None: self.preprocess_legislation(legislation_json) super(XmlBasedTaxBenefitSystem, self).__init__( entity_class_by_key_plural = entity_class_by_key_plural, legislation_json = legislation_json, ) class LegacyTaxBenefitSystem(XmlBasedTaxBenefitSystem): """The obsolete way of creating a TaxBenefitSystem. Don't use it anymore. In this kind of tax-benefit system, a lot of attributes are defined in class. """ check_consistency = None columns_name_tree_by_entity = None entities = None # class attribute def __init__(self): super(LegacyTaxBenefitSystem, self).__init__() PK 'GCg6g6openfisca_core/reforms.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import collections import copy import warnings from . import formulas, legislations, periods, taxbenefitsystems class AbstractReform(taxbenefitsystems.AbstractTaxBenefitSystem): """A reform is a variant of a TaxBenefitSystem, that refers to the real TaxBenefitSystem as its reference.""" CURRENCY = None DECOMP_DIR = None DEFAULT_DECOMP_FILE = None key = None name = None def __init__(self): assert self.key is not None assert self.name is not None assert self.reference is not None, 'Reform requires a reference tax-benefit-system.' assert isinstance(self.reference, taxbenefitsystems.AbstractTaxBenefitSystem) self.Scenario = self.reference.Scenario if self.CURRENCY is None: currency = getattr(self.reference, 'CURRENCY', None) if currency is not None: self.CURRENCY = currency if self.DECOMP_DIR is None: decomp_dir = getattr(self.reference, 'DECOMP_DIR', None) if decomp_dir is not None: self.DECOMP_DIR = decomp_dir if self.DEFAULT_DECOMP_FILE is None: default_decomp_file = getattr(self.reference, 'DEFAULT_DECOMP_FILE', None) if default_decomp_file is not None: self.DEFAULT_DECOMP_FILE = default_decomp_file super(AbstractReform, self).__init__( entity_class_by_key_plural = self.entity_class_by_key_plural or self.reference.entity_class_by_key_plural, legislation_json = self.reference.legislation_json, ) @property def full_key(self): key = self.key assert key is not None, 'key was not set for reform {} (name: {!r})'.format(self, self.name) if self.reference is not None: reference_key = getattr(self.reference, 'key', None) if reference_key is not None: key = u'.'.join([reference_key, key]) return key def modify_legislation_json(self, modifier_function): """ Copy the reference TaxBenefitSystem legislation_json attribute and return it. Used by reforms which need to modify the legislation_json, usually in the build_reform() function. Validates the new legislation. """ reference_legislation_json = self.reference.legislation_json reference_legislation_json_copy = copy.deepcopy(reference_legislation_json) reform_legislation_json = modifier_function(reference_legislation_json_copy) assert reform_legislation_json is not None, \ 'modifier_function {} in module {} must return the modified legislation_json'.format( modifier_function.__name__, modifier_function.__module__, ) reform_legislation_json, error = legislations.validate_legislation_json(reform_legislation_json) assert error is None, \ u'The modified legislation_json of the reform "{}" is invalid, error: {}, legislation_json: {}'.format( self.key, error, reform_legislation_json).encode('utf-8') self.legislation_json = reform_legislation_json def clone_entity_class(entity_class): class ReformEntity(entity_class): pass ReformEntity.column_by_name = entity_class.column_by_name.copy() return ReformEntity def compose_reforms(build_functions_and_keys, tax_benefit_system): """ Compose reforms: the first reform is built with the given base tax-benefit system, then each one is built with the previous one as the reference. """ def compose_reforms_reducer(memo, item): build_reform, key = item reform = build_reform(key = key, tax_benefit_system = memo) assert isinstance(reform, AbstractReform), 'Reform {} returned an invalid value {!r}'.format(key, reform) return reform assert isinstance(build_functions_and_keys, list) reform = reduce(compose_reforms_reducer, build_functions_and_keys, tax_benefit_system) return reform def make_reform(key, name, reference, decomposition_dir_name = None, decomposition_file_name = None, new_formulas = None): """ Return a Reform class inherited from AbstractReform. Warning: new_formulas argument is deprecated. """ assert isinstance(key, basestring) assert isinstance(name, basestring) assert isinstance(reference, taxbenefitsystems.AbstractTaxBenefitSystem) reform_entity_class_by_key_plural = { key_plural: clone_entity_class(entity_class) for key_plural, entity_class in reference.entity_class_by_key_plural.iteritems() } reform_entity_class_by_symbol = { entity_class.symbol: entity_class for entity_class in reform_entity_class_by_key_plural.itervalues() } reform_key = key reform_name = name reform_reference = reference class Reform(AbstractReform): _constructed = False DECOMP_DIR = decomposition_dir_name DEFAULT_DECOMP_FILE = decomposition_file_name entity_class_by_key_plural = reform_entity_class_by_key_plural key = reform_key name = reform_name reference = reform_reference def __init__(self): super(Reform, self).__init__() Reform._constructed = True @classmethod def formula(cls, column): assert not cls._constructed, \ 'You are trying to add a formula to a Reform but its constructor has already been called.' return formulas.make_reference_formula_decorator( entity_class_by_symbol = reform_entity_class_by_symbol, update = True, )(column) @classmethod def input_variable(cls, entity_class = None, **kwargs): assert not cls._constructed, \ 'You are trying to add an input variable to a Reform but its constructor has already been called.' # Ensure that entity_class belongs to reform (instead of reference tax-benefit system). entity_class = cls.entity_class_by_key_plural[entity_class.key_plural] assert 'update' not in kwargs kwargs['update'] = True return formulas.reference_input_variable(entity_class = entity_class, **kwargs) if new_formulas is not None: warnings.warn( "new_formulas is deprecated. Use reform.formula decorator instead on the formula classes, " "reform being the object returned by make_reform", DeprecationWarning, ) assert isinstance(new_formulas, collections.Sequence) for new_formula in new_formulas: Reform.formula(new_formula) return Reform # Legislation helpers def update_legislation(legislation_json, path, period = None, value = None, start = None, stop = None): """ This function is deprecated. Update legislation JSON with a value defined for a specific couple of period defined by its start and stop instant or a period object. This function does not modify input parameters. """ assert value is not None if period is not None: assert start is None and stop is None, u'period parameter can\'t be used with start and stop' start = period.start stop = period.stop assert start is not None and stop is not None, u'start and stop must be provided, or period' def build_node(root_node, path_index): if isinstance(root_node, collections.Sequence): return [ build_node(node, path_index + 1) if path[path_index] == index else node for index, node in enumerate(root_node) ] elif isinstance(root_node, collections.Mapping): return collections.OrderedDict(( ( key, ( updated_legislation_items(node, start, stop, value) if path_index == len(path) - 1 else build_node(node, path_index + 1) ) if path[path_index] == key else node ) for key, node in root_node.iteritems() )) else: raise ValueError(u'Unexpected type for node: {!r}'.format(root_node)) updated_legislation = build_node(legislation_json, 0) return updated_legislation def updated_legislation_items(items, start_instant, stop_instant, value): """ This function is deprecated. Iterates items (a dict with start, stop, value key) and returns new items sorted by start date, according to these rules: * if the period matches no existing item, the new item is yielded as-is * if the period strictly overlaps another one, the new item is yielded as-is * if the period non-strictly overlaps another one, the existing item is partitioned, the period in common removed, the new item is yielded as-is and the parts of the existing item are yielded """ assert isinstance(items, collections.Sequence) new_items = [] new_item = collections.OrderedDict(( ('start', start_instant), ('stop', stop_instant), ('value', value), )) inserted = False for item in items: item_start = periods.instant(item['start']) item_stop = periods.instant(item['stop']) if item_stop < start_instant or item_start > stop_instant: # non-overlapping items are kept: add and skip new_items.append( collections.OrderedDict(( ('start', item['start']), ('stop', item['stop']), ('value', item['value']), )) ) continue if item_stop == stop_instant and item_start == start_instant: # exact matching: replace if not inserted: new_items.append( collections.OrderedDict(( ('start', str(start_instant)), ('stop', str(stop_instant)), ('value', new_item['value']), )) ) inserted = True continue if item_start < start_instant and item_stop <= stop_instant: # left edge overlapping are corrected and new_item inserted new_items.append( collections.OrderedDict(( ('start', item['start']), ('stop', str(start_instant.offset(-1, 'day'))), ('value', item['value']), )) ) if not inserted: new_items.append( collections.OrderedDict(( ('start', str(start_instant)), ('stop', str(stop_instant)), ('value', new_item['value']), )) ) inserted = True if item_start < start_instant and item_stop > stop_instant: # new_item contained in item: divide, shrink left, insert, new, shrink right new_items.append( collections.OrderedDict(( ('start', item['start']), ('stop', str(start_instant.offset(-1, 'day'))), ('value', item['value']), )) ) if not inserted: new_items.append( collections.OrderedDict(( ('start', str(start_instant)), ('stop', str(stop_instant)), ('value', new_item['value']), )) ) inserted = True new_items.append( collections.OrderedDict(( ('start', str(stop_instant.offset(+1, 'day'))), ('stop', item['stop']), ('value', item['value']), )) ) if item_start >= start_instant and item_stop < stop_instant: # right edge overlapping are corrected if not inserted: new_items.append( collections.OrderedDict(( ('start', str(start_instant)), ('stop', str(stop_instant)), ('value', new_item['value']), )) ) inserted = True new_items.append( collections.OrderedDict(( ('start', str(stop_instant.offset(+1, 'day'))), ('stop', item['stop']), ('value', item['value']), )) ) if item_start >= start_instant and item_stop <= stop_instant: # drop those continue return sorted(new_items, key = lambda item: item['start']) PK 'Gy eeopenfisca_core/legislations.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Handle legislative parameters in JSON format.""" import collections import datetime import itertools import logging from . import conv, periods, taxscales def N_(message): return message log = logging.getLogger(__name__) units = [ u'currency', u'day', u'hour', u'month', u'year', ] class CompactNode(object): # Note: Attributes come from dated_node_json and are not defined in class. def __delitem__(self, key): del self.__dict__[key] def __getitem__(self, key): return self.__dict__[key] def __iter__(self): return self.__dict__.iterkeys() def __repr__(self): return '{}({})'.format(self.__class__.__name__, repr(self.__dict__)) def __setitem__(self, key, value): self.__dict__[key] = value def combine_tax_scales(self): """Combine all the MarginalRateTaxScales in the node into a single MarginalRateTaxScale.""" combined_tax_scales = None for name, child in self.iteritems(): assert isinstance(child, taxscales.AbstractTaxScale), child if combined_tax_scales is None: combined_tax_scales = taxscales.MarginalRateTaxScale(name = name) combined_tax_scales.add_bracket(0, 0) combined_tax_scales.add_tax_scale(child) return combined_tax_scales def copy(self, deep = False): new = self.__class__() for name, value in self.iteritems(): if deep: if isinstance(value, CompactNode): new[name] = value.copy(deep = deep) elif isinstance(value, taxscales.AbstractTaxScale): new[name] = value.copy() else: new[name] = value else: new[name] = value return new def get(self, key, default = None): return self.__dict__.get(key, default) def items(self): return self.__dict__.items() def iteritems(self): return self.__dict__.iteritems() def iterkeys(self): return self.__dict__.iterkeys() def itervalues(self): return self.__dict__.itervalues() def keys(self): return self.__dict__.keys() def pop(self, key, default = None): return self.__dict__.pop(key, default) def scale_tax_scales(self, factor): """Scale all the MarginalRateTaxScales in the node.""" scaled_node = CompactNode() for key, child in self.iteritems(): scaled_node[key] = child.scale_tax_scales(factor) return scaled_node def update(self, value): if isinstance(value, CompactNode): value = value.__dict__ return self.__dict__.update(value) def values(self): return self.__dict__.values() class CompactRootNode(CompactNode): instant = None class TracedCompactNode(object): """ A proxy for CompactNode which stores the a simulation instance. Used for simulations with trace mode enabled. Overload __delitem__, getitem__ and __setitem__ even if __getattribute__ is defined because of: http://stackoverflow.com/questions/11360020/why-is-getattribute-not-invoked-on-an-implicit-getitem-invocation """ compact_node = None full_name = None instant = None simulation = None traced_attributes_name = None def __init__(self, compact_node, full_name, instant, simulation, traced_attributes_name): self.compact_node = compact_node self.full_name = full_name self.instant = instant self.simulation = simulation self.traced_attributes_name = traced_attributes_name def __delitem__(self, key): del self.compact_node.__dict__[key] def __getattr__(self, name): value = getattr(self.compact_node, name) if name in self.traced_attributes_name: calling_frame = self.simulation.stack_trace[-1] caller_parameters_infos = calling_frame['parameters_infos'] parameter_name = u'.'.join([self.full_name, name]) parameter_infos = { "instant": str(self.instant), "name": parameter_name, } if isinstance(value, taxscales.AbstractTaxScale): # Do not serialize value in JSON for tax scales since they are too big. parameter_infos["@type"] = "Scale" else: parameter_infos.update({"@type": "Parameter", "value": value}) if parameter_infos not in caller_parameters_infos: caller_parameters_infos.append(collections.OrderedDict(sorted(parameter_infos.iteritems()))) return value def __getitem__(self, key): return self.compact_node.__dict__[key] def __setitem__(self, key, value): self.compact_node.__dict__[key] = value # Functions def compact_dated_node_json(dated_node_json, code = None, instant = None, parent_codes = None, traced_simulation = None): """ Compacts a dated node JSON into a hierarchy of CompactNode objects. The "traced_simulation" argument can be used for simulations with trace mode enabled, this stores parameter values in the traceback. """ node_type = dated_node_json['@type'] if node_type == u'Node': if code is None: # Root node assert instant is None, instant compact_node = CompactRootNode() compact_node.instant = instant = periods.instant(dated_node_json['instant']) else: assert instant is not None compact_node = CompactNode() compact_node_dict = compact_node.__dict__ for key, value in dated_node_json['children'].iteritems(): child_parent_codes = None if traced_simulation is not None: child_parent_codes = [] if parent_codes is None else parent_codes[:] if code is not None: child_parent_codes += [code] child_parent_codes = child_parent_codes or None compact_node_dict[key] = compact_dated_node_json( value, code = key, instant = instant, parent_codes = child_parent_codes, traced_simulation = traced_simulation, ) if traced_simulation is not None: traced_children_code = [ key for key, value in dated_node_json['children'].iteritems() if value['@type'] != u'Node' ] # Only trace Nodes which have at least one Parameter child. if traced_children_code: full_name = u'.'.join((parent_codes or []) + [code]) compact_node = TracedCompactNode( compact_node = compact_node, full_name = full_name, instant = instant, simulation = traced_simulation, traced_attributes_name = traced_children_code, ) return compact_node assert instant is not None if node_type == u'Parameter': return dated_node_json.get('value') assert node_type == u'Scale' if any('amount' in bracket for bracket in dated_node_json['brackets']): # AmountTaxScale tax_scale = taxscales.AmountTaxScale(name = code, option = dated_node_json.get('option')) for dated_bracket_json in dated_node_json['brackets']: amount = dated_bracket_json.get('amount') assert not isinstance(amount, list) threshold = dated_bracket_json.get('threshold') assert not isinstance(threshold, list) if amount is not None and threshold is not None: tax_scale.add_bracket(threshold, amount) return tax_scale rates_kind = dated_node_json.get('rates_kind', None) if rates_kind == "average": # LinearAverageRateTaxScale tax_scale = taxscales.LinearAverageRateTaxScale( name = code, option = dated_node_json.get('option'), unit = dated_node_json.get('unit'), ) else: # MarginalRateTaxScale tax_scale = taxscales.MarginalRateTaxScale(name = code, option = dated_node_json.get('option')) for dated_bracket_json in dated_node_json['brackets']: base = dated_bracket_json.get('base', 1) assert not isinstance(base, list) rate = dated_bracket_json.get('rate') assert not isinstance(rate, list) threshold = dated_bracket_json.get('threshold') assert not isinstance(threshold, list) if rate is not None and threshold is not None: tax_scale.add_bracket(threshold, rate * base) return tax_scale def generate_dated_bracket_json(bracket_json, legislation_start_str, legislation_stop_str, instant_str): dated_bracket_json = collections.OrderedDict() for key, value in bracket_json.iteritems(): if key in ('amount', 'base', 'rate', 'threshold'): dated_value = generate_dated_json_value(value, legislation_start_str, legislation_stop_str, instant_str) if dated_value is not None: dated_bracket_json[key] = dated_value else: dated_bracket_json[key] = value return dated_bracket_json def generate_dated_json_value(values_json, legislation_start_str, legislation_stop_str, instant_str): max_stop_str = None max_value = None min_start_str = None min_value = None for value_json in values_json: value_start_str = value_json['start'] value_stop_str = value_json['stop'] if value_start_str <= instant_str <= value_stop_str: return value_json['value'] if max_stop_str is None or value_stop_str > max_stop_str: max_stop_str = value_stop_str max_value = value_json['value'] if min_start_str is None or value_start_str < min_start_str: min_start_str = value_start_str min_value = value_json['value'] if instant_str > legislation_stop_str: # The requested date is after the end of the legislation. Use the value of the last period, when this # period ends the same day or after the legislation. if max_stop_str is not None and max_stop_str >= legislation_stop_str: return max_value elif instant_str < legislation_start_str: # The requested date is before the beginning of the legislation. Use the value of the first period, when this # period begins the same day or before the legislation. if min_start_str is not None and min_start_str <= legislation_start_str: return min_value return None def generate_dated_legislation_json(legislation_json, instant): instant_str = str(periods.instant(instant)) dated_legislation_json = generate_dated_node_json( legislation_json, legislation_json['start'], legislation_json['stop'], instant_str, ) dated_legislation_json['@context'] = u'http://openfisca.fr/contexts/dated-legislation.jsonld' dated_legislation_json['instant'] = instant_str return dated_legislation_json def generate_dated_node_json(node_json, legislation_start_str, legislation_stop_str, instant_str): dated_node_json = collections.OrderedDict() for key, value in node_json.iteritems(): if key == 'children': # Occurs when @type == 'Node'. dated_children_json = type(value)( (child_code, dated_child_json) for child_code, dated_child_json in ( ( child_code, generate_dated_node_json(child_json, legislation_start_str, legislation_stop_str, instant_str), ) for child_code, child_json in value.iteritems() ) if dated_child_json is not None ) if not dated_children_json: return None dated_node_json[key] = dated_children_json elif key in ('start', 'stop'): pass elif key == 'brackets': # Occurs when @type == 'Scale'. dated_brackets_json = [ dated_bracket_json for dated_bracket_json in ( generate_dated_bracket_json(bracket_json, legislation_start_str, legislation_stop_str, instant_str) for bracket_json in value ) if dated_bracket_json is not None ] if not dated_brackets_json: return None dated_node_json[key] = dated_brackets_json elif key == 'values': # Occurs when @type == 'Parameter'. dated_value = generate_dated_json_value(value, legislation_start_str, legislation_stop_str, instant_str) if dated_value is None: return None dated_node_json['value'] = dated_value else: dated_node_json[key] = value return dated_node_json # Level-1 Converters def make_validate_values_json_dates(require_consecutive_dates = False): def validate_values_json_dates(values_json, state = None): if not values_json: return values_json, None if state is None: state = conv.default_state errors = {} for index, value_json in enumerate(values_json): if value_json['start'] > value_json['stop']: errors[index] = dict(to = state._(u"Last date must be greater than first date")) sorted_values_json = sorted(values_json, key = lambda value_json: value_json['start'], reverse = True) next_value_json = sorted_values_json[0] for index, value_json in enumerate(itertools.islice(sorted_values_json, 1, None)): next_date_str = (datetime.date(*(int(fragment) for fragment in value_json['stop'].split('-'))) + datetime.timedelta(days = 1)).isoformat() if require_consecutive_dates and next_date_str < next_value_json['start']: errors.setdefault(index, {})['start'] = state._(u"Dates of values are not consecutive") elif next_date_str > next_value_json['start']: errors.setdefault(index, {})['start'] = state._(u"Dates of values overlap") next_value_json = value_json return sorted_values_json, errors or None return validate_values_json_dates def validate_dated_legislation_json(dated_legislation_json, state = None): if dated_legislation_json is None: return None, None if state is None: state = conv.default_state dated_legislation_json, error = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( instant = conv.pipe( conv.test_isinstance(basestring), conv.iso8601_input_to_date, conv.date_to_iso8601_str, conv.not_none, ), ), constructor = collections.OrderedDict, default = conv.noop, drop_none_values = 'missing', keep_value_order = True, ), )(dated_legislation_json, state = state) if error is not None: return dated_legislation_json, error instant = dated_legislation_json.pop('instant') dated_legislation_json, error = validate_dated_node_json(dated_legislation_json, state = state) dated_legislation_json['instant'] = instant return dated_legislation_json, error def validate_dated_node_json(node, state = None): if node is None: return None, None state = conv.add_ancestor_to_state(state, node) validated_node, error = conv.test_isinstance(dict)(node, state = state) if error is not None: conv.remove_ancestor_from_state(state, node) return validated_node, error validated_node, errors = conv.struct( { '@context': conv.pipe( conv.test_isinstance(basestring), conv.make_input_to_url(full = True), conv.test_equals(u'http://openfisca.fr/contexts/dated-legislation.jsonld'), ), '@type': conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.test_in((u'Node', u'Parameter', u'Scale')), conv.not_none, ), 'comment': conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), 'description': conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, ), }, constructor = collections.OrderedDict, default = conv.noop, drop_none_values = 'missing', keep_value_order = True, )(validated_node, state = state) if errors is not None: conv.remove_ancestor_from_state(state, node) return validated_node, errors validated_node.pop('@context', None) # Remove optional @context everywhere. It will be added to root node later. node_converters = { '@type': conv.noop, 'comment': conv.noop, 'description': conv.noop, } node_type = validated_node['@type'] if node_type == u'Node': node_converters.update(dict( children = conv.pipe( conv.test_isinstance(dict), conv.uniform_mapping( conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.not_none, ), conv.pipe( validate_dated_node_json, conv.not_none, ), ), conv.empty_to_none, conv.not_none, ), )) elif node_type == u'Parameter': node_converters.update(dict( format = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in([ 'boolean', 'float', 'integer', 'rate', ]), ), unit = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in(units), ), value = conv.pipe( conv.item_or_sequence( validate_dated_value_json, ), conv.not_none, ), )) else: assert node_type == u'Scale' node_converters.update(dict( option = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in(( 'contrib', 'main-d-oeuvre', 'noncontrib', )), ), brackets = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_dated_bracket_json, drop_none_items = True, ), validate_dated_brackets_json_types, conv.empty_to_none, conv.not_none, ), unit = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in(( 'currency', )), ), )) validated_node, errors = conv.struct( node_converters, constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, )(validated_node, state = state) conv.remove_ancestor_from_state(state, node) return validated_node, errors def validate_dated_bracket_json(bracket, state = None): if bracket is None: return None, None state = conv.add_ancestor_to_state(state, bracket) validated_bracket, errors = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( amount = conv.item_or_sequence( validate_dated_value_json, ), base = conv.item_or_sequence( conv.pipe( validate_dated_value_json, conv.test_greater_or_equal(0), ), ), comment = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), rate = conv.item_or_sequence( conv.pipe( validate_dated_value_json, conv.test_between(0, 1), ), ), threshold = conv.item_or_sequence( conv.pipe( validate_dated_value_json, conv.test_greater_or_equal(0), ), ), ), constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, ), )(bracket, state = state) conv.remove_ancestor_from_state(state, bracket) return validated_bracket, errors def validate_dated_brackets_json_types(brackets, state = None): if not brackets: return brackets, None has_amount = any( 'amount' in bracket for bracket in brackets ) if has_amount: if state is None: state = conv.default_state errors = {} for bracket_index, bracket in enumerate(brackets): if 'base' in bracket: errors.setdefault(bracket_index, {})['base'] = state._(u"A scale can't contain both amounts and bases") if 'rate' in bracket: errors.setdefault(bracket_index, {})['rate'] = state._(u"A scale can't contain both amounts and rates") if errors: return brackets, errors return brackets, None def validate_dated_value_json(value, state = None): if value is None: return None, None container = state.ancestors[-1] container_format = container.get('format') value_converters = dict( boolean = conv.condition( conv.test_isinstance(int), conv.test_in((0, 1)), conv.test_isinstance(bool), ), float = conv.condition( conv.test_isinstance(int), conv.anything_to_float, conv.test_isinstance(float), ), integer = conv.condition( conv.test_isinstance(float), conv.pipe( conv.test(lambda number: round(number) == number), conv.function(int), ), conv.test_isinstance(int), ), rate = conv.condition( conv.test_isinstance(int), conv.anything_to_float, conv.test_isinstance(float), ), ) value_converter = value_converters.get(container_format or 'float') # Only parameters have a "format". assert value_converter is not None, 'Wrong format "{}", allowed: {}, container: {}'.format( container_format, value_converters.keys(), container) return value_converter(value, state = state or conv.default_state) def validate_legislation_json(legislation, state = None): if legislation is None: return None, None if state is None: state = conv.default_state legislation, error = conv.pipe( conv.test_isinstance(dict), conv.struct( { 'start': conv.pipe( conv.test_isinstance(basestring), conv.iso8601_input_to_date, conv.date_to_iso8601_str, conv.not_none, ), 'stop': conv.pipe( conv.test_isinstance(basestring), conv.iso8601_input_to_date, conv.date_to_iso8601_str, conv.not_none, ), }, constructor = collections.OrderedDict, default = conv.noop, drop_none_values = 'missing', keep_value_order = True, ), )(legislation, state = state) if error is not None: return legislation, error start = legislation.pop('start') stop = legislation.pop('stop') legislation, error = validate_node_json(legislation, state = state) legislation['start'] = start legislation['stop'] = stop return legislation, error def validate_node_json(node, state = None): if node is None: return None, None state = conv.add_ancestor_to_state(state, node) validated_node, error = conv.test_isinstance(dict)(node, state = state) if error is not None: conv.remove_ancestor_from_state(state, node) return validated_node, error validated_node, errors = conv.struct( { '@context': conv.pipe( conv.test_isinstance(basestring), conv.make_input_to_url(full = True), conv.test_equals(u'http://openfisca.fr/contexts/legislation.jsonld'), ), '@type': conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.test_in((u'Node', u'Parameter', u'Scale')), conv.not_none, ), 'comment': conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), 'description': conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, ), 'end_line_number': conv.test_isinstance(int), 'start_line_number': conv.test_isinstance(int), }, constructor = collections.OrderedDict, default = conv.noop, drop_none_values = 'missing', keep_value_order = True, )(validated_node, state = state) if errors is not None: conv.remove_ancestor_from_state(state, node) return validated_node, errors validated_node.pop('@context', None) # Remove optional @context everywhere. It will be added to root node later. node_converters = { '@type': conv.noop, 'comment': conv.noop, 'description': conv.noop, 'end_line_number': conv.noop, 'start_line_number': conv.noop, } node_type = validated_node['@type'] if node_type == u'Node': node_converters.update(dict( children = conv.pipe( conv.test_isinstance(dict), conv.uniform_mapping( conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.not_none, ), conv.pipe( validate_node_json, conv.not_none, ), ), conv.empty_to_none, conv.not_none, ), )) elif node_type == u'Parameter': node_converters.update(dict( format = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in([ 'boolean', 'float', 'integer', 'rate', ]), ), unit = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in(units), ), values = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_value_json, drop_none_items = True, ), make_validate_values_json_dates(require_consecutive_dates = True), conv.empty_to_none, conv.not_none, ), )) else: assert node_type == u'Scale' node_converters.update(dict( brackets = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_bracket_json, drop_none_items = True, ), validate_brackets_json_types, validate_brackets_json_dates, conv.empty_to_none, conv.not_none, ), option = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in(( 'contrib', 'main-d-oeuvre', 'noncontrib', )), ), rates_kind = conv.pipe( conv.test_isinstance(basestring), conv.test_in(( 'average', )), ), unit = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in(( 'currency', )), ), )) validated_node, errors = conv.struct( node_converters, constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, )(validated_node, state = state) conv.remove_ancestor_from_state(state, node) return validated_node, errors def validate_bracket_json(bracket, state = None): if bracket is None: return None, None state = conv.add_ancestor_to_state(state, bracket) validated_bracket, errors = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( amount = validate_values_holder_json, base = validate_values_holder_json, comment = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), end_line_number = conv.test_isinstance(int), rate = validate_values_holder_json, start_line_number = conv.test_isinstance(int), threshold = conv.pipe( validate_values_holder_json, conv.not_none, ), ), constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, ), conv.test(lambda bracket: bool(bracket.get('amount')) ^ bool(bracket.get('rate')), error = N_(u"Either amount or rate must be provided")), )(bracket, state = state) conv.remove_ancestor_from_state(state, bracket) return validated_bracket, errors def validate_brackets_json_dates(brackets, state = None): if not brackets: return brackets, None if state is None: state = conv.default_state errors = {} previous_bracket = brackets[0] for bracket_index, bracket in enumerate(itertools.islice(brackets, 1, None), 1): for key in ('amount', 'base', 'rate', 'threshold'): valid_segments = [] for value_json in (previous_bracket.get(key) or []): from_date = datetime.date(*(int(fragment) for fragment in value_json['start'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_json['stop'].split('-'))) if valid_segments and valid_segments[-1][0] == to_date + datetime.timedelta(days = 1): valid_segments[-1] = (from_date, valid_segments[-1][1]) else: valid_segments.append((from_date, to_date)) for value_index, value_json in enumerate(bracket.get(key) or []): from_date = datetime.date(*(int(fragment) for fragment in value_json['start'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_json['stop'].split('-'))) for valid_segment in valid_segments: if valid_segment[0] <= from_date and to_date <= valid_segment[1]: break else: errors.setdefault(bracket_index, {}).setdefault(key, {}).setdefault(value_index, {})['start'] = state._(u"Dates don't belong to valid dates of previous bracket") previous_bracket = bracket if errors: return brackets, errors for bracket_index, bracket in enumerate(itertools.islice(brackets, 1, None), 1): amount_segments = [] for value_json in (bracket.get('amount') or []): from_date = datetime.date(*(int(fragment) for fragment in value_json['start'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_json['stop'].split('-'))) if amount_segments and amount_segments[-1][0] == to_date + datetime.timedelta(days = 1): amount_segments[-1] = (from_date, amount_segments[-1][1]) else: amount_segments.append((from_date, to_date)) rate_segments = [] for value_json in (bracket.get('rate') or []): from_date = datetime.date(*(int(fragment) for fragment in value_json['start'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_json['stop'].split('-'))) if rate_segments and rate_segments[-1][0] == to_date + datetime.timedelta(days = 1): rate_segments[-1] = (from_date, rate_segments[-1][1]) else: rate_segments.append((from_date, to_date)) threshold_segments = [] for value_json in (bracket.get('threshold') or []): from_date = datetime.date(*(int(fragment) for fragment in value_json['start'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_json['stop'].split('-'))) if threshold_segments and threshold_segments[-1][0] == to_date + datetime.timedelta(days = 1): threshold_segments[-1] = (from_date, threshold_segments[-1][1]) else: threshold_segments.append((from_date, to_date)) for value_index, value_json in enumerate(bracket.get('base') or []): from_date = datetime.date(*(int(fragment) for fragment in value_json['start'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_json['stop'].split('-'))) for rate_segment in rate_segments: if rate_segment[0] <= from_date and to_date <= rate_segment[1]: break else: errors.setdefault(bracket_index, {}).setdefault('base', {}).setdefault(value_index, {})['start'] = state._(u"Dates don't belong to rate dates") for value_index, value_json in enumerate(bracket.get('amount') or []): from_date = datetime.date(*(int(fragment) for fragment in value_json['start'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_json['stop'].split('-'))) for threshold_segment in threshold_segments: if threshold_segment[0] <= from_date and to_date <= threshold_segment[1]: break else: errors.setdefault(bracket_index, {}).setdefault('amount', {}).setdefault(value_index, {})['start'] = state._(u"Dates don't belong to threshold dates") for value_index, value_json in enumerate(bracket.get('rate') or []): from_date = datetime.date(*(int(fragment) for fragment in value_json['start'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_json['stop'].split('-'))) for threshold_segment in threshold_segments: if threshold_segment[0] <= from_date and to_date <= threshold_segment[1]: break else: errors.setdefault(bracket_index, {}).setdefault('rate', {}).setdefault(value_index, {})['start'] = state._(u"Dates don't belong to threshold dates") for value_index, value_json in enumerate(bracket.get('threshold') or []): from_date = datetime.date(*(int(fragment) for fragment in value_json['start'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_json['stop'].split('-'))) for amount_segment in amount_segments: if amount_segment[0] <= from_date and to_date <= amount_segment[1]: break else: for rate_segment in rate_segments: if rate_segment[0] <= from_date and to_date <= rate_segment[1]: break else: errors.setdefault(bracket_index, {}).setdefault('threshold', {}).setdefault(value_index, {})['start'] = state._(u"Dates don't belong to amount or rate dates") return brackets, errors or None def validate_brackets_json_types(brackets, state = None): if not brackets: return brackets, None has_amount = any( 'amount' in bracket for bracket in brackets ) if has_amount: if state is None: state = conv.default_state errors = {} for bracket_index, bracket in enumerate(brackets): if 'base' in bracket: errors.setdefault(bracket_index, {})['base'] = state._(u"A scale can't contain both amounts and bases") if 'rate' in bracket: errors.setdefault(bracket_index, {})['rate'] = state._(u"A scale can't contain both amounts and rates") if errors: return brackets, errors return brackets, None def validate_value_json(value, state = None): if value is None: return None, None container = state.ancestors[-1] container_format = container.get('format') value_converters = dict( boolean = conv.condition( conv.test_isinstance(int), conv.test_in((0, 1)), conv.test_isinstance(bool), ), float = conv.condition( conv.test_isinstance(int), conv.anything_to_float, conv.test_isinstance(float), ), integer = conv.condition( conv.test_isinstance(float), conv.pipe( conv.test(lambda number: round(number) == number), conv.function(int), ), conv.test_isinstance(int), ), rate = conv.condition( conv.test_isinstance(int), conv.anything_to_float, conv.test_isinstance(float), ), ) value_converter = value_converters.get(container_format or 'float') # Only parameters have a "format". assert value_converter is not None, 'Wrong format "{}", allowed: {}, container: {}'.format( container_format, value_converters.keys(), container) state = conv.add_ancestor_to_state(state, value) validated_value, errors = conv.pipe( conv.test_isinstance(dict), conv.struct( { u'comment': conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), u'end_line_number': conv.test_isinstance(int), u'start': conv.pipe( conv.test_isinstance(basestring), conv.iso8601_input_to_date, conv.date_to_iso8601_str, conv.not_none, ), u'start_line_number': conv.test_isinstance(int), u'stop': conv.pipe( conv.test_isinstance(basestring), conv.iso8601_input_to_date, conv.date_to_iso8601_str, conv.not_none, ), u'value': conv.pipe( value_converter, conv.not_none, ), }, constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, ), )(value, state = state) conv.remove_ancestor_from_state(state, value) return validated_value, errors validate_values_holder_json = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_value_json, drop_none_items = True, ), make_validate_values_json_dates(require_consecutive_dates = False), conv.empty_to_none, ) # Level-2 Converters validate_any_legislation_json = conv.pipe( conv.test_isinstance(dict), conv.condition( conv.test(lambda legislation_json: 'datesim' in legislation_json), validate_dated_legislation_json, validate_legislation_json, ), ) PK 'G->CCopenfisca_core/columns.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import collections import datetime import inspect import re from biryani import strings import numpy as np from . import conv, periods from .enumerations import Enum def N_(message): return message year_or_month_or_day_re = re.compile(ur'(18|19|20)\d{2}(-(0?[1-9]|1[0-2])(-([0-2]?\d|3[0-1]))?)?$') # Base Column class Column(object): cerfa_field = None default = 0 dtype = float end = None entity = None # Obsolete: To remove once build_..._couple() functions are no more used. entity_key_plural = None formula_class = None is_period_size_independent = False # When True, value of column doesn't depend from size of period (example: age) is_permanent = False # When True, value of column doesn't depend from time (example: ID, birth) # json_type = None # Defined in sub-classes label = None law_reference = None # Either a single reference or a list of references name = None start = None survey_only = False url = None val_type = None def __init__(self, cerfa_field = None, default = None, end = None, entity = None, function = None, is_permanent = False, label = None, law_reference = None, start = None, survey_only = False, url = None, val_type = None): if cerfa_field is not None: self.cerfa_field = cerfa_field if default is not None and default != self.default: self.default = default if end is not None: self.end = end self.entity = entity or 'ind' if function is not None: self.function = function if is_permanent: self.is_permanent = True if law_reference is not None: self.law_reference = law_reference if label is not None: self.label = label if start is not None: self.start = start if survey_only: self.survey_only = True if url is not None: self.url = url if val_type is not None and val_type != self.val_type: self.val_type = val_type def empty_clone(self): return self.__class__() def is_input_variable(self): """Returns true if the column (self) is an input variable.""" from . import formulas return issubclass(self.formula_class, formulas.SimpleFormula) and self.formula_class.function is None def json_default(self): return self.default def make_json_to_array_by_period(self, period): return conv.condition( conv.test_isinstance(dict), conv.pipe( # Value is a dict of (period, value) couples. conv.uniform_mapping( conv.pipe( periods.json_or_python_to_period, conv.not_none, ), conv.pipe( conv.make_item_to_singleton(), conv.uniform_sequence( self.json_to_dated_python, ), conv.empty_to_none, conv.function(lambda cells_list: np.array(cells_list, dtype = self.dtype)), ), drop_none_values = True, ), conv.empty_to_none, ), conv.pipe( conv.make_item_to_singleton(), conv.uniform_sequence( self.json_to_dated_python, ), conv.empty_to_none, conv.function(lambda cells_list: np.array(cells_list, dtype = self.dtype)), conv.function(lambda array: {period: array}), ), ) @property def json_to_python(self): return conv.condition( conv.test_isinstance(dict), conv.pipe( # Value is a dict of (period, value) couples. conv.uniform_mapping( conv.pipe( periods.json_or_python_to_period, conv.not_none, ), self.json_to_dated_python, ), ), self.json_to_dated_python, ) def to_json(self): self_json = collections.OrderedDict(( ('@type', self.json_type), )) if self.cerfa_field is not None: self_json['cerfa_field'] = self.cerfa_field if self.default is not None: self_json['default'] = self.json_default() end = self.end if end is not None: if isinstance(end, datetime.date): end = end.isoformat() self_json['end'] = end if self.entity is not None: self_json['entity'] = self.entity_key_plural if self.label is not None: self_json['label'] = self.label line_number = self.formula_class.line_number if line_number is not None: self_json['line_number'] = line_number module = self.formula_class.__module__ if module is not None: self_json['module'] = module if self.name is not None: self_json['name'] = self.name start = self.start if start is not None: if isinstance(start, datetime.date): start = start.isoformat() self_json['start'] = start if self.survey_only: self_json['survey_only'] = self.survey_only if self.url is not None: self_json['url'] = self.url if self.val_type is not None: self_json['val_type'] = self.val_type return self_json def transform_dated_value_to_json(self, value, use_label = False): # Convert a non-NumPy Python value to JSON. return value def transform_value_to_json(self, value, use_label = False): # Convert a non-NumPy Python value to JSON. if isinstance(value, dict): return collections.OrderedDict( (str(period), self.transform_dated_value_to_json(dated_value, use_label = use_label)) for period, dated_value in value.iteritems() ) return self.transform_dated_value_to_json(value, use_label = use_label) # Level-1 Columns class BoolCol(Column): ''' A column of boolean ''' default = False dtype = np.bool is_period_size_independent = True json_type = 'Boolean' @property def input_to_dated_python(self): return conv.guess_bool @property def json_to_dated_python(self): return conv.pipe( conv.test_isinstance((basestring, bool, int)), conv.guess_bool, ) class DateCol(Column): ''' A column of Datetime 64 to store dates of people ''' dtype = 'datetime64[D]' is_period_size_independent = True json_type = 'Date' val_type = 'date' @property def input_to_dated_python(self): return conv.pipe( conv.test(year_or_month_or_day_re.match, error = N_(u'Invalid date')), conv.function(lambda birth: u'-'.join((birth.split(u'-') + [u'01', u'01'])[:3])), conv.iso8601_input_to_date, ) def json_default(self): return unicode(np.array(self.default, self.dtype)) # 0 = 1970-01-01 @property def json_to_dated_python(self): return conv.pipe( conv.condition( conv.test_isinstance(datetime.date), conv.noop, conv.condition( conv.test_isinstance(int), conv.pipe( conv.test_between(1870, 2099), conv.function(lambda year: datetime.date(year, 1, 1)), ), conv.pipe( conv.test_isinstance(basestring), conv.test(year_or_month_or_day_re.match, error = N_(u'Invalid date')), conv.function(lambda birth: u'-'.join((birth.split(u'-') + [u'01', u'01'])[:3])), conv.iso8601_input_to_date, ), ), ), conv.test_between(datetime.date(1870, 1, 1), datetime.date(2099, 12, 31)), ) def transform_dated_value_to_json(self, value, use_label = False): # Convert a non-NumPy Python value to JSON. return value.isoformat() if value is not None else value class FixedStrCol(Column): default = u'' dtype = None is_period_size_independent = True json_type = 'String' max_length = None def __init__(self, max_length = None, **kwargs): super(FixedStrCol, self).__init__(**kwargs) assert isinstance(max_length, int) self.dtype = '|S{}'.format(max_length) self.max_length = max_length def empty_clone(self): return self.__class__(max_length = self.max_length) @property def input_to_dated_python(self): return conv.test(lambda value: len(value) <= self.max_length) @property def json_to_dated_python(self): return conv.pipe( conv.condition( conv.test_isinstance((float, int)), # YAML stores strings containing only digits as numbers. conv.function(unicode), ), conv.test_isinstance(basestring), conv.test(lambda value: len(value) <= self.max_length), ) class FloatCol(Column): ''' A column of float 32 ''' dtype = np.float32 json_type = 'Float' @property def input_to_dated_python(self): return conv.input_to_float @property def json_to_dated_python(self): return conv.pipe( conv.test_isinstance((float, int, basestring)), conv.make_anything_to_float(accept_expression = True), ) class IntCol(Column): ''' A column of integer ''' dtype = np.int32 json_type = 'Integer' @property def input_to_dated_python(self): return conv.input_to_int @property def json_to_dated_python(self): return conv.pipe( conv.test_isinstance((int, basestring)), conv.make_anything_to_int(accept_expression = True), ) class StrCol(Column): default = u'' dtype = object is_period_size_independent = True json_type = 'String' @property def input_to_dated_python(self): return conv.noop @property def json_to_dated_python(self): return conv.pipe( conv.condition( conv.test_isinstance((float, int)), # YAML stores strings containing only digits as numbers. conv.function(unicode), ), conv.test_isinstance(basestring), ) # Level-2 Columns class AgeCol(IntCol): ''' A column of Int to store ages of people ''' default = -9999 is_period_size_independent = True @property def input_to_dated_python(self): return conv.pipe( super(AgeCol, self).input_to_dated_python, conv.first_match( conv.test_greater_or_equal(0), conv.test_equals(-9999), ), ) @property def json_to_dated_python(self): return conv.pipe( super(AgeCol, self).json_to_dated_python, conv.first_match( conv.test_greater_or_equal(0), conv.test_equals(-9999), ), ) class EnumCol(IntCol): ''' A column of integer with an enum ''' dtype = np.int16 enum = None index_by_slug = None is_period_size_independent = True json_type = 'Enumeration' def __init__(self, enum = None, **kwargs): super(EnumCol, self).__init__(**kwargs) assert isinstance(enum, Enum) self.enum = enum def empty_clone(self): return self.__class__(enum = self.enum) @property def input_to_dated_python(self): enum = self.enum if enum is None: return conv.input_to_int # This converters accepts either an item number or an item name. index_by_slug = self.index_by_slug if index_by_slug is None: self.index_by_slug = index_by_slug = dict( (strings.slugify(name), index) for index, name in sorted(enum._vars.iteritems()) ) return conv.condition( conv.input_to_int, conv.pipe( # Verify that item index belongs to enumeration. conv.input_to_int, conv.test_in(enum._vars), ), conv.pipe( # Convert item name to its index. conv.input_to_slug, conv.test_in(index_by_slug), conv.function(lambda slug: index_by_slug[slug]), ), ) def json_default(self): return unicode(self.default) if self.default is not None else None @property def json_to_dated_python(self): enum = self.enum if enum is None: return conv.pipe( conv.test_isinstance((basestring, int)), conv.anything_to_int, ) # This converters accepts either an item number or an item name. index_by_slug = self.index_by_slug if index_by_slug is None: self.index_by_slug = index_by_slug = dict( (strings.slugify(name), index) for index, name in sorted(enum._vars.iteritems()) ) return conv.pipe( conv.test_isinstance((basestring, int)), conv.condition( conv.anything_to_int, conv.pipe( # Verify that item index belongs to enumeration. conv.anything_to_int, conv.test_in(enum._vars), ), conv.pipe( # Convert item name to its index. conv.input_to_slug, conv.test_in(index_by_slug), conv.function(lambda slug: index_by_slug[slug]), ), ), ) def to_json(self): self_json = super(EnumCol, self).to_json() if self.enum is not None: self_json['labels'] = collections.OrderedDict( (index, label) for label, index in self.enum ) return self_json def transform_dated_value_to_json(self, value, use_label = False): # Convert a non-NumPy Python value to JSON. if use_label and self.enum is not None: return self.enum._vars.get(value, value) return value class PeriodSizeIndependentIntCol(IntCol): is_period_size_independent = True # Column couple builder def build_column(name = None, column = None, entity_class_by_symbol = None): # Obsolete. Use reference_input_variable instead. from . import formulas assert isinstance(name, basestring), name name = unicode(name) entity_class = entity_class_by_symbol[column.entity] column.entity_key_plural = entity_class.key_plural if column.is_permanent: base_function = formulas.permanent_default_value elif column.is_period_size_independent: base_function = formulas.requested_period_last_value else: base_function = formulas.requested_period_default_value caller_frame = inspect.currentframe().f_back column.formula_class = type(name.encode('utf-8'), (formulas.SimpleFormula,), dict( __module__ = inspect.getmodule(caller_frame).__name__, base_function = base_function, line_number = caller_frame.f_lineno, )) if column.label is None: column.label = name assert column.name is None column.name = name entity_column_by_name = entity_class.column_by_name assert name not in entity_column_by_name, name entity_column_by_name[name] = column PKF\:ttopenfisca_core/conv.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Conversion functions""" from biryani.baseconv import * # noqa from biryani.datetimeconv import * # noqa from biryani.jsonconv import * # noqa from biryani.states import State default_state = State() def add_ancestor_to_state(state, ancestor): if state is None: state = default_state if getattr(state, 'ancestors', None) is None: state.ancestors = [] state.ancestors.append(ancestor) return state def anything_to_strict_int(value, state = None): """Convert any python data to an integer. .. warning:: Like most converters, a ``None`` value is not converted. >>> anything_to_strict_int(42) (42, None) >>> anything_to_strict_int('42') (42, None) >>> anything_to_strict_int(u'42') (42, None) >>> anything_to_strict_int(42.0) (42, None) >>> anything_to_strict_int(42.75) (42.75, u'Value must be an integer') >>> anything_to_strict_int(u'42.75') (u'42.75', u'Value must be an integer') >>> anything_to_strict_int(u'42,75') (u'42,75', u'Value must be an integer') >>> anything_to_strict_int(None) (None, None) """ if value is None: return value, None if state is None: state = default_state if isinstance(value, int): return int(value), None try: float_value = float(value) int_value = int(float_value) except ValueError: return value, state._(u'Value must be an integer') if float_value != int_value: return value, state._(u'Value must be an integer') return int_value, None def embed_error(value, error_key, error): """Embed errors inside value.""" if error is None: return None if isinstance(value, dict): if isinstance(error, dict): for child_key, child_error in error.iteritems(): child_error = embed_error(value.get(child_key), error_key, child_error) if child_error is not None: value.setdefault(error_key, {})[child_key] = child_error else: value[error_key] = error return None if isinstance(value, list) and isinstance(error, dict): if all(isinstance(key, int) and 0 <= key < len(value) for key in error): for child_index, child_error in error.iteritems(): child_error = embed_error(value[child_index], error_key, child_error) if child_error is not None: return error return None if all(isinstance(key, basestring) and key.isdigit() and 0 <= int(key) < len(value) for key in error): for child_key, child_error in error.iteritems(): child_error = embed_error(value[int(child_key)], error_key, child_error) if child_error is not None: return error return None return error input_to_strict_int = pipe(cleanup_line, anything_to_strict_int) """Convert a string to an integer. >>> input_to_strict_int('42') (42, None) >>> input_to_strict_int(u' 42 ') (42, None) >>> input_to_strict_int(u'42.0') (42, None) >>> input_to_strict_int(u'42.75') (u'42.75', u'Value must be an integer') >>> input_to_strict_int(u'42,75') (u'42,75', u'Value must be an integer') >>> input_to_strict_int(None) (None, None) """ json_to_natural_int = pipe( test_isinstance(int), test_greater_or_equal(0), ) def remove_ancestor_from_state(state, ancestor): assert state.ancestors.pop() is ancestor if len(state.ancestors) == 0: del state.ancestors def test_in_pop(values, error = None): def remove(value): values.remove(value) return value return pipe( test_in(values, error = error), function(remove), ) PK 'G ǰopenfisca_core/entities.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from . import holders from .tools import empty_clone class AbstractEntity(object): column_by_name = None # Class attribute. Must be overridden by subclasses with an OrderedDict. count = 0 holder_by_name = None key_plural = None key_singular = None index_for_person_variable_name = None # Class attribute. Not used for persons is_persons_entity = False # Class attribute roles_count = None # Not used for persons role_for_person_variable_name = None # Class attribute. Not used for persons step_size = 0 simulation = None symbol = None # Class attribute. Must be overridden by subclasses. def __init__(self, simulation = None): self.holder_by_name = {} if self.is_persons_entity: assert self.index_for_person_variable_name is None assert self.role_for_person_variable_name is None else: assert self.index_for_person_variable_name is not None assert self.role_for_person_variable_name is not None if simulation is not None: self.simulation = simulation def clone(self, simulation): """Copy the entity just enough to be able to run the simulation without modifying the original simulation.""" new = empty_clone(self) new_dict = new.__dict__ for key, value in self.__dict__.iteritems(): if key not in ('holder_by_name', 'simulation'): new_dict[key] = value new_dict['simulation'] = simulation # Caution: holders must be cloned after the simulation has been set into new. new_dict['holder_by_name'] = { name: holder.clone(new) for name, holder in self.holder_by_name.iteritems() } return new def compute(self, column_name, period = None, accept_other_period = False, requested_formulas_by_period = None): return self.get_or_new_holder(column_name).compute(period = period, accept_other_period = accept_other_period, requested_formulas_by_period = requested_formulas_by_period) def compute_add(self, column_name, period = None, requested_formulas_by_period = None): return self.get_or_new_holder(column_name).compute_add(period = period, requested_formulas_by_period = requested_formulas_by_period) def compute_add_divide(self, column_name, period = None, requested_formulas_by_period = None): return self.get_or_new_holder(column_name).compute_add_divide(period = period, requested_formulas_by_period = requested_formulas_by_period) def compute_divide(self, column_name, period = None, requested_formulas_by_period = None): return self.get_or_new_holder(column_name).compute_divide(period = period, requested_formulas_by_period = requested_formulas_by_period) def get_array(self, column_name, period = None): return self.get_or_new_holder(column_name).get_array(period) def get_or_new_holder(self, column_name): holder = self.holder_by_name.get(column_name) if holder is None: column = self.column_by_name[column_name] self.holder_by_name[column_name] = holder = holders.Holder(column = column, entity = self) if column.formula_class is not None: holder.formula = column.formula_class(holder = holder) return holder def graph(self, column_name, edges, get_input_variables_and_parameters, nodes, visited): self.get_or_new_holder(column_name).graph(edges, get_input_variables_and_parameters, nodes, visited) def iter_member_persons_role_and_id(self, member): assert not self.is_persons_entity raise NotImplementedError('Method "iter_member_persons_role_and_id" is not implemented for class {}'.format( self.__class__.__name__)) PK 'G&{!openfisca_core/legislationsxml.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Handle legislative parameters in XML format (and convert then to JSON).""" import collections import datetime import logging import itertools import xml.etree.ElementTree from . import conv # legislation_json_key_by_xml_tag = dict( # ASSIETTE = 'base', # "base" is singular, because a bracket has only one base. # BAREME = 'scales', # CODE = 'parameters', # MONTANT = 'amount', # NODE = 'nodes', # SEUIL= 'threshold', # "threshold" is singular, because a bracket has only one base. # TAUX = 'rate', # "rate" is singular, because a bracket has only one base. # TRANCHE = 'brackets', # TODO: should be renamed to bracket # VALUE = 'values', # ) default_format = 'float' log = logging.getLogger(__name__) json_unit_by_xml_json_type = dict( age = u'year', days = u'day', hours = u'hour', monetary = u'currency', months = u'month', ) xml_json_formats = ( 'bool', 'float', 'integer', 'percent', ) # Helper functions def N_(message): return message # Level 1 converters def make_validate_values_xml_json_dates(require_consecutive_dates = False): def validate_values_xml_json_dates(values_xml_json, state = None): if not values_xml_json: return values_xml_json, None if state is None: state = conv.default_state errors = {} for index, value_xml_json in enumerate(values_xml_json): if value_xml_json['deb'] > value_xml_json['fin']: errors[index] = dict(fin = state._(u"Last date must be greater than first date")) sorted_values_xml_json = sorted(values_xml_json, key = lambda value_xml_json: value_xml_json['deb'], reverse = True) next_value_xml_json = sorted_values_xml_json[0] for index, value_xml_json in enumerate(itertools.islice(sorted_values_xml_json, 1, None)): next_date_str = (datetime.date(*(int(fragment) for fragment in value_xml_json['fin'].split('-'))) + datetime.timedelta(days = 1)).isoformat() if require_consecutive_dates and next_date_str < next_value_xml_json['deb']: errors.setdefault(index, {})['deb'] = state._(u"Dates of values are not consecutive") elif next_date_str > next_value_xml_json['deb']: errors.setdefault(index, {})['deb'] = state._(u"Dates of values overlap") next_value_xml_json = value_xml_json return sorted_values_xml_json, errors or None return validate_values_xml_json_dates def translate_xml_element_to_json_item(xml_element): json_element = collections.OrderedDict() text = xml_element.text if text is not None: text = text.strip().strip('#').strip() or None if text is not None: json_element['text'] = text start_line_number = getattr(xml_element, "start_line_number", None) end_line_number = getattr(xml_element, "end_line_number", None) if start_line_number is not None: json_element['start_line_number'] = start_line_number if end_line_number is not None and end_line_number != start_line_number: json_element['end_line_number'] = end_line_number json_element.update(xml_element.attrib) for xml_child in xml_element: json_child_key, json_child = translate_xml_element_to_json_item(xml_child) json_element.setdefault(json_child_key, []).append(json_child) tail = xml_element.tail if tail is not None: tail = tail.strip().strip('#').strip() or None if tail is not None: json_element['tail'] = tail return xml_element.tag, json_element def transform_node_xml_json_to_json(node_xml_json, root = True): comments = [] node_json = collections.OrderedDict() if root: node_json['@context'] = u'http://openfisca.fr/contexts/legislation.jsonld' node_json['@type'] = 'Node' child_json_by_code = {} for key, value in node_xml_json.iteritems(): if key == 'BAREME': for child_xml_json in value: child_code, child_json = transform_scale_xml_json_to_json(child_xml_json) child_json_by_code[child_code] = child_json elif key == 'CODE': for child_xml_json in value: child_code, child_json = transform_parameter_xml_json_to_json(child_xml_json) child_json_by_code[child_code] = child_json elif key == 'code': pass elif key == 'deb': node_json['start'] = value elif key == 'fin': node_json['stop'] = value elif key == 'NODE': for child_xml_json in value: child_code, child_json = transform_node_xml_json_to_json(child_xml_json, root = False) child_json_by_code[child_code] = child_json elif key in ('tail', 'text'): comments.append(value) else: node_json[key] = value node_json['children'] = collections.OrderedDict(sorted(child_json_by_code.iteritems())) if comments: node_json['comment'] = u'\n\n'.join(comments) return node_xml_json['code'], node_json def transform_parameter_xml_json_to_json(parameter_xml_json): comments = [] parameter_json = collections.OrderedDict() parameter_json['@type'] = 'Parameter' xml_json_value_to_json_transformer = float for key, value in parameter_xml_json.iteritems(): if key in ('code', 'taille'): pass elif key == 'format': parameter_json[key] = dict( bool = u'boolean', percent = u'rate', ).get(value, value) if value == 'bool': xml_json_value_to_json_transformer = lambda xml_json_value: bool(int(xml_json_value)) elif value == 'integer': xml_json_value_to_json_transformer = int elif key in ('tail', 'text'): comments.append(value) elif key == 'type': parameter_json['unit'] = json_unit_by_xml_json_type.get(value, value) elif key == 'VALUE': parameter_json['values'] = [ transform_value_xml_json_to_json(item, xml_json_value_to_json_transformer) for item in value ] else: parameter_json[key] = value if parameter_json.get('format') is None: parameter_json['format'] = default_format if comments: parameter_json['comment'] = u'\n\n'.join(comments) return parameter_xml_json['code'], parameter_json def transform_scale_xml_json_to_json(scale_xml_json): comments = [] scale_json = collections.OrderedDict() scale_json['@type'] = 'Scale' for key, value in scale_xml_json.iteritems(): if key == 'code': pass elif key in ('tail', 'text'): comments.append(value) elif key == 'TRANCHE': scale_json['brackets'] = [ transform_bracket_xml_json_to_json(item) for item in value ] elif key == 'type': scale_json['unit'] = json_unit_by_xml_json_type.get(value, value) else: scale_json[key] = value if comments: scale_json['comment'] = u'\n\n'.join(comments) return scale_xml_json['code'], scale_json def transform_bracket_xml_json_to_json(bracket_xml_json): comments = [] bracket_json = collections.OrderedDict() for key, value in bracket_xml_json.iteritems(): if key == 'ASSIETTE': bracket_json['base'] = transform_values_holder_xml_json_to_json(value[0]) elif key == 'code': pass elif key == 'MONTANT': bracket_json['amount'] = transform_values_holder_xml_json_to_json(value[0]) elif key == 'SEUIL': bracket_json['threshold'] = transform_values_holder_xml_json_to_json(value[0]) elif key in ('tail', 'text'): comments.append(value) elif key == 'TAUX': bracket_json['rate'] = transform_values_holder_xml_json_to_json(value[0]) else: bracket_json[key] = value if comments: bracket_json['comment'] = u'\n\n'.join(comments) return bracket_json def transform_value_xml_json_to_json(value_xml_json, xml_json_value_to_json_transformer): comments = [] value_json = collections.OrderedDict() for key, value in value_xml_json.iteritems(): assert key not in ('code', 'format', 'type') if key == 'deb': value_json['start'] = value elif key == 'fin': value_json['stop'] = value elif key in ('tail', 'text'): comments.append(value) elif key == 'valeur': try: value_json['value'] = xml_json_value_to_json_transformer(value) except TypeError: log.error(u'Invalid value: {}'.format(value)) raise else: value_json[key] = value if comments: value_json['comment'] = u'\n\n'.join(comments) return value_json def transform_values_holder_xml_json_to_json(values_holder_xml_json): return [ transform_value_xml_json_to_json(item, float) for item in values_holder_xml_json['VALUE'] ] def validate_legislation_xml_json(legislation, state = None): if legislation is None: return None, None if state is None: state = conv.default_state legislation, error = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( deb = conv.pipe( conv.test_isinstance(basestring), conv.iso8601_input_to_date, conv.date_to_iso8601_str, conv.not_none, ), fin = conv.pipe( conv.test_isinstance(basestring), conv.iso8601_input_to_date, conv.date_to_iso8601_str, conv.not_none, ), ), constructor = collections.OrderedDict, default = conv.noop, drop_none_values = 'missing', keep_value_order = True, ), )(legislation, state = state) if error is not None: return legislation, error deb = legislation.pop('deb') fin = legislation.pop('fin') legislation, error = validate_node_xml_json(legislation, state = state) legislation['deb'] = deb legislation['fin'] = fin return legislation, error def validate_node_xml_json(node, state = None): if node is None: return None, None state = conv.add_ancestor_to_state(state, node) validated_node, errors = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( BAREME = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_scale_xml_json, drop_none_items = True, ), conv.empty_to_none, ), CODE = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_parameter_xml_json, drop_none_items = True, ), conv.empty_to_none, ), code = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.not_none, ), description = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, ), end_line_number = conv.test_isinstance(int), NODE = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_node_xml_json, drop_none_items = True, ), conv.empty_to_none, ), start_line_number = conv.test_isinstance(int), tail = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), text = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), ), constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, ), )(node, state = state) if errors is None: children_groups_key = ('BAREME', 'CODE', 'NODE') if all( validated_node.get(key) is None for key in children_groups_key ): error = state._(u"At least one of the following items must be present: {}").format(state._(u', ').join( u'"{}"'.format(key) for key in children_groups_key )) errors = dict( (key, error) for key in children_groups_key ) else: errors = {} children_code = set() for key in children_groups_key: for child_index, child in enumerate(validated_node.get(key) or []): child_code = child['code'] if child_code in children_code: errors.setdefault(key, {}).setdefault(child_index, {})['code'] = state._(u"Duplicate value") else: children_code.add(child_code) conv.remove_ancestor_from_state(state, node) return validated_node, errors or None def validate_parameter_xml_json(parameter, state = None): if parameter is None: return None, None state = conv.add_ancestor_to_state(state, parameter) validated_parameter, errors = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( code = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.not_none, ), description = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, ), end_line_number = conv.test_isinstance(int), format = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in(xml_json_formats), ), start_line_number = conv.test_isinstance(int), tail = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), taille = conv.pipe( conv.test_isinstance(basestring), conv.test_in([ 'moinsde20', 'plusde20', ]), ), text = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), type = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in(json_unit_by_xml_json_type), ), VALUE = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_value_xml_json, drop_none_items = True, ), make_validate_values_xml_json_dates(require_consecutive_dates = True), conv.empty_to_none, conv.not_none, ), ), constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, ), )(parameter, state = state) conv.remove_ancestor_from_state(state, parameter) return validated_parameter, errors def validate_scale_xml_json(scale, state = None): if scale is None: return None, None state = conv.add_ancestor_to_state(state, scale) validated_scale, errors = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( code = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.not_none, ), description = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, ), end_line_number = conv.test_isinstance(int), option = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in(( 'contrib', 'main-d-oeuvre', 'noncontrib', )), ), start_line_number = conv.test_isinstance(int), TRANCHE = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_bracket_xml_json, drop_none_items = True, ), validate_brackets_xml_json_types, validate_brackets_xml_json_dates, conv.empty_to_none, conv.not_none, ), tail = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), text = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), type = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in(( 'monetary', )), ), ), constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, ), )(scale, state = state) conv.remove_ancestor_from_state(state, scale) return validated_scale, errors def validate_bracket_xml_json(bracket, state = None): if bracket is None: return None, None state = conv.add_ancestor_to_state(state, bracket) validated_bracket, errors = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( ASSIETTE = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_values_holder_xml_json, drop_none_items = True, ), conv.empty_to_none, conv.test(lambda l: len(l) == 1, error = N_(u"List must contain one and only one item")), ), code = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, ), end_line_number = conv.test_isinstance(int), MONTANT = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_values_holder_xml_json, drop_none_items = True, ), conv.empty_to_none, conv.test(lambda l: len(l) == 1, error = N_(u"List must contain one and only one item")), ), SEUIL = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_values_holder_xml_json, drop_none_items = True, ), conv.empty_to_none, conv.test(lambda l: len(l) == 1, error = N_(u"List must contain one and only one item")), conv.not_none, ), start_line_number = conv.test_isinstance(int), tail = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), TAUX = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_values_holder_xml_json, drop_none_items = True, ), conv.empty_to_none, conv.test(lambda l: len(l) == 1, error = N_(u"List must contain one and only one item")), ), text = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), ), constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, ), conv.test(lambda bracket: bool(bracket.get('MONTANT')) ^ bool(bracket.get('TAUX')), error = N_(u"Either MONTANT or TAUX must be provided")), )(bracket, state = state) conv.remove_ancestor_from_state(state, bracket) return validated_bracket, errors def validate_brackets_xml_json_dates(brackets, state = None): if not brackets: return brackets, None if state is None: state = conv.default_state errors = {} previous_bracket = brackets[0] for bracket_index, bracket in enumerate(itertools.islice(brackets, 1, None), 1): for key in ('ASSIETTE', 'MONTANT', 'SEUIL', 'TAUX'): valid_segments = [] values_holder_xml_json = previous_bracket.get(key) values_xml_json = values_holder_xml_json[0]['VALUE'] if values_holder_xml_json else [] for value_xml_json in values_xml_json: from_date = datetime.date(*(int(fragment) for fragment in value_xml_json['deb'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_xml_json['fin'].split('-'))) if valid_segments and valid_segments[-1][0] == to_date + datetime.timedelta(days = 1): valid_segments[-1] = (from_date, valid_segments[-1][1]) else: valid_segments.append((from_date, to_date)) values_holder_xml_json = bracket.get(key) values_xml_json = values_holder_xml_json[0]['VALUE'] if values_holder_xml_json else [] for value_index, value_xml_json in enumerate(values_xml_json): from_date = datetime.date(*(int(fragment) for fragment in value_xml_json['deb'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_xml_json['fin'].split('-'))) for valid_segment in valid_segments: if valid_segment[0] <= from_date and to_date <= valid_segment[1]: break else: errors.setdefault(bracket_index, {}).setdefault(key, {}).setdefault(0, {}).setdefault('VALUE', {}).setdefault(value_index, {})['deb'] = state._( u"Dates don't belong to valid dates of previous bracket") previous_bracket = bracket if errors: return brackets, errors for bracket_index, bracket in enumerate(itertools.islice(brackets, 1, None), 1): amount_segments = [] values_holder_xml_json = bracket.get('MONTANT') values_xml_json = values_holder_xml_json[0]['VALUE'] if values_holder_xml_json else [] for value_xml_json in values_xml_json: from_date = datetime.date(*(int(fragment) for fragment in value_xml_json['deb'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_xml_json['fin'].split('-'))) if amount_segments and amount_segments[-1][0] == to_date + datetime.timedelta(days = 1): amount_segments[-1] = (from_date, amount_segments[-1][1]) else: amount_segments.append((from_date, to_date)) rate_segments = [] values_holder_xml_json = bracket.get('TAUX') values_xml_json = values_holder_xml_json[0]['VALUE'] if values_holder_xml_json else [] for value_xml_json in values_xml_json: from_date = datetime.date(*(int(fragment) for fragment in value_xml_json['deb'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_xml_json['fin'].split('-'))) if rate_segments and rate_segments[-1][0] == to_date + datetime.timedelta(days = 1): rate_segments[-1] = (from_date, rate_segments[-1][1]) else: rate_segments.append((from_date, to_date)) threshold_segments = [] values_holder_xml_json = bracket.get('SEUIL') values_xml_json = values_holder_xml_json[0]['VALUE'] if values_holder_xml_json else [] for value_xml_json in values_xml_json: from_date = datetime.date(*(int(fragment) for fragment in value_xml_json['deb'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_xml_json['fin'].split('-'))) if threshold_segments and threshold_segments[-1][0] == to_date + datetime.timedelta(days = 1): threshold_segments[-1] = (from_date, threshold_segments[-1][1]) else: threshold_segments.append((from_date, to_date)) values_holder_xml_json = bracket.get('ASSIETTE') values_xml_json = values_holder_xml_json[0]['VALUE'] if values_holder_xml_json else [] for value_index, value_xml_json in enumerate(values_xml_json): from_date = datetime.date(*(int(fragment) for fragment in value_xml_json['deb'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_xml_json['fin'].split('-'))) for rate_segment in rate_segments: if rate_segment[0] <= from_date and to_date <= rate_segment[1]: break else: errors.setdefault(bracket_index, {}).setdefault('ASSIETTE', {}).setdefault(0, {}).setdefault('VALUE', {}).setdefault(value_index, {})['deb'] = state._(u"Dates don't belong to TAUX dates") values_holder_xml_json = bracket.get('TAUX') values_xml_json = values_holder_xml_json[0]['VALUE'] if values_holder_xml_json else [] for value_index, value_xml_json in enumerate(values_xml_json): from_date = datetime.date(*(int(fragment) for fragment in value_xml_json['deb'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_xml_json['fin'].split('-'))) for threshold_segment in threshold_segments: if threshold_segment[0] <= from_date and to_date <= threshold_segment[1]: break else: errors.setdefault(bracket_index, {}).setdefault('TAUX', {}).setdefault(0, {}).setdefault('VALUE', {}).setdefault(value_index, {})['deb'] = state._(u"Dates don't belong to SEUIL dates") values_holder_xml_json = bracket.get('SEUIL') values_xml_json = values_holder_xml_json[0]['VALUE'] if values_holder_xml_json else [] for value_index, value_xml_json in enumerate(values_xml_json): from_date = datetime.date(*(int(fragment) for fragment in value_xml_json['deb'].split('-'))) to_date = datetime.date(*(int(fragment) for fragment in value_xml_json['fin'].split('-'))) for rate_segment in rate_segments: if rate_segment[0] <= from_date and to_date <= rate_segment[1]: break else: for amount_segment in amount_segments: if amount_segment[0] <= from_date and to_date <= amount_segment[1]: break else: errors.setdefault(bracket_index, {}).setdefault('SEUIL', {}).setdefault(0, {}).setdefault('VALUE', {}).setdefault(value_index, {})['deb'] = state._(u"Dates don't belong to TAUX or MONTANT dates") return brackets, errors or None def validate_brackets_xml_json_types(brackets, state = None): if not brackets: return brackets, None has_amount = any( 'MONTANT' in bracket for bracket in brackets ) if has_amount: if state is None: state = conv.default_state errors = {} for bracket_index, bracket in enumerate(brackets): if 'ASSIETTE' in bracket: errors.setdefault(bracket_index, {})['ASSIETTE'] = state._( u"A scale can't contain both MONTANT and ASSIETTE") if 'TAUX' in bracket: errors.setdefault(bracket_index, {})['TAUX'] = state._(u"A scale can't contain both MONTANT and TAUX") if errors: return brackets, errors return brackets, None def validate_value_xml_json(value, state = None): if value is None: return None, None container = state.ancestors[-1] value_converter = dict( bool = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.test_in([u'0', u'1']), ), float = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.test_conv(conv.anything_to_float), ), integer = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.test_conv(conv.anything_to_strict_int), ), percent = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, conv.test_conv(conv.anything_to_float), ), )[container.get('format') or default_format] # Only CODE have a "format". state = conv.add_ancestor_to_state(state, value) validated_value, errors = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( deb = conv.pipe( conv.test_isinstance(basestring), conv.iso8601_input_to_date, conv.date_to_iso8601_str, conv.not_none, ), end_line_number = conv.test_isinstance(int), fin = conv.pipe( conv.test_isinstance(basestring), conv.iso8601_input_to_date, conv.date_to_iso8601_str, conv.not_none, ), start_line_number = conv.test_isinstance(int), tail = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), text = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_text, ), valeur = conv.pipe( value_converter, conv.not_none, ), ), constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, ), )(value, state = state) conv.remove_ancestor_from_state(state, value) return validated_value, errors validate_values_holder_xml_json = conv.struct( dict( end_line_number = conv.test_isinstance(int), start_line_number = conv.test_isinstance(int), VALUE = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_value_xml_json, drop_none_items = True, ), make_validate_values_xml_json_dates(require_consecutive_dates = False), conv.empty_to_none, conv.not_none, ), ), constructor = collections.OrderedDict, drop_none_values = 'missing', keep_value_order = True, ) def xml_legislation_file_path_to_xml(value, state = None): # From # http://bugs.python.org/issue14078#msg153907 class XMLParserWithLineNumbers(xml.etree.ElementTree.XMLParser): def _end(self, *args, **kwargs): element = super(self.__class__, self)._end(*args, **kwargs) element.end_line_number = self._parser.CurrentLineNumber return element def _start_list(self, *args, **kwargs): element = super(self.__class__, self)._start_list(*args, **kwargs) element.start_line_number = self._parser.CurrentLineNumber return element parser = XMLParserWithLineNumbers() try: legislation_tree = xml.etree.ElementTree.parse(value, parser = parser) except xml.etree.ElementTree.ParseError as error: return value, unicode(error) xml_legislation = legislation_tree.getroot() return xml_legislation, None def xml_legislation_to_json(xml_element, state = None): if xml_element is None: return None, None json_key, json_element = translate_xml_element_to_json_item(xml_element) if json_key != 'NODE': if state is None: state = conv.default_state return json_element, state._(u'Invalid root element in XML: "{}" instead of "NODE"').format(xml_element.tag) return json_element, None # Level 2 converters xml_legislation_file_path_to_json = conv.pipe( xml_legislation_file_path_to_xml, xml_legislation_to_json, validate_legislation_xml_json, conv.function(lambda value: transform_node_xml_json_to_json(value)[1]), ) PKF-openfisca_core/enumerations.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . class Enum(object): def __init__(self, varlist, start = 0): self._vars = {} self._nums = {} self._count = 0 for var in varlist: self._vars.update({self._count + start: var}) self._nums.update({var: self._count + start}) self._count += 1 def __getitem__(self, var): return self._nums[var] def __iter__(self): return self.itervars() def __len__(self): return self._count def itervars(self): for key, val in self._vars.iteritems(): yield (val, key) def itervalues(self): for val in self._vars: yield val PK 'Gopenfisca_core/web_tools.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import json import urllib import webbrowser def open_trace_tool(scenario, variables, api_url = u'http://api.openfisca.fr', trace_tool_url = u'http://www.openfisca.fr/outils/trace'): scenario_json = scenario.to_json() simulation_json = { 'scenarios': [scenario_json], 'variables': variables, } url = trace_tool_url + '?' + urllib.urlencode({ 'simulation': json.dumps(simulation_json), 'api_url': api_url, }) webbrowser.open_new_tab(url) PK 'G U"U"openfisca_core/calmar.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from __future__ import division import logging import operator from numpy import exp, ones, zeros, unique, array, dot, float64 try: from scipy.optimize import fsolve except: pass log = logging.getLogger(__name__) def linear(u): return 1 + u def linear_prime(u): return ones(u.shape, dtype = float) def raking_ratio(u): return exp(u) def raking_ratio_prime(u): return exp(u) def logit(u, low, up): a = (up - low) / ((1 - low) * (up - 1)) return (low * (up - 1) + up * (1 - low) * exp(a * u)) / (up - 1 + (1 - low) * exp(a * u)) def logit_prime(u, low, up): a = (up - low) / ((1 - low) * (up - 1)) return ((a * up * (1 - low) * exp(a * u)) * (up - 1 + (1 - low) * exp(a * u)) - (low * (up - 1) + up * (1 - low) * exp(a * u)) * (1 - low) * a * exp(a * u)) \ / (up - 1 + (1 - low) * exp(a * u)) ** 2 def build_dummies_dict(data): ''' return a dict with unique values as keys and vectors as values ''' unique_val_list = unique(data) output = {} for val in unique_val_list: output[val] = (data == val) return output def calmar(data_in, margins, parameters = {}, pondini='wprm_init'): ''' Calibraters weights according to some margins - data_in is a dict containing individual data - pondini (char) is the inital weight margins is a dict containing for each var: - a scalar var numeric variables - a dict with categories key and population - eventually a key named total_population : total population. If absent initialized to actual total population parameters is a dict containing the following keys - method : 'linear', 'raking ratio', 'logit' - lo : lower bound on weights ratio <1 - up : upper bound on weights ration >1 - use_proportions : default FALSE; if TRUE use proportions if total population from margins doesn't match total population - param xtol : relative precision on lagrangian multipliers. By default xtol = 1.49012e-08 (default fsolve xtol) - param maxfev : maximum number of function evaluation TODO ''' # remove null weights and keep original data data = dict() is_weight_not_null = (data_in[pondini] > 0) for a in data_in: data[a] = data_in[a][is_weight_not_null] if not margins: raise Exception("Calmar requires non empty dict of margins") # choice of method if 'method' not in parameters: parameters['method'] = 'linear' if parameters['method'] == 'linear': F = linear F_prime = linear_prime elif parameters['method'] == 'raking ratio': F = raking_ratio F_prime = raking_ratio_prime elif parameters['method'] == 'logit': if 'up' not in parameters: raise Exception("When method is 'logit', 'up' parameter is needed in parameters") if 'lo' not in parameters: raise Exception("When method is 'logit', 'lo' parameter is needed in parameters") if parameters['up'] <= 1: raise Exception("When method is 'logit', 'up' should be strictly greater than 1") if parameters['lo'] >= 1: raise Exception("When method is 'logit', 'lo' should be strictly less than 1") F = lambda x: logit(x, parameters['lo'], parameters['up']) F_prime = lambda x: logit_prime(x, parameters['lo'], parameters['up']) else: raise Exception("method should be 'linear', 'raking ratio' or 'logit'") # construction observations matrix if 'total_population' in margins: total_population = margins.pop('total_population') else: total_population = data[pondini].sum() if 'use_proportions' in parameters: use_proportions = parameters['use_proportions'] else: use_proportions = False nk = len(data[pondini]) # number of Lagrange parameters (at least total population) nj = 1 margins_new = {} margins_new_dict = {} for var, val in margins.iteritems(): if isinstance(val, dict): dummies_dict = build_dummies_dict(data[var]) k, pop = 0, 0 for cat, nb in val.iteritems(): cat_varname = var + '_' + str(cat) data[cat_varname] = dummies_dict[cat] margins_new[cat_varname] = nb if var not in margins_new_dict: margins_new_dict[var] = {} margins_new_dict[var][cat] = nb pop += nb k += 1 nj += 1 # Check total popualtion if pop != total_population: if use_proportions: log.info( 'calmar: categorical variable {} is inconsistent with population; using proportions'.format( var ) ) for cat, nb in val.iteritems(): cat_varname = var + '_' + str(cat) margins_new[cat_varname] = nb * total_population / pop margins_new_dict[var][cat] = nb * total_population / pop else: raise Exception('calmar: categorical variable ', var, ' is inconsistent with population') else: margins_new[var] = val margins_new_dict[var] = val nj += 1 # On conserve systematiquement la population if hasattr(data, 'dummy_is_in_pop'): raise Exception('dummy_is_in_pop is not a valid variable name') data['dummy_is_in_pop'] = ones(nk) margins_new['dummy_is_in_pop'] = total_population # paramètres de Lagrange initialisés à zéro lambda0 = zeros(nj) # initial weights d = data[pondini] x = zeros((nk, nj)) # nb obs x nb constraints xmargins = zeros(nj) margins_dict = {} j = 0 for var, val in margins_new.iteritems(): x[:, j] = data[var] xmargins[j] = val margins_dict[var] = val j += 1 # Résolution des équations du premier ordre constraint = lambda l: dot(d * F(dot(x, l)), x) - xmargins constraint_prime = lambda l: dot(d * (x.T * F_prime(dot(x, l))), x) # le jacobien celui ci-dessus est constraintprime = @(l) x*(d.*Fprime(x'*l)*x'); tries, ier = 0, 2 if 'xtol' in parameters: xtol = parameters['xtol'] else: xtol = 1.49012e-08 err_max = 1 conv = 1 while (ier == 2 or ier == 5 or ier == 4) and not (tries >= 10 or (err_max < 1e-6 and conv < 1e-8)): lambdasol, infodict, ier, mesg = fsolve( constraint, lambda0, fprime = constraint_prime, maxfev = 256, xtol = xtol, full_output = 1) lambda0 = 1 * lambdasol tries += 1 pondfin = d * F(dot(x, lambdasol)) rel_error = {} for var, val in margins_new.iteritems(): rel_error[var] = abs((data[var] * pondfin).sum() - margins_dict[var]) / margins_dict[var] sorted_err = sorted(rel_error.iteritems(), key = operator.itemgetter(1), reverse = True) conv = abs(err_max - sorted_err[0][1]) err_max = sorted_err[0][1] if (ier == 2 or ier == 5 or ier == 4): log.info("calmar: stopped after {} tries".format(tries)) # rebuilding a weight vector with the same size of the initial one pondfin_out = array(data_in[pondini], dtype = float64) pondfin_out[is_weight_not_null] = pondfin return pondfin_out, lambdasol, margins_new_dict def check_calmar(data_in, margins, pondini='wprm_init', pondfin_out = None, lambdasol = None, margins_new_dict = None): for variable, margin in margins.iteritems(): if variable != 'total_population': print variable, margin, abs(margin - margins_new_dict[variable]) / abs(margin)PKFV|˒00openfisca_core/taxscales.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openxfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from __future__ import division from bisect import bisect_left, bisect_right import copy import logging import itertools import numpy as np from numpy import maximum as max_, minimum as min_ from .tools import empty_clone log = logging.getLogger(__name__) class AbstractTaxScale(object): """Abstract class for various types of tax scales (amount-based tax scales, rate-based tax scales) French translations: * base: assiette * bracket: tranche * rate: taux * tax scale: barème * threshold: seuil """ name = None option = None thresholds = None unit = None def __init__(self, name = None, option = None, unit = None): self.name = name or 'Untitled TaxScale' if option is not None: self.option = option self.thresholds = [] if unit is not None: self.unit = unit def __eq__(self, other): raise NotImplementedError('Method "__eq__" is not implemented for {}'.format(self.__class__.__name__)) def __ne__(self, other): raise NotImplementedError('Method "__ne__" is not implemented for {}'.format(self.__class__.__name__)) def __str__(self): raise NotImplementedError('Method "__str__" is not implemented for {}'.format(self.__class__.__name__)) def calc(self, base): raise NotImplementedError('Method "calc" is not implemented for {}'.format(self.__class__.__name__)) def copy(self): new = empty_clone(self) new.__dict__ = copy.deepcopy(self.__dict__) return new class AbstractRateTaxScale(AbstractTaxScale): """Abstract class for various types of rate-based tax scales (marginal rate, linear average rate)""" rates = None def __init__(self, name = None, option = None, unit = None): super(AbstractRateTaxScale, self).__init__(name = name, option = option, unit = unit) self.rates = [] def __str__(self): return '\n'.join(itertools.chain( ['{}: {}'.format(self.__class__.__name__, self.name)], ( '- {} {}'.format(threshold, rate) for threshold, rate in itertools.izip(self.thresholds, self.rates) ), )) def add_bracket(self, threshold, rate): if threshold in self.thresholds: i = self.thresholds.index(threshold) self.rates[i] += rate else: i = bisect_left(self.thresholds, threshold) self.thresholds.insert(i, threshold) self.rates.insert(i, rate) def multiply_rates(self, factor, inplace = True, new_name = None): if inplace: assert new_name is None for i, rate in enumerate(self.rates): self.rates[i] = rate * factor return self new_tax_scale = self.__class__(new_name or self.name, option = self.option, unit = self.unit) for threshold, rate in itertools.izip(self.thresholds, self.rates): new_tax_scale.thresholds.append(threshold) new_tax_scale.rates.append(rate * factor) return new_tax_scale def multiply_thresholds(self, factor, decimals = None, inplace = True, new_name = None): if inplace: assert new_name is None for i, threshold in enumerate(self.thresholds): if decimals is not None: self.thresholds[i] = np.around(threshold * factor, decimals = decimals) else: self.thresholds[i] = threshold * factor return self new_tax_scale = self.__class__(new_name or self.name, option = self.option, unit = self.unit) for threshold, rate in itertools.izip(self.thresholds, self.rates): if decimals is not None: new_tax_scale.thresholds.append(np.around(threshold * factor, decimals = decimals)) else: new_tax_scale.thresholds.append(threshold * factor) new_tax_scale.rates.append(rate) return new_tax_scale class AmountTaxScale(AbstractTaxScale): amounts = None def __init__(self, name = None, option = None, unit = None): super(AmountTaxScale, self).__init__(name = name, option = option, unit = unit) self.amounts = [] def __str__(self): return '\n'.join(itertools.chain( ['{}: {}'.format(self.__class__.__name__, self.name)], ( '- {} {}'.format(threshold, amount) for threshold, amount in itertools.izip(self.thresholds, self.amounts) ), )) def add_bracket(self, threshold, amount): if threshold in self.thresholds: i = self.thresholds.index(threshold) self.amounts[i] += amount else: i = bisect_left(self.thresholds, threshold) self.thresholds.insert(i, threshold) self.amounts.insert(i, amount) def calc(self, base): base1 = np.tile(base, (len(self.thresholds), 1)).T thresholds1 = np.tile(np.hstack((self.thresholds, np.inf)), (len(base), 1)) a = max_(min_(base1, thresholds1[:, 1:]) - thresholds1[:, :-1], 0) return np.dot(self.amounts, a.T > 0) class LinearAverageRateTaxScale(AbstractRateTaxScale): def calc(self, base): if len(self.rates) == 1: return base * self.rates[0] tiled_base = np.tile(base, (len(self.thresholds) - 1, 1)).T tiled_thresholds = np.tile(self.thresholds, (len(base), 1)) bracket_dummy = (tiled_base >= tiled_thresholds[:, :-1]) * (tiled_base < tiled_thresholds[:, 1:]) rates_array = np.array(self.rates) thresholds_array = np.array(self.thresholds) rate_slope = (rates_array[1:] - rates_array[:-1]) / (thresholds_array[1:] - thresholds_array[:-1]) average_rate_slope = np.dot(bracket_dummy, rate_slope.T) bracket_average_start_rate = np.dot(bracket_dummy, rates_array[:-1]) bracket_threshold = np.dot(bracket_dummy, thresholds_array[:-1]) log.info("bracket_average_start_rate : {}".format(bracket_average_start_rate)) log.info("average_rate_slope: {}".format(average_rate_slope)) return base * (bracket_average_start_rate + (base - bracket_threshold) * average_rate_slope) def to_marginal(self): marginal_tax_scale = MarginalRateTaxScale(name = self.name, option = self.option, unit = self.unit) previous_I = 0 previous_threshold = 0 for threshold, rate in itertools.izip(self.thresholds[1:], self.rates[1:]): if threshold != float('Inf'): I = rate * threshold marginal_tax_scale.add_bracket(previous_threshold, (I - previous_I) / (threshold - previous_threshold)) previous_I = I previous_threshold = threshold marginal_tax_scale.add_bracket(previous_threshold, rate) return marginal_tax_scale class MarginalRateTaxScale(AbstractRateTaxScale): def add_tax_scale(self, tax_scale): if tax_scale.thresholds > 0: # Pour ne pas avoir de problèmes avec les barèmes vides for threshold_low, threshold_high, rate in itertools.izip(tax_scale.thresholds[:-1], tax_scale.thresholds[1:], tax_scale.rates): self.combine_bracket(rate, threshold_low, threshold_high) self.combine_bracket(tax_scale.rates[-1], tax_scale.thresholds[-1]) # Pour traiter le dernier threshold def calc(self, base, factor = 1, round_base_decimals = None): base1 = np.tile(base, (len(self.thresholds), 1)).T if isinstance(factor, (float, int)): factor = np.ones(len(base)) * factor thresholds1 = np.outer(factor, np.array(self.thresholds + [np.inf])) if round_base_decimals is not None: thresholds1 = np.round(thresholds1, round_base_decimals) a = max_(min_(base1, thresholds1[:, 1:]) - thresholds1[:, :-1], 0) if round_base_decimals is None: return np.dot(self.rates, a.T) else: r = np.tile(self.rates, (len(base), 1)) b = np.round(a, round_base_decimals) return np.round(r * b, round_base_decimals).sum(axis = 1) def combine_bracket(self, rate, threshold_low = 0, threshold_high = False): # Insert threshold_low and threshold_high without modifying rates if threshold_low not in self.thresholds: index = bisect_right(self.thresholds, threshold_low) - 1 self.add_bracket(threshold_low, self.rates[index]) if threshold_high and threshold_high not in self.thresholds: index = bisect_right(self.thresholds, threshold_high) - 1 self.add_bracket(threshold_high, self.rates[index]) # Use add_bracket to add rates where they belongs i = self.thresholds.index(threshold_low) if threshold_high: j = self.thresholds.index(threshold_high) - 1 else: j = len(self.thresholds) - 1 while i <= j: self.add_bracket(self.thresholds[i], rate) i += 1 def inverse(self): """Returns a new instance of MarginalRateTaxScale Inverse un barème: étant donné des seuils et des taux exprimés en fonction du brut, renvoie un barème avec les seuils et les taux exprimés en net. si revnet = revbrut - BarmMar(revbrut, B) alors revbrut = BarmMar(revnet, B.inverse()) threshold : threshold de revenu brut taxable threshold imposable : threshold de revenu imposable/déclaré theta : ordonnée à l'origine des segments des différents seuils dans une représentation du revenu imposable comme fonction linéaire par morceaux du revenu brut """ # Actually 1/(1-global-rate) inverse = self.__class__(name = self.name + "'", option = self.option, unit = self.unit) taxable_threshold = 0 for threshold, rate in itertools.izip(self.thresholds, self.rates): if threshold == 0: previous_rate = 0 theta = 0 # On calcule le seuil de revenu imposable de la tranche considérée. taxable_threshold = (1 - previous_rate) * threshold + theta inverse.add_bracket(taxable_threshold, 1 / (1 - rate)) theta = (rate - previous_rate) * threshold + theta previous_rate = rate return inverse def scale_tax_scales(self, factor): """Scale all the MarginalRateTaxScales in the node.""" assert isinstance(factor, (float, int)) scaled_tax_scale = self.copy() return scaled_tax_scale.multiply_thresholds(factor) def to_average(self): average_tax_scale = LinearAverageRateTaxScale(name = self.name, option = self.option, unit = self.unit) average_tax_scale.add_bracket(0, 0) if self.thresholds: I = 0 previous_threshold = self.thresholds[0] previous_rate = self.rates[0] for threshold, rate in itertools.islice(itertools.izip(self.thresholds, self.rates), 1, None): I += previous_rate * (threshold - previous_threshold) average_tax_scale.add_bracket(threshold, I / threshold) previous_threshold = threshold previous_rate = rate average_tax_scale.add_bracket(float('Inf'), rate) return average_tax_scale PK 'G}o۹jj openfisca_core/decompositions.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import collections import copy import os import xml from . import conv from . import decompositionsxml def calculate(simulations, decomposition_json): assert decomposition_json is not None response_json = copy.deepcopy(decomposition_json) # Use decomposition as a skeleton for response. for node in iter_decomposition_nodes(response_json, children_first = True): children = node.get('children') if children: node['values'] = map(lambda *l: sum(l), *( child['values'] for child in children )) else: node['values'] = values = [] for simulation in simulations: simulation.calculate_output(node['code']) holder = simulation.get_holder(node['code']) column = holder.column values.extend( column.transform_value_to_json(value) for value in holder.new_test_case_array(simulation.period).tolist() ) return response_json def get_decomposition_json(tax_benefit_system, xml_file_path = None): if xml_file_path is None: xml_file_path = os.path.join(tax_benefit_system.DECOMP_DIR, tax_benefit_system.DEFAULT_DECOMP_FILE) decomposition_tree = xml.etree.ElementTree.parse(xml_file_path) decomposition_xml_json = conv.check(decompositionsxml.xml_decomposition_to_json)(decomposition_tree.getroot(), state = conv.State) decomposition_xml_json = conv.check(decompositionsxml.make_validate_node_xml_json(tax_benefit_system))( decomposition_xml_json, state = conv.State) decomposition_json = decompositionsxml.transform_node_xml_json_to_json(decomposition_xml_json) return decomposition_json def iter_decomposition_nodes(node_or_nodes, children_first = False): if isinstance(node_or_nodes, list): for node in node_or_nodes: for sub_node in iter_decomposition_nodes(node, children_first = children_first): yield sub_node else: if not children_first: yield node_or_nodes children = node_or_nodes.get('children') if children: for child_node in children: for sub_node in iter_decomposition_nodes(child_node, children_first = children_first): yield sub_node if children_first: yield node_or_nodes def make_validate_node_json(tax_benefit_system): def validate_node_json(node, state = None): if node is None: return None, None if state is None: state = conv.default_state validated_node, errors = conv.pipe( conv.condition( conv.test_isinstance(list), conv.uniform_sequence( validate_node_json, drop_none_items = True, ), conv.pipe( conv.condition( conv.test_isinstance(basestring), conv.function(lambda code: dict(code = code)), conv.test_isinstance(dict), ), conv.struct( dict( children = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( validate_node_json, drop_none_items = True, ), conv.empty_to_none, ), code = conv.pipe( conv.test_isinstance(basestring), conv.cleanup_line, ), ), constructor = collections.OrderedDict, default = conv.noop, drop_none_values = 'missing', keep_value_order = True, ), ), ), conv.empty_to_none, )(node, state = state) if validated_node is None or errors is not None: return validated_node, errors if isinstance(validated_node, dict) and not validated_node.get('children'): validated_node, errors = conv.struct( dict( code = conv.pipe( conv.test_in(tax_benefit_system.column_by_name), conv.not_none, ), ), default = conv.noop, )(validated_node, state = state) return validated_node, errors return validate_node_json PKFuFX٩٩openfisca_core/periods.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Toolbox to handle date intervals A period is a triple (unit, start, size), where unit is either "month" or "year", where start format is a (year, month, day) triple, and where size is an integer > 1. Since a period is a triple it can be used as a dictionary key. """ import calendar import collections import datetime import re from . import conv N_ = lambda message: message # Note: weak references are not used, because Python 2.7 can't create weak reference to 'datetime.date' objects. date_by_instant_cache = {} str_by_instant_cache = {} year_or_month_or_day_re = re.compile(ur'(18|19|20)\d{2}(-(0?[1-9]|1[0-2])(-([0-2]?\d|3[0-1]))?)?$') class Instant(tuple): def __repr__(self): """Transform instant to to its Python representation as a string. >>> repr(instant(2014)) 'Instant((2014, 1, 1))' >>> repr(instant('2014-2')) 'Instant((2014, 2, 1))' >>> repr(instant('2014-2-3')) 'Instant((2014, 2, 3))' """ return '{}({})'.format(self.__class__.__name__, super(Instant, self).__repr__()) def __str__(self): """Transform instant to a string. >>> str(instant(2014)) '2014-01-01' >>> str(instant('2014-2')) '2014-02-01' >>> str(instant('2014-2-3')) '2014-02-03' >>> unicode(instant(2014)) u'2014-01-01' >>> unicode(instant('2014-2')) u'2014-02-01' >>> unicode(instant('2014-2-3')) u'2014-02-03' """ instant_str = str_by_instant_cache.get(self) if instant_str is None: str_by_instant_cache[self] = instant_str = self.date.isoformat() return instant_str @property def date(self): """Convert instant to a date. >>> instant(2014).date datetime.date(2014, 1, 1) >>> instant('2014-2').date datetime.date(2014, 2, 1) >>> instant('2014-2-3').date datetime.date(2014, 2, 3) """ instant_date = date_by_instant_cache.get(self) if instant_date is None: date_by_instant_cache[self] = instant_date = datetime.date(*self) return instant_date @property def day(self): """Extract day from instant. >>> instant(2014).day 1 >>> instant('2014-2').day 1 >>> instant('2014-2-3').day 3 """ return self[2] @property def month(self): """Extract month from instant. >>> instant(2014).month 1 >>> instant('2014-2').month 2 >>> instant('2014-2-3').month 2 """ return self[1] def period(self, unit, size = 1): """Create a new period starting at instant. >>> instant(2014).period('month') Period((u'month', Instant((2014, 1, 1)), 1)) >>> instant('2014-2').period('year', 2) Period((u'year', Instant((2014, 2, 1)), 2)) >>> instant('2014-2-3').period('day', size = 2) Period((u'day', Instant((2014, 2, 3)), 2)) """ assert unit in (u'day', u'month', u'year'), 'Invalid unit: {} of type {}'.format(unit, type(unit)) assert isinstance(size, int) and size >= 1, 'Invalid size: {} of type {}'.format(size, type(size)) return Period((unicode(unit), self, size)) def offset(self, offset, unit): """Increment (or decrement) the given instant with offset units. >>> instant(2014).offset(1, 'day') Instant((2014, 1, 2)) >>> instant(2014).offset(1, 'month') Instant((2014, 2, 1)) >>> instant(2014).offset(1, 'year') Instant((2015, 1, 1)) >>> instant('2014-1-31').offset(1, 'day') Instant((2014, 2, 1)) >>> instant('2014-1-31').offset(1, 'month') Instant((2014, 2, 28)) >>> instant('2014-1-31').offset(1, 'year') Instant((2015, 1, 31)) >>> instant('2011-2-28').offset(1, 'day') Instant((2011, 3, 1)) >>> instant('2011-2-28').offset(1, 'month') Instant((2011, 3, 28)) >>> instant('2012-2-29').offset(1, 'year') Instant((2013, 2, 28)) >>> instant(2014).offset(-1, 'day') Instant((2013, 12, 31)) >>> instant(2014).offset(-1, 'month') Instant((2013, 12, 1)) >>> instant(2014).offset(-1, 'year') Instant((2013, 1, 1)) >>> instant('2011-3-1').offset(-1, 'day') Instant((2011, 2, 28)) >>> instant('2011-3-31').offset(-1, 'month') Instant((2011, 2, 28)) >>> instant('2012-2-29').offset(-1, 'year') Instant((2011, 2, 28)) >>> instant('2014-1-30').offset(3, 'day') Instant((2014, 2, 2)) >>> instant('2014-10-2').offset(3, 'month') Instant((2015, 1, 2)) >>> instant('2014-1-1').offset(3, 'year') Instant((2017, 1, 1)) >>> instant(2014).offset(-3, 'day') Instant((2013, 12, 29)) >>> instant(2014).offset(-3, 'month') Instant((2013, 10, 1)) >>> instant(2014).offset(-3, 'year') Instant((2011, 1, 1)) >>> instant(2014).offset('first-of', 'month') Instant((2014, 1, 1)) >>> instant('2014-2').offset('first-of', 'month') Instant((2014, 2, 1)) >>> instant('2014-2-3').offset('first-of', 'month') Instant((2014, 2, 1)) >>> instant(2014).offset('first-of', 'year') Instant((2014, 1, 1)) >>> instant('2014-2').offset('first-of', 'year') Instant((2014, 1, 1)) >>> instant('2014-2-3').offset('first-of', 'year') Instant((2014, 1, 1)) >>> instant(2014).offset('last-of', 'month') Instant((2014, 1, 31)) >>> instant('2014-2').offset('last-of', 'month') Instant((2014, 2, 28)) >>> instant('2012-2-3').offset('last-of', 'month') Instant((2012, 2, 29)) >>> instant(2014).offset('last-of', 'year') Instant((2014, 12, 31)) >>> instant('2014-2').offset('last-of', 'year') Instant((2014, 12, 31)) >>> instant('2014-2-3').offset('last-of', 'year') Instant((2014, 12, 31)) """ year, month, day = self if offset == 'first-of': if unit == u'month': day = 1 else: assert unit == u'year', 'Invalid unit: {} of type {}'.format(unit, type(unit)) month = 1 day = 1 elif offset == 'last-of': if unit == u'month': day = calendar.monthrange(year, month)[1] else: assert unit == u'year', 'Invalid unit: {} of type {}'.format(unit, type(unit)) month = 12 day = 31 else: assert isinstance(offset, int), 'Invalid offset: {} of type {}'.format(offset, type(offset)) if unit == u'day': day += offset if offset < 0: while day < 1: month -= 1 if month == 0: year -= 1 month = 12 day += calendar.monthrange(year, month)[1] elif offset > 0: month_last_day = calendar.monthrange(year, month)[1] while day > month_last_day: month += 1 if month == 13: year += 1 month = 1 day -= month_last_day month_last_day = calendar.monthrange(year, month)[1] elif unit == u'month': month += offset if offset < 0: while month < 1: year -= 1 month += 12 elif offset > 0: while month > 12: year += 1 month -= 12 month_last_day = calendar.monthrange(year, month)[1] if day > month_last_day: day = month_last_day else: assert unit == u'year', 'Invalid unit: {} of type {}'.format(unit, type(unit)) year += offset # Handle february month of leap year. month_last_day = calendar.monthrange(year, month)[1] if day > month_last_day: day = month_last_day return self.__class__((year, month, day)) @property def year(self): """Extract year from instant. >>> instant(2014).year 2014 >>> instant('2014-2').year 2014 >>> instant('2014-2-3').year 2014 """ return self[0] class Period(tuple): def __repr__(self): """Transform period to to its Python representation as a string. >>> repr(period('year', 2014)) "Period((u'year', Instant((2014, 1, 1)), 1))" >>> repr(period('month', '2014-2')) "Period((u'month', Instant((2014, 2, 1)), 1))" >>> repr(period('day', '2014-2-3')) "Period((u'day', Instant((2014, 2, 3)), 1))" """ return '{}({})'.format(self.__class__.__name__, super(Period, self).__repr__()) def __str__(self): """Transform period to a string. >>> unicode(period(u'year', 2014)) u'2014' >>> unicode(period(u'month', 2014)) u'month:2014' >>> unicode(period(u'day', 2014)) u'day:2014' >>> unicode(period(u'year', '2014-2')) u'year:2014-02' >>> unicode(period(u'month', '2014-2')) u'2014-02' >>> unicode(period(u'day', '2014-2')) u'day:2014-02' >>> unicode(period(u'year', '2014-3-2')) u'year:2014-03-02' >>> unicode(period(u'month', '2014-3-2')) u'month:2014-03-02' >>> unicode(period(u'day', '2014-3-2')) u'2014-03-02' >>> unicode(period(u'year', 2012, size = 2)) u'2012:2' >>> unicode(period(u'month', 2012, size = 2)) u'2012-01:2' >>> unicode(period(u'day', 2012, size = 2)) u'2012-01-01:2' >>> unicode(period(u'year', '2012-3', size = 2)) u'year:2012-03:2' >>> unicode(period(u'month', '2012-3', size = 2)) u'2012-03:2' >>> unicode(period(u'day', '2012-3', size = 2)) u'2012-03-01:2' >>> unicode(period(u'year', '2012-3-3', size = 2)) u'year:2012-03-03:2' >>> unicode(period(u'month', '2012-3-3', size = 2)) u'month:2012-03-03:2' >>> unicode(period(u'day', '2012-3-3', size = 2)) u'2012-03-03:2' """ unit, start_instant, size = self year, month, day = start_instant if day == 1: if month == 1 and (unit == u'day' and size == (366 if calendar.isleap(year) else 365) or unit == u'month' and size == 12 or unit == u'year'): start_instant = start_instant[:1] if unit != u'year': size = None elif unit == u'day' and size == calendar.monthrange(year, month)[1] or unit in (u'month', u'year'): start_instant = start_instant[:2] if unit not in (u'month', u'year'): size = None if unit == u'day' and len(start_instant) == 3 \ or unit == u'month' and len(start_instant) == 2 \ or unit == u'year' and len(start_instant) == 1: unit = None start_str = u'-'.join( unicode(fragment) if index == 0 else u'{:02d}'.format(fragment) for index, fragment in enumerate(start_instant) ) size_str = unicode(size) if size is not None and size > 1 else None return u':'.join( fragment for fragment in (unit, start_str, size_str) if fragment is not None ) @property def date(self): assert self.size == 1, '"date" is undefined for a period of size > 1: {}'.format(self) return self.start.date @property def days(self): """Count the number of days in period. >>> period('day', 2014).days 365 >>> period('month', 2014).days 365 >>> period('year', 2014).days 365 >>> period('day', '2014-2').days 28 >>> period('month', '2014-2').days 28 >>> period('year', '2014-2').days 365 >>> period('day', '2014-2-3').days 1 >>> period('month', '2014-2-3').days 28 >>> period('year', '2014-2-3').days 365 """ return (self.stop.date - self.start.date).days + 1 def intersection(self, start, stop): if start is None and stop is None: return self period_start = self[1] period_stop = self.stop if start is None: start = period_start if stop is None: stop = period_stop if stop < period_start or period_stop < start: return None intersection_start = max(period_start, start) intersection_stop = min(period_stop, stop) if intersection_start == period_start and intersection_stop == period_stop: return self if intersection_start.day == 1 and intersection_start.month == 1 \ and intersection_stop.day == 31 and intersection_stop.month == 12: return self.__class__(( u'year', intersection_start, intersection_stop.year - intersection_start.year + 1, )) if intersection_start.day == 1 and intersection_stop.day == calendar.monthrange(intersection_stop.year, intersection_stop.month)[1]: return self.__class__(( u'month', intersection_start, ((intersection_stop.year - intersection_start.year) * 12 + intersection_stop.month - intersection_start.month + 1), )) return self.__class__(( u'day', intersection_start, (intersection_stop.date - intersection_start.date).days + 1, )) def offset(self, offset, unit = None): """Increment (or decrement) the given period with offset units. >>> period('day', 2014).offset(1) Period((u'day', Instant((2014, 1, 2)), 365)) >>> period('day', 2014).offset(1, 'day') Period((u'day', Instant((2014, 1, 2)), 365)) >>> period('day', 2014).offset(1, 'month') Period((u'day', Instant((2014, 2, 1)), 365)) >>> period('day', 2014).offset(1, 'year') Period((u'day', Instant((2015, 1, 1)), 365)) >>> period('month', 2014).offset(1) Period((u'month', Instant((2014, 2, 1)), 12)) >>> period('month', 2014).offset(1, 'day') Period((u'month', Instant((2014, 1, 2)), 12)) >>> period('month', 2014).offset(1, 'month') Period((u'month', Instant((2014, 2, 1)), 12)) >>> period('month', 2014).offset(1, 'year') Period((u'month', Instant((2015, 1, 1)), 12)) >>> period('year', 2014).offset(1) Period((u'year', Instant((2015, 1, 1)), 1)) >>> period('year', 2014).offset(1, 'day') Period((u'year', Instant((2014, 1, 2)), 1)) >>> period('year', 2014).offset(1, 'month') Period((u'year', Instant((2014, 2, 1)), 1)) >>> period('year', 2014).offset(1, 'year') Period((u'year', Instant((2015, 1, 1)), 1)) >>> period('day', '2011-2-28').offset(1) Period((u'day', Instant((2011, 3, 1)), 1)) >>> period('month', '2011-2-28').offset(1) Period((u'month', Instant((2011, 3, 28)), 1)) >>> period('year', '2011-2-28').offset(1) Period((u'year', Instant((2012, 2, 28)), 1)) >>> period('day', '2011-3-1').offset(-1) Period((u'day', Instant((2011, 2, 28)), 1)) >>> period('month', '2011-3-1').offset(-1) Period((u'month', Instant((2011, 2, 1)), 1)) >>> period('year', '2011-3-1').offset(-1) Period((u'year', Instant((2010, 3, 1)), 1)) >>> period('day', '2014-1-30').offset(3) Period((u'day', Instant((2014, 2, 2)), 1)) >>> period('month', '2014-1-30').offset(3) Period((u'month', Instant((2014, 4, 30)), 1)) >>> period('year', '2014-1-30').offset(3) Period((u'year', Instant((2017, 1, 30)), 1)) >>> period('day', 2014).offset(-3) Period((u'day', Instant((2013, 12, 29)), 365)) >>> period('month', 2014).offset(-3) Period((u'month', Instant((2013, 10, 1)), 12)) >>> period('year', 2014).offset(-3) Period((u'year', Instant((2011, 1, 1)), 1)) >>> period('day', '2014-2-3').offset('first-of', 'month') Period((u'day', Instant((2014, 2, 1)), 1)) >>> period('day', '2014-2-3').offset('first-of', 'year') Period((u'day', Instant((2014, 1, 1)), 1)) >>> period('day', '2014-2-3', 4).offset('first-of', 'month') Period((u'day', Instant((2014, 2, 1)), 4)) >>> period('day', '2014-2-3', 4).offset('first-of', 'year') Period((u'day', Instant((2014, 1, 1)), 4)) >>> period('month', '2014-2-3').offset('first-of') Period((u'month', Instant((2014, 2, 1)), 1)) >>> period('month', '2014-2-3').offset('first-of', 'month') Period((u'month', Instant((2014, 2, 1)), 1)) >>> period('month', '2014-2-3').offset('first-of', 'year') Period((u'month', Instant((2014, 1, 1)), 1)) >>> period('month', '2014-2-3', 4).offset('first-of') Period((u'month', Instant((2014, 2, 1)), 4)) >>> period('month', '2014-2-3', 4).offset('first-of', 'month') Period((u'month', Instant((2014, 2, 1)), 4)) >>> period('month', '2014-2-3', 4).offset('first-of', 'year') Period((u'month', Instant((2014, 1, 1)), 4)) >>> period('year', 2014).offset('first-of') Period((u'year', Instant((2014, 1, 1)), 1)) >>> period('year', 2014).offset('first-of', 'month') Period((u'year', Instant((2014, 1, 1)), 1)) >>> period('year', 2014).offset('first-of', 'year') Period((u'year', Instant((2014, 1, 1)), 1)) >>> period('year', '2014-2-3').offset('first-of') Period((u'year', Instant((2014, 1, 1)), 1)) >>> period('year', '2014-2-3').offset('first-of', 'month') Period((u'year', Instant((2014, 2, 1)), 1)) >>> period('year', '2014-2-3').offset('first-of', 'year') Period((u'year', Instant((2014, 1, 1)), 1)) >>> period('day', '2014-2-3').offset('last-of', 'month') Period((u'day', Instant((2014, 2, 28)), 1)) >>> period('day', '2014-2-3').offset('last-of', 'year') Period((u'day', Instant((2014, 12, 31)), 1)) >>> period('day', '2014-2-3', 4).offset('last-of', 'month') Period((u'day', Instant((2014, 2, 28)), 4)) >>> period('day', '2014-2-3', 4).offset('last-of', 'year') Period((u'day', Instant((2014, 12, 31)), 4)) >>> period('month', '2014-2-3').offset('last-of') Period((u'month', Instant((2014, 2, 28)), 1)) >>> period('month', '2014-2-3').offset('last-of', 'month') Period((u'month', Instant((2014, 2, 28)), 1)) >>> period('month', '2014-2-3').offset('last-of', 'year') Period((u'month', Instant((2014, 12, 31)), 1)) >>> period('month', '2014-2-3', 4).offset('last-of') Period((u'month', Instant((2014, 2, 28)), 4)) >>> period('month', '2014-2-3', 4).offset('last-of', 'month') Period((u'month', Instant((2014, 2, 28)), 4)) >>> period('month', '2014-2-3', 4).offset('last-of', 'year') Period((u'month', Instant((2014, 12, 31)), 4)) >>> period('year', 2014).offset('last-of') Period((u'year', Instant((2014, 12, 31)), 1)) >>> period('year', 2014).offset('last-of', 'month') Period((u'year', Instant((2014, 1, 31)), 1)) >>> period('year', 2014).offset('last-of', 'year') Period((u'year', Instant((2014, 12, 31)), 1)) >>> period('year', '2014-2-3').offset('last-of') Period((u'year', Instant((2014, 12, 31)), 1)) >>> period('year', '2014-2-3').offset('last-of', 'month') Period((u'year', Instant((2014, 2, 28)), 1)) >>> period('year', '2014-2-3').offset('last-of', 'year') Period((u'year', Instant((2014, 12, 31)), 1)) """ return self.__class__((self[0], self[1].offset(offset, self[0] if unit is None else unit), self[2])) @property def size(self): """Return the size of the period. >>> period('month', '2012-2-29', 4).size 4 """ return self[2] @property def start(self): """Return the first day of the period as an Instant instance. >>> period('month', '2012-2-29', 4).start Instant((2012, 2, 29)) """ return self[1] @property def stop(self): """Return the last day of the period as an Instant instance. >>> period('year', 2014).stop Instant((2014, 12, 31)) >>> period('month', 2014).stop Instant((2014, 12, 31)) >>> period('day', 2014).stop Instant((2014, 12, 31)) >>> period('year', '2012-2-29').stop Instant((2013, 2, 28)) >>> period('month', '2012-2-29').stop Instant((2012, 3, 28)) >>> period('day', '2012-2-29').stop Instant((2012, 2, 29)) >>> period('year', '2012-2-29', 2).stop Instant((2014, 2, 28)) >>> period('month', '2012-2-29', 2).stop Instant((2012, 4, 28)) >>> period('day', '2012-2-29', 2).stop Instant((2012, 3, 1)) """ unit, start_instant, size = self year, month, day = start_instant if unit == u'day': if size > 1: day += size - 1 month_last_day = calendar.monthrange(year, month)[1] while day > month_last_day: month += 1 if month == 13: year += 1 month = 1 day -= month_last_day month_last_day = calendar.monthrange(year, month)[1] else: if unit == u'month': month += size while month > 12: year += 1 month -= 12 else: assert unit == u'year', 'Invalid unit: {} of type {}'.format(unit, type(unit)) year += size day -= 1 if day < 1: month -= 1 if month == 0: year -= 1 month = 12 day += calendar.monthrange(year, month)[1] else: month_last_day = calendar.monthrange(year, month)[1] if day > month_last_day: month += 1 if month == 13: year += 1 month = 1 day -= month_last_day return Instant((year, month, day)) def to_json_dict(self): return collections.OrderedDict(( ('unit', self[0]), ('start', unicode(self[1])), ('size', self[2]), )) @property def unit(self): return self[0] def instant(instant): """Return a new instant, aka a triple of integers (year, month, day). >>> instant(2014) Instant((2014, 1, 1)) >>> instant(u'2014') Instant((2014, 1, 1)) >>> instant(u'2014-02') Instant((2014, 2, 1)) >>> instant(u'2014-3-2') Instant((2014, 3, 2)) >>> instant(instant(u'2014-3-2')) Instant((2014, 3, 2)) >>> instant(period('month', u'2014-3-2')) Instant((2014, 3, 2)) >>> instant(None) """ if instant is None: return None if isinstance(instant, basestring): instant = Instant( int(fragment) for fragment in instant.split(u'-', 2)[:3] ) elif isinstance(instant, datetime.date): instant = Instant((instant.year, instant.month, instant.day)) elif isinstance(instant, int): instant = (instant,) elif isinstance(instant, list): assert 1 <= len(instant) <= 3 instant = tuple(instant) elif isinstance(instant, Period): instant = instant.start else: assert isinstance(instant, tuple), instant assert 1 <= len(instant) <= 3 if len(instant) == 1: return Instant((instant[0], 1, 1)) if len(instant) == 2: return Instant((instant[0], instant[1], 1)) return Instant(instant) def instant_date(instant): if instant is None: return None instant_date = date_by_instant_cache.get(instant) if instant_date is None: date_by_instant_cache[instant] = instant_date = datetime.date(*instant) return instant_date def period(value, start = None, size = None): """Return a new period, aka a triple (unit, start_instant, size). >>> period(u'2014') Period((u'year', Instant((2014, 1, 1)), 1)) >>> period(u'2014:2') Period((u'year', Instant((2014, 1, 1)), 2)) >>> period(u'2014-2') Period((u'month', Instant((2014, 2, 1)), 1)) >>> period(u'2014-2:2') Period((u'month', Instant((2014, 2, 1)), 2)) >>> period(u'2014-2-3') Period((u'day', Instant((2014, 2, 3)), 1)) >>> period(u'2014-2-3:2') Period((u'day', Instant((2014, 2, 3)), 2)) >>> period(u'year:2014') Period((u'year', Instant((2014, 1, 1)), 1)) >>> period(u'month:2014') Period((u'month', Instant((2014, 1, 1)), 12)) >>> period(u'day:2014') Period((u'day', Instant((2014, 1, 1)), 365)) >>> period(u'year:2014-2') Period((u'year', Instant((2014, 2, 1)), 1)) >>> period(u'month:2014-2') Period((u'month', Instant((2014, 2, 1)), 1)) >>> period(u'day:2014-2') Period((u'day', Instant((2014, 2, 1)), 28)) >>> period(u'year:2014-2-3') Period((u'year', Instant((2014, 2, 3)), 1)) >>> period(u'month:2014-2-3') Period((u'month', Instant((2014, 2, 3)), 1)) >>> period(u'day:2014-2-3') Period((u'day', Instant((2014, 2, 3)), 1)) >>> period(u'year:2014-2-3:2') Period((u'year', Instant((2014, 2, 3)), 2)) >>> period(u'month:2014-2-3:2') Period((u'month', Instant((2014, 2, 3)), 2)) >>> period(u'day:2014-2-3:2') Period((u'day', Instant((2014, 2, 3)), 2)) >>> period('year', 2014) Period((u'year', Instant((2014, 1, 1)), 1)) >>> period('month', 2014) Period((u'month', Instant((2014, 1, 1)), 12)) >>> period('day', 2014) Period((u'day', Instant((2014, 1, 1)), 365)) >>> period('year', u'2014') Period((u'year', Instant((2014, 1, 1)), 1)) >>> period('month', u'2014') Period((u'month', Instant((2014, 1, 1)), 12)) >>> period('day', u'2014') Period((u'day', Instant((2014, 1, 1)), 365)) >>> period('year', u'2014-02') Period((u'year', Instant((2014, 2, 1)), 1)) >>> period('month', u'2014-02') Period((u'month', Instant((2014, 2, 1)), 1)) >>> period('day', u'2014-02') Period((u'day', Instant((2014, 2, 1)), 28)) >>> period('year', u'2014-3-2') Period((u'year', Instant((2014, 3, 2)), 1)) >>> period('month', u'2014-3-2') Period((u'month', Instant((2014, 3, 2)), 1)) >>> period('day', u'2014-3-2') Period((u'day', Instant((2014, 3, 2)), 1)) >>> period('year', u'2014-3-2', size = 2) Period((u'year', Instant((2014, 3, 2)), 2)) >>> period('month', u'2014-3-2', size = 2) Period((u'month', Instant((2014, 3, 2)), 2)) >>> period('day', u'2014-3-2', size = 2) Period((u'day', Instant((2014, 3, 2)), 2)) >>> period('month', instant(u'2014-3-2'), size = 2) Period((u'month', Instant((2014, 3, 2)), 2)) >>> period('month', period(u'year', u'2014-3-2'), size = 2) Period((u'month', Instant((2014, 3, 2)), 2)) """ if not isinstance(value, basestring) or value not in (u'day', u'month', u'year'): assert start is None, start assert size is None, size return conv.check(json_or_python_to_period)(value) unit = unicode(value) assert size is None or isinstance(size, int) and size > 0, size if isinstance(start, basestring): start = tuple( int(fragment) for fragment in start.split(u'-', 2)[:3] ) elif isinstance(start, datetime.date): start = (start.year, start.month, start.day) elif isinstance(start, int): start = (start,) elif isinstance(start, list): assert 1 <= len(start) <= 3 start = tuple(start) elif isinstance(start, Period): start = start.start else: assert isinstance(start, tuple) assert 1 <= len(start) <= 3 if len(start) == 1: start = Instant((start[0], 1, 1)) if size is None: if unit == u'day': size = 366 if calendar.isleap(start[0]) else 365 elif unit == u'month': size = 12 else: size = 1 elif len(start) == 2: start = Instant((start[0], start[1], 1)) if size is None: if unit == u'day': size = calendar.monthrange(start[0], start[1])[1] else: size = 1 else: start = Instant(start) if size is None: size = 1 return Period((unit, start, size)) # Level-1 converters def input_to_period_tuple(value, state = None): """Convert an input string to a period tuple. .. note:: This function doesn't return a period, but a tuple that allows to construct a period. >>> input_to_period_tuple(u'2014') ((u'year', 2014), None) >>> input_to_period_tuple(u'2014:2') ((u'year', 2014, 2), None) >>> input_to_period_tuple(u'2014-2') ((u'month', (2014, 2)), None) >>> input_to_period_tuple(u'2014-3:12') ((u'month', (2014, 3), 12), None) >>> input_to_period_tuple(u'2014-2-3') ((u'day', (2014, 2, 3)), None) >>> input_to_period_tuple(u'2014-3-4:2') ((u'day', (2014, 3, 4), 2), None) >>> input_to_period_tuple(u'year:2014') ((u'year', u'2014'), None) >>> input_to_period_tuple(u'year:2014:2') ((u'year', u'2014', u'2'), None) >>> input_to_period_tuple(u'year:2014-2:2') ((u'year', u'2014-2', u'2'), None) """ if value is None: return value, None if state is None: state = conv.default_state split_value = tuple( clean_fragment for clean_fragment in ( fragment.strip() for fragment in value.split(u':') ) if clean_fragment ) if not split_value: return None, None if len(split_value) == 1: split_value = tuple( clean_fragment for clean_fragment in ( fragment.strip() for fragment in split_value[0].split(u'-') ) if clean_fragment ) if len(split_value) == 1: return conv.pipe( conv.input_to_strict_int, conv.test_greater_or_equal(0), conv.function(lambda year: (u'year', year)), )(split_value[0], state = state) if len(split_value) == 2: return conv.pipe( conv.struct( ( conv.pipe( conv.input_to_strict_int, conv.test_greater_or_equal(0), ), conv.pipe( conv.input_to_strict_int, conv.test_between(1, 12), ), ), ), conv.function(lambda month_tuple: (u'month', month_tuple)), )(split_value, state = state) if len(split_value) == 3: return conv.pipe( conv.struct( ( conv.pipe( conv.input_to_strict_int, conv.test_greater_or_equal(0), ), conv.pipe( conv.input_to_strict_int, conv.test_between(1, 12), ), conv.pipe( conv.input_to_strict_int, conv.test_between(1, 31), ), ), ), conv.function(lambda day_tuple: (u'day', day_tuple)), )(split_value, state = state) return split_value, state._(u'Instant string contains too much "-" for a year, a month or a day') if len(split_value) == 2: split_start = tuple( clean_fragment for clean_fragment in ( fragment.strip() for fragment in split_value[0].split(u'-') ) if clean_fragment ) size, error = conv.input_to_int(split_value[1], state = state) if error is None: if len(split_start) == 1: start, error = conv.pipe( conv.input_to_strict_int, conv.test_greater_or_equal(0), )(split_start[0], state = state) if error is None: return (u'year', start, size), None elif len(split_start) == 2: start, error = conv.struct( ( conv.pipe( conv.input_to_strict_int, conv.test_greater_or_equal(0), ), conv.pipe( conv.input_to_strict_int, conv.test_between(1, 12), ), ), )(split_start, state = state) if error is None: return (u'month', start, size), None elif len(split_start) == 3: start, error = conv.struct( ( conv.pipe( conv.input_to_strict_int, conv.test_greater_or_equal(0), ), conv.pipe( conv.input_to_strict_int, conv.test_between(1, 12), ), conv.pipe( conv.input_to_strict_int, conv.test_between(1, 31), ), ), )(split_start, state = state) if error is None: return (u'day', start, size), None return split_value, None def json_or_python_to_instant_tuple(value, state = None): """Convert a JSON or Python object to an instant tuple. .. note:: This function doesn't return an instant, but a tuple that allows to construct an instant. >>> json_or_python_to_instant_tuple('2014') ((2014,), None) >>> json_or_python_to_instant_tuple('2014-2') ((2014, 2), None) >>> json_or_python_to_instant_tuple('2014-2-3') ((2014, 2, 3), None) >>> json_or_python_to_instant_tuple(datetime.date(2014, 2, 3)) ((2014, 2, 3), None) >>> json_or_python_to_instant_tuple([2014]) ((2014,), None) >>> json_or_python_to_instant_tuple([2014, 2]) ((2014, 2), None) >>> json_or_python_to_instant_tuple([2014, 2, 3]) ((2014, 2, 3), None) >>> json_or_python_to_instant_tuple(2014) ((2014,), None) >>> json_or_python_to_instant_tuple((2014,)) ((2014,), None) >>> json_or_python_to_instant_tuple((2014, 2)) ((2014, 2), None) >>> json_or_python_to_instant_tuple((2014, 2, 3)) ((2014, 2, 3), None) """ if value is None: return value, None if state is None: state = conv.default_state if isinstance(value, basestring): if year_or_month_or_day_re.match(value) is None: return value, state._(u'Invalid date string') instant = tuple( int(fragment) for fragment in value.split(u'-', 2) ) elif isinstance(value, datetime.date): instant = (value.year, value.month, value.day) elif isinstance(value, int): instant = (value,) elif isinstance(value, list): if not (1 <= len(value) <= 3): return value, state._(u'Invalid size for date list') instant = tuple(value) else: if not isinstance(value, tuple): return value, state._(u'Invalid type') if not (1 <= len(value) <= 3): return value, state._(u'Invalid size for date tuple') instant = value return instant, None def make_json_or_python_to_period(min_date = None, max_date = None): """Return a converter that creates a period from a JSON or Python object. >>> json_or_python_to_period(u'2014') (Period((u'year', Instant((2014, 1, 1)), 1)), None) >>> json_or_python_to_period(u'2014:2') (Period((u'year', Instant((2014, 1, 1)), 2)), None) >>> json_or_python_to_period(u'2014-2') (Period((u'month', Instant((2014, 2, 1)), 1)), None) >>> json_or_python_to_period(u'2014-2:2') (Period((u'month', Instant((2014, 2, 1)), 2)), None) >>> json_or_python_to_period(u'2014-2-3') (Period((u'day', Instant((2014, 2, 3)), 1)), None) >>> json_or_python_to_period(u'2014-2-3:2') (Period((u'day', Instant((2014, 2, 3)), 2)), None) >>> json_or_python_to_period(u'year:2014') (Period((u'year', Instant((2014, 1, 1)), 1)), None) >>> json_or_python_to_period(u'month:2014') (Period((u'month', Instant((2014, 1, 1)), 12)), None) >>> json_or_python_to_period(u'day:2014') (Period((u'day', Instant((2014, 1, 1)), 365)), None) >>> json_or_python_to_period(u'year:2014-2') (Period((u'year', Instant((2014, 2, 1)), 1)), None) >>> json_or_python_to_period(u'month:2014-2') (Period((u'month', Instant((2014, 2, 1)), 1)), None) >>> json_or_python_to_period(u'day:2014-2') (Period((u'day', Instant((2014, 2, 1)), 28)), None) >>> json_or_python_to_period(u'year:2014-2-3') (Period((u'year', Instant((2014, 2, 3)), 1)), None) >>> json_or_python_to_period(u'month:2014-2-3') (Period((u'month', Instant((2014, 2, 3)), 1)), None) >>> json_or_python_to_period(u'day:2014-2-3') (Period((u'day', Instant((2014, 2, 3)), 1)), None) >>> json_or_python_to_period(u'year:2014-2-3:2') (Period((u'year', Instant((2014, 2, 3)), 2)), None) >>> json_or_python_to_period(u'month:2014-2-3:2') (Period((u'month', Instant((2014, 2, 3)), 2)), None) >>> json_or_python_to_period(u'day:2014-2-3:2') (Period((u'day', Instant((2014, 2, 3)), 2)), None) """ min_instant = (1870, 1, 1) if min_date is None else (min_date.year, min_date.month, min_date.day) max_instant = (2099, 12, 31) if max_date is None else (max_date.year, max_date.month, max_date.day) return conv.pipe( conv.condition( conv.test_isinstance(basestring), input_to_period_tuple, conv.condition( conv.test_isinstance(int), conv.pipe( conv.test_greater_or_equal(0), conv.function(lambda year: (u'year', year)), ), ), ), conv.condition( conv.test_isinstance(dict), conv.pipe( conv.struct( dict( size = conv.pipe( conv.test_isinstance((basestring, int)), conv.anything_to_int, conv.test_greater_or_equal(1), ), start = conv.pipe( json_or_python_to_instant_tuple, conv.not_none, ), unit = conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in((u'day', u'month', u'year')), conv.not_none, ), ), ), conv.function(lambda value: period(value['unit'], value['start'], value['size'])), ), conv.pipe( conv.test_isinstance((list, tuple)), conv.test(lambda period_tuple: 2 <= len(period_tuple) <= 3, error = N_(u'Invalid period tuple')), conv.function(lambda period_tuple: (tuple(period_tuple) + (None,))[:3]), conv.struct( ( # unit conv.pipe( conv.test_isinstance(basestring), conv.input_to_slug, conv.test_in((u'day', u'month', u'year')), conv.not_none, ), # start conv.pipe( json_or_python_to_instant_tuple, conv.not_none, ), # size conv.pipe( conv.test_isinstance((basestring, int)), conv.anything_to_int, conv.test_greater_or_equal(1), ), ), ), conv.function(lambda value: period(*value)), ), ), conv.struct( Period(( # unit conv.noop, # start conv.test_between(min_instant, max_instant), # stop conv.noop, )), ), ) # Level-2 converters json_or_python_to_period = make_json_or_python_to_period() PK 'Gjc  openfisca_core/formulas.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from __future__ import division import collections import datetime import inspect import itertools import logging import textwrap import numpy as np from . import columns, holders, periods from .tools import empty_clone, stringify_array log = logging.getLogger(__name__) # Exceptions class NaNCreationError(Exception): pass # Formulas class AbstractFormula(object): comments = None holder = None line_number = None source_code = None source_file_path = None def __init__(self, holder = None): assert holder is not None self.holder = holder def calculate_output(self, period): return self.holder.compute(period).array def clone(self, holder, keys_to_skip = None): """Copy the formula just enough to be able to run a new simulation without modifying the original simulation.""" new = empty_clone(self) new_dict = new.__dict__ if keys_to_skip is None: keys_to_skip = set() keys_to_skip.add('holder') for key, value in self.__dict__.iteritems(): if key not in keys_to_skip: new_dict[key] = value new_dict['holder'] = holder return new @property def real_formula(self): return self def set_input(self, period, array): self.holder.set_array(period, array) class AbstractEntityToEntity(AbstractFormula): _variable_holder = None roles = None # class attribute. When None the entity value is duplicated to each person belonging to entity. variable_name = None # class attribute def clone(self, holder, keys_to_skip = None): """Copy the formula just enough to be able to run a new simulation without modifying the original simulation.""" if keys_to_skip is None: keys_to_skip = set() keys_to_skip.add('_variable_holder') return super(AbstractEntityToEntity, self).clone(holder, keys_to_skip = keys_to_skip) def compute(self, period = None, requested_formulas_by_period = None): """Call the formula function (if needed) and return a dated holder containing its result.""" assert period is not None holder = self.holder column = holder.column entity = holder.entity simulation = entity.simulation debug = simulation.debug debug_all = simulation.debug_all trace = simulation.trace if debug or trace: simulation.stack_trace.append(dict( parameters_infos = [], input_variables_infos = [], )) variable_holder = self.variable_holder variable_dated_holder = variable_holder.compute(period = period, accept_other_period = True, requested_formulas_by_period = requested_formulas_by_period) output_period = variable_dated_holder.period array = self.transform(variable_dated_holder, roles = self.roles) if array.dtype != column.dtype: array = array.astype(column.dtype) if debug or trace: variable_infos = (column.name, output_period) step = simulation.traceback.get(variable_infos) if step is None: simulation.traceback[variable_infos] = step = dict( holder = holder, ) step.update(simulation.stack_trace.pop()) input_variables_infos = step['input_variables_infos'] if not debug_all or trace: step['default_input_variables'] = has_only_default_input_variables = all( np.all(input_holder.get_array(input_variable_period) == input_holder.column.default) for input_holder, input_variable_period in ( (simulation.get_holder(input_variable_name), input_variable_period1) for input_variable_name, input_variable_period1 in input_variables_infos ) ) step['is_computed'] = True if debug and (debug_all or not has_only_default_input_variables): log.info(u'<=> {}@{}<{}>({}) --> <{}>{}'.format(column.name, entity.key_plural, str(period), simulation.stringify_input_variables_infos(input_variables_infos), stringify_array(array), str(output_period))) dated_holder = holder.at_period(output_period) dated_holder.array = array return dated_holder def graph_parameters(self, edges, get_input_variables_and_parameters, nodes, visited): """Recursively build a graph of formulas.""" holder = self.holder column = holder.column variable_holder = self.variable_holder variable_holder.graph(edges, get_input_variables_and_parameters, nodes, visited) edges.append({ 'from': variable_holder.column.name, 'to': column.name, }) def to_json(self, get_input_variables_and_parameters = None, with_input_variables_details = False): cls = self.__class__ comments = inspect.getcomments(cls) doc = inspect.getdoc(cls) source_lines, line_number = inspect.getsourcelines(cls) variable_holder = self.variable_holder variable_column = variable_holder.column self_json = collections.OrderedDict(( ('@type', cls.__bases__[0].__name__), ('comments', comments.decode('utf-8') if comments is not None else None), ('doc', doc.decode('utf-8') if doc is not None else None), ('line_number', line_number), ('module', inspect.getmodule(cls).__name__), ('source', ''.join(source_lines).decode('utf-8')), )) if get_input_variables_and_parameters is not None: input_variable_json = collections.OrderedDict(( ('entity', variable_holder.entity.key_plural), ('label', variable_column.label), ('name', variable_column.name), )) if with_input_variables_details else variable_column.name self_json['input_variables'] = [input_variable_json] return self_json @property def variable_holder(self): # Note: This property is not precomputed at __init__ time, to ease the cloning of the formula. variable_holder = self._variable_holder if variable_holder is None: self._variable_holder = variable_holder = self.holder.entity.simulation.get_or_new_holder( self.variable_name) return variable_holder class AbstractGroupedFormula(AbstractFormula): used_formula = None @property def real_formula(self): used_formula = self.used_formula if used_formula is None: return None return used_formula.real_formula class DatedFormula(AbstractGroupedFormula): base_function = None # Class attribute. Overridden by subclasses dated_formulas = None # A list of dictionaries containing a formula jointly with start and stop instants dated_formulas_class = None # Class attribute def __init__(self, holder = None): super(DatedFormula, self).__init__(holder = holder) self.dated_formulas = [ dict( formula = dated_formula_class['formula_class'](holder = holder), start_instant = dated_formula_class['start_instant'], stop_instant = dated_formula_class['stop_instant'], ) for dated_formula_class in self.dated_formulas_class ] assert self.dated_formulas @classmethod def at_instant(cls, instant, default = UnboundLocalError): assert isinstance(instant, periods.Instant) for dated_formula_class in cls.dated_formulas_class: start_instant = dated_formula_class['start_instant'] stop_instant = dated_formula_class['stop_instant'] if (start_instant is None or start_instant <= instant) and ( stop_instant is None or instant <= stop_instant): return dated_formula_class['formula_class'] if default is UnboundLocalError: raise KeyError(instant) return default def clone(self, holder, keys_to_skip = None): """Copy the formula just enough to be able to run a new simulation without modifying the original simulation.""" if keys_to_skip is None: keys_to_skip = set() keys_to_skip.add('dated_formulas') new = super(DatedFormula, self).clone(holder, keys_to_skip = keys_to_skip) new.dated_formulas = [ { key: value.clone(holder) if key == 'formula' else value for key, value in dated_formula.iteritems() } for dated_formula in self.dated_formulas ] return new def compute(self, period = None, requested_formulas_by_period = None): dated_holder = None stop_instant = period.stop for dated_formula in self.dated_formulas: if dated_formula['start_instant'] > stop_instant: break output_period = period.intersection(dated_formula['start_instant'], dated_formula['stop_instant']) if output_period is None: continue dated_holder = dated_formula['formula'].compute(period = output_period, requested_formulas_by_period = requested_formulas_by_period) if dated_holder.array is None: break self.used_formula = dated_formula['formula'] return dated_holder holder = self.holder column = holder.column array = np.empty(holder.entity.count, dtype = column.dtype) array.fill(column.default) if dated_holder is None: dated_holder = holder.at_period(period) dated_holder.array = array return dated_holder def graph_parameters(self, edges, get_input_variables_and_parameters, nodes, visited): """Recursively build a graph of formulas.""" for dated_formula in self.dated_formulas: dated_formula['formula'].graph_parameters(edges, get_input_variables_and_parameters, nodes, visited) def to_json(self, get_input_variables_and_parameters = None, with_input_variables_details = False): return collections.OrderedDict(( ('@type', u'DatedFormula'), ('dated_formulas', [ dict( formula = dated_formula['formula'].to_json( get_input_variables_and_parameters = get_input_variables_and_parameters, with_input_variables_details = with_input_variables_details, ), start_instant = (None if dated_formula['start_instant'] is None else str(dated_formula['start_instant'])), stop_instant = (None if dated_formula['stop_instant'] is None else str(dated_formula['stop_instant'])), ) for dated_formula in self.dated_formulas ]), )) class EntityToPerson(AbstractEntityToEntity): def transform(self, dated_holder, roles = None): """Cast an entity array to a persons array, setting only cells of persons having one of the given roles. When no roles are given, it means "all the roles" => every cell is set. """ holder = self.holder persons = holder.entity assert persons.is_persons_entity entity = dated_holder.entity assert not entity.is_persons_entity array = dated_holder.array target_array = np.empty(persons.count, dtype = array.dtype) target_array.fill(dated_holder.column.default) entity_index_array = persons.holder_by_name[entity.index_for_person_variable_name].array if roles is None: roles = range(entity.roles_count) for role in roles: boolean_filter = persons.holder_by_name[entity.role_for_person_variable_name].array == role try: target_array[boolean_filter] = array[entity_index_array[boolean_filter]] except: log.error(u'An error occurred while transforming array for role {}[{}] in function {}'.format( entity.key_singular, role, holder.column.name)) raise return target_array class PersonToEntity(AbstractEntityToEntity): operation = None def transform(self, dated_holder, roles = None): """Convert an array of persons to an array of non-person entities. When no roles are given, it means "all the roles". """ holder = self.holder entity = holder.entity assert not entity.is_persons_entity persons = dated_holder.entity assert persons.is_persons_entity array = dated_holder.array target_array = np.empty(entity.count, dtype = array.dtype) target_array.fill(dated_holder.column.default) entity_index_array = persons.holder_by_name[entity.index_for_person_variable_name].array if roles is not None and len(roles) == 1: assert self.operation is None, 'Unexpected operation {} in formula {}'.format(self.operation, holder.column.name) role = roles[0] # TODO: Cache filter. boolean_filter = persons.holder_by_name[entity.role_for_person_variable_name].array == role try: target_array[entity_index_array[boolean_filter]] = array[boolean_filter] except: log.error(u'An error occurred while filtering array for role {}[{}] in function {}'.format( entity.key_singular, role, holder.column.name)) raise else: operation = self.operation assert operation in ('add', 'or'), 'Invalid operation {} in formula {}'.format(operation, holder.column.name) if roles is None: roles = range(entity.roles_count) target_array = np.zeros(entity.count, dtype = np.bool if operation == 'or' else array.dtype if array.dtype != np.bool else np.int16) for role in roles: # TODO: Cache filters. boolean_filter = persons.holder_by_name[entity.role_for_person_variable_name].array == role target_array[entity_index_array[boolean_filter]] += array[boolean_filter] return target_array class SimpleFormula(AbstractFormula): base_function = None # Class attribute. Overridden by subclasses function = None # Class attribute. Overridden by subclasses def any_by_roles(self, array_or_dated_holder, entity = None, roles = None): holder = self.holder target_entity = holder.entity simulation = target_entity.simulation persons = simulation.persons if entity is None: entity = holder.entity else: assert entity in simulation.entity_by_key_singular, u"Unknown entity: {}".format(entity).encode('utf-8') entity = simulation.entity_by_key_singular[entity] assert not entity.is_persons_entity if isinstance(array_or_dated_holder, (holders.DatedHolder, holders.Holder)): assert array_or_dated_holder.entity.is_persons_entity array = array_or_dated_holder.array else: array = array_or_dated_holder assert isinstance(array, np.ndarray), u"Expected a holder or a Numpy array. Got: {}".format(array).encode( 'utf-8') assert array.size == persons.count, u"Expected an array of size {}. Got: {}".format(persons.count, array.size) entity_index_array = persons.holder_by_name[entity.index_for_person_variable_name].array if roles is None: roles = range(entity.roles_count) target_array = np.zeros(entity.count, dtype = np.bool) for role in roles: # TODO Mettre les filtres en cache dans la simulation boolean_filter = persons.holder_by_name[entity.role_for_person_variable_name].array == role target_array[entity_index_array[boolean_filter]] += array[boolean_filter] return target_array def cast_from_entity_to_role(self, array_or_dated_holder, default = None, entity = None, role = None): """Cast an entity array to a persons array, setting only cells of persons having the given role.""" assert isinstance(role, int) return self.cast_from_entity_to_roles(array_or_dated_holder, default = default, entity = entity, roles = [role]) def cast_from_entity_to_roles(self, array_or_dated_holder, default = None, entity = None, roles = None): """Cast an entity array to a persons array, setting only cells of persons having one of the given roles. When no roles are given, it means "all the roles" => every cell is set. """ holder = self.holder target_entity = holder.entity simulation = target_entity.simulation persons = simulation.persons if isinstance(array_or_dated_holder, (holders.DatedHolder, holders.Holder)): if entity is None: entity = array_or_dated_holder.entity else: assert entity in simulation.entity_by_key_singular, u"Unknown entity: {}".format(entity).encode('utf-8') entity = simulation.entity_by_key_singular[entity] assert entity == array_or_dated_holder.entity, \ u"""Holder entity "{}" and given entity "{}" don't match""".format(entity.key_plural, array_or_dated_holder.entity.key_plural).encode('utf-8') array = array_or_dated_holder.array if default is None: default = array_or_dated_holder.column.default else: assert entity in simulation.entity_by_key_singular, u"Unknown entity: {}".format(entity).encode('utf-8') entity = simulation.entity_by_key_singular[entity] array = array_or_dated_holder assert isinstance(array, np.ndarray), u"Expected a holder or a Numpy array. Got: {}".format(array).encode( 'utf-8') assert array.size == entity.count, u"Expected an array of size {}. Got: {}".format(entity.count, array.size) if default is None: default = 0 assert not entity.is_persons_entity target_array = np.empty(persons.count, dtype = array.dtype) target_array.fill(default) entity_index_array = persons.holder_by_name[entity.index_for_person_variable_name].array if roles is None: roles = range(entity.roles_count) for role in roles: boolean_filter = persons.holder_by_name[entity.role_for_person_variable_name].array == role try: target_array[boolean_filter] = array[entity_index_array[boolean_filter]] except: log.error(u'An error occurred while transforming array for role {}[{}] in function {}'.format( entity.key_singular, role, holder.column.name)) raise return target_array def compute(self, period = None, requested_formulas_by_period = None): """Call the formula function (if needed) and return a dated holder containing its result.""" assert period is not None holder = self.holder column = holder.column entity = holder.entity simulation = entity.simulation debug = simulation.debug debug_all = simulation.debug_all trace = simulation.trace # Note: Don't compute intersection with column.start & column.end, because holder already does it: # output_period = output_period.intersection(periods.instant(column.start), periods.instant(column.end)) # Note: Don't verify that the function result has already been computed, because this is the task of # holder.compute(). # Ensure that method is not called several times for the same period (infinite loop). if requested_formulas_by_period is None: requested_formulas_by_period = {} period_or_none = None if column.is_permanent else period period_requested_formulas = requested_formulas_by_period.get(period_or_none) if period_requested_formulas is None: requested_formulas_by_period[period_or_none] = period_requested_formulas = set() else: assert self not in period_requested_formulas, \ 'Infinite loop in formula {}<{}>. Missing values for columns: {}'.format( column.name, period, u', '.join(sorted(set( u'{}<{}>'.format(requested_formula.holder.column.name, period1) for period1, period_requested_formulas1 in requested_formulas_by_period.iteritems() for requested_formula in period_requested_formulas1 ))).encode('utf-8'), ) period_requested_formulas.add(self) if debug or trace: simulation.stack_trace.append(dict( parameters_infos = [], input_variables_infos = [], )) try: formula_result = self.base_function(simulation, period) except: log.error(u'An error occurred while calling formula {}@{}<{}> in module {}'.format( column.name, entity.key_plural, str(period), self.function.__module__, )) raise else: try: output_period, array = formula_result except ValueError: raise ValueError(u'A formula must return "period, array": {}@{}<{}> in module {}'.format( column.name, entity.key_plural, str(period), self.function.__module__, ).encode('utf-8')) assert output_period[1] <= period[1] <= output_period.stop, \ u"Function {}@{}<{}>() --> <{}>{} returns an output period that doesn't include start instant of" \ u"requested period".format(column.name, entity.key_plural, str(period), str(output_period), stringify_array(array)).encode('utf-8') assert isinstance(array, np.ndarray), u"Function {}@{}<{}>() --> <{}>{} doesn't return a numpy array".format( column.name, entity.key_plural, str(period), str(output_period), array).encode('utf-8') assert array.size == entity.count, \ u"Function {}@{}<{}>() --> <{}>{} returns an array of size {}, but size {} is expected for {}".format( column.name, entity.key_plural, str(period), str(output_period), stringify_array(array), array.size, entity.count, entity.key_singular).encode('utf-8') if debug: try: # cf http://stackoverflow.com/questions/6736590/fast-check-for-nan-in-numpy if np.isnan(np.min(array)): nan_count = np.count_nonzero(np.isnan(array)) raise NaNCreationError(u"Function {}@{}<{}>() --> <{}>{} returns {} NaN value(s)".format( column.name, entity.key_plural, str(period), str(output_period), stringify_array(array), nan_count).encode('utf-8')) except TypeError: pass if array.dtype != column.dtype: array = array.astype(column.dtype) if debug or trace: variable_infos = (column.name, output_period) step = simulation.traceback.get(variable_infos) if step is None: simulation.traceback[variable_infos] = step = dict( holder = holder, ) step.update(simulation.stack_trace.pop()) input_variables_infos = step['input_variables_infos'] if not debug_all or trace: step['default_input_variables'] = has_only_default_input_variables = all( np.all(input_holder.get_array(input_variable_period) == input_holder.column.default) for input_holder, input_variable_period in ( (simulation.get_holder(input_variable_name), input_variable_period1) for input_variable_name, input_variable_period1 in input_variables_infos ) ) step['is_computed'] = True if debug and (debug_all or not has_only_default_input_variables): log.info(u'<=> {}@{}<{}>({}) --> <{}>{}'.format(column.name, entity.key_plural, str(period), simulation.stringify_input_variables_infos(input_variables_infos), str(output_period), stringify_array(array))) dated_holder = holder.at_period(output_period) dated_holder.array = array period_requested_formulas.remove(self) return dated_holder def filter_role(self, array_or_dated_holder, default = None, entity = None, role = None): """Convert a persons array to an entity array, copying only cells of persons having the given role.""" holder = self.holder simulation = holder.entity.simulation persons = simulation.persons if entity is None: entity = holder.entity else: assert entity in simulation.entity_by_key_singular, u"Unknown entity: {}".format(entity).encode('utf-8') entity = simulation.entity_by_key_singular[entity] assert not entity.is_persons_entity if isinstance(array_or_dated_holder, (holders.DatedHolder, holders.Holder)): assert array_or_dated_holder.entity.is_persons_entity array = array_or_dated_holder.array if default is None: default = array_or_dated_holder.column.default else: array = array_or_dated_holder assert isinstance(array, np.ndarray), u"Expected a holder or a Numpy array. Got: {}".format(array).encode( 'utf-8') assert array.size == persons.count, u"Expected an array of size {}. Got: {}".format(persons.count, array.size) if default is None: default = 0 entity_index_array = persons.holder_by_name[entity.index_for_person_variable_name].array assert isinstance(role, int) target_array = np.empty(entity.count, dtype = array.dtype) target_array.fill(default) boolean_filter = persons.holder_by_name[entity.role_for_person_variable_name].array == role try: target_array[entity_index_array[boolean_filter]] = array[boolean_filter] except: log.error(u'An error occurred while filtering array for role {}[{}] in function {}'.format( entity.key_singular, role, holder.column.name)) raise return target_array def graph_parameters(self, edges, get_input_variables_and_parameters, nodes, visited): """Recursively build a graph of formulas.""" holder = self.holder column = holder.column entity = holder.entity simulation = entity.simulation variables_name, parameters_name = get_input_variables_and_parameters(column) if variables_name is not None: for variable_name in sorted(variables_name): variable_holder = simulation.get_or_new_holder(variable_name) variable_holder.graph(edges, get_input_variables_and_parameters, nodes, visited) edges.append({ 'from': variable_holder.column.name, 'to': column.name, }) def split_by_roles(self, array_or_dated_holder, default = None, entity = None, roles = None): """dispatch a persons array to several entity arrays (one for each role).""" holder = self.holder simulation = holder.entity.simulation persons = simulation.persons if entity is None: entity = holder.entity else: assert entity in simulation.entity_by_key_singular, u"Unknown entity: {}".format(entity).encode('utf-8') entity = simulation.entity_by_key_singular[entity] assert not entity.is_persons_entity if isinstance(array_or_dated_holder, (holders.DatedHolder, holders.Holder)): assert array_or_dated_holder.entity.is_persons_entity array = array_or_dated_holder.array if default is None: default = array_or_dated_holder.column.default else: array = array_or_dated_holder assert isinstance(array, np.ndarray), u"Expected a holder or a Numpy array. Got: {}".format(array).encode( 'utf-8') assert array.size == persons.count, u"Expected an array of size {}. Got: {}".format(persons.count, array.size) if default is None: default = 0 entity_index_array = persons.holder_by_name[entity.index_for_person_variable_name].array if roles is None: # To ensure that existing formulas don't fail, ensure there is always at least 11 roles. # roles = range(entity.roles_count) roles = range(max(entity.roles_count, 11)) target_array_by_role = {} for role in roles: target_array_by_role[role] = target_array = np.empty(entity.count, dtype = array.dtype) target_array.fill(default) boolean_filter = persons.holder_by_name[entity.role_for_person_variable_name].array == role try: target_array[entity_index_array[boolean_filter]] = array[boolean_filter] except: log.error(u'An error occurred while filtering array for role {}[{}] in function {}'.format( entity.key_singular, role, holder.column.name)) raise return target_array_by_role def sum_by_entity(self, array_or_dated_holder, entity = None, roles = None): holder = self.holder target_entity = holder.entity simulation = target_entity.simulation persons = simulation.persons if entity is None: entity = holder.entity else: assert entity in simulation.entity_by_key_singular, u"Unknown entity: {}".format(entity).encode('utf-8') entity = simulation.entity_by_key_singular[entity] assert not entity.is_persons_entity if isinstance(array_or_dated_holder, (holders.DatedHolder, holders.Holder)): assert array_or_dated_holder.entity.is_persons_entity array = array_or_dated_holder.array else: array = array_or_dated_holder assert isinstance(array, np.ndarray), u"Expected a holder or a Numpy array. Got: {}".format(array).encode( 'utf-8') assert array.size == persons.count, u"Expected an array of size {}. Got: {}".format(persons.count, array.size) entity_index_array = persons.holder_by_name[entity.index_for_person_variable_name].array if roles is None: roles = range(entity.roles_count) target_array = np.zeros(entity.count, dtype = array.dtype if array.dtype != np.bool else np.int16) for role in roles: # TODO: Mettre les filtres en cache dans la simulation boolean_filter = persons.holder_by_name[entity.role_for_person_variable_name].array == role target_array[entity_index_array[boolean_filter]] += array[boolean_filter] return target_array def to_json(self, get_input_variables_and_parameters = None, with_input_variables_details = False): function = self.function if function is None: return None comments = inspect.getcomments(function) doc = inspect.getdoc(function) source_lines, line_number = inspect.getsourcelines(function) source = textwrap.dedent(''.join(source_lines).decode('utf-8')) self_json = collections.OrderedDict(( ('@type', u'SimpleFormula'), ('comments', comments.decode('utf-8') if comments is not None else None), ('doc', doc.decode('utf-8') if doc is not None else None), ('line_number', line_number), ('module', inspect.getmodule(function).__name__), ('source', source), )) if get_input_variables_and_parameters is not None: holder = self.holder column = holder.column entity = holder.entity simulation = entity.simulation variables_name, parameters_name = get_input_variables_and_parameters(column) if variables_name: if with_input_variables_details: input_variables_json = [] for variable_name in sorted(variables_name): variable_holder = simulation.get_or_new_holder(variable_name) variable_column = variable_holder.column input_variables_json.append(collections.OrderedDict(( ('entity', variable_holder.entity.key_plural), ('label', variable_column.label), ('name', variable_column.name), ))) self_json['input_variables'] = input_variables_json else: self_json['input_variables'] = list(variables_name) if parameters_name: self_json['parameters'] = list(parameters_name) return self_json # Formulas Generators class ConversionColumnMetaclass(type): """The metaclass of ConversionColumn classes: It generates a column instead of a formula ConversionColumn class.""" def __new__(cls, name, bases, attributes): """Return a column containing a casting formula, built from ConversionColumn class definition.""" assert len(bases) == 1, bases base_class = bases[0] if base_class is object: # Do nothing when creating classes DatedFormulaColumn, SimpleFormulaColumn, etc. return super(ConversionColumnMetaclass, cls).__new__(cls, name, bases, attributes) # Extract attributes. formula_class = attributes.pop('formula_class', base_class.formula_class) assert issubclass(formula_class, AbstractFormula), formula_class cerfa_field = attributes.pop('cerfa_field', None) if cerfa_field is not None: assert isinstance(cerfa_field, basestring), cerfa_field cerfa_field = unicode(cerfa_field) doc = attributes.pop('__doc__', None) entity_class = attributes.pop('entity_class') name = unicode(name) label = attributes.pop('label', None) label = name if label is None else unicode(label) law_reference = attributes.pop('law_reference', None) if law_reference is not None: assert isinstance(law_reference, (basestring, list)) url = attributes.pop('url', None) if url is not None: url = unicode(url) variable = attributes.pop('variable') assert isinstance(variable, columns.Column) # Build formula class and column from extracted attributes. formula_class_attributes = dict( __module__ = attributes.pop('__module__'), ) if doc is not None: formula_class_attributes['__doc__'] = doc self = super(ConversionColumnMetaclass, cls).__new__(cls, name.encode('utf-8'), bases, attributes) comments = inspect.getcomments(self) if comments is not None: if isinstance(comments, str): comments = comments.decode('utf-8') formula_class_attributes['comments'] = comments source_file_path = inspect.getsourcefile(self).decode('utf-8') if source_file_path is not None: formula_class_attributes['source_file_path'] = source_file_path try: source_lines, line_number = inspect.getsourcelines(self) except IOError: line_number = None source_code = None else: source_code = textwrap.dedent(''.join(source_lines).decode('utf-8')) if source_code is not None: formula_class_attributes['source_code'] = source_code if line_number is not None: formula_class_attributes['line_number'] = line_number role = attributes.pop('role', None) roles = attributes.pop('roles', None) if role is None: if roles is not None: assert isinstance(roles, (list, tuple)) and all(isinstance(role, int) for role in roles) else: assert isinstance(role, int) assert roles is None roles = [role] if roles is not None: formula_class_attributes['roles'] = roles formula_class_attributes['variable_name'] = variable.name if issubclass(formula_class, EntityToPerson): assert entity_class.is_persons_entity column = variable.empty_clone() else: assert issubclass(formula_class, PersonToEntity) assert not entity_class.is_persons_entity if roles is None or len(roles) > 1: operation = attributes.pop('operation') assert operation in ('add', 'or'), 'Invalid operation: {}'.format(operation) formula_class_attributes['operation'] = operation if operation == 'add': if variable.__class__ is columns.BoolCol: column = columns.IntCol() else: column = variable.empty_clone() else: assert operation == 'or' column = variable.empty_clone() else: column = variable.empty_clone() # Ensure that all attributes defined in ConversionColumn class are used. assert not attributes, 'Unexpected attributes in definition of filled column {}: {}'.format(name, ', '.join(attributes.iterkeys())) formula_class = type(name.encode('utf-8'), (formula_class,), formula_class_attributes) # Fill column attributes. if cerfa_field is not None: column.cerfa_field = cerfa_field if variable.end is not None: column.end = variable.end column.entity = entity_class.symbol # Obsolete: To remove once build_..._couple() functions are no more used. column.entity_key_plural = entity_class.key_plural column.formula_class = formula_class if variable.is_permanent: column.is_permanent = True column.label = label column.law_reference = law_reference column.name = name if variable.start is not None: column.start = variable.start if url is not None: column.url = url return column class FormulaColumnMetaclass(type): """The metaclass of FormulaColumn classes: It generates a column instead of a formula FormulaColumn class.""" def __new__(cls, name, bases, attributes): """Return a column containing a formula, built from FormulaColumn class definition.""" assert len(bases) == 1, bases base_class = bases[0] if base_class is object: # Do nothing when creating classes DatedFormulaColumn, SimpleFormulaColumn, etc. return super(FormulaColumnMetaclass, cls).__new__(cls, name, bases, attributes) formula_class = attributes.pop('formula_class', UnboundLocalError) reference_column = attributes.pop('reference', None) if formula_class is UnboundLocalError: formula_class = base_class.formula_class \ if reference_column is None or reference_column.formula_class is None \ else reference_column.formula_class self = super(FormulaColumnMetaclass, cls).__new__(cls, name, bases, attributes) comments = inspect.getcomments(self) source_file_path = inspect.getsourcefile(self) source_lines, line_number = inspect.getsourcelines(self) source_code = textwrap.dedent(''.join(source_lines)) return new_filled_column( base_function = attributes.pop('base_function', UnboundLocalError), calculate_output = attributes.pop('calculate_output', UnboundLocalError), cerfa_field = attributes.pop('cerfa_field', UnboundLocalError), column = attributes.pop('column', UnboundLocalError), comments = comments, doc = attributes.pop('__doc__', None), entity_class = attributes.pop('entity_class', UnboundLocalError), formula_class = formula_class, is_permanent = attributes.pop('is_permanent', UnboundLocalError), label = attributes.pop('label', UnboundLocalError), law_reference = attributes.pop('law_reference', UnboundLocalError), line_number = line_number, module = attributes.pop('__module__'), name = unicode(name), reference_column = reference_column, set_input = attributes.pop('set_input', UnboundLocalError), source_code = source_code, source_file_path = source_file_path, start_date = attributes.pop('start_date', UnboundLocalError), stop_date = attributes.pop('stop_date', UnboundLocalError), url = attributes.pop('url', UnboundLocalError), **attributes ) class DatedFormulaColumn(object): """Syntactic sugar to generate a DatedFormula class and fill its column""" __metaclass__ = FormulaColumnMetaclass formula_class = DatedFormula class EntityToPersonColumn(object): """Syntactic sugar to generate an EntityToPerson class and fill its column""" __metaclass__ = ConversionColumnMetaclass formula_class = EntityToPerson class PersonToEntityColumn(object): """Syntactic sugar to generate an PersonToEntity class and fill its column""" __metaclass__ = ConversionColumnMetaclass formula_class = PersonToEntity class SimpleFormulaColumn(object): """Syntactic sugar to generate a SimpleFormula class and fill its column""" __metaclass__ = FormulaColumnMetaclass formula_class = SimpleFormula def calculate_output_add(formula, period): return formula.holder.compute_add(period).array def calculate_output_add_divide(formula, period): return formula.holder.compute_add_divide(period).array def calculate_output_divide(formula, period): return formula.holder.compute_divide(period).array def dated_function(start = None, stop = None): """Function decorator used to give start & stop instants to a method of a function in class DatedFormulaColumn.""" def dated_function_decorator(function): function.start_instant = periods.instant(start) function.stop_instant = periods.instant(stop) return function return dated_function_decorator def last_duration_last_value(formula, simulation, period): # This formula is used for variables that are constants between events but are period size dependent. # It returns the latest known value for the requested start of period but with the last period size. holder = formula.holder if holder._array_by_period is not None: for last_period, last_array in sorted(holder._array_by_period.iteritems(), reverse = True): if last_period.start <= period.start and (formula.function is None or last_period.stop >= period.stop): return periods.Period((last_period[0], period.start, last_period[2])), last_array if formula.function is not None: return formula.function(simulation, period) column = holder.column array = np.empty(holder.entity.count, dtype = column.dtype) array.fill(column.default) return period, array def make_reference_formula_decorator(entity_class_by_symbol = None, update = False): assert isinstance(entity_class_by_symbol, dict) def reference_formula_decorator(column): """Class decorator used to declare a formula to the relevant entity class.""" assert isinstance(column, columns.Column) assert column.formula_class is not None entity_class = entity_class_by_symbol[column.entity] entity_column_by_name = entity_class.column_by_name name = column.name if not update: assert name not in entity_column_by_name, name entity_column_by_name[name] = column return column return reference_formula_decorator def missing_value(formula, simulation, period): if formula.function is not None: return formula.function(simulation, period) holder = formula.holder column = holder.column raise ValueError(u"Missing value for variable {} at {}".format(column.name, period)) def neutralize_column(column): """Return a new neutralized column (to be used by reforms).""" return new_filled_column( base_function = requested_period_default_value_neutralized, label = u'[Neutralized]' if column.label is None else u'[Neutralized] {}'.format(column.label), reference_column = column, set_input = set_input_neutralized, ) def new_filled_column(base_function = UnboundLocalError, calculate_output = UnboundLocalError, cerfa_field = UnboundLocalError, column = UnboundLocalError, comments = UnboundLocalError, doc = None, entity_class = UnboundLocalError, formula_class = UnboundLocalError, is_permanent = UnboundLocalError, label = UnboundLocalError, law_reference = UnboundLocalError, line_number = UnboundLocalError, module = None, name = None, reference_column = None, set_input = UnboundLocalError, source_code = UnboundLocalError, source_file_path = UnboundLocalError, start_date = UnboundLocalError, stop_date = UnboundLocalError, url = UnboundLocalError, **specific_attributes): # Validate arguments. if reference_column is not None: assert isinstance(reference_column, columns.Column) if name is None: name = reference_column.name assert isinstance(name, unicode) if calculate_output is UnboundLocalError: calculate_output = None if reference_column is None else reference_column.formula_class.calculate_output if cerfa_field is UnboundLocalError: cerfa_field = None if reference_column is None else reference_column.cerfa_field elif cerfa_field is not None: assert isinstance(cerfa_field, basestring), cerfa_field cerfa_field = unicode(cerfa_field) assert column is not None, """Missing attribute "column" in definition of filled column {}""".format(name) if column is UnboundLocalError: assert reference_column is not None, """Missing attribute "column" in definition of filled column {}""".format( name) column = reference_column.empty_clone() elif not isinstance(column, columns.Column): column = column() assert isinstance(column, columns.Column) if comments is UnboundLocalError: comments = None if reference_column is None else reference_column.formula_class.comments elif isinstance(comments, str): comments = comments.decode('utf-8') assert entity_class is not None, """Missing attribute "entity_class" in definition of filled column {}""".format( name) if entity_class is UnboundLocalError: assert reference_column is not None, \ """Missing attribute "entity_class" in definition of filled column {}""".format(name) entity_class_key_plural = reference_column.entity_key_plural entity_class_symbol = reference_column.entity else: entity_class_key_plural = entity_class.key_plural entity_class_symbol = entity_class.symbol assert formula_class is not None, """Missing attribute "formula_class" in definition of filled column {}""".format( name) if formula_class is UnboundLocalError: assert reference_column is not None, \ """Missing attribute "formula_class" in definition of filled column {}""".format(name) formula_class = reference_column.formula_class.__bases__[0] assert issubclass(formula_class, AbstractFormula), formula_class if is_permanent is UnboundLocalError: is_permanent = False if reference_column is None else reference_column.is_permanent else: assert is_permanent in (False, True), is_permanent if label is UnboundLocalError: label = name if reference_column is None else reference_column.label else: label = name if label is None else unicode(label) if law_reference is UnboundLocalError: law_reference = None if reference_column is None else reference_column.law_reference else: assert isinstance(law_reference, (basestring, list)) if line_number is UnboundLocalError: line_number = None if reference_column is None else reference_column.formula_class.line_number elif isinstance(line_number, str): line_number = line_number.decode('utf-8') if set_input is UnboundLocalError: set_input = None if reference_column is None else reference_column.formula_class.set_input if source_code is UnboundLocalError: source_code = None if reference_column is None else reference_column.formula_class.source_code elif isinstance(source_code, str): source_code = source_code.decode('utf-8') if source_file_path is UnboundLocalError: source_file_path = None if reference_column is None else reference_column.formula_class.source_file_path elif isinstance(source_file_path, str): source_file_path = source_file_path.decode('utf-8') if start_date is UnboundLocalError: start_date = None if reference_column is None else reference_column.start elif start_date is not None: assert isinstance(start_date, datetime.date) if stop_date is UnboundLocalError: stop_date = None if reference_column is None else reference_column.end elif stop_date is not None: assert isinstance(stop_date, datetime.date) if url is UnboundLocalError: url = None if reference_column is None else reference_column.url elif url is not None: url = unicode(url) # Build formula class and column. formula_class_attributes = {} if doc is not None: formula_class_attributes['__doc__'] = doc if module is not None: assert isinstance(module, basestring) formula_class_attributes['__module__'] = module if comments is not None: formula_class_attributes['comments'] = comments if line_number is not None: formula_class_attributes['line_number'] = line_number if source_code is not None: formula_class_attributes['source_code'] = source_code if source_file_path is not None: formula_class_attributes['source_file_path'] = source_file_path if is_permanent: assert base_function is UnboundLocalError base_function = permanent_default_value elif column.is_period_size_independent: assert base_function in (missing_value, requested_period_last_value, UnboundLocalError) if base_function is UnboundLocalError: base_function = requested_period_last_value elif base_function is UnboundLocalError: base_function = requested_period_default_value if base_function is UnboundLocalError: assert reference_column is not None \ and issubclass(reference_column.formula_class, (DatedFormula, SimpleFormula)), \ """Missing attribute "base_function" in definition of filled column {}""".format(name) base_function = reference_column.formula_class.base_function else: assert base_function is not None, \ """Missing attribute "base_function" in definition of filled column {}""".format(name) formula_class_attributes['base_function'] = base_function if calculate_output is not None: formula_class_attributes['calculate_output'] = calculate_output if set_input is not None: formula_class_attributes['set_input'] = set_input if issubclass(formula_class, DatedFormula): assert not is_permanent dated_formulas_class = [] for function_name, function in specific_attributes.copy().iteritems(): start_instant = getattr(function, 'start_instant', UnboundLocalError) if start_instant is UnboundLocalError: # Function is not dated (and may not even be a function). Skip it. continue stop_instant = function.stop_instant if stop_instant is not None: assert start_instant <= stop_instant, 'Invalid instant interval for function {}: {} - {}'.format( function_name, start_instant, stop_instant) dated_formula_class_attributes = formula_class_attributes.copy() dated_formula_class_attributes['function'] = function dated_formula_class = type(name.encode('utf-8'), (SimpleFormula,), dated_formula_class_attributes) del specific_attributes[function_name] dated_formulas_class.append(dict( formula_class = dated_formula_class, start_instant = start_instant, stop_instant = stop_instant, )) # Sort dated formulas by start instant and add missing stop instants. dated_formulas_class.sort(key = lambda dated_formula_class: dated_formula_class['start_instant']) for dated_formula_class, next_dated_formula_class in itertools.izip(dated_formulas_class, itertools.islice(dated_formulas_class, 1, None)): if dated_formula_class['stop_instant'] is None: dated_formula_class['stop_instant'] = next_dated_formula_class['start_instant'].offset(-1, 'day') else: assert dated_formula_class['stop_instant'] < next_dated_formula_class['start_instant'], \ "Dated formulas overlap: {} & {}".format(dated_formula_class, next_dated_formula_class) # Add dated formulas defined in (optional) reference column when they are not overridden by new dated # formulas. if reference_column is not None and issubclass(reference_column.formula_class, DatedFormula): for reference_dated_formula_class in reference_column.formula_class.dated_formulas_class: reference_dated_formula_class = reference_dated_formula_class.copy() for dated_formula_class in dated_formulas_class: if reference_dated_formula_class['start_instant'] == dated_formula_class['start_instant'] \ and reference_dated_formula_class['stop_instant'] == dated_formula_class[ 'stop_instant']: break if reference_dated_formula_class['start_instant'] >= dated_formula_class['start_instant'] \ and reference_dated_formula_class['start_instant'] < dated_formula_class[ 'stop_instant']: reference_dated_formula_class['start_instant'] = dated_formula_class['stop_instant'].offset( 1, 'day') if reference_dated_formula_class['stop_instant'] > dated_formula_class['start_instant'] \ and reference_dated_formula_class['stop_instant'] <= dated_formula_class[ 'stop_instant']: reference_dated_formula_class['stop_instant'] = dated_formula_class['start_instant'].offset( -1, 'day') if reference_dated_formula_class['start_instant'] > reference_dated_formula_class[ 'stop_instant']: break else: dated_formulas_class.append(reference_dated_formula_class) dated_formulas_class.sort(key = lambda dated_formula_class: dated_formula_class['start_instant']) formula_class_attributes['dated_formulas_class'] = dated_formulas_class else: assert issubclass(formula_class, SimpleFormula), formula_class function = specific_attributes.pop('function', UnboundLocalError) if is_permanent: assert function is UnboundLocalError if function is UnboundLocalError: assert reference_column is not None and issubclass(reference_column.formula_class, SimpleFormula), \ """Missing attribute "function" in definition of filled column {}""".format(name) function = reference_column.formula_class.function else: assert function is not None, """Missing attribute "function" in definition of filled column {}""".format( name) formula_class_attributes['function'] = function # Ensure that all attributes defined in ConversionColumn class are used. assert not specific_attributes, 'Unexpected attributes in definition of variable {}: {}'.format(name, ', '.join(sorted(specific_attributes.iterkeys()))) formula_class = type(name.encode('utf-8'), (formula_class,), formula_class_attributes) # Fill column attributes. if cerfa_field is not None: column.cerfa_field = cerfa_field if stop_date is not None: column.end = stop_date column.entity = entity_class_symbol # Obsolete: To remove once build_..._couple() functions are no more used. column.entity_key_plural = entity_class_key_plural column.formula_class = formula_class if is_permanent: column.is_permanent = True column.label = label column.law_reference = law_reference column.name = name if start_date is not None: column.start = start_date if url is not None: column.url = url return column def permanent_default_value(formula, simulation, period): if formula.function is not None: return formula.function(simulation, period) holder = formula.holder column = holder.column array = np.empty(holder.entity.count, dtype = column.dtype) array.fill(column.default) return period, array def reference_input_variable(base_function = None, calculate_output = None, column = None, entity_class = None, is_permanent = False, label = None, name = None, set_input = None, start_date = None, stop_date = None, update = False, url = None): """Define an input variable and add it to relevant entity class.""" if not isinstance(column, columns.Column): column = column() assert isinstance(column, columns.Column) if is_permanent: assert base_function is None base_function = permanent_default_value elif column.is_period_size_independent: assert base_function is None base_function = requested_period_last_value elif base_function is None: base_function = requested_period_default_value assert isinstance(name, basestring), name name = unicode(name) label = name if label is None else unicode(label) caller_frame = inspect.currentframe().f_back column.formula_class = formula_class = type(name.encode('utf-8'), (SimpleFormula,), dict( __module__ = inspect.getmodule(caller_frame).__name__, base_function = base_function, line_number = caller_frame.f_lineno, )) if calculate_output is not None: formula_class.calculate_output = calculate_output if set_input is not None: formula_class.set_input = set_input if stop_date is not None: assert isinstance(stop_date, datetime.date) column.end = stop_date column.entity = entity_class.symbol # Obsolete: To remove once build_..._couple() functions are no more used. column.entity_key_plural = entity_class.key_plural if is_permanent: column.is_permanent = True column.label = label column.name = name if start_date is not None: assert isinstance(start_date, datetime.date) column.start = start_date if url is not None: column.url = unicode(url) entity_column_by_name = entity_class.column_by_name if not update: assert name not in entity_column_by_name, name entity_column_by_name[name] = column def requested_period_added_value(formula, simulation, period): # This formula is used for variables that can be added to match requested period. holder = formula.holder column = holder.column period_size = period.size period_unit = period.unit if holder._array_by_period is not None and (period_size > 1 or period_unit == u'year'): after_instant = period.start.offset(period_size, period_unit) if period_size > 1: array = np.zeros(holder.entity.count, dtype = column.dtype) sub_period = period.start.period(period_unit) while sub_period.start < after_instant: sub_array = holder._array_by_period.get(sub_period) if sub_array is None: array = None break array += sub_array sub_period = sub_period.offset(1) if array is not None: return period, array if period_unit == u'year': array = np.zeros(holder.entity.count, dtype = column.dtype) month = period.start.period(u'month') while month.start < after_instant: month_array = holder._array_by_period.get(month) if month_array is None: array = None break array += month_array month = month.offset(1) if array is not None: return period, array if formula.function is not None: return formula.function(simulation, period) array = np.empty(holder.entity.count, dtype = column.dtype) array.fill(column.default) return period, array def requested_period_default_value(formula, simulation, period): if formula.function is not None: return formula.function(simulation, period) holder = formula.holder column = holder.column array = np.empty(holder.entity.count, dtype = column.dtype) array.fill(column.default) return period, array def requested_period_default_value_neutralized(formula, simulation, period): holder = formula.holder column = holder.column array = np.empty(holder.entity.count, dtype = column.dtype) array.fill(column.default) return period, array def requested_period_last_value(formula, simulation, period): # This formula is used for variables that are constants between events and period size independent. # It returns the latest known value for the requested period. holder = formula.holder if holder._array_by_period is not None: for last_period, last_array in sorted(holder._array_by_period.iteritems(), reverse = True): if last_period.start <= period.start and (formula.function is None or last_period.stop >= period.stop): return period, last_array if formula.function is not None: return formula.function(simulation, period) column = holder.column array = np.empty(holder.entity.count, dtype = column.dtype) array.fill(column.default) return period, array def set_input_dispatch_by_period(formula, period, array): holder = formula.holder holder.set_array(period, array) period_size = period.size period_unit = period.unit if period_unit == u'year' or period_size > 1: after_instant = period.start.offset(period_size, period_unit) if period_size > 1: sub_period = period.start.period(period_unit) while sub_period.start < after_instant: existing_array = holder.get_array(sub_period) if existing_array is None: holder.set_array(sub_period, array) else: # The array of the current sub-period is reused for the next ones. array = existing_array sub_period = sub_period.offset(1) if period_unit == u'year': month = period.start.period(u'month') while month.start < after_instant: existing_array = holder.get_array(month) if existing_array is None: holder.set_array(month, array) else: # The array of the current sub-period is reused for the next ones. array = existing_array month = month.offset(1) def set_input_divide_by_period(formula, period, array): holder = formula.holder holder.set_array(period, array) period_size = period.size period_unit = period.unit if period_unit == u'year' or period_size > 1: after_instant = period.start.offset(period_size, period_unit) if period_size > 1: remaining_array = array.copy() sub_period = period.start.period(period_unit) sub_periods_count = period_size while sub_period.start < after_instant: existing_array = holder.get_array(sub_period) if existing_array is not None: remaining_array -= existing_array sub_periods_count -= 1 sub_period = sub_period.offset(1) if sub_periods_count > 0: divided_array = remaining_array / sub_periods_count sub_period = period.start.period(period_unit) while sub_period.start < after_instant: if holder.get_array(sub_period) is None: holder.set_array(sub_period, divided_array) sub_period = sub_period.offset(1) if period_unit == u'year': remaining_array = array.copy() month = period.start.period(u'month') months_count = 12 * period_size while month.start < after_instant: existing_array = holder.get_array(month) if existing_array is not None: remaining_array -= existing_array months_count -= 1 month = month.offset(1) if months_count > 0: divided_array = remaining_array / months_count month = period.start.period(u'month') while month.start < after_instant: if holder.get_array(month) is None: holder.set_array(month, divided_array) month = month.offset(1) def set_input_neutralized(formula, period, array): pass PK^PF openfisca_core/tests/__init__.pyPK 'GjAYY&openfisca_core/tests/test_countries.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import collections import datetime import itertools import numpy as np from numpy.core.defchararray import startswith from openfisca_core import conv, periods from openfisca_core.columns import BoolCol, DateCol, FixedStrCol, FloatCol, IntCol from openfisca_core.entities import AbstractEntity from openfisca_core.formulas import (dated_function, DatedFormulaColumn, EntityToPersonColumn, make_reference_formula_decorator, PersonToEntityColumn, reference_input_variable, set_input_divide_by_period, SimpleFormulaColumn) from openfisca_core.scenarios import AbstractScenario, set_entities_json_id from openfisca_core.taxbenefitsystems import AbstractTaxBenefitSystem from openfisca_core.tools import assert_near # Entities class Familles(AbstractEntity): column_by_name = collections.OrderedDict() index_for_person_variable_name = 'id_famille' key_plural = 'familles' key_singular = 'famille' label = u'Famille' max_cardinality_by_role_key = {'parents': 2} role_for_person_variable_name = 'role_dans_famille' roles_key = ['parents', 'enfants'] label_by_role_key = { 'enfants': u'Enfants', 'parents': u'Parents', } symbol = 'fam' def iter_member_persons_role_and_id(self, member): role = 0 parents_id = member['parents'] assert 1 <= len(parents_id) <= 2 for parent_role, parent_id in enumerate(parents_id, role): assert parent_id is not None yield parent_role, parent_id role += 2 enfants_id = member.get('enfants') if enfants_id is not None: for enfant_role, enfant_id in enumerate(enfants_id, role): assert enfant_id is not None yield enfant_role, enfant_id class Individus(AbstractEntity): column_by_name = collections.OrderedDict() is_persons_entity = True key_plural = 'individus' key_singular = 'individu' label = u'Personne' symbol = 'ind' entity_class_by_symbol = dict( fam = Familles, ind = Individus, ) # Scenarios class Scenario(AbstractScenario): def init_single_entity(self, axes = None, enfants = None, famille = None, parent1 = None, parent2 = None, period = None): if enfants is None: enfants = [] assert parent1 is not None famille = famille.copy() if famille is not None else {} individus = [] for index, individu in enumerate([parent1, parent2] + (enfants or [])): if individu is None: continue id = individu.get('id') if id is None: individu = individu.copy() individu['id'] = id = 'ind{}'.format(index) individus.append(individu) if index <= 1: famille.setdefault('parents', []).append(id) else: famille.setdefault('enfants', []).append(id) conv.check(self.make_json_or_python_to_attributes())(dict( axes = axes, period = period, test_case = dict( familles = [famille], individus = individus, ), )) return self def make_json_or_python_to_test_case(self, period = None, repair = False): assert period is not None def json_or_python_to_test_case(value, state = None): if value is None: return value, None if state is None: state = conv.default_state column_by_name = self.tax_benefit_system.column_by_name # First validation and conversion step test_case, error = conv.pipe( conv.test_isinstance(dict), conv.struct( dict( familles = conv.pipe( conv.make_item_to_singleton(), conv.test_isinstance(list), conv.uniform_sequence( conv.test_isinstance(dict), drop_none_items = True, ), conv.function(set_entities_json_id), conv.uniform_sequence( conv.struct( dict(itertools.chain( dict( enfants = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( conv.test_isinstance((basestring, int)), drop_none_items = True, ), conv.default([]), ), id = conv.pipe( conv.test_isinstance((basestring, int)), conv.not_none, ), parents = conv.pipe( conv.test_isinstance(list), conv.uniform_sequence( conv.test_isinstance((basestring, int)), drop_none_items = True, ), conv.default([]), ), ).iteritems(), ( (column.name, column.json_to_python) for column in column_by_name.itervalues() if column.entity == 'fam' ), )), drop_none_values = True, ), drop_none_items = True, ), conv.default({}), ), individus = conv.pipe( conv.make_item_to_singleton(), conv.test_isinstance(list), conv.uniform_sequence( conv.test_isinstance(dict), drop_none_items = True, ), conv.function(set_entities_json_id), conv.uniform_sequence( conv.struct( dict(itertools.chain( dict( id = conv.pipe( conv.test_isinstance((basestring, int)), conv.not_none, ), ).iteritems(), ( (column.name, column.json_to_python) for column in column_by_name.itervalues() if column.entity == 'ind' and column.name not in ( 'idfam', 'idfoy', 'idmen', 'quifam', 'quifoy', 'quimen') ), )), drop_none_values = True, ), drop_none_items = True, ), conv.empty_to_none, conv.not_none, ), ), ), )(value, state = state) if error is not None: return test_case, error # Second validation step familles_individus_id = [individu['id'] for individu in test_case['individus']] test_case, error = conv.struct( dict( familles = conv.uniform_sequence( conv.struct( dict( enfants = conv.uniform_sequence(conv.test_in_pop(familles_individus_id)), parents = conv.uniform_sequence(conv.test_in_pop(familles_individus_id)), ), default = conv.noop, ), ), ), default = conv.noop, )(test_case, state = state) remaining_individus_id = set(familles_individus_id) if remaining_individus_id: individu_index_by_id = { individu[u'id']: individu_index for individu_index, individu in enumerate(test_case[u'individus']) } if error is None: error = {} for individu_id in remaining_individus_id: error.setdefault('individus', {})[individu_index_by_id[individu_id]] = state._( u"Individual is missing from {}").format( state._(u' & ').join( word for word in [ u'familles' if individu_id in familles_individus_id else None, ] if word is not None )) if error is not None: return test_case, error return test_case, error return json_or_python_to_test_case # TaxBenefitSystems def init_country(): class TaxBenefitSystem(AbstractTaxBenefitSystem): entity_class_by_key_plural = { entity_class.key_plural: entity_class for entity_class in entity_class_by_symbol.itervalues() } # Define class attributes after class declaration to avoid "name is not defined" exceptions. TaxBenefitSystem.Scenario = Scenario return TaxBenefitSystem # Input variables reference_input_variable( column = IntCol, entity_class = Individus, label = u"Âge (en nombre de mois)", name = 'age_en_mois', ) reference_input_variable( column = DateCol, entity_class = Individus, label = u"Date de naissance", name = 'birth', ) reference_input_variable( column = FixedStrCol(max_length = 5), entity_class = Familles, is_permanent = True, label = u"""Code INSEE "depcom" de la commune de résidence de la famille""", name = 'depcom', ) reference_input_variable( column = IntCol, entity_class = Individus, is_permanent = True, label = u"Identifiant de la famille", name = 'id_famille', ) reference_input_variable( column = IntCol, entity_class = Individus, is_permanent = True, label = u"Rôle dans la famille", name = 'role_dans_famille', ) reference_input_variable( column = FloatCol, entity_class = Individus, label = "Salaire brut", name = 'salaire_brut', set_input = set_input_divide_by_period, ) # Calculated variables reference_formula = make_reference_formula_decorator(entity_class_by_symbol = entity_class_by_symbol) @reference_formula class age(SimpleFormulaColumn): column = IntCol entity_class = Individus label = u"Âge (en nombre d'années)" def function(self, simulation, period): birth = simulation.get_array('birth', period) if birth is None: age_en_mois = simulation.get_array('age_en_mois', period) if age_en_mois is not None: return period, age_en_mois // 12 birth = simulation.calculate('birth', period) return period, (np.datetime64(period.date) - birth).astype('timedelta64[Y]') @reference_formula class dom_tom(SimpleFormulaColumn): column = BoolCol entity_class = Familles label = u"La famille habite-t-elle les DOM-TOM ?" def function(self, simulation, period): period = period.start.period(u'year').offset('first-of') depcom = simulation.calculate('depcom', period) return period, np.logical_or(startswith(depcom, '97'), startswith(depcom, '98')) @reference_formula class dom_tom_individu(EntityToPersonColumn): entity_class = Individus label = u"La personne habite-t-elle les DOM-TOM ?" variable = dom_tom @reference_formula class revenu_disponible(SimpleFormulaColumn): column = FloatCol entity_class = Individus label = u"Revenu disponible de l'individu" def function(self, simulation, period): period = period.start.period(u'year').offset('first-of') rsa = simulation.calculate_add('rsa', period) salaire_imposable = simulation.calculate('salaire_imposable', period) return period, rsa + salaire_imposable * 0.7 @reference_formula class revenu_disponible_famille(PersonToEntityColumn): entity_class = Familles label = u"Revenu disponible de la famille" operation = 'add' variable = revenu_disponible @reference_formula class rsa(DatedFormulaColumn): column = FloatCol entity_class = Individus label = u"RSA" @dated_function(datetime.date(2010, 1, 1)) def function_2010(self, simulation, period): period = period.start.period(u'month').offset('first-of') salaire_imposable = simulation.calculate_divide('salaire_imposable', period) return period, (salaire_imposable < 500) * 100.0 @dated_function(datetime.date(2011, 1, 1), datetime.date(2012, 12, 31)) def function_2011_2012(self, simulation, period): period = period.start.period(u'month').offset('first-of') salaire_imposable = simulation.calculate_divide('salaire_imposable', period) return period, (salaire_imposable < 500) * 200.0 @dated_function(datetime.date(2013, 1, 1)) def function_2013(self, simulation, period): period = period.start.period(u'month').offset('first-of') salaire_imposable = simulation.calculate_divide('salaire_imposable', period) return period, (salaire_imposable < 500) * 300 @reference_formula class salaire_imposable(SimpleFormulaColumn): column = FloatCol entity_class = Individus label = u"Salaire imposable" def function(self, simulation, period): period = period.start.period(u'year').offset('first-of') dom_tom_individu = simulation.calculate('dom_tom_individu', period) salaire_net = simulation.calculate('salaire_net', period) return period, salaire_net * 0.9 - 100 * dom_tom_individu @reference_formula class salaire_net(SimpleFormulaColumn): column = FloatCol entity_class = Individus label = u"Salaire net" def function(self, simulation, period): period = period.start.period(u'year').offset('first-of') salaire_brut = simulation.calculate('salaire_brut', period) return period, salaire_brut * 0.8 # TaxBenefitSystem instance declared after formulas TaxBenefitSystem = init_country() tax_benefit_system = TaxBenefitSystem() def test_1_axis(): year = 2013 simulation = tax_benefit_system.new_scenario().init_single_entity( axes = [ dict( count = 3, name = 'salaire_brut', max = 100000, min = 0, ), ], period = year, parent1 = {}, parent2 = {}, ).new_simulation(debug = True) assert_near(simulation.calculate('revenu_disponible_famille'), [7200, 28800, 54000], absolute_error_margin = 0.005) def test_2_parallel_axes_1_constant(): year = 2013 simulation = tax_benefit_system.new_scenario().init_single_entity( axes = [ [ dict( count = 3, name = 'salaire_brut', max = 100000, min = 0, ), dict( count = 3, index = 1, name = 'salaire_brut', max = 0.0001, min = 0, ), ], ], period = year, parent1 = {}, parent2 = {}, ).new_simulation(debug = True) assert_near(simulation.calculate('revenu_disponible_famille'), [7200, 28800, 54000], absolute_error_margin = 0.005) def test_2_parallel_axes_different_periods(): year = 2013 simulation = tax_benefit_system.new_scenario().init_single_entity( axes = [ [ dict( count = 3, name = 'salaire_brut', max = 120000, min = 0, period = year - 1, ), dict( count = 3, index = 1, name = 'salaire_brut', max = 120000, min = 0, period = year, ), ], ], period = year, parent1 = {}, parent2 = {}, ).new_simulation(debug = True) assert_near(simulation.calculate('salaire_brut', year - 1), [0, 0, 60000, 0, 120000, 0], absolute_error_margin = 0) assert_near(simulation.calculate('salaire_brut', '{}-01'.format(year - 1)), [0, 0, 5000, 0, 10000, 0], absolute_error_margin = 0) assert_near(simulation.calculate('salaire_brut', year), [0, 0, 0, 60000, 0, 120000], absolute_error_margin = 0) assert_near(simulation.calculate('salaire_brut', '{}-01'.format(year)), [0, 0, 0, 5000, 0, 10000], absolute_error_margin = 0) def test_2_parallel_axes_same_values(): year = 2013 simulation = tax_benefit_system.new_scenario().init_single_entity( axes = [ [ dict( count = 3, name = 'salaire_brut', max = 100000, min = 0, ), dict( count = 3, index = 1, name = 'salaire_brut', max = 100000, min = 0, ), ], ], period = year, parent1 = {}, parent2 = {}, ).new_simulation(debug = True) assert_near(simulation.calculate('revenu_disponible_famille'), [7200, 50400, 100800], absolute_error_margin = 0.005) def test_age(): year = 2013 simulation = tax_benefit_system.new_scenario().init_single_entity( period = year, parent1 = dict( birth = datetime.date(year - 40, 1, 1), ), ).new_simulation(debug = True) assert_near(simulation.calculate('age'), [40], absolute_error_margin = 0.005) simulation = tax_benefit_system.new_scenario().init_single_entity( period = year, parent1 = dict( age_en_mois = 40 * 12 + 11, ), ).new_simulation(debug = True) assert_near(simulation.calculate('age'), [40], absolute_error_margin = 0.005) def check_revenu_disponible(year, depcom, expected_revenu_disponible): global tax_benefit_system simulation = tax_benefit_system.new_scenario().init_single_entity( axes = [ dict( count = 3, name = 'salaire_brut', max = 100000, min = 0, ), ], famille = dict(depcom = depcom), period = periods.period(year), parent1 = dict(), parent2 = dict(), ).new_simulation(debug = True) revenu_disponible = simulation.calculate('revenu_disponible') assert_near(revenu_disponible, expected_revenu_disponible, absolute_error_margin = 0.005) revenu_disponible_famille = simulation.calculate('revenu_disponible_famille') expected_revenu_disponible_famille = np.array([ expected_revenu_disponible[i] + expected_revenu_disponible[i + 1] for i in range(0, len(expected_revenu_disponible), 2) ]) assert_near(revenu_disponible_famille, expected_revenu_disponible_famille, absolute_error_margin = 0.005) def test_revenu_disponible(): yield check_revenu_disponible, 2009, '75101', np.array([0, 0, 25200, 0, 50400, 0]) yield check_revenu_disponible, 2010, '75101', np.array([1200, 1200, 25200, 1200, 50400, 1200]) yield check_revenu_disponible, 2011, '75101', np.array([2400, 2400, 25200, 2400, 50400, 2400]) yield check_revenu_disponible, 2012, '75101', np.array([2400, 2400, 25200, 2400, 50400, 2400]) yield check_revenu_disponible, 2013, '75101', np.array([3600, 3600, 25200, 3600, 50400, 3600]) yield check_revenu_disponible, 2009, '97123', np.array([-70.0, -70.0, 25130.0, -70.0, 50330.0, -70.0]) yield check_revenu_disponible, 2010, '97123', np.array([1130.0, 1130.0, 25130.0, 1130.0, 50330.0, 1130.0]) yield check_revenu_disponible, 2011, '98456', np.array([2330.0, 2330.0, 25130.0, 2330.0, 50330.0, 2330.0]) yield check_revenu_disponible, 2012, '98456', np.array([2330.0, 2330.0, 25130.0, 2330.0, 50330.0, 2330.0]) yield check_revenu_disponible, 2013, '98456', np.array([3530.0, 3530.0, 25130.0, 3530.0, 50330.0, 3530.0]) PK 'GA$openfisca_core/tests/test_reforms.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from nose.tools import assert_equal from .. import periods, reforms from ..formulas import neutralize_column from ..tools import assert_near from .test_countries import tax_benefit_system def test_formula_neutralization(): Reform = reforms.make_reform( key = u'test_rsa_neutralization', name = u'Test rsa neutralization', reference = tax_benefit_system, ) Reform.formula(neutralize_column(tax_benefit_system.column_by_name['rsa'])) reform = Reform() year = 2013 scenario = reform.new_scenario().init_single_entity( period = year, famille = dict(depcom = '75101'), parent1 = dict(), parent2 = dict(), ) simulation = scenario.new_simulation(debug = True, reference = True) rsa = simulation.calculate('rsa', period = '2013-01') assert_near(rsa, 300, absolute_error_margin = 0) revenu_disponible = simulation.calculate('revenu_disponible') assert_near(revenu_disponible, 3600, absolute_error_margin = 0) reform_simulation = scenario.new_simulation(debug = True) rsa_reform = reform_simulation.calculate('rsa', period = '2013-01') assert_near(rsa_reform, 0, absolute_error_margin = 0) revenu_disponible_reform = reform_simulation.calculate('revenu_disponible') assert_near(revenu_disponible_reform, 0, absolute_error_margin = 0) # def test_input_variable_neutralization(): Reform = reforms.make_reform( key = u'test_salaire_brut_neutralization', name = u'Test salaire_brut neutralization', reference = tax_benefit_system, ) Reform.formula(neutralize_column(tax_benefit_system.column_by_name['salaire_brut'])) reform = Reform() year = 2013 scenario = reform.new_scenario().init_single_entity( period = year, famille = dict(depcom = '75101'), parent1 = dict( salaire_brut = 120000, ), parent2 = dict( salaire_brut = 60000, ), ) simulation = scenario.new_simulation(debug = True, reference = True) salaire_brut_annuel = simulation.calculate('salaire_brut') assert_near(salaire_brut_annuel, [120000, 60000], absolute_error_margin = 0) salaire_brut_mensuel = simulation.calculate('salaire_brut', period = '2013-01') assert_near(salaire_brut_mensuel, [10000, 5000], absolute_error_margin = 0) revenu_disponible = simulation.calculate('revenu_disponible') assert_near(revenu_disponible, [60480, 30240], absolute_error_margin = 0) reform_simulation = scenario.new_simulation(debug = True) salaire_brut_annuel_reform = reform_simulation.calculate('salaire_brut') assert_near(salaire_brut_annuel_reform, [0, 0], absolute_error_margin = 0) salaire_brut_mensuel_reform = reform_simulation.calculate('salaire_brut', period = '2013-01') assert_near(salaire_brut_mensuel_reform, [0, 0], absolute_error_margin = 0) revenu_disponible_reform = reform_simulation.calculate('revenu_disponible') assert_near(revenu_disponible_reform, [3600, 3600], absolute_error_margin = 0) def test_updated_legislation_items(): def check_updated_legislation_items(description, items, start_instant, stop_instant, value, expected_items): new_items = reforms.updated_legislation_items(items, start_instant, stop_instant, value) assert_equal(map(dict, new_items), expected_items) # yield( # check_updated_legislation_items, # u'Insert a new item before the first existing item', # [ # { # "start": "2012-01-01", # "stop": "2013-12-31", # "value": 0.0, # }, # ], # periods.period('year', 2010).start, # periods.period('year', 2010).stop, # 1, # [ # { # "start": "2010-01-01", # "stop": "2010-12-31", # "value": 1.0, # }, # { # "start": "2012-01-01", # "stop": "2013-12-31", # "value": 0.0, # }, # ], # ) # yield( # check_updated_legislation_items, # u'Insert a new item after the last existing item', # [ # { # "start": "2012-01-01", # "stop": "2013-12-31", # "value": 0.0, # }, # ], # periods.period('year', 2014).start, # periods.period('year', 2014).stop, # 1, # [ # { # "start": "2012-01-01", # "stop": "2013-12-31", # "value": 0.0, # }, # { # "start": "2014-01-01", # "stop": "2014-12-31", # "value": 1.0, # }, # ], # ) yield( check_updated_legislation_items, u'Replace an item by a new item', [ { "start": "2013-01-01", "stop": "2013-12-31", "value": 0.0, }, ], periods.period('year', 2013).start, periods.period('year', 2013).stop, 1, [ { "start": "2013-01-01", "stop": "2013-12-31", "value": 1.0, }, ], ) yield( check_updated_legislation_items, u'Insert a new item in the middle of an existing item', [ { "start": "2010-01-01", "stop": "2013-12-31", "value": 0.0, }, ], periods.period('year', 2011).start, periods.period('year', 2011).stop, 1, [ { "start": "2010-01-01", "stop": "2010-12-31", "value": 0.0, }, { "start": "2011-01-01", "stop": "2011-12-31", "value": 1.0, }, { "start": "2012-01-01", "stop": "2013-12-31", "value": 0.0, }, ], ) PKFEy$openfisca_core/tests/test_holders.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import numpy from . import test_countries def test_new_test_case_array(): axis_count = 3 axis_max = 100000 axis_min = 0 simulation = test_countries.tax_benefit_system.new_scenario().init_single_entity( axes = [ dict( count = axis_count, name = 'salaire_brut', max = axis_max, min = axis_min, ), ], period = 2014, parent1 = {}, ).new_simulation(debug = True) simulation.calculate('salaire_brut') salaire_brut = simulation.get_holder('salaire_brut').new_test_case_array(simulation.period) assert (salaire_brut - numpy.linspace(axis_min, axis_max, axis_count) == 0).all(), \ u'salaire_brut: {}'.format(salaire_brut) PKH$G3 'openfisca_core/tests/test_tax_scales.py# -*- coding: utf-8 -*- # OpenFisca -- A versatile microsimulation software # By: OpenFisca Team # # Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team # https://github.com/openfisca # # This file is part of OpenFisca. # # OpenFisca is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # OpenFisca is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import numpy as np from openfisca_core.taxscales import MarginalRateTaxScale from openfisca_core.tools import assert_near def test_linear_average_rate_tax_scale(): base = np.array([1, 1.5, 2, 2.5]) marginal_tax_scale = MarginalRateTaxScale() marginal_tax_scale.add_bracket(0, 0) marginal_tax_scale.add_bracket(1, 0.1) marginal_tax_scale.add_bracket(2, 0.2) assert_near(marginal_tax_scale.calc(base), [0, .05, .1, .2], absolute_error_margin = 0) average_tax_scale = marginal_tax_scale.to_average() # Note: assert_near doesn't work for inf. # assert_near(average_tax_scale.thresholds, [0, 1, 2, np.inf], absolute_error_margin = 0) assert average_tax_scale.thresholds == [0, 1, 2, np.inf] assert_near(average_tax_scale.rates, [0, 0, 0.05, 0.2], absolute_error_margin = 0) assert_near(average_tax_scale.calc(base), [0, 0.0375, 0.1, 0.125], absolute_error_margin = 1e-10) new_marginal_tax_scale = average_tax_scale.to_marginal() assert_near(new_marginal_tax_scale.thresholds, marginal_tax_scale.thresholds, absolute_error_margin = 0) assert_near(new_marginal_tax_scale.rates, marginal_tax_scale.rates, absolute_error_margin = 0) assert_near(average_tax_scale.rates, [0, 0, 0.05, 0.2], absolute_error_margin = 0) def test_round_marginal_tax_scale(): base = np.array([200, 200.2, 200.002, 200.6, 200.006, 200.5, 200.005]) marginal_tax_scale = MarginalRateTaxScale() marginal_tax_scale.add_bracket(0, 0) marginal_tax_scale.add_bracket(100, 0.1) assert_near( marginal_tax_scale.calc(base), [10, 10.02, 10.0002, 10.06, 10.0006, 10.05, 10.0005], absolute_error_margin = 1e-10, ) assert_near( marginal_tax_scale.calc(base, round_base_decimals = 1), [10, 10., 10., 10.1, 10., 10, 10.], absolute_error_margin = 1e-10, ) assert_near( marginal_tax_scale.calc(base, round_base_decimals = 2), [10, 10.02, 10., 10.06, 10.00, 10.05, 10], absolute_error_margin = 1e-10, ) assert_near( marginal_tax_scale.calc(base, round_base_decimals = 3), [10, 10.02, 10., 10.06, 10.001, 10.05, 10], absolute_error_margin = 1e-10, ) if __name__ == '__main__': import logging import sys logging.basicConfig(level = logging.ERROR, stream = sys.stdout) PK'GBΆLOpenFisca_Core-0.5.0.data/data/share/locale/fr/LC_MESSAGES/openfisca-core.mo+t/+,0,]O7(!; ]+~* %5#Rv'& A' iv3 /4G)|'(/)'/Q/"|  / + , , O9 7 ( ! +- *Y % 5 # % = 'M &u   A %93N /4)+'U(}/)//0`"y & , A scale can't contain both MONTANT and ASSIETTEA scale can't contain both MONTANT and TAUXA scale can't contain both amounts and basesA scale can't contain both amounts and ratesArray has not the same length as other variables of entity {}: {} instead of {}At least one of the following items must be present: {}Axes can't be used with input_variables.Dates don't belong to SEUIL datesDates don't belong to TAUX datesDates don't belong to TAUX or MONTANT datesDates don't belong to amount or rate datesDates don't belong to rate datesDates don't belong to threshold datesDates don't belong to valid dates of previous bracketDates of values are not consecutiveDates of values overlapDuplicate valueEither MONTANT or TAUX must be providedEither amount or rate must be providedIndex must be lower than {}Individual is missing from {}Instant string contains too much "-" for a year, a month or a dayInvalid dateInvalid date stringInvalid period tupleInvalid root element in XML: "{}" instead of "NODE"Invalid size for date listInvalid size for date tupleInvalid typeInvalid type for axe: integer or float expectedItems input_variables and test_case can't both existLast date must be greater than first dateList must contain one and only one itemMax value must be greater than min valueParallel indexes must belong to the same entityParallel indexes must have the same countParallel indexes must have the same period sizeParallel indexes must have the same period unitValue must be an integerWrong number of colors in triplet.Project-Id-Version: OpenFisca-Core 0.5.0.dev0 Report-Msgid-Bugs-To: contact@openfisca.fr POT-Creation-Date: 2015-09-07 17:38+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: fr Plural-Forms: nplurals=2; plural=(n > 1) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel 1.3 & , A scale can't contain both MONTANT and ASSIETTEA scale can't contain both MONTANT and TAUXA scale can't contain both amounts and basesA scale can't contain both amounts and ratesArray has not the same length as other variables of entity {}: {} instead of {}At least one of the following items must be present: {}Axes can't be used with input_variables.Dates don't belong to SEUIL datesDates don't belong to TAUX datesDates don't belong to TAUX or MONTANT datesDates don't belong to amount or rate datesDates don't belong to rate datesDates don't belong to threshold datesDates don't belong to valid dates of previous bracketDates of values are not consecutiveDates of values overlapDuplicate valueEither MONTANT or TAUX must be providedEither amount or rate must be providedIndex must be lower than {}Individual is missing from {}Instant string contains too much "-" for a year, a month or a dayInvalid dateInvalid date stringInvalid period tupleInvalid root element in XML: "{}" instead of "NODE"Invalid size for date listInvalid size for date tupleInvalid typeInvalid type for axe: integer or float expectedItems input_variables and test_case can't both existLast date must be greater than first dateList must contain one and only one itemMax value must be greater than min valueParallel indexes must belong to the same entityParallel indexes must have the same countParallel indexes must have the same period sizeParallel indexes must have the same period unitValue must be an integerWrong number of colors in triplet.PK'G^- .OpenFisca_Core-0.5.0.dist-info/DESCRIPTION.rstUNKNOWN PK'Gj,OpenFisca_Core-0.5.0.dist-info/metadata.json{"classifiers": ["Development Status :: 2 - Pre-Alpha", "License :: OSI Approved :: GNU Affero General Public License v3", "Operating System :: POSIX", "Programming Language :: Python", "Topic :: Scientific/Engineering :: Information Analysis"], "extensions": {"python.details": {"contacts": [{"email": "contact@openfisca.fr", "name": "OpenFisca Team", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/openfisca/openfisca-core"}}}, "extras": [], "generator": "bdist_wheel (0.24.0)", "keywords": ["benefit", "microsimulation", "social", "tax"], "license": "http://www.fsf.org/licensing/licenses/agpl-3.0.html", "metadata_version": "2.0", "name": "OpenFisca-Core", "run_requires": [{"requires": ["Babel (>=0.9.4)", "Biryani[datetimeconv] (>=0.10.4)", "numpy"]}], "summary": "A versatile microsimulation free software", "version": "0.5.0"}PK'G|//'OpenFisca_Core-0.5.0.dist-info/pbr.json{"is_release": false, "git_version": "31f82c9"}PK'Gw9,OpenFisca_Core-0.5.0.dist-info/top_level.txtopenfisca_core PK'G4\\$OpenFisca_Core-0.5.0.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.24.0) Root-Is-Purelib: true Tag: py2-none-any PK'G>'OpenFisca_Core-0.5.0.dist-info/METADATAMetadata-Version: 2.0 Name: OpenFisca-Core Version: 0.5.0 Summary: A versatile microsimulation free software Home-page: https://github.com/openfisca/openfisca-core Author: OpenFisca Team Author-email: contact@openfisca.fr License: http://www.fsf.org/licensing/licenses/agpl-3.0.html Keywords: benefit microsimulation social tax Platform: UNKNOWN Classifier: Development Status :: 2 - Pre-Alpha Classifier: License :: OSI Approved :: GNU Affero General Public License v3 Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Topic :: Scientific/Engineering :: Information Analysis Requires-Dist: Babel (>=0.9.4) Requires-Dist: Biryani[datetimeconv] (>=0.10.4) Requires-Dist: numpy UNKNOWN PK'GK3 %OpenFisca_Core-0.5.0.dist-info/RECORDopenfisca_core/tools.py,sha256=09kDmqI6mr1E07FXo3GWDMhXUfzvXkIEkUK0xu9chfU,3418 openfisca_core/decompositionsxml.py,sha256=Rt_pG72KAQK7yZZoIrXptsBKHfu73xR4ruryCh_oNls,6511 openfisca_core/rates.py,sha256=loljoW86xMo_GA9J0y4WHN4fMcVjlh1_35R3P-PSV2A,1272 openfisca_core/holders.py,sha256=eMqWiVQzWLhp97eyOj-C0s0ONDxzbXXskauaZyUvbIs,16762 openfisca_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 openfisca_core/simulations.py,sha256=GUNFWxtEnnLEvMsaErWeYnH0hQDmfEnkP1EpC1r2e5k,13369 openfisca_core/scenarios.py,sha256=AO0tKvppQJ5XxXZzvgj9KLcxSdUXkOX-n5TQbUmn0Eg,32002 openfisca_core/taxbenefitsystems.py,sha256=cAzv7Lk4kkp_QxIJ8VHev2ER1LSv5IoIcKTSvtzfbEs,6376 openfisca_core/reforms.py,sha256=zY7PdvGr8HZ0hLB_s7TCIWUY0UmdXT9sQ0XhRRfe2gA,13927 openfisca_core/legislations.py,sha256=MeeYOayliR9CcV8TG3FeFcgjsIHHRMWS1gPzlxvQPkc,42597 openfisca_core/columns.py,sha256=Ow1Utg0y_quoExYomllJcmAlsDKcCavOG0SjMZ56Sn4,17160 openfisca_core/conv.py,sha256=gNkR9DrMbgD-FgOX1v7EyXt9n-OlXjAQ6gFZ5XWH0mc,4724 openfisca_core/entities.py,sha256=RKRc4i5vCCbGKqad08YCOH0oPMwMPx0SI-LvaHhtNXA,4754 openfisca_core/legislationsxml.py,sha256=U2QDegceHXjUNgX_OEPZrBU0jvnqgGGgpplyYxjzEmQ,35068 openfisca_core/enumerations.py,sha256=hWUWAJN4mH_1xTyRin0Hft2qLo4imfqtH8WX1DLRnqg,1566 openfisca_core/web_tools.py,sha256=MzW4tmzEXM27ddMYEwb2-Rp0qfxeH6jmb-KGp8ObhOc,1414 openfisca_core/calmar.py,sha256=2Y1sI3bE2arTV8lNCLMXaNnBm_qYohR2GpCbcM2eEY8,8789 openfisca_core/taxscales.py,sha256=reM62DwavlDkh4aMoOIlkUzucX8vemivQC37QZukzC4,12434 openfisca_core/decompositions.py,sha256=M5991t-o_6NS8l31ghA0icSakxCelqJbykk0UrKSfnw,5738 openfisca_core/periods.py,sha256=BMJfzB4zfxch-3hvzchggcBbKtK0f20iiRjguT4EstQ,43481 openfisca_core/formulas.py,sha256=9-kGJNKsIuhHVTzCTWrOY4UweQSksDzi4j5SA-jND30,68384 openfisca_core/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 openfisca_core/tests/test_countries.py,sha256=NOWB9Khl7v_hkpVMqcNFbQhdbVTu5NeTBjgbqin8gRU,22951 openfisca_core/tests/test_reforms.py,sha256=BUZ1ZB0CsnLJ2fR0033eXB2Yrr5D76_-oidXuGBsyok,7150 openfisca_core/tests/test_holders.py,sha256=X1geS2_2b7t8ugBfgGdGww8LSjI3x7CZDWkvW_IzpGc,1692 openfisca_core/tests/test_tax_scales.py,sha256=WvIfhRpQ-QsKlCE7r1r183wRP2osVaisK5FjmA1T6u4,3267 OpenFisca_Core-0.5.0.data/data/share/locale/fr/LC_MESSAGES/openfisca-core.mo,sha256=G-vdqh3W8XD6czHOTHKLpprNcxSTqkkqvgbLp9VNazA,4252 OpenFisca_Core-0.5.0.dist-info/RECORD,, OpenFisca_Core-0.5.0.dist-info/WHEEL,sha256=54bVun1KfEBTJ68SHUmbxNPj80VxlQ0sHi4gZdGZXEY,92 OpenFisca_Core-0.5.0.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10 OpenFisca_Core-0.5.0.dist-info/pbr.json,sha256=Jgf3EnyEmrwraAQdT8b4Vn2VxstOYTrLzjWHd0s-MPg,47 OpenFisca_Core-0.5.0.dist-info/METADATA,sha256=ukqOnOkFu5IDe4MpHIiFuVYLgo-mD60KtuA4tpSBdNo,730 OpenFisca_Core-0.5.0.dist-info/top_level.txt,sha256=gwfbao6MHPzt-8rghyMUtMB-vflAnsTI_KZ0gSFmzlw,15 OpenFisca_Core-0.5.0.dist-info/metadata.json,sha256=kQSAO3CdjY6zUyEUKqA-Bh5GfqaC2oB81ikobLc5Y5E,906 PKFENZ Z openfisca_core/tools.pyPKFtEnoo# openfisca_core/decompositionsxml.pyPKF0,?'openfisca_core/rates.pyPK 'GzAzAl,openfisca_core/holders.pyPK3yIEnopenfisca_core/__init__.pyPK 'GS9494Unopenfisca_core/simulations.pyPK 'GL0}}ɢopenfisca_core/scenarios.pyPK 'G # openfisca_core/taxbenefitsystems.pyPK 'GCg6g6-9openfisca_core/reforms.pyPK 'Gy eeoopenfisca_core/legislations.pyPK 'G->CClopenfisca_core/columns.pyPKF\:ttYopenfisca_core/conv.pyPK 'G ǰSlopenfisca_core/entities.pyPK 'G&{!openfisca_core/legislationsxml.pyPKF-Xopenfisca_core/enumerations.pyPK 'Gopenfisca_core/web_tools.pyPK 'G U"U"qopenfisca_core/calmar.pyPKFV|˒006openfisca_core/taxscales.pyPK 'G}o۹jj gopenfisca_core/decompositions.pyPKFuFX٩٩o~openfisca_core/periods.pyPK 'Gjc  (openfisca_core/formulas.pyPK^PF 3openfisca_core/tests/__init__.pyPK 'GjAYY&4openfisca_core/tests/test_countries.pyPK 'GA$openfisca_core/tests/test_reforms.pyPKFEy$0openfisca_core/tests/test_holders.pyPKH$G3 'openfisca_core/tests/test_tax_scales.pyPK'GBΆLOpenFisca_Core-0.5.0.data/data/share/locale/fr/LC_MESSAGES/openfisca-core.moPK'G^- .OpenFisca_Core-0.5.0.dist-info/DESCRIPTION.rstPK'Gj,rOpenFisca_Core-0.5.0.dist-info/metadata.jsonPK'G|//'FOpenFisca_Core-0.5.0.dist-info/pbr.jsonPK'Gw9,OpenFisca_Core-0.5.0.dist-info/top_level.txtPK'G4\\$OpenFisca_Core-0.5.0.dist-info/WHEELPK'G>'OpenFisca_Core-0.5.0.dist-info/METADATAPK'GK3 %OpenFisca_Core-0.5.0.dist-info/RECORDPK""y