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,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
|