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,618 @@
1
+ from __future__ import annotations
2
+
3
+ import tempfile
4
+ import warnings
5
+ from collections.abc import Mapping
6
+ from typing import NamedTuple
7
+
8
+ import numpy
9
+
10
+ from openfisca_core import (
11
+ commons,
12
+ errors,
13
+ indexed_enums,
14
+ periods,
15
+ tracers,
16
+ )
17
+ from openfisca_core import (
18
+ warnings as core_warnings,
19
+ )
20
+ from openfisca_core.types import (
21
+ CorePopulation as Population,
22
+ )
23
+ from openfisca_core.types import (
24
+ TaxBenefitSystem,
25
+ Variable,
26
+ )
27
+
28
+
29
+ class Simulation:
30
+ """Represents a simulation, and handles the calculation logic."""
31
+
32
+ tax_benefit_system: TaxBenefitSystem
33
+ populations: dict[str, Population]
34
+ invalidated_caches: set[Cache]
35
+
36
+ def __init__(
37
+ self,
38
+ tax_benefit_system: TaxBenefitSystem,
39
+ populations: Mapping[str, Population],
40
+ ) -> None:
41
+ """This constructor is reserved for internal use; see :any:`SimulationBuilder`,
42
+ which is the preferred way to obtain a Simulation initialized with a consistent
43
+ set of Entities.
44
+ """
45
+ self.tax_benefit_system = tax_benefit_system
46
+ assert tax_benefit_system is not None
47
+
48
+ self.populations = populations
49
+ self.persons = self.populations[tax_benefit_system.person_entity.key]
50
+ self.link_to_entities_instances()
51
+ self.create_shortcuts()
52
+
53
+ self.invalidated_caches = set()
54
+
55
+ self.debug = False
56
+ self.trace = False
57
+ self.tracer = tracers.SimpleTracer()
58
+ self.opt_out_cache = False
59
+
60
+ # controls the spirals detection; check for performance impact if > 1
61
+ self.max_spiral_loops: int = 1
62
+ self.memory_config = None
63
+ self._data_storage_dir = None
64
+ self.start_computation_period = None
65
+
66
+ @property
67
+ def trace(self):
68
+ return self._trace
69
+
70
+ @trace.setter
71
+ def trace(self, trace) -> None:
72
+ self._trace = trace
73
+ if trace:
74
+ self.tracer = tracers.FullTracer()
75
+ else:
76
+ self.tracer = tracers.SimpleTracer()
77
+
78
+ def link_to_entities_instances(self) -> None:
79
+ for entity_instance in self.populations.values():
80
+ entity_instance.simulation = self
81
+
82
+ def create_shortcuts(self) -> None:
83
+ for population in self.populations.values():
84
+ # create shortcut simulation.person and simulation.household (for instance)
85
+ setattr(self, population.entity.key, population)
86
+
87
+ @property
88
+ def data_storage_dir(self):
89
+ """Temporary folder used to store intermediate calculation data in case the memory is saturated."""
90
+ if self._data_storage_dir is None:
91
+ self._data_storage_dir = tempfile.mkdtemp(prefix="openfisca_")
92
+ message = [
93
+ (
94
+ f"Intermediate results will be stored on disk in {self._data_storage_dir} in case of memory overflow."
95
+ ),
96
+ "You should remove this directory once you're done with your simulation.",
97
+ ]
98
+ warnings.warn(
99
+ " ".join(message),
100
+ core_warnings.TempfileWarning,
101
+ stacklevel=2,
102
+ )
103
+ return self._data_storage_dir
104
+
105
+ # ----- Calculation methods ----- #
106
+
107
+ def calculate(self, variable_name: str, period):
108
+ """Calculate ``variable_name`` for ``period``."""
109
+ if period is not None and not isinstance(period, periods.Period):
110
+ period = periods.period(period)
111
+
112
+ self.tracer.record_calculation_start(variable_name, period)
113
+
114
+ try:
115
+ result = self._calculate(variable_name, period)
116
+ self.tracer.record_calculation_result(result)
117
+ return result
118
+
119
+ finally:
120
+ self.tracer.record_calculation_end()
121
+ self.purge_cache_of_invalid_values()
122
+
123
+ def _calculate(self, variable_name: str, period: periods.Period):
124
+ """Calculate the variable ``variable_name`` for the period ``period``, using the variable formula if it exists.
125
+
126
+ :returns: A numpy array containing the result of the calculation
127
+ """
128
+ variable: Variable | None
129
+
130
+ population = self.get_variable_population(variable_name)
131
+ holder = population.get_holder(variable_name)
132
+ variable = self.tax_benefit_system.get_variable(
133
+ variable_name,
134
+ check_existence=True,
135
+ )
136
+
137
+ if variable is None:
138
+ raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system)
139
+
140
+ self._check_period_consistency(period, variable)
141
+
142
+ # First look for a value already cached
143
+ cached_array = holder.get_array(period)
144
+ if cached_array is not None:
145
+ return cached_array
146
+
147
+ array = None
148
+
149
+ # First, try to run a formula
150
+ if self.start_computation_period is not None:
151
+ if not isinstance(self.start_computation_period, periods.Period):
152
+ self.start_computation_period = periods.period(
153
+ self.start_computation_period
154
+ )
155
+ if period < self.start_computation_period:
156
+ return holder.default_array()
157
+ try:
158
+ self._check_for_cycle(variable.name, period)
159
+ array = self._run_formula(variable, population, period)
160
+
161
+ # If no result, use the default value and cache it
162
+ if array is None:
163
+ array = holder.default_array()
164
+
165
+ array = self._cast_formula_result(array, variable)
166
+ holder.put_in_cache(array, period)
167
+
168
+ except errors.SpiralError:
169
+ array = holder.default_array()
170
+
171
+ return array
172
+
173
+ def purge_cache_of_invalid_values(self) -> None:
174
+ # We wait for the end of calculate(), signalled by an empty stack, before purging the cache
175
+ if self.tracer.stack:
176
+ return
177
+ for _name, _period in self.invalidated_caches:
178
+ holder = self.get_holder(_name)
179
+ holder.delete_arrays(_period)
180
+ self.invalidated_caches = set()
181
+
182
+ def calculate_add(self, variable_name: str, period):
183
+ variable: Variable | None
184
+
185
+ variable = self.tax_benefit_system.get_variable(
186
+ variable_name,
187
+ check_existence=True,
188
+ )
189
+
190
+ if variable is None:
191
+ raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system)
192
+
193
+ if period is not None and not isinstance(period, periods.Period):
194
+ period = periods.period(period)
195
+
196
+ # Check that the requested period matches definition_period
197
+ if periods.unit_weight(variable.definition_period) > periods.unit_weight(
198
+ period.unit,
199
+ ):
200
+ msg = (
201
+ f"Unable to compute variable '{variable.name}' for period "
202
+ f"{period}: '{variable.name}' can only be computed for "
203
+ f"{variable.definition_period}-long periods. You can use the "
204
+ f"DIVIDE option to get an estimate of {variable.name}."
205
+ )
206
+ raise ValueError(
207
+ msg,
208
+ )
209
+
210
+ if variable.definition_period not in (
211
+ periods.DateUnit.isoformat + periods.DateUnit.isocalendar
212
+ ):
213
+ msg = (
214
+ f"Unable to ADD constant variable '{variable.name}' over "
215
+ f"the period {period}: eternal variables can't be summed "
216
+ "over time."
217
+ )
218
+ raise ValueError(
219
+ msg,
220
+ )
221
+
222
+ return sum(
223
+ self.calculate(variable_name, sub_period)
224
+ for sub_period in period.get_subperiods(variable.definition_period)
225
+ )
226
+
227
+ def calculate_divide(self, variable_name: str, period):
228
+ variable: Variable | None
229
+
230
+ variable = self.tax_benefit_system.get_variable(
231
+ variable_name,
232
+ check_existence=True,
233
+ )
234
+
235
+ if variable is None:
236
+ raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system)
237
+
238
+ if period is not None and not isinstance(period, periods.Period):
239
+ period = periods.period(period)
240
+
241
+ if (
242
+ periods.unit_weight(variable.definition_period)
243
+ < periods.unit_weight(period.unit)
244
+ or period.size > 1
245
+ ):
246
+ msg = (
247
+ f"Can't calculate variable '{variable.name}' for period "
248
+ f"{period}: '{variable.name}' can only be computed for "
249
+ f"{variable.definition_period}-long periods. You can use the "
250
+ f"ADD option to get an estimate of {variable.name}."
251
+ )
252
+ raise ValueError(
253
+ msg,
254
+ )
255
+
256
+ if variable.definition_period not in (
257
+ periods.DateUnit.isoformat + periods.DateUnit.isocalendar
258
+ ):
259
+ msg = (
260
+ f"Unable to DIVIDE constant variable '{variable.name}' over "
261
+ f"the period {period}: eternal variables can't be divided "
262
+ "over time."
263
+ )
264
+ raise ValueError(
265
+ msg,
266
+ )
267
+
268
+ if (
269
+ period.unit
270
+ not in (periods.DateUnit.isoformat + periods.DateUnit.isocalendar)
271
+ or period.size != 1
272
+ ):
273
+ msg = (
274
+ f"Unable to DIVIDE constant variable '{variable.name}' over "
275
+ f"the period {period}: eternal variables can't be used "
276
+ "as a denominator to divide a variable over time."
277
+ )
278
+ raise ValueError(
279
+ msg,
280
+ )
281
+
282
+ if variable.definition_period == periods.DateUnit.YEAR:
283
+ calculation_period = period.this_year
284
+
285
+ elif variable.definition_period == periods.DateUnit.MONTH:
286
+ calculation_period = period.first_month
287
+
288
+ elif variable.definition_period == periods.DateUnit.DAY:
289
+ calculation_period = period.first_day
290
+
291
+ elif variable.definition_period == periods.DateUnit.WEEK:
292
+ calculation_period = period.first_week
293
+
294
+ else:
295
+ calculation_period = period.first_weekday
296
+
297
+ if period.unit == periods.DateUnit.YEAR:
298
+ denominator = calculation_period.size_in_years
299
+
300
+ elif period.unit == periods.DateUnit.MONTH:
301
+ denominator = calculation_period.size_in_months
302
+
303
+ elif period.unit == periods.DateUnit.DAY:
304
+ denominator = calculation_period.size_in_days
305
+
306
+ elif period.unit == periods.DateUnit.WEEK:
307
+ denominator = calculation_period.size_in_weeks
308
+
309
+ else:
310
+ denominator = calculation_period.size_in_weekdays
311
+
312
+ return self.calculate(variable_name, calculation_period) / denominator
313
+
314
+ def calculate_output(self, variable_name: str, period):
315
+ """Calculate the value of a variable using the ``calculate_output`` attribute of the variable."""
316
+ variable: Variable | None
317
+
318
+ variable = self.tax_benefit_system.get_variable(
319
+ variable_name,
320
+ check_existence=True,
321
+ )
322
+
323
+ if variable is None:
324
+ raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system)
325
+
326
+ if variable.calculate_output is None:
327
+ return self.calculate(variable_name, period)
328
+
329
+ return variable.calculate_output(self, variable_name, period)
330
+
331
+ def trace_parameters_at_instant(self, formula_period):
332
+ return tracers.TracingParameterNodeAtInstant(
333
+ self.tax_benefit_system.get_parameters_at_instant(formula_period),
334
+ self.tracer,
335
+ )
336
+
337
+ def _run_formula(self, variable, population, period):
338
+ """Find the ``variable`` formula for the given ``period`` if it exists, and apply it to ``population``."""
339
+ formula = variable.get_formula(period)
340
+ if formula is None:
341
+ return None
342
+
343
+ if self.trace:
344
+ parameters_at = self.trace_parameters_at_instant
345
+ else:
346
+ parameters_at = self.tax_benefit_system.get_parameters_at_instant
347
+
348
+ if formula.__code__.co_argcount == 2:
349
+ array = formula(population, period)
350
+ else:
351
+ array = formula(population, period, parameters_at)
352
+
353
+ return array
354
+
355
+ def _check_period_consistency(self, period, variable) -> None:
356
+ """Check that a period matches the variable definition_period."""
357
+ if variable.definition_period == periods.DateUnit.ETERNITY:
358
+ return # For variables which values are constant in time, all periods are accepted
359
+
360
+ if (
361
+ variable.definition_period == periods.DateUnit.YEAR
362
+ and period.unit != periods.DateUnit.YEAR
363
+ ):
364
+ msg = f"Unable to compute variable '{variable.name}' for period {period}: '{variable.name}' must be computed for a whole year. You can use the DIVIDE option to get an estimate of {variable.name} by dividing the yearly value by 12, or change the requested period to 'period.this_year'."
365
+ raise ValueError(
366
+ msg,
367
+ )
368
+
369
+ if (
370
+ variable.definition_period == periods.DateUnit.MONTH
371
+ and period.unit != periods.DateUnit.MONTH
372
+ ):
373
+ msg = f"Unable to compute variable '{variable.name}' for period {period}: '{variable.name}' must be computed for a whole month. You can use the ADD option to sum '{variable.name}' over the requested period, or change the requested period to 'period.first_month'."
374
+ raise ValueError(
375
+ msg,
376
+ )
377
+
378
+ if (
379
+ variable.definition_period == periods.DateUnit.WEEK
380
+ and period.unit != periods.DateUnit.WEEK
381
+ ):
382
+ msg = f"Unable to compute variable '{variable.name}' for period {period}: '{variable.name}' must be computed for a whole week. You can use the ADD option to sum '{variable.name}' over the requested period, or change the requested period to 'period.first_week'."
383
+ raise ValueError(
384
+ msg,
385
+ )
386
+
387
+ if period.size != 1:
388
+ msg = f"Unable to compute variable '{variable.name}' for period {period}: '{variable.name}' must be computed for a whole {variable.definition_period}. You can use the ADD option to sum '{variable.name}' over the requested period."
389
+ raise ValueError(
390
+ msg,
391
+ )
392
+
393
+ def _cast_formula_result(self, value, variable):
394
+ if variable.value_type == indexed_enums.Enum and not isinstance(
395
+ value,
396
+ indexed_enums.EnumArray,
397
+ ):
398
+ return variable.possible_values.encode(value)
399
+
400
+ if not isinstance(value, numpy.ndarray):
401
+ population = self.get_variable_population(variable.name)
402
+ value = population.filled_array(value)
403
+
404
+ if value.dtype != variable.dtype:
405
+ return value.astype(variable.dtype)
406
+
407
+ return value
408
+
409
+ # ----- Handle circular dependencies in a calculation ----- #
410
+
411
+ def _check_for_cycle(self, variable: str, period) -> None:
412
+ """Raise an exception in the case of a circular definition, where evaluating a variable for
413
+ a given period loops around to evaluating the same variable/period pair. Also guards, as
414
+ a heuristic, against "quasicircles", where the evaluation of a variable at a period involves
415
+ the same variable at a different period.
416
+ """
417
+ # The last frame is the current calculation, so it should be ignored from cycle detection
418
+ previous_periods = [
419
+ frame["period"]
420
+ for frame in self.tracer.stack[:-1]
421
+ if frame["name"] == variable
422
+ ]
423
+ if period in previous_periods:
424
+ msg = f"Circular definition detected on formula {variable}@{period}"
425
+ raise errors.CycleError(
426
+ msg,
427
+ )
428
+ spiral = len(previous_periods) >= self.max_spiral_loops
429
+ if spiral:
430
+ self.invalidate_spiral_variables(variable)
431
+ message = f"Quasicircular definition detected on formula {variable}@{period} involving {self.tracer.stack}"
432
+ raise errors.SpiralError(message, variable)
433
+
434
+ def invalidate_cache_entry(self, variable: str, period) -> None:
435
+ self.invalidated_caches.add(Cache(variable, period))
436
+
437
+ def invalidate_spiral_variables(self, variable: str) -> None:
438
+ # Visit the stack, from the bottom (most recent) up; we know that we'll find
439
+ # the variable implicated in the spiral (max_spiral_loops+1) times; we keep the
440
+ # intermediate values computed (to avoid impacting performance) but we mark them
441
+ # for deletion from the cache once the calculation ends.
442
+ count = 0
443
+ for frame in reversed(self.tracer.stack):
444
+ self.invalidate_cache_entry(str(frame["name"]), frame["period"])
445
+ if frame["name"] == variable:
446
+ count += 1
447
+ if count > self.max_spiral_loops:
448
+ break
449
+
450
+ # ----- Methods to access stored values ----- #
451
+
452
+ def get_array(self, variable_name: str, period):
453
+ """Return the value of ``variable_name`` for ``period``, if this value is already in the cache (if it has been set as an input or previously calculated).
454
+
455
+ Unlike :meth:`.calculate`, this method *does not* trigger calculations and *does not* use any formula.
456
+ """
457
+ if period is not None and not isinstance(period, periods.Period):
458
+ period = periods.period(period)
459
+ return self.get_holder(variable_name).get_array(period)
460
+
461
+ def get_holder(self, variable_name: str):
462
+ """Get the holder associated with the variable."""
463
+ return self.get_variable_population(variable_name).get_holder(variable_name)
464
+
465
+ def get_memory_usage(self, variables=None):
466
+ """Get data about the virtual memory usage of the simulation."""
467
+ result = {"total_nb_bytes": 0, "by_variable": {}}
468
+ for entity in self.populations.values():
469
+ entity_memory_usage = entity.get_memory_usage(variables=variables)
470
+ result["total_nb_bytes"] += entity_memory_usage["total_nb_bytes"]
471
+ result["by_variable"].update(entity_memory_usage["by_variable"])
472
+ return result
473
+
474
+ # ----- Misc ----- #
475
+
476
+ def delete_arrays(self, variable, period=None) -> None:
477
+ """Delete a variable's value for a given period.
478
+
479
+ :param variable: the variable to be set
480
+ :param period: the period for which the value should be deleted
481
+
482
+ Example:
483
+ >>> from openfisca_country_template import CountryTaxBenefitSystem
484
+ >>> simulation = Simulation(CountryTaxBenefitSystem())
485
+ >>> simulation.set_input("age", "2018-04", [12, 14])
486
+ >>> simulation.set_input("age", "2018-05", [13, 14])
487
+ >>> simulation.get_array("age", "2018-05")
488
+ array([13, 14], dtype=int32)
489
+ >>> simulation.delete_arrays("age", "2018-05")
490
+ >>> simulation.get_array("age", "2018-04")
491
+ array([12, 14], dtype=int32)
492
+ >>> simulation.get_array("age", "2018-05") is None
493
+ True
494
+ >>> simulation.set_input("age", "2018-05", [13, 14])
495
+ >>> simulation.delete_arrays("age")
496
+ >>> simulation.get_array("age", "2018-04") is None
497
+ True
498
+ >>> simulation.get_array("age", "2018-05") is None
499
+ True
500
+
501
+ """
502
+ self.get_holder(variable).delete_arrays(period)
503
+
504
+ def get_known_periods(self, variable):
505
+ """Get a list variable's known period, i.e. the periods where a value has been initialized and.
506
+
507
+ :param variable: the variable to be set
508
+
509
+ Example:
510
+ >>> from openfisca_country_template import CountryTaxBenefitSystem
511
+ >>> simulation = Simulation(CountryTaxBenefitSystem())
512
+ >>> simulation.set_input("age", "2018-04", [12, 14])
513
+ >>> simulation.set_input("age", "2018-05", [13, 14])
514
+ >>> simulation.get_known_periods("age")
515
+ [Period((u'month', Instant((2018, 5, 1)), 1)), Period((u'month', Instant((2018, 4, 1)), 1))]
516
+
517
+ """
518
+ return self.get_holder(variable).get_known_periods()
519
+
520
+ def set_input(self, variable_name: str, period, value) -> None:
521
+ """Set a variable's value for a given period.
522
+
523
+ :param variable: the variable to be set
524
+ :param value: the input value for the variable
525
+ :param period: the period for which the value is set
526
+
527
+ Example:
528
+ >>> from openfisca_country_template import CountryTaxBenefitSystem
529
+ >>> simulation = Simulation(CountryTaxBenefitSystem())
530
+ >>> simulation.set_input("age", "2018-04", [12, 14])
531
+ >>> simulation.get_array("age", "2018-04")
532
+ array([12, 14], dtype=int32)
533
+
534
+ If a ``set_input`` property has been set for the variable, this method may accept inputs for periods not matching the ``definition_period`` of the variable. To read more about this, check the `documentation <https://openfisca.org/doc/coding-the-legislation/35_periods.html#automatically-process-variable-inputs-defined-for-periods-not-matching-the-definitionperiod>`_.
535
+
536
+ """
537
+ variable: Variable | None
538
+
539
+ variable = self.tax_benefit_system.get_variable(
540
+ variable_name,
541
+ check_existence=True,
542
+ )
543
+
544
+ if variable is None:
545
+ raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system)
546
+
547
+ period = periods.period(period)
548
+ if (variable.end is not None) and (period.start.date > variable.end):
549
+ return
550
+ self.get_holder(variable_name).set_input(period, value)
551
+
552
+ def get_variable_population(self, variable_name: str) -> Population:
553
+ variable: Variable | None
554
+
555
+ variable = self.tax_benefit_system.get_variable(
556
+ variable_name,
557
+ check_existence=True,
558
+ )
559
+
560
+ if variable is None:
561
+ raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system)
562
+
563
+ return self.populations[variable.entity.key]
564
+
565
+ def get_population(self, plural: str | None = None) -> Population | None:
566
+ return next(
567
+ (
568
+ population
569
+ for population in self.populations.values()
570
+ if population.entity.plural == plural
571
+ ),
572
+ None,
573
+ )
574
+
575
+ def get_entity(
576
+ self,
577
+ plural: str | None = None,
578
+ ) -> Population | None:
579
+ population = self.get_population(plural)
580
+ return population and population.entity
581
+
582
+ def describe_entities(self):
583
+ return {
584
+ population.entity.plural: population.ids
585
+ for population in self.populations.values()
586
+ }
587
+
588
+ def clone(self, debug=False, trace=False):
589
+ """Copy the simulation just enough to be able to run the copy without modifying the original simulation."""
590
+ new = commons.empty_clone(self)
591
+ new_dict = new.__dict__
592
+
593
+ for key, value in self.__dict__.items():
594
+ if key not in ("debug", "trace", "tracer"):
595
+ new_dict[key] = value
596
+
597
+ new.persons = self.persons.clone(new)
598
+ setattr(new, new.persons.entity.key, new.persons)
599
+ new.populations = {new.persons.entity.key: new.persons}
600
+
601
+ for entity in self.tax_benefit_system.group_entities:
602
+ population = self.populations[entity.key].clone(new)
603
+ new.populations[entity.key] = population
604
+ setattr(
605
+ new,
606
+ entity.key,
607
+ population,
608
+ ) # create shortcut simulation.household (for instance)
609
+
610
+ new.debug = debug
611
+ new.trace = trace
612
+
613
+ return new
614
+
615
+
616
+ class Cache(NamedTuple):
617
+ variable: str
618
+ period: periods.Period