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.
Files changed (237) hide show
  1. openfisca_core/__init__.py +0 -0
  2. openfisca_core/commons/__init__.py +18 -0
  3. openfisca_core/commons/dummy.py +25 -0
  4. openfisca_core/commons/formulas.py +121 -0
  5. openfisca_core/commons/misc.py +109 -0
  6. openfisca_core/commons/py.typed +0 -0
  7. openfisca_core/commons/rates.py +114 -0
  8. openfisca_core/commons/tests/__init__.py +0 -0
  9. openfisca_core/commons/tests/test_dummy.py +9 -0
  10. openfisca_core/commons/tests/test_formulas.py +75 -0
  11. openfisca_core/commons/tests/test_rates.py +26 -0
  12. openfisca_core/data_storage/__init__.py +7 -0
  13. openfisca_core/data_storage/in_memory_storage.py +198 -0
  14. openfisca_core/data_storage/on_disk_storage.py +307 -0
  15. openfisca_core/data_storage/types.py +14 -0
  16. openfisca_core/entities/__init__.py +23 -0
  17. openfisca_core/entities/_core_entity.py +217 -0
  18. openfisca_core/entities/_description.py +55 -0
  19. openfisca_core/entities/entity.py +60 -0
  20. openfisca_core/entities/group_entity.py +123 -0
  21. openfisca_core/entities/helpers.py +165 -0
  22. openfisca_core/entities/py.typed +0 -0
  23. openfisca_core/entities/role.py +92 -0
  24. openfisca_core/entities/tests/__init__.py +0 -0
  25. openfisca_core/entities/tests/test_entity.py +10 -0
  26. openfisca_core/entities/tests/test_group_entity.py +70 -0
  27. openfisca_core/entities/tests/test_role.py +11 -0
  28. openfisca_core/entities/types.py +44 -0
  29. openfisca_core/errors/__init__.py +64 -0
  30. openfisca_core/errors/cycle_error.py +2 -0
  31. openfisca_core/errors/empty_argument_error.py +39 -0
  32. openfisca_core/errors/nan_creation_error.py +2 -0
  33. openfisca_core/errors/parameter_not_found_error.py +16 -0
  34. openfisca_core/errors/parameter_parsing_error.py +18 -0
  35. openfisca_core/errors/period_mismatch_error.py +9 -0
  36. openfisca_core/errors/situation_parsing_error.py +26 -0
  37. openfisca_core/errors/spiral_error.py +2 -0
  38. openfisca_core/errors/variable_name_config_error.py +2 -0
  39. openfisca_core/errors/variable_not_found_error.py +29 -0
  40. openfisca_core/experimental/__init__.py +9 -0
  41. openfisca_core/experimental/_errors.py +5 -0
  42. openfisca_core/experimental/_memory_config.py +41 -0
  43. openfisca_core/formula_helpers.py +9 -0
  44. openfisca_core/holders/__init__.py +33 -0
  45. openfisca_core/holders/helpers.py +93 -0
  46. openfisca_core/holders/holder.py +328 -0
  47. openfisca_core/holders/tests/__init__.py +0 -0
  48. openfisca_core/holders/tests/test_helpers.py +134 -0
  49. openfisca_core/holders/types.py +3 -0
  50. openfisca_core/indexed_enums/__init__.py +18 -0
  51. openfisca_core/indexed_enums/_enum_type.py +79 -0
  52. openfisca_core/indexed_enums/_errors.py +35 -0
  53. openfisca_core/indexed_enums/_guards.py +209 -0
  54. openfisca_core/indexed_enums/_utils.py +180 -0
  55. openfisca_core/indexed_enums/config.py +6 -0
  56. openfisca_core/indexed_enums/enum.py +218 -0
  57. openfisca_core/indexed_enums/enum_array.py +316 -0
  58. openfisca_core/indexed_enums/py.typed +0 -0
  59. openfisca_core/indexed_enums/tests/__init__.py +0 -0
  60. openfisca_core/indexed_enums/tests/test_enum.py +135 -0
  61. openfisca_core/indexed_enums/tests/test_enum_array.py +30 -0
  62. openfisca_core/indexed_enums/types.py +49 -0
  63. openfisca_core/memory_config.py +9 -0
  64. openfisca_core/model_api.py +71 -0
  65. openfisca_core/parameters/__init__.py +71 -0
  66. openfisca_core/parameters/at_instant_like.py +17 -0
  67. openfisca_core/parameters/config.py +55 -0
  68. openfisca_core/parameters/helpers.py +106 -0
  69. openfisca_core/parameters/parameter.py +217 -0
  70. openfisca_core/parameters/parameter_at_instant.py +75 -0
  71. openfisca_core/parameters/parameter_node.py +176 -0
  72. openfisca_core/parameters/parameter_node_at_instant.py +57 -0
  73. openfisca_core/parameters/parameter_scale.py +121 -0
  74. openfisca_core/parameters/parameter_scale_bracket.py +7 -0
  75. openfisca_core/parameters/values_history.py +5 -0
  76. openfisca_core/parameters/vectorial_parameter_node_at_instant.py +179 -0
  77. openfisca_core/periods/__init__.py +77 -0
  78. openfisca_core/periods/_errors.py +28 -0
  79. openfisca_core/periods/_parsers.py +122 -0
  80. openfisca_core/periods/config.py +20 -0
  81. openfisca_core/periods/date_unit.py +110 -0
  82. openfisca_core/periods/helpers.py +314 -0
  83. openfisca_core/periods/instant_.py +226 -0
  84. openfisca_core/periods/period_.py +919 -0
  85. openfisca_core/periods/py.typed +0 -0
  86. openfisca_core/periods/tests/__init__.py +0 -0
  87. openfisca_core/periods/tests/helpers/__init__.py +0 -0
  88. openfisca_core/periods/tests/helpers/test_helpers.py +65 -0
  89. openfisca_core/periods/tests/helpers/test_instant.py +73 -0
  90. openfisca_core/periods/tests/helpers/test_period.py +134 -0
  91. openfisca_core/periods/tests/test_instant.py +32 -0
  92. openfisca_core/periods/tests/test_parsers.py +129 -0
  93. openfisca_core/periods/tests/test_period.py +283 -0
  94. openfisca_core/populations/__init__.py +64 -0
  95. openfisca_core/populations/_core_population.py +454 -0
  96. openfisca_core/populations/_errors.py +65 -0
  97. openfisca_core/populations/group_population.py +326 -0
  98. openfisca_core/populations/population.py +142 -0
  99. openfisca_core/populations/tests/__init__.py +0 -0
  100. openfisca_core/populations/tests/test_members_position.py +116 -0
  101. openfisca_core/populations/types.py +110 -0
  102. openfisca_core/projectors/__init__.py +39 -0
  103. openfisca_core/projectors/entity_to_person_projector.py +12 -0
  104. openfisca_core/projectors/first_person_to_entity_projector.py +13 -0
  105. openfisca_core/projectors/helpers.py +139 -0
  106. openfisca_core/projectors/projector.py +38 -0
  107. openfisca_core/projectors/typing.py +27 -0
  108. openfisca_core/projectors/unique_role_to_entity_projector.py +14 -0
  109. openfisca_core/rates.py +9 -0
  110. openfisca_core/reforms/__init__.py +24 -0
  111. openfisca_core/reforms/in_yaml_test_reform.py +28 -0
  112. openfisca_core/reforms/reform.py +86 -0
  113. openfisca_core/scripts/__init__.py +106 -0
  114. openfisca_core/scripts/__pycache__/__init__.cpython-310-pytest-8.3.3.pyc +0 -0
  115. openfisca_core/scripts/__pycache__/__init__.cpython-310-pytest-8.4.2.pyc +0 -0
  116. openfisca_core/scripts/__pycache__/__init__.cpython-310.pyc +0 -0
  117. openfisca_core/scripts/__pycache__/__init__.cpython-311-pytest-8.3.3.pyc +0 -0
  118. openfisca_core/scripts/__pycache__/__init__.cpython-311-pytest-8.4.2.pyc +0 -0
  119. openfisca_core/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  120. openfisca_core/scripts/__pycache__/__init__.cpython-312-pytest-8.4.2.pyc +0 -0
  121. openfisca_core/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  122. openfisca_core/scripts/__pycache__/__init__.cpython-313-pytest-8.3.3.pyc +0 -0
  123. openfisca_core/scripts/__pycache__/__init__.cpython-313-pytest-8.4.2.pyc +0 -0
  124. openfisca_core/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  125. openfisca_core/scripts/__pycache__/find_placeholders.cpython-311.pyc +0 -0
  126. openfisca_core/scripts/__pycache__/find_placeholders.cpython-313.pyc +0 -0
  127. openfisca_core/scripts/__pycache__/measure_numpy_condition_notations.cpython-311.pyc +0 -0
  128. openfisca_core/scripts/__pycache__/measure_numpy_condition_notations.cpython-313.pyc +0 -0
  129. openfisca_core/scripts/__pycache__/measure_performances.cpython-311.pyc +0 -0
  130. openfisca_core/scripts/__pycache__/measure_performances.cpython-313.pyc +0 -0
  131. openfisca_core/scripts/__pycache__/measure_performances_fancy_indexing.cpython-311.pyc +0 -0
  132. openfisca_core/scripts/__pycache__/measure_performances_fancy_indexing.cpython-313.pyc +0 -0
  133. openfisca_core/scripts/__pycache__/openfisca_command.cpython-310.pyc +0 -0
  134. openfisca_core/scripts/__pycache__/openfisca_command.cpython-311.pyc +0 -0
  135. openfisca_core/scripts/__pycache__/openfisca_command.cpython-312.pyc +0 -0
  136. openfisca_core/scripts/__pycache__/openfisca_command.cpython-313.pyc +0 -0
  137. openfisca_core/scripts/__pycache__/remove_fuzzy.cpython-311.pyc +0 -0
  138. openfisca_core/scripts/__pycache__/remove_fuzzy.cpython-313.pyc +0 -0
  139. openfisca_core/scripts/__pycache__/run_test.cpython-310.pyc +0 -0
  140. openfisca_core/scripts/__pycache__/run_test.cpython-311.pyc +0 -0
  141. openfisca_core/scripts/__pycache__/run_test.cpython-312.pyc +0 -0
  142. openfisca_core/scripts/__pycache__/run_test.cpython-313.pyc +0 -0
  143. openfisca_core/scripts/__pycache__/simulation_generator.cpython-311.pyc +0 -0
  144. openfisca_core/scripts/__pycache__/simulation_generator.cpython-313.pyc +0 -0
  145. openfisca_core/scripts/assets/__init__.py +0 -0
  146. openfisca_core/scripts/assets/__pycache__/__init__.cpython-311.pyc +0 -0
  147. openfisca_core/scripts/assets/__pycache__/__init__.cpython-313.pyc +0 -0
  148. openfisca_core/scripts/assets/index.html +140 -0
  149. openfisca_core/scripts/find_placeholders.py +51 -0
  150. openfisca_core/scripts/measure_numpy_condition_notations.py +137 -0
  151. openfisca_core/scripts/measure_performances.py +291 -0
  152. openfisca_core/scripts/measure_performances_fancy_indexing.py +89 -0
  153. openfisca_core/scripts/migrations/__init__.py +0 -0
  154. openfisca_core/scripts/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
  155. openfisca_core/scripts/migrations/__pycache__/__init__.cpython-313.pyc +0 -0
  156. openfisca_core/scripts/migrations/__pycache__/v24_to_25.cpython-311.pyc +0 -0
  157. openfisca_core/scripts/migrations/__pycache__/v24_to_25.cpython-313.pyc +0 -0
  158. openfisca_core/scripts/migrations/v16_2_to_v17/__init__.py +0 -0
  159. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/__init__.cpython-311.pyc +0 -0
  160. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/__init__.cpython-313.pyc +0 -0
  161. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_country_template.cpython-311.pyc +0 -0
  162. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_country_template.cpython-313.pyc +0 -0
  163. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_extension_template.cpython-311.pyc +0 -0
  164. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_extension_template.cpython-313.pyc +0 -0
  165. openfisca_core/scripts/migrations/v16_2_to_v17/legislation.xsd +102 -0
  166. openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py +30 -0
  167. openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py +26 -0
  168. openfisca_core/scripts/migrations/v24_to_25.py +156 -0
  169. openfisca_core/scripts/openfisca_command.py +192 -0
  170. openfisca_core/scripts/remove_fuzzy.py +213 -0
  171. openfisca_core/scripts/run_test.py +51 -0
  172. openfisca_core/scripts/simulation_generator.py +90 -0
  173. openfisca_core/simulation_builder.py +16 -0
  174. openfisca_core/simulations/__init__.py +45 -0
  175. openfisca_core/simulations/_build_default_simulation.py +158 -0
  176. openfisca_core/simulations/_build_from_variables.py +230 -0
  177. openfisca_core/simulations/_type_guards.py +298 -0
  178. openfisca_core/simulations/helpers.py +111 -0
  179. openfisca_core/simulations/simulation.py +618 -0
  180. openfisca_core/simulations/simulation_builder.py +855 -0
  181. openfisca_core/simulations/typing.py +203 -0
  182. openfisca_core/taxbenefitsystems/__init__.py +26 -0
  183. openfisca_core/taxbenefitsystems/tax_benefit_system.py +584 -0
  184. openfisca_core/taxscales/__init__.py +35 -0
  185. openfisca_core/taxscales/abstract_rate_tax_scale.py +41 -0
  186. openfisca_core/taxscales/abstract_tax_scale.py +53 -0
  187. openfisca_core/taxscales/amount_tax_scale_like.py +55 -0
  188. openfisca_core/taxscales/helpers.py +44 -0
  189. openfisca_core/taxscales/linear_average_rate_tax_scale.py +76 -0
  190. openfisca_core/taxscales/marginal_amount_tax_scale.py +34 -0
  191. openfisca_core/taxscales/marginal_rate_tax_scale.py +286 -0
  192. openfisca_core/taxscales/rate_tax_scale_like.py +214 -0
  193. openfisca_core/taxscales/single_amount_tax_scale.py +32 -0
  194. openfisca_core/taxscales/tax_scale_like.py +65 -0
  195. openfisca_core/tools/__init__.py +94 -0
  196. openfisca_core/tools/parallel_plugin.py +55 -0
  197. openfisca_core/tools/simulation_dumper.py +137 -0
  198. openfisca_core/tools/test_runner.py +802 -0
  199. openfisca_core/tracers/__init__.py +40 -0
  200. openfisca_core/tracers/computation_log.py +196 -0
  201. openfisca_core/tracers/flat_trace.py +87 -0
  202. openfisca_core/tracers/full_tracer.py +172 -0
  203. openfisca_core/tracers/performance_log.py +133 -0
  204. openfisca_core/tracers/simple_tracer.py +64 -0
  205. openfisca_core/tracers/trace_node.py +121 -0
  206. openfisca_core/tracers/tracing_parameter_node_at_instant.py +81 -0
  207. openfisca_core/types.py +548 -0
  208. openfisca_core/variables/__init__.py +26 -0
  209. openfisca_core/variables/config.py +53 -0
  210. openfisca_core/variables/helpers.py +67 -0
  211. openfisca_core/variables/tests/__init__.py +0 -0
  212. openfisca_core/variables/tests/test_definition_period.py +43 -0
  213. openfisca_core/variables/variable.py +474 -0
  214. openfisca_core/warnings/__init__.py +25 -0
  215. openfisca_core/warnings/libyaml_warning.py +2 -0
  216. openfisca_core/warnings/tempfile_warning.py +2 -0
  217. openfisca_core_test-46.0.0.data/data/share/openfisca/openfisca-core/CHANGELOG.md +4209 -0
  218. openfisca_core_test-46.0.0.data/data/share/openfisca/openfisca-core/README.md +152 -0
  219. openfisca_core_test-46.0.0.dist-info/METADATA +194 -0
  220. openfisca_core_test-46.0.0.dist-info/RECORD +237 -0
  221. openfisca_core_test-46.0.0.dist-info/WHEEL +5 -0
  222. openfisca_core_test-46.0.0.dist-info/entry_points.txt +3 -0
  223. openfisca_core_test-46.0.0.dist-info/licenses/LICENSE +661 -0
  224. openfisca_core_test-46.0.0.dist-info/top_level.txt +2 -0
  225. openfisca_web_api/__init__.py +0 -0
  226. openfisca_web_api/app.py +226 -0
  227. openfisca_web_api/errors.py +11 -0
  228. openfisca_web_api/handlers.py +90 -0
  229. openfisca_web_api/loader/__init__.py +22 -0
  230. openfisca_web_api/loader/entities.py +28 -0
  231. openfisca_web_api/loader/parameters.py +122 -0
  232. openfisca_web_api/loader/spec.py +168 -0
  233. openfisca_web_api/loader/tax_benefit_system.py +27 -0
  234. openfisca_web_api/loader/variables.py +121 -0
  235. openfisca_web_api/openAPI.yml +434 -0
  236. openfisca_web_api/scripts/__init__.py +0 -0
  237. 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)