OpenFisca-Core-test 46.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openfisca_core/__init__.py +0 -0
- openfisca_core/commons/__init__.py +18 -0
- openfisca_core/commons/dummy.py +25 -0
- openfisca_core/commons/formulas.py +121 -0
- openfisca_core/commons/misc.py +109 -0
- openfisca_core/commons/py.typed +0 -0
- openfisca_core/commons/rates.py +114 -0
- openfisca_core/commons/tests/__init__.py +0 -0
- openfisca_core/commons/tests/test_dummy.py +9 -0
- openfisca_core/commons/tests/test_formulas.py +75 -0
- openfisca_core/commons/tests/test_rates.py +26 -0
- openfisca_core/data_storage/__init__.py +7 -0
- openfisca_core/data_storage/in_memory_storage.py +198 -0
- openfisca_core/data_storage/on_disk_storage.py +307 -0
- openfisca_core/data_storage/types.py +14 -0
- openfisca_core/entities/__init__.py +23 -0
- openfisca_core/entities/_core_entity.py +217 -0
- openfisca_core/entities/_description.py +55 -0
- openfisca_core/entities/entity.py +60 -0
- openfisca_core/entities/group_entity.py +123 -0
- openfisca_core/entities/helpers.py +165 -0
- openfisca_core/entities/py.typed +0 -0
- openfisca_core/entities/role.py +92 -0
- openfisca_core/entities/tests/__init__.py +0 -0
- openfisca_core/entities/tests/test_entity.py +10 -0
- openfisca_core/entities/tests/test_group_entity.py +70 -0
- openfisca_core/entities/tests/test_role.py +11 -0
- openfisca_core/entities/types.py +44 -0
- openfisca_core/errors/__init__.py +64 -0
- openfisca_core/errors/cycle_error.py +2 -0
- openfisca_core/errors/empty_argument_error.py +39 -0
- openfisca_core/errors/nan_creation_error.py +2 -0
- openfisca_core/errors/parameter_not_found_error.py +16 -0
- openfisca_core/errors/parameter_parsing_error.py +18 -0
- openfisca_core/errors/period_mismatch_error.py +9 -0
- openfisca_core/errors/situation_parsing_error.py +26 -0
- openfisca_core/errors/spiral_error.py +2 -0
- openfisca_core/errors/variable_name_config_error.py +2 -0
- openfisca_core/errors/variable_not_found_error.py +29 -0
- openfisca_core/experimental/__init__.py +9 -0
- openfisca_core/experimental/_errors.py +5 -0
- openfisca_core/experimental/_memory_config.py +41 -0
- openfisca_core/formula_helpers.py +9 -0
- openfisca_core/holders/__init__.py +33 -0
- openfisca_core/holders/helpers.py +93 -0
- openfisca_core/holders/holder.py +328 -0
- openfisca_core/holders/tests/__init__.py +0 -0
- openfisca_core/holders/tests/test_helpers.py +134 -0
- openfisca_core/holders/types.py +3 -0
- openfisca_core/indexed_enums/__init__.py +18 -0
- openfisca_core/indexed_enums/_enum_type.py +79 -0
- openfisca_core/indexed_enums/_errors.py +35 -0
- openfisca_core/indexed_enums/_guards.py +209 -0
- openfisca_core/indexed_enums/_utils.py +180 -0
- openfisca_core/indexed_enums/config.py +6 -0
- openfisca_core/indexed_enums/enum.py +218 -0
- openfisca_core/indexed_enums/enum_array.py +316 -0
- openfisca_core/indexed_enums/py.typed +0 -0
- openfisca_core/indexed_enums/tests/__init__.py +0 -0
- openfisca_core/indexed_enums/tests/test_enum.py +135 -0
- openfisca_core/indexed_enums/tests/test_enum_array.py +30 -0
- openfisca_core/indexed_enums/types.py +49 -0
- openfisca_core/memory_config.py +9 -0
- openfisca_core/model_api.py +71 -0
- openfisca_core/parameters/__init__.py +71 -0
- openfisca_core/parameters/at_instant_like.py +17 -0
- openfisca_core/parameters/config.py +55 -0
- openfisca_core/parameters/helpers.py +106 -0
- openfisca_core/parameters/parameter.py +217 -0
- openfisca_core/parameters/parameter_at_instant.py +75 -0
- openfisca_core/parameters/parameter_node.py +176 -0
- openfisca_core/parameters/parameter_node_at_instant.py +57 -0
- openfisca_core/parameters/parameter_scale.py +121 -0
- openfisca_core/parameters/parameter_scale_bracket.py +7 -0
- openfisca_core/parameters/values_history.py +5 -0
- openfisca_core/parameters/vectorial_parameter_node_at_instant.py +179 -0
- openfisca_core/periods/__init__.py +77 -0
- openfisca_core/periods/_errors.py +28 -0
- openfisca_core/periods/_parsers.py +122 -0
- openfisca_core/periods/config.py +20 -0
- openfisca_core/periods/date_unit.py +110 -0
- openfisca_core/periods/helpers.py +314 -0
- openfisca_core/periods/instant_.py +226 -0
- openfisca_core/periods/period_.py +919 -0
- openfisca_core/periods/py.typed +0 -0
- openfisca_core/periods/tests/__init__.py +0 -0
- openfisca_core/periods/tests/helpers/__init__.py +0 -0
- openfisca_core/periods/tests/helpers/test_helpers.py +65 -0
- openfisca_core/periods/tests/helpers/test_instant.py +73 -0
- openfisca_core/periods/tests/helpers/test_period.py +134 -0
- openfisca_core/periods/tests/test_instant.py +32 -0
- openfisca_core/periods/tests/test_parsers.py +129 -0
- openfisca_core/periods/tests/test_period.py +283 -0
- openfisca_core/populations/__init__.py +64 -0
- openfisca_core/populations/_core_population.py +454 -0
- openfisca_core/populations/_errors.py +65 -0
- openfisca_core/populations/group_population.py +326 -0
- openfisca_core/populations/population.py +142 -0
- openfisca_core/populations/tests/__init__.py +0 -0
- openfisca_core/populations/tests/test_members_position.py +116 -0
- openfisca_core/populations/types.py +110 -0
- openfisca_core/projectors/__init__.py +39 -0
- openfisca_core/projectors/entity_to_person_projector.py +12 -0
- openfisca_core/projectors/first_person_to_entity_projector.py +13 -0
- openfisca_core/projectors/helpers.py +139 -0
- openfisca_core/projectors/projector.py +38 -0
- openfisca_core/projectors/typing.py +27 -0
- openfisca_core/projectors/unique_role_to_entity_projector.py +14 -0
- openfisca_core/rates.py +9 -0
- openfisca_core/reforms/__init__.py +24 -0
- openfisca_core/reforms/in_yaml_test_reform.py +28 -0
- openfisca_core/reforms/reform.py +86 -0
- openfisca_core/scripts/__init__.py +106 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-310-pytest-8.3.3.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-310-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-310.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-311-pytest-8.3.3.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-311-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-312-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-313-pytest-8.3.3.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-313-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/find_placeholders.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/find_placeholders.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_numpy_condition_notations.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_numpy_condition_notations.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances_fancy_indexing.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances_fancy_indexing.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-310.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-312.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/remove_fuzzy.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/remove_fuzzy.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-310.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-312.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/simulation_generator.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/simulation_generator.cpython-313.pyc +0 -0
- openfisca_core/scripts/assets/__init__.py +0 -0
- openfisca_core/scripts/assets/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/assets/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/assets/index.html +140 -0
- openfisca_core/scripts/find_placeholders.py +51 -0
- openfisca_core/scripts/measure_numpy_condition_notations.py +137 -0
- openfisca_core/scripts/measure_performances.py +291 -0
- openfisca_core/scripts/measure_performances_fancy_indexing.py +89 -0
- openfisca_core/scripts/migrations/__init__.py +0 -0
- openfisca_core/scripts/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/__pycache__/v24_to_25.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/__pycache__/v24_to_25.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__init__.py +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_country_template.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_country_template.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_extension_template.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_extension_template.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/legislation.xsd +102 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py +30 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py +26 -0
- openfisca_core/scripts/migrations/v24_to_25.py +156 -0
- openfisca_core/scripts/openfisca_command.py +192 -0
- openfisca_core/scripts/remove_fuzzy.py +213 -0
- openfisca_core/scripts/run_test.py +51 -0
- openfisca_core/scripts/simulation_generator.py +90 -0
- openfisca_core/simulation_builder.py +16 -0
- openfisca_core/simulations/__init__.py +45 -0
- openfisca_core/simulations/_build_default_simulation.py +158 -0
- openfisca_core/simulations/_build_from_variables.py +230 -0
- openfisca_core/simulations/_type_guards.py +298 -0
- openfisca_core/simulations/helpers.py +111 -0
- openfisca_core/simulations/simulation.py +618 -0
- openfisca_core/simulations/simulation_builder.py +855 -0
- openfisca_core/simulations/typing.py +203 -0
- openfisca_core/taxbenefitsystems/__init__.py +26 -0
- openfisca_core/taxbenefitsystems/tax_benefit_system.py +584 -0
- openfisca_core/taxscales/__init__.py +35 -0
- openfisca_core/taxscales/abstract_rate_tax_scale.py +41 -0
- openfisca_core/taxscales/abstract_tax_scale.py +53 -0
- openfisca_core/taxscales/amount_tax_scale_like.py +55 -0
- openfisca_core/taxscales/helpers.py +44 -0
- openfisca_core/taxscales/linear_average_rate_tax_scale.py +76 -0
- openfisca_core/taxscales/marginal_amount_tax_scale.py +34 -0
- openfisca_core/taxscales/marginal_rate_tax_scale.py +286 -0
- openfisca_core/taxscales/rate_tax_scale_like.py +214 -0
- openfisca_core/taxscales/single_amount_tax_scale.py +32 -0
- openfisca_core/taxscales/tax_scale_like.py +65 -0
- openfisca_core/tools/__init__.py +94 -0
- openfisca_core/tools/parallel_plugin.py +55 -0
- openfisca_core/tools/simulation_dumper.py +137 -0
- openfisca_core/tools/test_runner.py +802 -0
- openfisca_core/tracers/__init__.py +40 -0
- openfisca_core/tracers/computation_log.py +196 -0
- openfisca_core/tracers/flat_trace.py +87 -0
- openfisca_core/tracers/full_tracer.py +172 -0
- openfisca_core/tracers/performance_log.py +133 -0
- openfisca_core/tracers/simple_tracer.py +64 -0
- openfisca_core/tracers/trace_node.py +121 -0
- openfisca_core/tracers/tracing_parameter_node_at_instant.py +81 -0
- openfisca_core/types.py +548 -0
- openfisca_core/variables/__init__.py +26 -0
- openfisca_core/variables/config.py +53 -0
- openfisca_core/variables/helpers.py +67 -0
- openfisca_core/variables/tests/__init__.py +0 -0
- openfisca_core/variables/tests/test_definition_period.py +43 -0
- openfisca_core/variables/variable.py +474 -0
- openfisca_core/warnings/__init__.py +25 -0
- openfisca_core/warnings/libyaml_warning.py +2 -0
- openfisca_core/warnings/tempfile_warning.py +2 -0
- openfisca_core_test-46.0.0.data/data/share/openfisca/openfisca-core/CHANGELOG.md +4209 -0
- openfisca_core_test-46.0.0.data/data/share/openfisca/openfisca-core/README.md +152 -0
- openfisca_core_test-46.0.0.dist-info/METADATA +194 -0
- openfisca_core_test-46.0.0.dist-info/RECORD +237 -0
- openfisca_core_test-46.0.0.dist-info/WHEEL +5 -0
- openfisca_core_test-46.0.0.dist-info/entry_points.txt +3 -0
- openfisca_core_test-46.0.0.dist-info/licenses/LICENSE +661 -0
- openfisca_core_test-46.0.0.dist-info/top_level.txt +2 -0
- openfisca_web_api/__init__.py +0 -0
- openfisca_web_api/app.py +226 -0
- openfisca_web_api/errors.py +11 -0
- openfisca_web_api/handlers.py +90 -0
- openfisca_web_api/loader/__init__.py +22 -0
- openfisca_web_api/loader/entities.py +28 -0
- openfisca_web_api/loader/parameters.py +122 -0
- openfisca_web_api/loader/spec.py +168 -0
- openfisca_web_api/loader/tax_benefit_system.py +27 -0
- openfisca_web_api/loader/variables.py +121 -0
- openfisca_web_api/openAPI.yml +434 -0
- openfisca_web_api/scripts/__init__.py +0 -0
- openfisca_web_api/scripts/serve.py +99 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from collections.abc import Iterable, Sequence
|
|
5
|
+
from typing import NoReturn
|
|
6
|
+
|
|
7
|
+
import dpath
|
|
8
|
+
import numpy
|
|
9
|
+
from numpy.typing import NDArray as Array
|
|
10
|
+
|
|
11
|
+
from openfisca_core import entities, errors, periods, populations, variables
|
|
12
|
+
|
|
13
|
+
from . import helpers
|
|
14
|
+
from ._build_default_simulation import _BuildDefaultSimulation
|
|
15
|
+
from ._build_from_variables import _BuildFromVariables
|
|
16
|
+
from ._type_guards import (
|
|
17
|
+
are_entities_fully_specified,
|
|
18
|
+
are_entities_short_form,
|
|
19
|
+
are_entities_specified,
|
|
20
|
+
has_axes,
|
|
21
|
+
)
|
|
22
|
+
from .simulation import Simulation
|
|
23
|
+
from .typing import (
|
|
24
|
+
Axis,
|
|
25
|
+
Entity,
|
|
26
|
+
FullySpecifiedEntities,
|
|
27
|
+
GroupEntities,
|
|
28
|
+
GroupEntity,
|
|
29
|
+
ImplicitGroupEntities,
|
|
30
|
+
Params,
|
|
31
|
+
ParamsWithoutAxes,
|
|
32
|
+
Population,
|
|
33
|
+
Role,
|
|
34
|
+
SingleEntity,
|
|
35
|
+
TaxBenefitSystem,
|
|
36
|
+
Variables,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SimulationBuilder:
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
self.default_period = (
|
|
43
|
+
None # Simulation period used for variables when no period is defined
|
|
44
|
+
)
|
|
45
|
+
self.persons_plural = (
|
|
46
|
+
None # Plural name for person entity in current tax and benefits system
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# JSON input - Memory of known input values. Indexed by variable or axis name.
|
|
50
|
+
self.input_buffer: dict[
|
|
51
|
+
variables.Variable.name,
|
|
52
|
+
dict[str(periods.period), numpy.array],
|
|
53
|
+
] = {}
|
|
54
|
+
self.populations: dict[entities.Entity.key, populations.Population] = {}
|
|
55
|
+
# JSON input - Number of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_ids``, including axes.
|
|
56
|
+
self.entity_counts: dict[entities.Entity.plural, int] = {}
|
|
57
|
+
# JSON input - List of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_counts``.
|
|
58
|
+
self.entity_ids: dict[entities.Entity.plural, list[int]] = {}
|
|
59
|
+
|
|
60
|
+
# Links entities with persons. For each person index in persons ids list, set entity index in entity ids id. E.g.: self.memberships[entity.plural][person_index] = entity_ids.index(instance_id)
|
|
61
|
+
self.memberships: dict[entities.Entity.plural, list[int]] = {}
|
|
62
|
+
self.roles: dict[entities.Entity.plural, list[int]] = {}
|
|
63
|
+
|
|
64
|
+
self.variable_entities: dict[variables.Variable.name, entities.Entity] = {}
|
|
65
|
+
|
|
66
|
+
self.axes = [[]]
|
|
67
|
+
self.axes_entity_counts: dict[entities.Entity.plural, int] = {}
|
|
68
|
+
self.axes_entity_ids: dict[entities.Entity.plural, list[int]] = {}
|
|
69
|
+
self.axes_memberships: dict[entities.Entity.plural, list[int]] = {}
|
|
70
|
+
self.axes_roles: dict[entities.Entity.plural, list[int]] = {}
|
|
71
|
+
|
|
72
|
+
def build_from_dict(
|
|
73
|
+
self,
|
|
74
|
+
tax_benefit_system: TaxBenefitSystem,
|
|
75
|
+
input_dict: Params,
|
|
76
|
+
) -> Simulation:
|
|
77
|
+
"""Build a simulation from an input dictionary.
|
|
78
|
+
|
|
79
|
+
This method uses :meth:`.SimulationBuilder.build_from_entities` if
|
|
80
|
+
entities are fully specified, or
|
|
81
|
+
:meth:`.SimulationBuilder.build_from_variables` if they are not.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
tax_benefit_system: The system to use.
|
|
85
|
+
input_dict: The input of the simulation.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Simulation: The built simulation.
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
>>> entities = {"person", "household"}
|
|
92
|
+
|
|
93
|
+
>>> params = {
|
|
94
|
+
... "persons": {"Javier": {"salary": {"2018-11": 2000}}},
|
|
95
|
+
... "household": {"parents": ["Javier"]},
|
|
96
|
+
... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]],
|
|
97
|
+
... }
|
|
98
|
+
|
|
99
|
+
>>> are_entities_short_form(params, entities)
|
|
100
|
+
True
|
|
101
|
+
|
|
102
|
+
>>> entities = {"persons", "households"}
|
|
103
|
+
|
|
104
|
+
>>> params = {
|
|
105
|
+
... "axes": [
|
|
106
|
+
... [
|
|
107
|
+
... {
|
|
108
|
+
... "count": 2,
|
|
109
|
+
... "max": 3000,
|
|
110
|
+
... "min": 0,
|
|
111
|
+
... "name": "rent",
|
|
112
|
+
... "period": "2018-11",
|
|
113
|
+
... }
|
|
114
|
+
... ]
|
|
115
|
+
... ],
|
|
116
|
+
... "households": {
|
|
117
|
+
... "housea": {"parents": ["Alicia", "Javier"]},
|
|
118
|
+
... "houseb": {"parents": ["Tom"]},
|
|
119
|
+
... },
|
|
120
|
+
... "persons": {
|
|
121
|
+
... "Alicia": {"salary": {"2018-11": 0}},
|
|
122
|
+
... "Javier": {},
|
|
123
|
+
... "Tom": {},
|
|
124
|
+
... },
|
|
125
|
+
... }
|
|
126
|
+
|
|
127
|
+
>>> are_entities_short_form(params, entities)
|
|
128
|
+
True
|
|
129
|
+
|
|
130
|
+
>>> params = {"salary": [12000, 13000]}
|
|
131
|
+
|
|
132
|
+
>>> not are_entities_specified(params, {"salary"})
|
|
133
|
+
True
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
#: The plural names of the entities in the tax and benefits system.
|
|
137
|
+
plural: Iterable[str] = tax_benefit_system.entities_plural()
|
|
138
|
+
|
|
139
|
+
#: The singular names of the entities in the tax and benefits system.
|
|
140
|
+
singular: Iterable[str] = tax_benefit_system.entities_by_singular()
|
|
141
|
+
|
|
142
|
+
#: The names of the variables in the tax and benefits system.
|
|
143
|
+
variables: Iterable[str] = tax_benefit_system.variables.keys()
|
|
144
|
+
|
|
145
|
+
if are_entities_short_form(input_dict, singular):
|
|
146
|
+
params = self.explicit_singular_entities(tax_benefit_system, input_dict)
|
|
147
|
+
return self.build_from_entities(tax_benefit_system, params)
|
|
148
|
+
|
|
149
|
+
if are_entities_fully_specified(params := input_dict, plural):
|
|
150
|
+
return self.build_from_entities(tax_benefit_system, params)
|
|
151
|
+
|
|
152
|
+
if not are_entities_specified(params := input_dict, variables):
|
|
153
|
+
return self.build_from_variables(tax_benefit_system, params)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def build_from_entities(
|
|
157
|
+
self,
|
|
158
|
+
tax_benefit_system: TaxBenefitSystem,
|
|
159
|
+
input_dict: FullySpecifiedEntities,
|
|
160
|
+
) -> Simulation:
|
|
161
|
+
"""Build a simulation from a Python dict ``input_dict`` fully specifying
|
|
162
|
+
entities.
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
>>> entities = {"person", "household"}
|
|
166
|
+
|
|
167
|
+
>>> params = {
|
|
168
|
+
... "persons": {"Javier": {"salary": {"2018-11": 2000}}},
|
|
169
|
+
... "household": {"parents": ["Javier"]},
|
|
170
|
+
... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]],
|
|
171
|
+
... }
|
|
172
|
+
|
|
173
|
+
>>> are_entities_short_form(params, entities)
|
|
174
|
+
True
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
# Create the populations
|
|
178
|
+
populations = tax_benefit_system.instantiate_entities()
|
|
179
|
+
|
|
180
|
+
# Create the simulation
|
|
181
|
+
simulation = Simulation(tax_benefit_system, populations)
|
|
182
|
+
|
|
183
|
+
# Why?
|
|
184
|
+
input_dict = copy.deepcopy(input_dict)
|
|
185
|
+
|
|
186
|
+
# The plural names of the entities in the tax and benefits system.
|
|
187
|
+
plural: Iterable[str] = tax_benefit_system.entities_plural()
|
|
188
|
+
|
|
189
|
+
# Register variables so get_variable_entity can find them
|
|
190
|
+
self.register_variables(simulation)
|
|
191
|
+
|
|
192
|
+
# Declare axes
|
|
193
|
+
axes: list[list[Axis]] | None = None
|
|
194
|
+
|
|
195
|
+
# ?
|
|
196
|
+
helpers.check_type(input_dict, dict, ["error"])
|
|
197
|
+
|
|
198
|
+
# Remove axes from input_dict
|
|
199
|
+
params: ParamsWithoutAxes = {
|
|
200
|
+
key: value for key, value in input_dict.items() if key != "axes"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Save axes for later
|
|
204
|
+
if has_axes(axes_params := input_dict):
|
|
205
|
+
axes = copy.deepcopy(axes_params.get("axes", None))
|
|
206
|
+
|
|
207
|
+
# Check for unexpected entities
|
|
208
|
+
helpers.check_unexpected_entities(params, plural)
|
|
209
|
+
|
|
210
|
+
person_entity: SingleEntity = tax_benefit_system.person_entity
|
|
211
|
+
|
|
212
|
+
persons_json = params.get(person_entity.plural, None)
|
|
213
|
+
|
|
214
|
+
if not persons_json:
|
|
215
|
+
raise errors.SituationParsingError(
|
|
216
|
+
[person_entity.plural],
|
|
217
|
+
f"No {person_entity.key} found. At least one {person_entity.key} must be defined to run a simulation.",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
persons_ids = self.add_person_entity(simulation.persons.entity, persons_json)
|
|
221
|
+
|
|
222
|
+
for entity_class in tax_benefit_system.group_entities:
|
|
223
|
+
instances_json = params.get(entity_class.plural)
|
|
224
|
+
|
|
225
|
+
if instances_json is not None:
|
|
226
|
+
self.add_group_entity(
|
|
227
|
+
self.persons_plural,
|
|
228
|
+
persons_ids,
|
|
229
|
+
entity_class,
|
|
230
|
+
instances_json,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
elif axes is not None:
|
|
234
|
+
message = (
|
|
235
|
+
f"We could not find any specified {entity_class.plural}. "
|
|
236
|
+
"In order to expand over axes, all group entities and roles "
|
|
237
|
+
"must be fully specified. For further support, please do "
|
|
238
|
+
"not hesitate to take a look at the official documentation: "
|
|
239
|
+
"https://openfisca.org/doc/simulate/replicate-simulation-inputs.html."
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
raise errors.SituationParsingError([entity_class.plural], message)
|
|
243
|
+
|
|
244
|
+
else:
|
|
245
|
+
self.add_default_group_entity(persons_ids, entity_class)
|
|
246
|
+
|
|
247
|
+
if axes is not None:
|
|
248
|
+
for axis in axes[0]:
|
|
249
|
+
self.add_parallel_axis(axis)
|
|
250
|
+
|
|
251
|
+
if len(axes) >= 1:
|
|
252
|
+
for axis in axes[1:]:
|
|
253
|
+
self.add_perpendicular_axis(axis[0])
|
|
254
|
+
|
|
255
|
+
self.expand_axes()
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
self.finalize_variables_init(simulation.persons)
|
|
259
|
+
except errors.PeriodMismatchError as e:
|
|
260
|
+
self.raise_period_mismatch(simulation.persons.entity, persons_json, e)
|
|
261
|
+
|
|
262
|
+
for entity_class in tax_benefit_system.group_entities:
|
|
263
|
+
try:
|
|
264
|
+
population = simulation.populations[entity_class.key]
|
|
265
|
+
self.finalize_variables_init(population)
|
|
266
|
+
except errors.PeriodMismatchError as e:
|
|
267
|
+
self.raise_period_mismatch(population.entity, instances_json, e)
|
|
268
|
+
|
|
269
|
+
return simulation
|
|
270
|
+
|
|
271
|
+
def build_from_variables(
|
|
272
|
+
self,
|
|
273
|
+
tax_benefit_system: TaxBenefitSystem,
|
|
274
|
+
input_dict: Variables,
|
|
275
|
+
) -> Simulation:
|
|
276
|
+
"""Build a simulation from a Python dict ``input_dict`` describing
|
|
277
|
+
variables values without expliciting entities.
|
|
278
|
+
|
|
279
|
+
This method uses :meth:`.SimulationBuilder.build_default_simulation` to
|
|
280
|
+
infer an entity structure.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
tax_benefit_system: The system to use.
|
|
284
|
+
input_dict: The input of the simulation.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Simulation: The built simulation.
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
SituationParsingError: If the input is not valid.
|
|
291
|
+
|
|
292
|
+
Examples:
|
|
293
|
+
>>> params = {"salary": {"2016-10": 12000}}
|
|
294
|
+
|
|
295
|
+
>>> are_entities_specified(params, {"salary"})
|
|
296
|
+
False
|
|
297
|
+
|
|
298
|
+
>>> params = {"salary": 12000}
|
|
299
|
+
|
|
300
|
+
>>> are_entities_specified(params, {"salary"})
|
|
301
|
+
False
|
|
302
|
+
|
|
303
|
+
"""
|
|
304
|
+
return (
|
|
305
|
+
_BuildFromVariables(tax_benefit_system, input_dict, self.default_period)
|
|
306
|
+
.add_dated_values()
|
|
307
|
+
.add_undated_values()
|
|
308
|
+
.simulation
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def build_default_simulation(
|
|
313
|
+
tax_benefit_system: TaxBenefitSystem,
|
|
314
|
+
count: int = 1,
|
|
315
|
+
) -> Simulation:
|
|
316
|
+
"""Build a default simulation.
|
|
317
|
+
|
|
318
|
+
Where:
|
|
319
|
+
- There are ``count`` persons
|
|
320
|
+
- There are ``count`` of each group entity, containing one person
|
|
321
|
+
- Every person has, in each entity, the first role
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
return (
|
|
325
|
+
_BuildDefaultSimulation(tax_benefit_system, count)
|
|
326
|
+
.add_count()
|
|
327
|
+
.add_ids()
|
|
328
|
+
.add_members_entity_id()
|
|
329
|
+
.simulation
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def create_entities(self, tax_benefit_system) -> None:
|
|
333
|
+
self.populations = tax_benefit_system.instantiate_entities()
|
|
334
|
+
|
|
335
|
+
def declare_person_entity(self, person_singular, persons_ids: Iterable) -> None:
|
|
336
|
+
person_instance = self.populations[person_singular]
|
|
337
|
+
person_instance.ids = numpy.array(list(persons_ids))
|
|
338
|
+
person_instance.count = len(person_instance.ids)
|
|
339
|
+
|
|
340
|
+
self.persons_plural = person_instance.entity.plural
|
|
341
|
+
|
|
342
|
+
def declare_entity(self, entity_singular, entity_ids: Iterable):
|
|
343
|
+
entity_instance = self.populations[entity_singular]
|
|
344
|
+
entity_instance.ids = numpy.array(list(entity_ids))
|
|
345
|
+
entity_instance.count = len(entity_instance.ids)
|
|
346
|
+
return entity_instance
|
|
347
|
+
|
|
348
|
+
def nb_persons(self, entity_singular, role=None):
|
|
349
|
+
return self.populations[entity_singular].nb_persons(role=role)
|
|
350
|
+
|
|
351
|
+
def join_with_persons(
|
|
352
|
+
self,
|
|
353
|
+
group_population,
|
|
354
|
+
persons_group_assignment,
|
|
355
|
+
roles: Iterable[str],
|
|
356
|
+
) -> None:
|
|
357
|
+
# Maps group's identifiers to a 0-based integer range, for indexing into members_roles (see PR#876)
|
|
358
|
+
group_sorted_indices = numpy.unique(
|
|
359
|
+
persons_group_assignment,
|
|
360
|
+
return_inverse=True,
|
|
361
|
+
)[1]
|
|
362
|
+
group_population.members_entity_id = numpy.argsort(group_population.ids)[
|
|
363
|
+
group_sorted_indices
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
flattened_roles = group_population.entity.flattened_roles
|
|
367
|
+
roles_array = numpy.array(roles)
|
|
368
|
+
if numpy.issubdtype(roles_array.dtype, numpy.integer):
|
|
369
|
+
group_population.members_role = numpy.array(flattened_roles)[roles_array]
|
|
370
|
+
elif len(flattened_roles) == 0:
|
|
371
|
+
group_population.members_role = numpy.int16(0)
|
|
372
|
+
else:
|
|
373
|
+
group_population.members_role = numpy.select(
|
|
374
|
+
[roles_array == role.key for role in flattened_roles],
|
|
375
|
+
flattened_roles,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def build(self, tax_benefit_system):
|
|
379
|
+
return Simulation(tax_benefit_system, self.populations)
|
|
380
|
+
|
|
381
|
+
def explicit_singular_entities(
|
|
382
|
+
self,
|
|
383
|
+
tax_benefit_system: TaxBenefitSystem,
|
|
384
|
+
input_dict: ImplicitGroupEntities,
|
|
385
|
+
) -> GroupEntities:
|
|
386
|
+
"""Preprocess ``input_dict`` to explicit entities defined using the
|
|
387
|
+
single-entity shortcut.
|
|
388
|
+
|
|
389
|
+
Examples:
|
|
390
|
+
>>> params = {
|
|
391
|
+
... "persons": {
|
|
392
|
+
... "Javier": {},
|
|
393
|
+
... },
|
|
394
|
+
... "household": {"parents": ["Javier"]},
|
|
395
|
+
... }
|
|
396
|
+
|
|
397
|
+
>>> are_entities_fully_specified(params, {"persons", "households"})
|
|
398
|
+
False
|
|
399
|
+
|
|
400
|
+
>>> are_entities_short_form(params, {"person", "household"})
|
|
401
|
+
True
|
|
402
|
+
|
|
403
|
+
>>> params = {
|
|
404
|
+
... "persons": {"Javier": {}},
|
|
405
|
+
... "households": {"household": {"parents": ["Javier"]}},
|
|
406
|
+
... }
|
|
407
|
+
|
|
408
|
+
>>> are_entities_fully_specified(params, {"persons", "households"})
|
|
409
|
+
True
|
|
410
|
+
|
|
411
|
+
>>> are_entities_short_form(params, {"person", "household"})
|
|
412
|
+
False
|
|
413
|
+
|
|
414
|
+
"""
|
|
415
|
+
singular_keys = set(input_dict).intersection(
|
|
416
|
+
tax_benefit_system.entities_by_singular(),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
result = {
|
|
420
|
+
entity_id: entity_description
|
|
421
|
+
for (entity_id, entity_description) in input_dict.items()
|
|
422
|
+
if entity_id in tax_benefit_system.entities_plural()
|
|
423
|
+
} # filter out the singular entities
|
|
424
|
+
|
|
425
|
+
for singular in singular_keys:
|
|
426
|
+
plural = tax_benefit_system.entities_by_singular()[singular].plural
|
|
427
|
+
result[plural] = {singular: input_dict[singular]}
|
|
428
|
+
|
|
429
|
+
return result
|
|
430
|
+
|
|
431
|
+
def add_person_entity(self, entity, instances_json):
|
|
432
|
+
"""Add the simulation's instances of the persons entity as described in ``instances_json``."""
|
|
433
|
+
helpers.check_type(instances_json, dict, [entity.plural])
|
|
434
|
+
entity_ids = list(map(str, instances_json.keys()))
|
|
435
|
+
self.persons_plural = entity.plural
|
|
436
|
+
self.entity_ids[self.persons_plural] = entity_ids
|
|
437
|
+
self.entity_counts[self.persons_plural] = len(entity_ids)
|
|
438
|
+
|
|
439
|
+
for instance_id, instance_object in instances_json.items():
|
|
440
|
+
helpers.check_type(instance_object, dict, [entity.plural, instance_id])
|
|
441
|
+
self.init_variable_values(entity, instance_object, str(instance_id))
|
|
442
|
+
|
|
443
|
+
return self.get_ids(entity.plural)
|
|
444
|
+
|
|
445
|
+
def add_default_group_entity(
|
|
446
|
+
self,
|
|
447
|
+
persons_ids: list[str],
|
|
448
|
+
entity: GroupEntity,
|
|
449
|
+
) -> None:
|
|
450
|
+
persons_count = len(persons_ids)
|
|
451
|
+
roles = list(entity.flattened_roles)
|
|
452
|
+
self.entity_ids[entity.plural] = persons_ids
|
|
453
|
+
self.entity_counts[entity.plural] = persons_count
|
|
454
|
+
self.memberships[entity.plural] = list(
|
|
455
|
+
numpy.arange(0, persons_count, dtype=numpy.int32),
|
|
456
|
+
)
|
|
457
|
+
self.roles[entity.plural] = [roles[0]] * persons_count
|
|
458
|
+
|
|
459
|
+
def add_group_entity(
|
|
460
|
+
self,
|
|
461
|
+
persons_plural: str,
|
|
462
|
+
persons_ids: list[str],
|
|
463
|
+
entity: GroupEntity,
|
|
464
|
+
instances_json,
|
|
465
|
+
) -> None:
|
|
466
|
+
"""Add all instances of one of the model's entities as described in ``instances_json``."""
|
|
467
|
+
helpers.check_type(instances_json, dict, [entity.plural])
|
|
468
|
+
entity_ids = list(map(str, instances_json.keys()))
|
|
469
|
+
|
|
470
|
+
self.entity_ids[entity.plural] = entity_ids
|
|
471
|
+
self.entity_counts[entity.plural] = len(entity_ids)
|
|
472
|
+
|
|
473
|
+
persons_count = len(persons_ids)
|
|
474
|
+
persons_to_allocate = set(persons_ids)
|
|
475
|
+
self.memberships[entity.plural] = numpy.empty(persons_count, dtype=numpy.int32)
|
|
476
|
+
self.roles[entity.plural] = numpy.empty(persons_count, dtype=object)
|
|
477
|
+
|
|
478
|
+
self.entity_ids[entity.plural] = entity_ids
|
|
479
|
+
self.entity_counts[entity.plural] = len(entity_ids)
|
|
480
|
+
|
|
481
|
+
for instance_id, instance_object in instances_json.items():
|
|
482
|
+
helpers.check_type(instance_object, dict, [entity.plural, instance_id])
|
|
483
|
+
|
|
484
|
+
variables_json = instance_object.copy() # Don't mutate function input
|
|
485
|
+
|
|
486
|
+
roles_json = {
|
|
487
|
+
role.plural or role.key: helpers.transform_to_strict_syntax(
|
|
488
|
+
variables_json.pop(role.plural or role.key, []),
|
|
489
|
+
)
|
|
490
|
+
for role in entity.roles
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
for role_id, role_definition in roles_json.items():
|
|
494
|
+
helpers.check_type(
|
|
495
|
+
role_definition,
|
|
496
|
+
list,
|
|
497
|
+
[entity.plural, instance_id, role_id],
|
|
498
|
+
)
|
|
499
|
+
for index, person_id in enumerate(role_definition):
|
|
500
|
+
entity_plural = entity.plural
|
|
501
|
+
self.check_persons_to_allocate(
|
|
502
|
+
persons_plural,
|
|
503
|
+
entity_plural,
|
|
504
|
+
persons_ids,
|
|
505
|
+
person_id,
|
|
506
|
+
instance_id,
|
|
507
|
+
role_id,
|
|
508
|
+
persons_to_allocate,
|
|
509
|
+
index,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
persons_to_allocate.discard(person_id)
|
|
513
|
+
|
|
514
|
+
entity_index = entity_ids.index(instance_id)
|
|
515
|
+
role_by_plural = {role.plural or role.key: role for role in entity.roles}
|
|
516
|
+
|
|
517
|
+
for role_plural, persons_with_role in roles_json.items():
|
|
518
|
+
role = role_by_plural[role_plural]
|
|
519
|
+
|
|
520
|
+
if role.max is not None and len(persons_with_role) > role.max:
|
|
521
|
+
raise errors.SituationParsingError(
|
|
522
|
+
[entity.plural, instance_id, role_plural],
|
|
523
|
+
f"There can be at most {role.max} {role_plural} in a {entity.key}. {len(persons_with_role)} were declared in '{instance_id}'.",
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
for index_within_role, person_id in enumerate(persons_with_role):
|
|
527
|
+
person_index = persons_ids.index(person_id)
|
|
528
|
+
self.memberships[entity.plural][person_index] = entity_index
|
|
529
|
+
person_role = (
|
|
530
|
+
role.subroles[index_within_role] if role.subroles else role
|
|
531
|
+
)
|
|
532
|
+
self.roles[entity.plural][person_index] = person_role
|
|
533
|
+
|
|
534
|
+
self.init_variable_values(entity, variables_json, instance_id)
|
|
535
|
+
|
|
536
|
+
if persons_to_allocate:
|
|
537
|
+
entity_ids = entity_ids + list(persons_to_allocate)
|
|
538
|
+
for person_id in persons_to_allocate:
|
|
539
|
+
person_index = persons_ids.index(person_id)
|
|
540
|
+
self.memberships[entity.plural][person_index] = entity_ids.index(
|
|
541
|
+
person_id,
|
|
542
|
+
)
|
|
543
|
+
self.roles[entity.plural][person_index] = entity.flattened_roles[0]
|
|
544
|
+
# Adjust previously computed ids and counts
|
|
545
|
+
self.entity_ids[entity.plural] = entity_ids
|
|
546
|
+
self.entity_counts[entity.plural] = len(entity_ids)
|
|
547
|
+
|
|
548
|
+
# Convert back to Python array
|
|
549
|
+
self.roles[entity.plural] = self.roles[entity.plural].tolist()
|
|
550
|
+
self.memberships[entity.plural] = self.memberships[entity.plural].tolist()
|
|
551
|
+
|
|
552
|
+
def set_default_period(self, period_str) -> None:
|
|
553
|
+
if period_str:
|
|
554
|
+
self.default_period = str(periods.period(period_str))
|
|
555
|
+
|
|
556
|
+
def get_input(self, variable: str, period_str: str) -> Array | None:
|
|
557
|
+
if variable not in self.input_buffer:
|
|
558
|
+
self.input_buffer[variable] = {}
|
|
559
|
+
|
|
560
|
+
return self.input_buffer[variable].get(period_str)
|
|
561
|
+
|
|
562
|
+
def check_persons_to_allocate(
|
|
563
|
+
self,
|
|
564
|
+
persons_plural,
|
|
565
|
+
entity_plural,
|
|
566
|
+
persons_ids,
|
|
567
|
+
person_id,
|
|
568
|
+
entity_id,
|
|
569
|
+
role_id,
|
|
570
|
+
persons_to_allocate,
|
|
571
|
+
index,
|
|
572
|
+
) -> None:
|
|
573
|
+
helpers.check_type(
|
|
574
|
+
person_id,
|
|
575
|
+
str,
|
|
576
|
+
[entity_plural, entity_id, role_id, str(index)],
|
|
577
|
+
)
|
|
578
|
+
if person_id not in persons_ids:
|
|
579
|
+
raise errors.SituationParsingError(
|
|
580
|
+
[entity_plural, entity_id, role_id],
|
|
581
|
+
f"Unexpected value: {person_id}. {person_id} has been declared in {entity_id} {role_id}, but has not been declared in {persons_plural}.",
|
|
582
|
+
)
|
|
583
|
+
if person_id not in persons_to_allocate:
|
|
584
|
+
raise errors.SituationParsingError(
|
|
585
|
+
[entity_plural, entity_id, role_id],
|
|
586
|
+
f"{person_id} has been declared more than once in {entity_plural}",
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
def init_variable_values(self, entity, instance_object, instance_id) -> None:
|
|
590
|
+
for variable_name, variable_values in instance_object.items():
|
|
591
|
+
path_in_json = [entity.plural, instance_id, variable_name]
|
|
592
|
+
try:
|
|
593
|
+
entity.check_variable_defined_for_entity(variable_name)
|
|
594
|
+
except ValueError as e: # The variable is defined for another entity
|
|
595
|
+
raise errors.SituationParsingError(path_in_json, e.args[0])
|
|
596
|
+
except errors.VariableNotFoundError as e: # The variable doesn't exist
|
|
597
|
+
raise errors.SituationParsingError(path_in_json, str(e), code=404)
|
|
598
|
+
|
|
599
|
+
instance_index = self.get_ids(entity.plural).index(instance_id)
|
|
600
|
+
|
|
601
|
+
if not isinstance(variable_values, dict):
|
|
602
|
+
if self.default_period is None:
|
|
603
|
+
raise errors.SituationParsingError(
|
|
604
|
+
path_in_json,
|
|
605
|
+
"Can't deal with type: expected object. Input variables should be set for specific periods. For instance: {'salary': {'2017-01': 2000, '2017-02': 2500}}, or {'birth_date': {'ETERNITY': '1980-01-01'}}.",
|
|
606
|
+
)
|
|
607
|
+
variable_values = {self.default_period: variable_values}
|
|
608
|
+
|
|
609
|
+
for period_str, value in variable_values.items():
|
|
610
|
+
try:
|
|
611
|
+
periods.period(period_str)
|
|
612
|
+
except ValueError as e:
|
|
613
|
+
raise errors.SituationParsingError(path_in_json, e.args[0])
|
|
614
|
+
variable = entity.get_variable(variable_name)
|
|
615
|
+
self.add_variable_value(
|
|
616
|
+
entity,
|
|
617
|
+
variable,
|
|
618
|
+
instance_index,
|
|
619
|
+
instance_id,
|
|
620
|
+
period_str,
|
|
621
|
+
value,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
def add_variable_value(
|
|
625
|
+
self,
|
|
626
|
+
entity,
|
|
627
|
+
variable,
|
|
628
|
+
instance_index,
|
|
629
|
+
instance_id,
|
|
630
|
+
period_str,
|
|
631
|
+
value,
|
|
632
|
+
) -> None:
|
|
633
|
+
path_in_json = [entity.plural, instance_id, variable.name, period_str]
|
|
634
|
+
|
|
635
|
+
if value is None:
|
|
636
|
+
return
|
|
637
|
+
|
|
638
|
+
array = self.get_input(variable.name, str(period_str))
|
|
639
|
+
|
|
640
|
+
if array is None:
|
|
641
|
+
array_size = self.get_count(entity.plural)
|
|
642
|
+
array = variable.default_array(array_size)
|
|
643
|
+
|
|
644
|
+
try:
|
|
645
|
+
value = variable.check_set_value(value)
|
|
646
|
+
except ValueError as error:
|
|
647
|
+
raise errors.SituationParsingError(path_in_json, *error.args)
|
|
648
|
+
|
|
649
|
+
array[instance_index] = value
|
|
650
|
+
|
|
651
|
+
self.input_buffer[variable.name][str(periods.period(period_str))] = array
|
|
652
|
+
|
|
653
|
+
def finalize_variables_init(self, population) -> None:
|
|
654
|
+
# Due to set_input mechanism, we must bufferize all inputs, then actually set them,
|
|
655
|
+
# so that the months are set first and the years last.
|
|
656
|
+
plural_key = population.entity.plural
|
|
657
|
+
if plural_key in self.entity_counts:
|
|
658
|
+
population.count = self.get_count(plural_key)
|
|
659
|
+
population.ids = self.get_ids(plural_key)
|
|
660
|
+
if plural_key in self.memberships:
|
|
661
|
+
population.members_entity_id = numpy.array(self.get_memberships(plural_key))
|
|
662
|
+
population.members_role = numpy.array(self.get_roles(plural_key))
|
|
663
|
+
for variable_name in self.input_buffer:
|
|
664
|
+
try:
|
|
665
|
+
holder = population.get_holder(variable_name)
|
|
666
|
+
except ValueError: # Wrong entity, we can just ignore that
|
|
667
|
+
continue
|
|
668
|
+
buffer = self.input_buffer[variable_name]
|
|
669
|
+
unsorted_periods = [
|
|
670
|
+
periods.period(period_str)
|
|
671
|
+
for period_str in self.input_buffer[variable_name]
|
|
672
|
+
]
|
|
673
|
+
# We need to handle small periods first for set_input to work
|
|
674
|
+
sorted_periods = sorted(unsorted_periods, key=periods.key_period_size)
|
|
675
|
+
for period_value in sorted_periods:
|
|
676
|
+
values = buffer[str(period_value)]
|
|
677
|
+
# Hack to replicate the values in the persons entity
|
|
678
|
+
# when we have an axis along a group entity but not persons
|
|
679
|
+
array = numpy.tile(values, population.count // len(values))
|
|
680
|
+
variable = holder.variable
|
|
681
|
+
# TODO - this duplicates the check in Simulation.set_input, but
|
|
682
|
+
# fixing that requires improving Simulation's handling of entities
|
|
683
|
+
if (variable.end is None) or (period_value.start.date <= variable.end):
|
|
684
|
+
holder.set_input(period_value, array)
|
|
685
|
+
|
|
686
|
+
def raise_period_mismatch(self, entity, json, e) -> NoReturn:
|
|
687
|
+
# This error happens when we try to set a variable value for a period that doesn't match its definition period
|
|
688
|
+
# It is only raised when we consume the buffer. We thus don't know which exact key caused the error.
|
|
689
|
+
# We do a basic research to find the culprit path
|
|
690
|
+
culprit_path = next(
|
|
691
|
+
dpath.search(
|
|
692
|
+
json,
|
|
693
|
+
f"*/{e.variable_name}/{e.period!s}",
|
|
694
|
+
yielded=True,
|
|
695
|
+
),
|
|
696
|
+
None,
|
|
697
|
+
)
|
|
698
|
+
if culprit_path:
|
|
699
|
+
path = [entity.plural, *culprit_path[0].split("/")]
|
|
700
|
+
else:
|
|
701
|
+
path = [
|
|
702
|
+
entity.plural,
|
|
703
|
+
] # Fallback: if we can't find the culprit, just set the error at the entities level
|
|
704
|
+
|
|
705
|
+
raise errors.SituationParsingError(path, e.message)
|
|
706
|
+
|
|
707
|
+
# Returns the total number of instances of this entity, including when there is replication along axes
|
|
708
|
+
def get_count(self, entity_name: str) -> int:
|
|
709
|
+
return self.axes_entity_counts.get(entity_name, self.entity_counts[entity_name])
|
|
710
|
+
|
|
711
|
+
# Returns the ids of instances of this entity, including when there is replication along axes
|
|
712
|
+
def get_ids(self, entity_name: str) -> list[str]:
|
|
713
|
+
return self.axes_entity_ids.get(entity_name, self.entity_ids[entity_name])
|
|
714
|
+
|
|
715
|
+
# Returns the memberships of individuals in this entity, including when there is replication along axes
|
|
716
|
+
def get_memberships(self, entity_name):
|
|
717
|
+
# Return empty array for the "persons" entity
|
|
718
|
+
return self.axes_memberships.get(
|
|
719
|
+
entity_name,
|
|
720
|
+
self.memberships.get(entity_name, []),
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# Returns the roles of individuals in this entity, including when there is replication along axes
|
|
724
|
+
def get_roles(self, entity_name: str) -> Sequence[Role]:
|
|
725
|
+
# Return empty array for the "persons" entity
|
|
726
|
+
return self.axes_roles.get(entity_name, self.roles.get(entity_name, []))
|
|
727
|
+
|
|
728
|
+
def add_parallel_axis(self, axis: Axis) -> None:
|
|
729
|
+
# All parallel axes have the same count and entity.
|
|
730
|
+
# Search for a compatible axis, if none exists, error out
|
|
731
|
+
self.axes[0].append(axis)
|
|
732
|
+
|
|
733
|
+
def add_perpendicular_axis(self, axis: Axis) -> None:
|
|
734
|
+
# This adds an axis perpendicular to all previous dimensions
|
|
735
|
+
self.axes.append([axis])
|
|
736
|
+
|
|
737
|
+
def expand_axes(self) -> None:
|
|
738
|
+
# This method should be idempotent & allow change in axes
|
|
739
|
+
perpendicular_dimensions: list[list[Axis]] = self.axes
|
|
740
|
+
cell_count: int = 1
|
|
741
|
+
|
|
742
|
+
for parallel_axes in perpendicular_dimensions:
|
|
743
|
+
first_axis: Axis = parallel_axes[0]
|
|
744
|
+
axis_count: int = first_axis["count"]
|
|
745
|
+
cell_count *= axis_count
|
|
746
|
+
|
|
747
|
+
# Scale the "prototype" situation, repeating it cell_count times
|
|
748
|
+
for entity_name in self.entity_counts:
|
|
749
|
+
# Adjust counts
|
|
750
|
+
self.axes_entity_counts[entity_name] = (
|
|
751
|
+
self.get_count(entity_name) * cell_count
|
|
752
|
+
)
|
|
753
|
+
# Adjust ids
|
|
754
|
+
original_ids: list[str] = self.get_ids(entity_name) * cell_count
|
|
755
|
+
indices: Array[numpy.int16] = numpy.arange(
|
|
756
|
+
0,
|
|
757
|
+
cell_count * self.entity_counts[entity_name],
|
|
758
|
+
)
|
|
759
|
+
adjusted_ids: list[str] = [
|
|
760
|
+
original_id + str(index)
|
|
761
|
+
for original_id, index in zip(original_ids, indices)
|
|
762
|
+
]
|
|
763
|
+
self.axes_entity_ids[entity_name] = adjusted_ids
|
|
764
|
+
|
|
765
|
+
# Adjust roles
|
|
766
|
+
original_roles = self.get_roles(entity_name)
|
|
767
|
+
adjusted_roles = original_roles * cell_count
|
|
768
|
+
self.axes_roles[entity_name] = adjusted_roles
|
|
769
|
+
# Adjust memberships, for group entities only
|
|
770
|
+
if entity_name != self.persons_plural:
|
|
771
|
+
original_memberships = self.get_memberships(entity_name)
|
|
772
|
+
repeated_memberships = original_memberships * cell_count
|
|
773
|
+
indices = (
|
|
774
|
+
numpy.repeat(numpy.arange(0, cell_count), len(original_memberships))
|
|
775
|
+
* self.entity_counts[entity_name]
|
|
776
|
+
)
|
|
777
|
+
adjusted_memberships = (
|
|
778
|
+
numpy.array(repeated_memberships) + indices
|
|
779
|
+
).tolist()
|
|
780
|
+
self.axes_memberships[entity_name] = adjusted_memberships
|
|
781
|
+
|
|
782
|
+
# Now generate input values along the specified axes
|
|
783
|
+
# TODO - factor out the common logic here
|
|
784
|
+
if len(self.axes) == 1 and len(self.axes[0]):
|
|
785
|
+
parallel_axes = self.axes[0]
|
|
786
|
+
first_axis = parallel_axes[0]
|
|
787
|
+
axis_count: int = first_axis["count"]
|
|
788
|
+
axis_entity = self.get_variable_entity(first_axis["name"])
|
|
789
|
+
axis_entity_step_size = self.entity_counts[axis_entity.plural]
|
|
790
|
+
# Distribute values along axes
|
|
791
|
+
for axis in parallel_axes:
|
|
792
|
+
axis_index = axis.get("index", 0)
|
|
793
|
+
axis_period = axis.get("period", self.default_period)
|
|
794
|
+
axis_name = axis["name"]
|
|
795
|
+
variable = axis_entity.get_variable(axis_name)
|
|
796
|
+
array = self.get_input(axis_name, str(axis_period))
|
|
797
|
+
if array is None:
|
|
798
|
+
array = variable.default_array(axis_count * axis_entity_step_size)
|
|
799
|
+
elif array.size == axis_entity_step_size:
|
|
800
|
+
array = numpy.tile(array, axis_count)
|
|
801
|
+
array[axis_index::axis_entity_step_size] = numpy.linspace(
|
|
802
|
+
axis["min"],
|
|
803
|
+
axis["max"],
|
|
804
|
+
num=axis_count,
|
|
805
|
+
)
|
|
806
|
+
# Set input
|
|
807
|
+
self.input_buffer[axis_name][str(axis_period)] = array
|
|
808
|
+
else:
|
|
809
|
+
first_axes_count: list[int] = (
|
|
810
|
+
parallel_axes[0]["count"] for parallel_axes in self.axes
|
|
811
|
+
)
|
|
812
|
+
axes_linspaces = [
|
|
813
|
+
numpy.linspace(0, axis_count - 1, num=axis_count)
|
|
814
|
+
for axis_count in first_axes_count
|
|
815
|
+
]
|
|
816
|
+
axes_meshes = numpy.meshgrid(*axes_linspaces)
|
|
817
|
+
for parallel_axes, mesh in zip(self.axes, axes_meshes):
|
|
818
|
+
first_axis = parallel_axes[0]
|
|
819
|
+
axis_count = first_axis["count"]
|
|
820
|
+
axis_entity = self.get_variable_entity(first_axis["name"])
|
|
821
|
+
axis_entity_step_size = self.entity_counts[axis_entity.plural]
|
|
822
|
+
# Distribute values along the grid
|
|
823
|
+
for axis in parallel_axes:
|
|
824
|
+
axis_index = axis.get("index", 0)
|
|
825
|
+
axis_period = axis.get("period", self.default_period)
|
|
826
|
+
axis_name = axis["name"]
|
|
827
|
+
variable = axis_entity.get_variable(axis_name, check_existence=True)
|
|
828
|
+
array = self.get_input(axis_name, str(axis_period))
|
|
829
|
+
if array is None:
|
|
830
|
+
array = variable.default_array(
|
|
831
|
+
cell_count * axis_entity_step_size,
|
|
832
|
+
)
|
|
833
|
+
elif array.size == axis_entity_step_size:
|
|
834
|
+
array = numpy.tile(array, cell_count)
|
|
835
|
+
array[axis_index::axis_entity_step_size] = axis[
|
|
836
|
+
"min"
|
|
837
|
+
] + mesh.reshape(cell_count) * (axis["max"] - axis["min"]) / (
|
|
838
|
+
axis_count - 1
|
|
839
|
+
)
|
|
840
|
+
self.input_buffer[axis_name][str(axis_period)] = array
|
|
841
|
+
|
|
842
|
+
def get_variable_entity(self, variable_name: str) -> Entity:
|
|
843
|
+
return self.variable_entities[variable_name]
|
|
844
|
+
|
|
845
|
+
def register_variable(self, variable_name: str, entity: Entity) -> None:
|
|
846
|
+
self.variable_entities[variable_name] = entity
|
|
847
|
+
|
|
848
|
+
def register_variables(self, simulation: Simulation) -> None:
|
|
849
|
+
tax_benefit_system: TaxBenefitSystem = simulation.tax_benefit_system
|
|
850
|
+
variables: Iterable[str] = tax_benefit_system.variables.keys()
|
|
851
|
+
|
|
852
|
+
for name in variables:
|
|
853
|
+
population: Population = simulation.get_variable_population(name)
|
|
854
|
+
entity: Entity = population.entity
|
|
855
|
+
self.register_variable(name, entity)
|