From 6ada246eefb45531075219a5ae7a37ef6209eda8 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 2 Oct 2022 19:39:38 +0100 Subject: [PATCH 01/25] Add quantity type (stock vs flow) --- policyengine_core/variables/variable.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/policyengine_core/variables/variable.py b/policyengine_core/variables/variable.py index 7b65df5c..a137bfce 100644 --- a/policyengine_core/variables/variable.py +++ b/policyengine_core/variables/variable.py @@ -13,6 +13,10 @@ from . import config, helpers +class QuantityType: + STOCK = "stock" + FLOW = "flow" + class Variable: """ @@ -93,6 +97,10 @@ class Variable: .. attribute:: documentation Free multilines text field describing the variable context and usage. + + .. attribute:: quantity_type + + Categorical attribute describing whether the variable is a stock or a flow. """ def __init__(self, baseline_variable=None): @@ -150,6 +158,13 @@ def __init__(self, baseline_variable=None): periods.ETERNITY, ), ) + self.quantity_type = self.set( + attr, + "quantity_type", + required=False, + allowed_values=(QuantityType.STOCK, QuantityType.FLOW), + default=QuantityType.FLOW, + ) self.label = self.set( attr, "label", allowed_type=str, setter=self.set_label ) @@ -214,7 +229,7 @@ def set( attribute_name, self.name ) ) - if allowed_values is not None and value not in allowed_values: + if required and allowed_values is not None and value not in allowed_values: raise ValueError( "Invalid value '{}' for attribute '{}' in variable '{}'. Allowed values are '{}'.".format( value, attribute_name, self.name, allowed_values From 3506d6272778cdcad725a95f7ee5dad64350c61f Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sat, 8 Oct 2022 21:06:33 +0100 Subject: [PATCH 02/25] Remove deprecated modules --- policyengine_core/formula_helpers.py | 13 ------------- policyengine_core/memory_config.py | 9 --------- policyengine_core/rates.py | 9 --------- policyengine_core/simulation_builder.py | 16 ---------------- 4 files changed, 47 deletions(-) delete mode 100644 policyengine_core/formula_helpers.py delete mode 100644 policyengine_core/memory_config.py delete mode 100644 policyengine_core/rates.py delete mode 100644 policyengine_core/simulation_builder.py diff --git a/policyengine_core/formula_helpers.py b/policyengine_core/formula_helpers.py deleted file mode 100644 index 665e9161..00000000 --- a/policyengine_core/formula_helpers.py +++ /dev/null @@ -1,13 +0,0 @@ -# The formula_helpers module has been deprecated since X.X.X, -# and will be removed in the future. -# -# The helpers have been moved to the commons module. -# -# The following are transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. - -from policyengine_core.commons import ( - apply_thresholds, - concat, - switch, -) # noqa: F401 diff --git a/policyengine_core/memory_config.py b/policyengine_core/memory_config.py deleted file mode 100644 index 43074720..00000000 --- a/policyengine_core/memory_config.py +++ /dev/null @@ -1,9 +0,0 @@ -# The memory config module has been deprecated since X.X.X, -# and will be removed in the future. -# -# Module's contents have been moved to the experimental module. -# -# The following are transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. - -from policyengine_core.experimental import MemoryConfig # noqa: F401 diff --git a/policyengine_core/rates.py b/policyengine_core/rates.py deleted file mode 100644 index dbd802bf..00000000 --- a/policyengine_core/rates.py +++ /dev/null @@ -1,9 +0,0 @@ -# The formula_helpers module has been deprecated since X.X.X, -# and will be removed in the future. -# -# The helpers have been moved to the commons module. -# -# The following are transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. - -from policyengine_core.commons import average_rate, marginal_rate # noqa: F401 diff --git a/policyengine_core/simulation_builder.py b/policyengine_core/simulation_builder.py deleted file mode 100644 index 36e5a51b..00000000 --- a/policyengine_core/simulation_builder.py +++ /dev/null @@ -1,16 +0,0 @@ -# The simulation builder module has been deprecated since X.X.X, -# and will be removed in the future. -# -# Module's contents have been moved to the simulation module. -# -# The following are transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. - -from policyengine_core.simulations import ( # noqa: F401 - Simulation, - SimulationBuilder, - calculate_output_add, - calculate_output_divide, - check_type, - transform_to_strict_syntax, -) From 516414fd74a9a7243f1f98e193435168a48ffa0e Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sat, 8 Oct 2022 22:21:37 +0100 Subject: [PATCH 03/25] Add documentation starter --- .gitignore | 2 + docs/_config.yml | 9 ++ docs/_toc.yml | 9 ++ docs/intro.md | 18 +++ docs/python_api/country_template.ipynb | 142 ++++++++++++++++++ docs/python_api/data_storage.ipynb | 30 ++++ docs/python_api/entities.ipynb | 30 ++++ docs/python_api/intro.md | 3 + policyengine_core/__init__.py | 2 + .../country_template/__init__.py | 3 +- ...sca_command.py => policyengine_command.py} | 59 -------- .../taxbenefitsystems/tax_benefit_system.py | 6 +- policyengine_core/tools/test_runner.py | 7 +- setup.py | 3 +- tests/core/variables/test_variables.py | 2 +- 15 files changed, 259 insertions(+), 66 deletions(-) create mode 100644 docs/_config.yml create mode 100644 docs/_toc.yml create mode 100644 docs/intro.md create mode 100644 docs/python_api/country_template.ipynb create mode 100644 docs/python_api/data_storage.ipynb create mode 100644 docs/python_api/entities.ipynb create mode 100644 docs/python_api/intro.md rename policyengine_core/scripts/{openfisca_command.py => policyengine_command.py} (64%) diff --git a/.gitignore b/.gitignore index 08e18b6c..3728b981 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ coverage.xml .coverage build/ dist/ +docs/_build/ +**/*.ipynb_checkpoints/ diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 00000000..ffc5353c --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,9 @@ +title: PolicyEngine Core documentation +author: PolicyEngine + +execute: + execute_notebooks: force + +sphinx: + config: + html_theme: furo diff --git a/docs/_toc.yml b/docs/_toc.yml new file mode 100644 index 00000000..e2f3c929 --- /dev/null +++ b/docs/_toc.yml @@ -0,0 +1,9 @@ +format: jb-book +root: intro +parts: + - caption: Python API + chapters: + - file: python_api/country_template + - file: python_api/data_storage + - file: python_api/entities + \ No newline at end of file diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 00000000..96e4026e --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,18 @@ +# Introduction + +This is the documentation for PolicyEngine Core, the open-source Python package powering PolicyEngine's tax-benefit microsimulation models. It is a fork of [OpenFisca-Core](https://github.com/openfisca/openfisca-core), developed and maintained by [OpenFisca](https://www.openfisca.org/). + +PolicyEngine Core does not simulate any specific tax-benefit policy: instead, it is a general framework for building tax-benefit microsimulation models. It is currently used by PolicyEngine UK and PolicyEngine US, which each define the custom logic, parameters and data required to simulate the tax-benefit systems of the UK and the US respectively. + +The country models each provide: + +* A set of *entity types* (e.g. `Person`). +* A set of *parameters* (e.g. `Flat tax rate`). Parameters are global data points that have different values for different time periods. +* A set of *variables* (e.g. `Tax liability`). Variables are properties of entities that can be dependent on entities (including other variable values), parameters and the time period. + +PolicyEngine Core then enables users to: + +* Calculate the value of a variable in a specific time period. +* Trace the computation tree of a calculation. + +PolicyEngine Core also includes many helper functions designed to simplify the process of modelling a country's policy. diff --git a/docs/python_api/country_template.ipynb b/docs/python_api/country_template.ipynb new file mode 100644 index 00000000..d5f9dc6f --- /dev/null +++ b/docs/python_api/country_template.ipynb @@ -0,0 +1,142 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Country template\n", + "\n", + "The `country_template` folder is a working example of a country model. To create a new country model, create a new repository with the folder's contents:\n", + "\n", + "* `parameters/` contains the parameter tree, with examples.\n", + "* `reforms/` contains several example reforms written using the Python reforms API.\n", + "* `situation_examples/` contains several example situations, in JSON format and accessible as Python objects.\n", + "* `tests/` contains a set of YAML tests to check policy implementations function.\n", + "* `variables/` contain definitions of variables with formulas.\n", + "* `entities.py` contains definitions of entities.\n", + "\n", + "## Running a simulation\n", + "\n", + "The below example runs a simulation using the example country model template." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[600.]\n" + ] + } + ], + "source": [ + "from policyengine_core import SimulationBuilder\n", + "from policyengine_core.country_template import CountryTaxBenefitSystem\n", + "from policyengine_core.country_template.situation_examples import single\n", + "\n", + "system = CountryTaxBenefitSystem()\n", + "\n", + "simulation = SimulationBuilder().build_from_dict(\n", + " system,\n", + " single,\n", + ")\n", + "\n", + "print(simulation.calculate(\"disposable_income\", \"2017-01\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Testing a country model\n", + "\n", + "The example below runs all tests defined for the example country model. This can also be done by running:\n", + "\n", + "`policyengine-core test -c policyengine_core.country_template policyengine_core/country_template/tests`" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "============================= test session starts ==============================\n", + "platform darwin -- Python 3.9.12, pytest-5.4.3, py-1.11.0, pluggy-0.13.1\n", + "rootdir: /Users/nikhil/policyengine/policyengine-core\n", + "plugins: anyio-3.5.0\n", + "collected 0 items\n", + "\n", + "============================ no tests ran in 0.00s =============================\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "ERROR: file not found: /Users/nikhil/policyengine/policyengine-core/docs/python_api/policyengine_core/country_template/tests\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "CompletedProcess(args=['policyengine-core', 'test', '-c', 'policyengine_core.country_template', 'policyengine_core/country_template/tests'], returncode=4)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import subprocess\n", + "from policyengine_core.country_template import COUNTRY_DIR\n", + "\n", + "subprocess.run(\n", + " [\n", + " \"policyengine-core\",\n", + " \"test\",\n", + " \"-c\",\n", + " \"policyengine_core.country_template\",\n", + " COUNTRY_DIR / \"tests\",\n", + " ]\n", + ")\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9.12 ('base')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "40d3a090f54c6569ab1632332b64b2c03c39dcf918b08424e98f38b5ae0af88f" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/python_api/data_storage.ipynb b/docs/python_api/data_storage.ipynb new file mode 100644 index 00000000..34f52a59 --- /dev/null +++ b/docs/python_api/data_storage.ipynb @@ -0,0 +1,30 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Data storage" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9.12 ('base')", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.9.12" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "40d3a090f54c6569ab1632332b64b2c03c39dcf918b08424e98f38b5ae0af88f" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/python_api/entities.ipynb b/docs/python_api/entities.ipynb new file mode 100644 index 00000000..b4f09e83 --- /dev/null +++ b/docs/python_api/entities.ipynb @@ -0,0 +1,30 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Entities" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9.12 ('base')", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.9.12" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "40d3a090f54c6569ab1632332b64b2c03c39dcf918b08424e98f38b5ae0af88f" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/python_api/intro.md b/docs/python_api/intro.md new file mode 100644 index 00000000..8e957979 --- /dev/null +++ b/docs/python_api/intro.md @@ -0,0 +1,3 @@ +# Using PolicyEngine Core + +This chapter contains documentation for individual modules in the PolicyEngine Core source code. diff --git a/policyengine_core/__init__.py b/policyengine_core/__init__.py index e69de29b..7220358c 100644 --- a/policyengine_core/__init__.py +++ b/policyengine_core/__init__.py @@ -0,0 +1,2 @@ +from policyengine_core.simulations import SimulationBuilder +from policyengine_core.taxbenefitsystems import TaxBenefitSystem \ No newline at end of file diff --git a/policyengine_core/country_template/__init__.py b/policyengine_core/country_template/__init__.py index fc1bda4d..9deddff4 100644 --- a/policyengine_core/country_template/__init__.py +++ b/policyengine_core/country_template/__init__.py @@ -14,9 +14,10 @@ from policyengine_core.country_template import entities from policyengine_core.country_template.situation_examples import couple +from pathlib import Path -COUNTRY_DIR = os.path.dirname(os.path.abspath(__file__)) +COUNTRY_DIR = Path(__file__).parent # Our country tax and benefit class inherits from the general TaxBenefitSystem class. diff --git a/policyengine_core/scripts/openfisca_command.py b/policyengine_core/scripts/policyengine_command.py similarity index 64% rename from policyengine_core/scripts/openfisca_command.py rename to policyengine_core/scripts/policyengine_command.py index a992a6e2..4446d32d 100644 --- a/policyengine_core/scripts/openfisca_command.py +++ b/policyengine_core/scripts/policyengine_command.py @@ -19,57 +19,6 @@ def get_parser(): True # Can be added as an argument of add_subparsers in Python 3 ) - def build_serve_parser(parser): - # Define OpenFisca modules configuration - parser = add_tax_benefit_system_arguments(parser) - - # Define server configuration - parser.add_argument( - "-p", - "--port", - action="store", - help="port to serve on (use --bind to specify host and port)", - type=int, - ) - parser.add_argument( - "--tracker-url", - action="store", - help="tracking service url", - type=str, - ) - parser.add_argument( - "--tracker-idsite", - action="store", - help="tracking service id site", - type=int, - ) - parser.add_argument( - "--tracker-token", - action="store", - help="tracking service authentication token", - type=str, - ) - parser.add_argument( - "--welcome-message", - action="store", - help="welcome message users will get when visiting the API root", - type=str, - ) - parser.add_argument( - "-f", - "--configuration-file", - action="store", - help="configuration file", - type=str, - ) - - return parser - - parser_serve = subparsers.add_parser( - "serve", help="Run the OpenFisca Web API" - ) - parser_serve = build_serve_parser(parser_serve) - def build_test_parser(parser): parser.add_argument( "path", @@ -150,19 +99,11 @@ def build_test_parser(parser): def main(): - if sys.argv[0].endswith("openfisca-run-test"): - sys.argv[0:1] = ["openfisca", "test"] - message = "The 'openfisca-run-test' command has been deprecated in favor of 'openfisca test' since version 25.0, and will be removed in the future." - warnings.warn(message, Warning) parser = get_parser() args, _ = parser.parse_known_args() - if args.command == "serve": - from openfisca_web_api.scripts.serve import main - - return sys.exit(main(parser)) if args.command == "test": from policyengine_core.scripts.run_test import main diff --git a/policyengine_core/taxbenefitsystems/tax_benefit_system.py b/policyengine_core/taxbenefitsystems/tax_benefit_system.py index 0abaf712..4fed8fed 100644 --- a/policyengine_core/taxbenefitsystems/tax_benefit_system.py +++ b/policyengine_core/taxbenefitsystems/tax_benefit_system.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Dict, Optional, Sequence +from typing import Any, Dict, List, Optional, Sequence, Union import copy import glob @@ -23,6 +23,7 @@ from policyengine_core.periods import Instant, Period from policyengine_core.populations import Population, GroupPopulation from policyengine_core.simulations import SimulationBuilder +from policyengine_core.tools.test_runner import run_tests from policyengine_core.variables import Variable log = logging.getLogger(__name__) @@ -540,3 +541,6 @@ def entities_plural(self): def entities_by_singular(self): return {entity.key: entity for entity in self.entities} + + def test(self, paths: str, verbose: bool = False) -> None: + run_tests(self, paths, options=dict(verbose=verbose)) diff --git a/policyengine_core/tools/test_runner.py b/policyengine_core/tools/test_runner.py index 9eae03fd..a2b5c002 100644 --- a/policyengine_core/tools/test_runner.py +++ b/policyengine_core/tools/test_runner.py @@ -10,7 +10,7 @@ import pytest from policyengine_core.tools import assert_near -from policyengine_core.simulation_builder import SimulationBuilder +from policyengine_core.simulations import SimulationBuilder from policyengine_core.errors import SituationParsingError, VariableNotFound from policyengine_core.warnings import LibYAMLWarning @@ -87,8 +87,11 @@ def run_tests(tax_benefit_system, paths, options=None): if options.get("verbose"): argv.append("--verbose") - if isinstance(paths, str): + if not isinstance(paths, list): paths = [paths] + + if not isinstance(paths[0], str): + paths = [str(path) for path in paths] return pytest.main( [*argv, *paths] if True else paths, diff --git a/setup.py b/setup.py index cf97b00f..acc4c1a2 100644 --- a/setup.py +++ b/setup.py @@ -56,8 +56,7 @@ ], entry_points={ "console_scripts": [ - "openfisca=policyengine_core.scripts.openfisca_command:main", - "openfisca-run-test=policyengine_core.scripts.openfisca_command:main", + "policyengine-core=policyengine_core.scripts.policyengine_command:main", ], }, extras_require={ diff --git a/tests/core/variables/test_variables.py b/tests/core/variables/test_variables.py index 5a423e51..36f97517 100644 --- a/tests/core/variables/test_variables.py +++ b/tests/core/variables/test_variables.py @@ -4,7 +4,7 @@ from policyengine_core.model_api import Variable from policyengine_core.periods import MONTH, ETERNITY -from policyengine_core.simulation_builder import SimulationBuilder +from policyengine_core.simulations import SimulationBuilder from policyengine_core.tools import assert_near import policyengine_core.country_template as country_template From 40a2a7d7d5d908e4a33146a45aa8970da3656bbe Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sat, 8 Oct 2022 23:03:57 +0100 Subject: [PATCH 04/25] Add data storage documentation --- Makefile | 3 ++ docs/_config.yml | 2 ++ docs/_toc.yml | 4 ++- docs/contributing/intro.md | 17 +++++++++++ docs/python_api/data_storage.ipynb | 30 ------------------- docs/python_api/data_storage.md | 19 ++++++++++++ .../data_storage/in_memory_storage.py | 20 ++++++++----- .../data_storage/on_disk_storage.py | 21 ++++++------- 8 files changed, 67 insertions(+), 49 deletions(-) create mode 100644 docs/contributing/intro.md delete mode 100644 docs/python_api/data_storage.ipynb create mode 100644 docs/python_api/data_storage.md diff --git a/Makefile b/Makefile index a5f7056f..23bd9712 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ all: install format test build changelog +docs: + jb build docs + format: black . -l 79 diff --git a/docs/_config.yml b/docs/_config.yml index ffc5353c..5f7e7b0b 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -7,3 +7,5 @@ execute: sphinx: config: html_theme: furo + extra_extensions: + - "sphinx.ext.autodoc" diff --git a/docs/_toc.yml b/docs/_toc.yml index e2f3c929..6c54ab70 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -6,4 +6,6 @@ parts: - file: python_api/country_template - file: python_api/data_storage - file: python_api/entities - \ No newline at end of file + - caption: Contributing + chapters: + - file: contributing/intro diff --git a/docs/contributing/intro.md b/docs/contributing/intro.md new file mode 100644 index 00000000..0c26322e --- /dev/null +++ b/docs/contributing/intro.md @@ -0,0 +1,17 @@ +# How to contribute + +Any and all contributions are welcome to this project. You can help by: + +* Filing issues. Tell us about bugs you've found, or features you'd like to see. +* Fixing issues. File a pull request to fix an issue you or someone else has filed. + +If you file an issue or a pull request, one of the maintainers (primarily @nikhilwoodruff) will respond to it within at least a week. If you don't hear back, feel free to ping us on the issue or pull request. + +## Pull requests + +Each pull request should: +* Close an issue. If there isn't an issue that the pull request completely addresses, please file one. +* Have a description that makes sense to a layperson. If you're fixing a bug, describe what the bug is and how you fixed it. If you're adding a feature, describe what the feature is and why you added it. +* Have tests. If you're fixing a bug, write a test that fails without your fix and passes with it. If you're adding a feature, write tests that cover the feature. Sometimes this isn't necessary (for example, documentation changes), but if in doubt, err on the side of including tests. +* Pass all GitHub actions. If you're not sure why a GitHub action is failing, feel free to ask for help in the issue or pull request. + diff --git a/docs/python_api/data_storage.ipynb b/docs/python_api/data_storage.ipynb deleted file mode 100644 index 34f52a59..00000000 --- a/docs/python_api/data_storage.ipynb +++ /dev/null @@ -1,30 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Data storage" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3.9.12 ('base')", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.9.12" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "40d3a090f54c6569ab1632332b64b2c03c39dcf918b08424e98f38b5ae0af88f" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/python_api/data_storage.md b/docs/python_api/data_storage.md new file mode 100644 index 00000000..b5a81e6b --- /dev/null +++ b/docs/python_api/data_storage.md @@ -0,0 +1,19 @@ +# Data storage + +The data storage module `policyengine_core.data_storage` contains two classes that are used to handle the storage of data in simulations: `InMemoryStorage` and `OnDiskStorage`. + +## In-memory storage + +```{eval-rst} +.. autoclass:: policyengine_core.data_storage.InMemoryStorage + :members: + :undoc-members: +``` + +## On-disk storage + +```{eval-rst} +.. autoclass:: policyengine_core.data_storage.OnDiskStorage + :members: + :undoc-members: +``` \ No newline at end of file diff --git a/policyengine_core/data_storage/in_memory_storage.py b/policyengine_core/data_storage/in_memory_storage.py index 7b62a0ed..d4efacaf 100644 --- a/policyengine_core/data_storage/in_memory_storage.py +++ b/policyengine_core/data_storage/in_memory_storage.py @@ -1,18 +1,22 @@ +from typing import Dict, Union import numpy - +from numpy.typing import ArrayLike from policyengine_core import periods +from policyengine_core.periods import Period class InMemoryStorage: """ Low-level class responsible for storing and retrieving calculated vectors in memory """ + _arrays: Dict[Period, ArrayLike] + is_eternal: bool - def __init__(self, is_eternal=False): + def __init__(self, is_eternal: bool): self._arrays = {} self.is_eternal = is_eternal - def get(self, period): + def get(self, period: Period) -> ArrayLike: if self.is_eternal: period = periods.period(periods.ETERNITY) period = periods.period(period) @@ -22,14 +26,14 @@ def get(self, period): return None return values - def put(self, value, period): + def put(self, value: ArrayLike, period: Period) -> None: if self.is_eternal: period = periods.period(periods.ETERNITY) period = periods.period(period) self._arrays[period] = value - def delete(self, period=None): + def delete(self, period: Period = None) -> None: if period is None: self._arrays = {} return @@ -44,10 +48,10 @@ def delete(self, period=None): if not period.contains(period_item) } - def get_known_periods(self): - return self._arrays.keys() + def get_known_periods(self) -> list: + return list(self._arrays.keys()) - def get_memory_usage(self): + def get_memory_usage(self) -> dict: if not self._arrays: return dict( nb_arrays=0, diff --git a/policyengine_core/data_storage/on_disk_storage.py b/policyengine_core/data_storage/on_disk_storage.py index 9467cb5b..65cf0992 100644 --- a/policyengine_core/data_storage/on_disk_storage.py +++ b/policyengine_core/data_storage/on_disk_storage.py @@ -2,9 +2,10 @@ import shutil import numpy - +from numpy.typing import ArrayLike from policyengine_core import periods from policyengine_core.indexed_enums import EnumArray +from policyengine_core.periods import Period class OnDiskStorage: @@ -13,7 +14,7 @@ class OnDiskStorage: """ def __init__( - self, storage_dir, is_eternal=False, preserve_storage_dir=False + self, storage_dir: str, is_eternal: bool = False, preserve_storage_dir: bool = False ): self._files = {} self._enums = {} @@ -21,14 +22,14 @@ def __init__( self.preserve_storage_dir = preserve_storage_dir self.storage_dir = storage_dir - def _decode_file(self, file): + def _decode_file(self, file: str) -> ArrayLike: enum = self._enums.get(file) if enum is not None: return EnumArray(numpy.load(file), enum) else: return numpy.load(file) - def get(self, period): + def get(self, period: Period) -> ArrayLike: if self.is_eternal: period = periods.period(periods.ETERNITY) period = periods.period(period) @@ -38,7 +39,7 @@ def get(self, period): return None return self._decode_file(values) - def put(self, value, period): + def put(self, value: ArrayLike, period: Period) -> None: if self.is_eternal: period = periods.period(periods.ETERNITY) period = periods.period(period) @@ -51,7 +52,7 @@ def put(self, value, period): numpy.save(path, value) self._files[period] = path - def delete(self, period=None): + def delete(self, period: Period = None) -> None: if period is None: self._files = {} return @@ -67,10 +68,10 @@ def delete(self, period=None): if not period.contains(period_item) } - def get_known_periods(self): - return self._files.keys() + def get_known_periods(self) -> list: + return list(self._files.keys()) - def restore(self): + def restore(self) -> None: self._files = files = {} # Restore self._files from content of storage_dir. for filename in os.listdir(self.storage_dir): @@ -81,7 +82,7 @@ def restore(self): period = periods.period(filename_core) files[period] = path - def __del__(self): + def __del__(self) -> None: if self.preserve_storage_dir: return shutil.rmtree(self.storage_dir) # Remove the holder temporary files From 23f44129d4f58fde0ff1d666299128aca74a912a Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sat, 8 Oct 2022 23:32:37 +0100 Subject: [PATCH 05/25] Fix bugs --- .coverage | Bin 516096 -> 544768 bytes tests/core/test_holders.py | 2 +- tests/core/test_yaml.py | 8 ++++---- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.coverage b/.coverage index 5157ee4650b1253551b76cdb1598d28abe00209f..1e1f95ee3c5a87ef6b6af5103f666863373ddd83 100644 GIT binary patch delta 34853 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%0DF56~_Co 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 delta 11886 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 Date: Sat, 8 Oct 2022 23:34:11 +0100 Subject: [PATCH 06/25] Remove noqa statements --- policyengine_core/commons/__init__.py | 8 +++---- policyengine_core/data_storage/__init__.py | 4 ++-- policyengine_core/entities/__init__.py | 8 +++---- policyengine_core/errors/__init__.py | 20 ++++++++-------- policyengine_core/experimental/__init__.py | 2 +- policyengine_core/holders/__init__.py | 4 ++-- policyengine_core/indexed_enums/__init__.py | 6 ++--- policyengine_core/model_api.py | 22 ++++++++--------- policyengine_core/parameters/__init__.py | 24 +++++++++---------- policyengine_core/periods/__init__.py | 8 +++---- policyengine_core/populations/__init__.py | 10 ++++---- policyengine_core/projectors/__init__.py | 10 ++++---- policyengine_core/reforms/__init__.py | 2 +- .../measure_performances_fancy_indexing.py | 2 +- policyengine_core/simulations/__init__.py | 8 +++---- .../taxbenefitsystems/__init__.py | 4 ++-- policyengine_core/taxscales/__init__.py | 22 ++++++++--------- policyengine_core/tools/test_runner.py | 2 +- policyengine_core/tracers/__init__.py | 14 +++++------ policyengine_core/tracers/computation_log.py | 2 +- policyengine_core/types/__init__.py | 2 +- .../types/data_types/__init__.py | 2 +- policyengine_core/variables/__init__.py | 8 +++---- policyengine_core/warnings/__init__.py | 6 ++--- .../test_fancy_indexing.py | 2 +- tests/core/test_reforms.py | 2 +- tests/core/variables/test_annualize.py | 2 +- 27 files changed, 103 insertions(+), 103 deletions(-) diff --git a/policyengine_core/commons/__init__.py b/policyengine_core/commons/__init__.py index 89dce24e..b054c88d 100644 --- a/policyengine_core/commons/__init__.py +++ b/policyengine_core/commons/__init__.py @@ -52,9 +52,9 @@ # Official Public API -from .formulas import apply_thresholds, concat, switch # noqa: F401 -from .misc import empty_clone, stringify_array # noqa: F401 -from .rates import average_rate, marginal_rate # noqa: F401 +from .formulas import apply_thresholds, concat, switch +from .misc import empty_clone, stringify_array +from .rates import average_rate, marginal_rate __all__ = ["apply_thresholds", "concat", "switch"] __all__ = ["empty_clone", "stringify_array", *__all__] @@ -62,6 +62,6 @@ # Deprecated -from .dummy import Dummy # noqa: F401 +from .dummy import Dummy __all__ = ["Dummy", *__all__] diff --git a/policyengine_core/data_storage/__init__.py b/policyengine_core/data_storage/__init__.py index 0d490637..326f7184 100644 --- a/policyengine_core/data_storage/__init__.py +++ b/policyengine_core/data_storage/__init__.py @@ -21,5 +21,5 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .in_memory_storage import InMemoryStorage # noqa: F401 -from .on_disk_storage import OnDiskStorage # noqa: F401 +from .in_memory_storage import InMemoryStorage +from .on_disk_storage import OnDiskStorage diff --git a/policyengine_core/entities/__init__.py b/policyengine_core/entities/__init__.py index 2ecd1d79..866d45d5 100644 --- a/policyengine_core/entities/__init__.py +++ b/policyengine_core/entities/__init__.py @@ -21,7 +21,7 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .helpers import build_entity # noqa: F401 -from .role import Role # noqa: F401 -from .entity import Entity # noqa: F401 -from .group_entity import GroupEntity # noqa: F401 +from .helpers import build_entity +from .role import Role +from .entity import Entity +from .group_entity import GroupEntity diff --git a/policyengine_core/errors/__init__.py b/policyengine_core/errors/__init__.py index 2f2d547e..e23b5a30 100644 --- a/policyengine_core/errors/__init__.py +++ b/policyengine_core/errors/__init__.py @@ -21,22 +21,22 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .cycle_error import CycleError # noqa: F401 -from .empty_argument_error import EmptyArgumentError # noqa: F401 -from .nan_creation_error import NaNCreationError # noqa: F401 +from .cycle_error import CycleError +from .empty_argument_error import EmptyArgumentError +from .nan_creation_error import NaNCreationError from .parameter_not_found_error import ( ParameterNotFoundError, ParameterNotFoundError as ParameterNotFound, -) # noqa: F401 -from .parameter_parsing_error import ParameterParsingError # noqa: F401 -from .period_mismatch_error import PeriodMismatchError # noqa: F401 -from .situation_parsing_error import SituationParsingError # noqa: F401 -from .spiral_error import SpiralError # noqa: F401 +) +from .parameter_parsing_error import ParameterParsingError +from .period_mismatch_error import PeriodMismatchError +from .situation_parsing_error import SituationParsingError +from .spiral_error import SpiralError from .variable_name_config_error import ( VariableNameConflictError, VariableNameConflictError as VariableNameConflict, -) # noqa: F401 +) from .variable_not_found_error import ( VariableNotFoundError, VariableNotFoundError as VariableNotFound, -) # noqa: F401 +) diff --git a/policyengine_core/experimental/__init__.py b/policyengine_core/experimental/__init__.py index dbf4f2ad..373fd678 100644 --- a/policyengine_core/experimental/__init__.py +++ b/policyengine_core/experimental/__init__.py @@ -21,4 +21,4 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .memory_config import MemoryConfig # noqa: F401 +from .memory_config import MemoryConfig diff --git a/policyengine_core/holders/__init__.py b/policyengine_core/holders/__init__.py index 870bcbf8..d471cc24 100644 --- a/policyengine_core/holders/__init__.py +++ b/policyengine_core/holders/__init__.py @@ -24,5 +24,5 @@ from .helpers import ( set_input_dispatch_by_period, set_input_divide_by_period, -) # noqa: F401 -from .holder import Holder # noqa: F401 +) +from .holder import Holder diff --git a/policyengine_core/indexed_enums/__init__.py b/policyengine_core/indexed_enums/__init__.py index f3f222c2..b5a89c7f 100644 --- a/policyengine_core/indexed_enums/__init__.py +++ b/policyengine_core/indexed_enums/__init__.py @@ -21,6 +21,6 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .config import ENUM_ARRAY_DTYPE # noqa: F401 -from .enum_array import EnumArray # noqa: F401 -from .enum import Enum # noqa: F401 +from .config import ENUM_ARRAY_DTYPE +from .enum_array import EnumArray +from .enum import Enum diff --git a/policyengine_core/model_api.py b/policyengine_core/model_api.py index 905a59db..b5365997 100644 --- a/policyengine_core/model_api.py +++ b/policyengine_core/model_api.py @@ -1,6 +1,6 @@ -from datetime import date # noqa: F401 +from datetime import date -from numpy import ( # noqa: F401 +from numpy import ( logical_not as not_, maximum as max_, minimum as min_, @@ -13,16 +13,16 @@ apply_thresholds, concat, switch, -) # noqa: F401 +) -from policyengine_core.holders import ( # noqa: F401 +from policyengine_core.holders import ( set_input_dispatch_by_period, set_input_divide_by_period, ) -from policyengine_core.indexed_enums import Enum # noqa: F401 +from policyengine_core.indexed_enums import Enum -from policyengine_core.parameters import ( # noqa: F401 +from policyengine_core.parameters import ( load_parameter_file, ParameterNode, Scale, @@ -37,13 +37,13 @@ YEAR, ETERNITY, period, -) # noqa: F401 -from policyengine_core.populations import ADD, DIVIDE # noqa: F401 -from policyengine_core.reforms import Reform # noqa: F401 +) +from policyengine_core.populations import ADD, DIVIDE +from policyengine_core.reforms import Reform -from policyengine_core.simulations import ( # noqa: F401 +from policyengine_core.simulations import ( calculate_output_add, calculate_output_divide, ) -from policyengine_core.variables import Variable # noqa: F401 +from policyengine_core.variables import Variable diff --git a/policyengine_core/parameters/__init__.py b/policyengine_core/parameters/__init__.py index 178ee730..ffd60394 100644 --- a/policyengine_core/parameters/__init__.py +++ b/policyengine_core/parameters/__init__.py @@ -24,10 +24,10 @@ from policyengine_core.errors import ( ParameterNotFound, ParameterParsingError, -) # noqa: F401 +) -from .config import ( # noqa: F401 +from .config import ( ALLOWED_PARAM_TYPES, COMMON_KEYS, FILE_EXTENSIONS, @@ -35,21 +35,21 @@ dict_no_duplicate_constructor, ) -from .at_instant_like import AtInstantLike # noqa: F401 -from .helpers import contains_nan, load_parameter_file # noqa: F401 -from .parameter_at_instant import ParameterAtInstant # noqa: F401 -from .parameter_node_at_instant import ParameterNodeAtInstant # noqa: F401 +from .at_instant_like import AtInstantLike +from .helpers import contains_nan, load_parameter_file +from .parameter_at_instant import ParameterAtInstant +from .parameter_node_at_instant import ParameterNodeAtInstant from .vectorial_parameter_node_at_instant import ( VectorialParameterNodeAtInstant, -) # noqa: F401 -from .parameter import Parameter # noqa: F401 -from .parameter_node import ParameterNode # noqa: F401 +) +from .parameter import Parameter +from .parameter_node import ParameterNode from .parameter_scale import ( ParameterScale, ParameterScale as Scale, -) # noqa: F401 +) from .parameter_scale_bracket import ( ParameterScaleBracket, ParameterScaleBracket as Bracket, -) # noqa: F401 -from .values_history import ValuesHistory # noqa: F401 +) +from .values_history import ValuesHistory diff --git a/policyengine_core/periods/__init__.py b/policyengine_core/periods/__init__.py index 1386a877..27e4209e 100644 --- a/policyengine_core/periods/__init__.py +++ b/policyengine_core/periods/__init__.py @@ -21,7 +21,7 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .config import ( # noqa: F401 +from .config import ( DAY, MONTH, YEAR, @@ -32,7 +32,7 @@ year_or_month_or_day_re, ) -from .helpers import ( # noqa: F401 +from .helpers import ( N_, instant, instant_date, @@ -42,5 +42,5 @@ unit_weight, ) -from .instant_ import Instant # noqa: F401 -from .period_ import Period # noqa: F401 +from .instant_ import Instant +from .period_ import Period diff --git a/policyengine_core/populations/__init__.py b/policyengine_core/populations/__init__.py index 7c7f01f6..9b3f827a 100644 --- a/policyengine_core/populations/__init__.py +++ b/policyengine_core/populations/__init__.py @@ -21,18 +21,18 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from policyengine_core.projectors import ( # noqa: F401 +from policyengine_core.projectors import ( Projector, EntityToPersonProjector, FirstPersonToEntityProjector, UniqueRoleToEntityProjector, ) -from policyengine_core.projectors.helpers import ( # noqa: F401 +from policyengine_core.projectors.helpers import ( projectable, get_projector_from_shortcut, ) -from .config import ADD, DIVIDE # noqa: F401 -from .population import Population # noqa: F401 -from .group_population import GroupPopulation # noqa: F401 +from .config import ADD, DIVIDE +from .population import Population +from .group_population import GroupPopulation diff --git a/policyengine_core/projectors/__init__.py b/policyengine_core/projectors/__init__.py index c277cdf1..898bef76 100644 --- a/policyengine_core/projectors/__init__.py +++ b/policyengine_core/projectors/__init__.py @@ -21,12 +21,12 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .helpers import projectable, get_projector_from_shortcut # noqa: F401 -from .projector import Projector # noqa: F401 -from .entity_to_person_projector import EntityToPersonProjector # noqa: F401 +from .helpers import projectable, get_projector_from_shortcut +from .projector import Projector +from .entity_to_person_projector import EntityToPersonProjector from .first_person_to_entity_projector import ( FirstPersonToEntityProjector, -) # noqa: F401 +) from .unique_role_to_entity_projector import ( UniqueRoleToEntityProjector, -) # noqa: F401 +) diff --git a/policyengine_core/reforms/__init__.py b/policyengine_core/reforms/__init__.py index dc539677..e10f6446 100644 --- a/policyengine_core/reforms/__init__.py +++ b/policyengine_core/reforms/__init__.py @@ -21,4 +21,4 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .reform import Reform # noqa: F401 +from .reform import Reform diff --git a/policyengine_core/scripts/measure_performances_fancy_indexing.py b/policyengine_core/scripts/measure_performances_fancy_indexing.py index c3b72f6f..95a96d77 100644 --- a/policyengine_core/scripts/measure_performances_fancy_indexing.py +++ b/policyengine_core/scripts/measure_performances_fancy_indexing.py @@ -3,7 +3,7 @@ import numpy as np import timeit from openfisca_france import CountryTaxBenefitSystem -from policyengine_core.model_api import * # noqa analysis:ignore +from policyengine_core.model_api import * tbs = CountryTaxBenefitSystem() diff --git a/policyengine_core/simulations/__init__.py b/policyengine_core/simulations/__init__.py index 94eee178..32f4dcb3 100644 --- a/policyengine_core/simulations/__init__.py +++ b/policyengine_core/simulations/__init__.py @@ -25,13 +25,13 @@ CycleError, NaNCreationError, SpiralError, -) # noqa: F401 +) from .helpers import ( calculate_output_add, calculate_output_divide, check_type, transform_to_strict_syntax, -) # noqa: F401 -from .simulation import Simulation # noqa: F401 -from .simulation_builder import SimulationBuilder # noqa: F401 +) +from .simulation import Simulation +from .simulation_builder import SimulationBuilder diff --git a/policyengine_core/taxbenefitsystems/__init__.py b/policyengine_core/taxbenefitsystems/__init__.py index 7763c208..5cb34ba1 100644 --- a/policyengine_core/taxbenefitsystems/__init__.py +++ b/policyengine_core/taxbenefitsystems/__init__.py @@ -24,6 +24,6 @@ from policyengine_core.errors import ( VariableNameConflict, VariableNotFound, -) # noqa: F401 +) -from .tax_benefit_system import TaxBenefitSystem # noqa: F401 +from .tax_benefit_system import TaxBenefitSystem diff --git a/policyengine_core/taxscales/__init__.py b/policyengine_core/taxscales/__init__.py index 261deabe..eb709cf2 100644 --- a/policyengine_core/taxscales/__init__.py +++ b/policyengine_core/taxscales/__init__.py @@ -21,17 +21,17 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from policyengine_core.errors import EmptyArgumentError # noqa: F401 +from policyengine_core.errors import EmptyArgumentError -from .helpers import combine_tax_scales # noqa: F401 -from .tax_scale_like import TaxScaleLike # noqa: F401 -from .rate_tax_scale_like import RateTaxScaleLike # noqa: F401 -from .marginal_rate_tax_scale import MarginalRateTaxScale # noqa: F401 +from .helpers import combine_tax_scales +from .tax_scale_like import TaxScaleLike +from .rate_tax_scale_like import RateTaxScaleLike +from .marginal_rate_tax_scale import MarginalRateTaxScale from .linear_average_rate_tax_scale import ( LinearAverageRateTaxScale, -) # noqa: F401 -from .abstract_tax_scale import AbstractTaxScale # noqa: F401 -from .amount_tax_scale_like import AmountTaxScaleLike # noqa: F401 -from .abstract_rate_tax_scale import AbstractRateTaxScale # noqa: F401 -from .marginal_amount_tax_scale import MarginalAmountTaxScale # noqa: F401 -from .single_amount_tax_scale import SingleAmountTaxScale # noqa: F401 +) +from .abstract_tax_scale import AbstractTaxScale +from .amount_tax_scale_like import AmountTaxScaleLike +from .abstract_rate_tax_scale import AbstractRateTaxScale +from .marginal_amount_tax_scale import MarginalAmountTaxScale +from .single_amount_tax_scale import SingleAmountTaxScale diff --git a/policyengine_core/tools/test_runner.py b/policyengine_core/tools/test_runner.py index a2b5c002..fe0b0f61 100644 --- a/policyengine_core/tools/test_runner.py +++ b/policyengine_core/tools/test_runner.py @@ -225,7 +225,7 @@ def runtest(self): self.generate_performance_tables(tracer) def print_computation_log(self, tracer, aggregate, max_depth): - print("Computation log:") # noqa T001 + print("Computation log:") tracer.print_computation_log(aggregate, max_depth) def generate_performance_graph(self, tracer): diff --git a/policyengine_core/tracers/__init__.py b/policyengine_core/tracers/__init__.py index f8b0e6be..d157a2f7 100644 --- a/policyengine_core/tracers/__init__.py +++ b/policyengine_core/tracers/__init__.py @@ -21,12 +21,12 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .computation_log import ComputationLog # noqa: F401 -from .flat_trace import FlatTrace # noqa: F401 -from .full_tracer import FullTracer # noqa: F401 -from .performance_log import PerformanceLog # noqa: F401 -from .simple_tracer import SimpleTracer # noqa: F401 -from .trace_node import TraceNode # noqa: F401 +from .computation_log import ComputationLog +from .flat_trace import FlatTrace +from .full_tracer import FullTracer +from .performance_log import PerformanceLog +from .simple_tracer import SimpleTracer +from .trace_node import TraceNode from .tracing_parameter_node_at_instant import ( TracingParameterNodeAtInstant, -) # noqa: F401 +) diff --git a/policyengine_core/tracers/computation_log.py b/policyengine_core/tracers/computation_log.py index 431f00a5..593fe04a 100644 --- a/policyengine_core/tracers/computation_log.py +++ b/policyengine_core/tracers/computation_log.py @@ -62,7 +62,7 @@ def print_log(self, aggregate=False, max_depth=None) -> None: vectors up to a depth of ``max_depth``. """ for line in self.lines(aggregate, max_depth): - print(line) # noqa T001 + print(line) def _get_node_log( self, diff --git a/policyengine_core/types/__init__.py b/policyengine_core/types/__init__.py index 03c81434..3175a489 100644 --- a/policyengine_core/types/__init__.py +++ b/policyengine_core/types/__init__.py @@ -37,7 +37,7 @@ # Official Public API -from .data_types import ( # noqa: F401 +from .data_types import ( ArrayLike, ArrayType, ) diff --git a/policyengine_core/types/data_types/__init__.py b/policyengine_core/types/data_types/__init__.py index 6dd38194..d599964d 100644 --- a/policyengine_core/types/data_types/__init__.py +++ b/policyengine_core/types/data_types/__init__.py @@ -1 +1 @@ -from .arrays import ArrayLike, ArrayType # noqa: F401 +from .arrays import ArrayLike, ArrayType diff --git a/policyengine_core/variables/__init__.py b/policyengine_core/variables/__init__.py index d8ca3cd8..e2a8b292 100644 --- a/policyengine_core/variables/__init__.py +++ b/policyengine_core/variables/__init__.py @@ -21,10 +21,10 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .config import VALUE_TYPES, FORMULA_NAME_PREFIX # noqa: F401 +from .config import VALUE_TYPES, FORMULA_NAME_PREFIX from .helpers import ( get_annualized_variable, get_neutralized_variable, -) # noqa: F401 -from .variable import Variable # noqa: F401 -from .typing import Formula # noqa: F401 +) +from .variable import Variable +from .typing import Formula diff --git a/policyengine_core/warnings/__init__.py b/policyengine_core/warnings/__init__.py index 5e79dccb..d8a6c4b4 100644 --- a/policyengine_core/warnings/__init__.py +++ b/policyengine_core/warnings/__init__.py @@ -21,6 +21,6 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .libyaml_warning import LibYAMLWarning # noqa: F401 -from .memory_warning import MemoryConfigWarning # noqa: F401 -from .tempfile_warning import TempfileWarning # noqa: F401 +from .libyaml_warning import LibYAMLWarning +from .memory_warning import MemoryConfigWarning +from .tempfile_warning import TempfileWarning diff --git a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py index 00466f62..dcc881aa 100644 --- a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py +++ b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py @@ -13,7 +13,7 @@ Parameter, ParameterNotFound, ) -from policyengine_core.model_api import * # noqa +from policyengine_core.model_api import * LOCAL_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/tests/core/test_reforms.py b/tests/core/test_reforms.py index fdcdaa91..784146dd 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -7,7 +7,7 @@ from policyengine_core.tools import assert_near from policyengine_core.parameters import ValuesHistory, ParameterNode from policyengine_core.country_template.entities import Household, Person -from policyengine_core.model_api import * # noqa analysis:ignore +from policyengine_core.model_api import * class goes_to_school(Variable): diff --git a/tests/core/variables/test_annualize.py b/tests/core/variables/test_annualize.py index 510a3e50..af85f314 100644 --- a/tests/core/variables/test_annualize.py +++ b/tests/core/variables/test_annualize.py @@ -2,7 +2,7 @@ from pytest import fixture from policyengine_core import periods -from policyengine_core.model_api import * # noqa analysis:ignore +from policyengine_core.model_api import * from policyengine_core.country_template.entities import Person from policyengine_core.variables import get_annualized_variable From d821fd1196f8c888576a53c53e010c3cd50ef4fc Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 00:28:05 +0100 Subject: [PATCH 07/25] Complete up to entities --- Makefile | 3 +- docs/_toc.yml | 1 + docs/python_api/commons.md | 23 +++ docs/python_api/country_template.ipynb | 142 ------------------ docs/python_api/country_template.md | 11 ++ docs/python_api/data_storage.md | 14 +- docs/python_api/entities.ipynb | 30 ---- docs/python_api/entities.md | 36 +++++ policyengine_core/commons/__init__.py | 60 -------- policyengine_core/commons/dummy.py | 23 --- policyengine_core/commons/formulas.py | 2 - policyengine_core/commons/misc.py | 1 - policyengine_core/commons/rates.py | 2 - policyengine_core/commons/tests/test_dummy.py | 10 -- .../country_template/__init__.py | 12 -- policyengine_core/data_storage/__init__.py | 23 --- policyengine_core/entities/__init__.py | 23 --- policyengine_core/entities/entity.py | 14 +- policyengine_core/entities/group_entity.py | 6 +- policyengine_core/entities/helpers.py | 20 +-- policyengine_core/entities/role.py | 5 +- policyengine_core/model_api.py | 6 - .../tests => tests/core/commons}/__init__.py | 0 .../core/commons}/test_formulas.py | 0 .../core/commons}/test_rates.py | 0 25 files changed, 106 insertions(+), 361 deletions(-) create mode 100644 docs/python_api/commons.md delete mode 100644 docs/python_api/country_template.ipynb create mode 100644 docs/python_api/country_template.md delete mode 100644 docs/python_api/entities.ipynb create mode 100644 docs/python_api/entities.md delete mode 100644 policyengine_core/commons/dummy.py delete mode 100644 policyengine_core/commons/tests/test_dummy.py rename {policyengine_core/commons/tests => tests/core/commons}/__init__.py (100%) rename {policyengine_core/commons/tests => tests/core/commons}/test_formulas.py (100%) rename {policyengine_core/commons/tests => tests/core/commons}/test_rates.py (100%) diff --git a/Makefile b/Makefile index 23bd9712..9976758b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ all: install format test build changelog -docs: +documentation: + jb clean docs jb build docs format: diff --git a/docs/_toc.yml b/docs/_toc.yml index 6c54ab70..494286eb 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -3,6 +3,7 @@ root: intro parts: - caption: Python API chapters: + - file: python_api/commons - file: python_api/country_template - file: python_api/data_storage - file: python_api/entities diff --git a/docs/python_api/commons.md b/docs/python_api/commons.md new file mode 100644 index 00000000..caec93f4 --- /dev/null +++ b/docs/python_api/commons.md @@ -0,0 +1,23 @@ +# Commons + +The `policyengine_core.commons` module contains a number of classes and functions that are used throughout the rest of the library. + +## formulas + +### apply_thresholds + +```{eval-rst} +.. autofunction:: policyengine_core.commons.formulas.apply_thresholds +``` + +### concat + +```{eval-rst} +.. autofunction:: policyengine_core.commons.formulas.concat +``` + +### switch + +```{eval-rst} +.. autofunction:: policyengine_core.commons.formulas.switch +``` diff --git a/docs/python_api/country_template.ipynb b/docs/python_api/country_template.ipynb deleted file mode 100644 index d5f9dc6f..00000000 --- a/docs/python_api/country_template.ipynb +++ /dev/null @@ -1,142 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Country template\n", - "\n", - "The `country_template` folder is a working example of a country model. To create a new country model, create a new repository with the folder's contents:\n", - "\n", - "* `parameters/` contains the parameter tree, with examples.\n", - "* `reforms/` contains several example reforms written using the Python reforms API.\n", - "* `situation_examples/` contains several example situations, in JSON format and accessible as Python objects.\n", - "* `tests/` contains a set of YAML tests to check policy implementations function.\n", - "* `variables/` contain definitions of variables with formulas.\n", - "* `entities.py` contains definitions of entities.\n", - "\n", - "## Running a simulation\n", - "\n", - "The below example runs a simulation using the example country model template." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[600.]\n" - ] - } - ], - "source": [ - "from policyengine_core import SimulationBuilder\n", - "from policyengine_core.country_template import CountryTaxBenefitSystem\n", - "from policyengine_core.country_template.situation_examples import single\n", - "\n", - "system = CountryTaxBenefitSystem()\n", - "\n", - "simulation = SimulationBuilder().build_from_dict(\n", - " system,\n", - " single,\n", - ")\n", - "\n", - "print(simulation.calculate(\"disposable_income\", \"2017-01\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Testing a country model\n", - "\n", - "The example below runs all tests defined for the example country model. This can also be done by running:\n", - "\n", - "`policyengine-core test -c policyengine_core.country_template policyengine_core/country_template/tests`" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "============================= test session starts ==============================\n", - "platform darwin -- Python 3.9.12, pytest-5.4.3, py-1.11.0, pluggy-0.13.1\n", - "rootdir: /Users/nikhil/policyengine/policyengine-core\n", - "plugins: anyio-3.5.0\n", - "collected 0 items\n", - "\n", - "============================ no tests ran in 0.00s =============================\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ERROR: file not found: /Users/nikhil/policyengine/policyengine-core/docs/python_api/policyengine_core/country_template/tests\n", - "\n" - ] - }, - { - "data": { - "text/plain": [ - "CompletedProcess(args=['policyengine-core', 'test', '-c', 'policyengine_core.country_template', 'policyengine_core/country_template/tests'], returncode=4)" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import subprocess\n", - "from policyengine_core.country_template import COUNTRY_DIR\n", - "\n", - "subprocess.run(\n", - " [\n", - " \"policyengine-core\",\n", - " \"test\",\n", - " \"-c\",\n", - " \"policyengine_core.country_template\",\n", - " COUNTRY_DIR / \"tests\",\n", - " ]\n", - ")\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3.9.12 ('base')", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "40d3a090f54c6569ab1632332b64b2c03c39dcf918b08424e98f38b5ae0af88f" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/python_api/country_template.md b/docs/python_api/country_template.md new file mode 100644 index 00000000..66cb0f75 --- /dev/null +++ b/docs/python_api/country_template.md @@ -0,0 +1,11 @@ +# Country template + +The `policyengine_core.country_template` module contains a template for a country model. It is intended to be used as a starting point for new country models. To create a new country model, simply copy the contents into a new repo, renaming the `country_template` folder to the name of your country, then remove the starter code at will. + +## CountryTaxBenefitSystem +```{eval-rst} +.. autoclass:: policyengine_core.country_template.CountryTaxBenefitSystem + :members: + :undoc-members: + :show-inheritance: +``` \ No newline at end of file diff --git a/docs/python_api/data_storage.md b/docs/python_api/data_storage.md index b5a81e6b..e0371dcd 100644 --- a/docs/python_api/data_storage.md +++ b/docs/python_api/data_storage.md @@ -1,19 +1,21 @@ # Data storage -The data storage module `policyengine_core.data_storage` contains two classes that are used to handle the storage of data in simulations: `InMemoryStorage` and `OnDiskStorage`. +The `policyengine_core.data_storage` module contains two classes that are used to handle the storage of data in simulations. -## In-memory storage +## InMemoryStorage -```{eval-rst} -.. autoclass:: policyengine_core.data_storage.InMemoryStorage +```{eval-rst} +.. autoclass:: policyengine_core.data_storage.in_memory_storage.InMemoryStorage :members: :undoc-members: + :show-inheritance: ``` -## On-disk storage +## OnDiskStorage ```{eval-rst} -.. autoclass:: policyengine_core.data_storage.OnDiskStorage +.. autoclass:: policyengine_core.data_storage.on_disk_storage.OnDiskStorage :members: :undoc-members: + :show-inheritance: ``` \ No newline at end of file diff --git a/docs/python_api/entities.ipynb b/docs/python_api/entities.ipynb deleted file mode 100644 index b4f09e83..00000000 --- a/docs/python_api/entities.ipynb +++ /dev/null @@ -1,30 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Entities" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3.9.12 ('base')", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.9.12" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "40d3a090f54c6569ab1632332b64b2c03c39dcf918b08424e98f38b5ae0af88f" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/python_api/entities.md b/docs/python_api/entities.md new file mode 100644 index 00000000..168ac345 --- /dev/null +++ b/docs/python_api/entities.md @@ -0,0 +1,36 @@ +# Entities + +The `policyengine_core.entities` module contains the classes that define how entities (and group entities) function. + +## Entity + +```{eval-rst} +.. autoclass:: policyengine_core.entities.entity.Entity + :members: + :undoc-members: + :show-inheritance: +``` + +## GroupEntity + +```{eval-rst} +.. autoclass:: policyengine_core.entities.group_entity.GroupEntity + :members: + :undoc-members: + :show-inheritance: +``` + +## Role + +```{eval-rst} +.. autoclass:: policyengine_core.entities.role.Role + :members: + :undoc-members: + :show-inheritance: +``` + +## build_entity + +```{eval-rst} +.. autofunction:: policyengine_core.entities.helpers.build_entity +``` diff --git a/policyengine_core/commons/__init__.py b/policyengine_core/commons/__init__.py index b054c88d..34d6f3a9 100644 --- a/policyengine_core/commons/__init__.py +++ b/policyengine_core/commons/__init__.py @@ -1,57 +1,3 @@ -"""Common tools for contributors and users. - -The tools in this sub-package are intended, to help both contributors -to OpenFisca Core and to country packages. - -Official Public API: - * :func:`.apply_thresholds` - * :func:`.average_rate` - * :func:`.concat` - * :func:`.empty_clone` - * :func:`.marginal_rate` - * :func:`.stringify_array` - * :func:`.switch` - -Deprecated: - * :class:`.Dummy` - -Note: - The ``deprecated`` imports are transitional, in order to ensure non-breaking - changes, and could be removed from the codebase in the next - major release. - -Note: - How imports are being used today:: - - from policyengine_core.commons import * # Bad - from policyengine_core.commons.formulas import switch # Bad - from policyengine_core.commons.decorators import deprecated # Bad - - - The previous examples provoke cyclic dependency problems, that prevent us - from modularizing the different components of the library, which would make - them easier to test and to maintain. - - How they could be used in a future release: - - from policyengine_core import commons - from policyengine_core.commons import deprecated - - deprecated() # Good: import classes as publicly exposed - commons.switch() # Good: use functions as publicly exposed - - .. seealso:: `PEP8#Imports`_ and `OpenFisca's Styleguide`_. - - .. _PEP8#Imports: - https://www.python.org/dev/peps/pep-0008/#imports - - .. _OpenFisca's Styleguide: - https://github.com/openfisca/openfisca-core/blob/master/STYLEGUIDE.md - -""" - -# Official Public API - from .formulas import apply_thresholds, concat, switch from .misc import empty_clone, stringify_array from .rates import average_rate, marginal_rate @@ -59,9 +5,3 @@ __all__ = ["apply_thresholds", "concat", "switch"] __all__ = ["empty_clone", "stringify_array", *__all__] __all__ = ["average_rate", "marginal_rate", *__all__] - -# Deprecated - -from .dummy import Dummy - -__all__ = ["Dummy", *__all__] diff --git a/policyengine_core/commons/dummy.py b/policyengine_core/commons/dummy.py deleted file mode 100644 index dfc4da1e..00000000 --- a/policyengine_core/commons/dummy.py +++ /dev/null @@ -1,23 +0,0 @@ -import warnings - - -class Dummy: - """A class that did nothing. - - Examples: - >>> Dummy() - None: - message = [ - "The 'Dummy' class has been deprecated since version 34.7.0,", - "and will be removed in the future.", - ] - warnings.warn(" ".join(message), DeprecationWarning) - pass diff --git a/policyengine_core/commons/formulas.py b/policyengine_core/commons/formulas.py index 95a1f62a..085b75d4 100644 --- a/policyengine_core/commons/formulas.py +++ b/policyengine_core/commons/formulas.py @@ -1,7 +1,5 @@ from typing import Any, Dict, Sequence, TypeVar - import numpy - from policyengine_core.types import ArrayLike, ArrayType T = TypeVar("T") diff --git a/policyengine_core/commons/misc.py b/policyengine_core/commons/misc.py index 668997ae..267b0290 100644 --- a/policyengine_core/commons/misc.py +++ b/policyengine_core/commons/misc.py @@ -1,5 +1,4 @@ from typing import TypeVar - from policyengine_core.types import ArrayType T = TypeVar("T") diff --git a/policyengine_core/commons/rates.py b/policyengine_core/commons/rates.py index 1dee745b..a4997d2b 100644 --- a/policyengine_core/commons/rates.py +++ b/policyengine_core/commons/rates.py @@ -1,7 +1,5 @@ from typing import Optional - import numpy - from policyengine_core.types import ArrayLike, ArrayType diff --git a/policyengine_core/commons/tests/test_dummy.py b/policyengine_core/commons/tests/test_dummy.py deleted file mode 100644 index 5a3a3425..00000000 --- a/policyengine_core/commons/tests/test_dummy.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - -from policyengine_core.commons import Dummy - - -def test_dummy_deprecation(): - """Dummy throws a deprecation warning when instantiated.""" - - with pytest.warns(DeprecationWarning): - assert Dummy() diff --git a/policyengine_core/country_template/__init__.py b/policyengine_core/country_template/__init__.py index 9deddff4..8ce99722 100644 --- a/policyengine_core/country_template/__init__.py +++ b/policyengine_core/country_template/__init__.py @@ -1,17 +1,5 @@ -""" -This file defines our country's tax and benefit system. - -A tax and benefit system is the higher-level instance in OpenFisca. -Its goal is to model the legislation of a country. -Basically a tax and benefit system contains simulation variables (source code) and legislation parameters (data). - -See https://openfisca.org/doc/key-concepts/tax_and_benefit_system.html -""" - import os - from policyengine_core.taxbenefitsystems import TaxBenefitSystem - from policyengine_core.country_template import entities from policyengine_core.country_template.situation_examples import couple from pathlib import Path diff --git a/policyengine_core/data_storage/__init__.py b/policyengine_core/data_storage/__init__.py index 326f7184..e1727c4f 100644 --- a/policyengine_core/data_storage/__init__.py +++ b/policyengine_core/data_storage/__init__.py @@ -1,25 +1,2 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from .in_memory_storage import InMemoryStorage from .on_disk_storage import OnDiskStorage diff --git a/policyengine_core/entities/__init__.py b/policyengine_core/entities/__init__.py index 866d45d5..a02b4d73 100644 --- a/policyengine_core/entities/__init__.py +++ b/policyengine_core/entities/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core import module -# >>> module.Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from .helpers import build_entity from .role import Role from .entity import Entity diff --git a/policyengine_core/entities/entity.py b/policyengine_core/entities/entity.py index 2f2d80d8..f92b37e0 100644 --- a/policyengine_core/entities/entity.py +++ b/policyengine_core/entities/entity.py @@ -1,7 +1,7 @@ import os import textwrap - -from policyengine_core.entities import Role +from typing import Any +from policyengine_core.entities.role import Role class Entity: @@ -9,7 +9,7 @@ class Entity: Represents an entity (e.g. a person, a household, etc.) on which calculations can be run. """ - def __init__(self, key, plural, label, doc): + def __init__(self, key: str, plural: str, label: str, doc: str): self.key = key self.label = label self.plural = plural @@ -17,19 +17,19 @@ def __init__(self, key, plural, label, doc): self.is_person = True self._tax_benefit_system = None - def set_tax_benefit_system(self, tax_benefit_system): + def set_tax_benefit_system(self, tax_benefit_system) -> None: self._tax_benefit_system = tax_benefit_system - def check_role_validity(self, role): + def check_role_validity(self, role: Any) -> None: if role is not None and not type(role) == Role: raise ValueError("{} is not a valid role".format(role)) - def get_variable(self, variable_name, check_existence=False): + def get_variable(self, variable_name: str, check_existence: bool = False): return self._tax_benefit_system.get_variable( variable_name, check_existence ) - def check_variable_defined_for_entity(self, variable_name): + def check_variable_defined_for_entity(self, variable_name: str) -> None: variable_entity = self.get_variable( variable_name, check_existence=True ).entity diff --git a/policyengine_core/entities/group_entity.py b/policyengine_core/entities/group_entity.py index a00fb571..d90554c1 100644 --- a/policyengine_core/entities/group_entity.py +++ b/policyengine_core/entities/group_entity.py @@ -1,4 +1,6 @@ -from policyengine_core.entities import Entity, Role +from typing import List +from policyengine_core.entities.entity import Entity +from policyengine_core.entities.role import Role class GroupEntity(Entity): @@ -23,7 +25,7 @@ class GroupEntity(Entity): """ - def __init__(self, key, plural, label, doc, roles, containing_entities=()): + def __init__(self, key: str, plural: str, label: str, doc: str, roles: List[str], containing_entities: List[str] = ()): super().__init__(key, plural, label, doc) self.roles_description = roles self.roles = [] diff --git a/policyengine_core/entities/helpers.py b/policyengine_core/entities/helpers.py index f1079b3b..42ec22f1 100644 --- a/policyengine_core/entities/helpers.py +++ b/policyengine_core/entities/helpers.py @@ -1,16 +1,16 @@ +from typing import List from policyengine_core import entities - +from policyengine_core.entities.entity import Entity def build_entity( - key, - plural, - label, - doc="", - roles=None, - is_person=False, - class_override=None, - containing_entities=(), -): + key: str, + plural: str, + label: str, + doc: str= "", + roles: List[str] = None, + is_person: bool = False, + containing_entities: List[str] = (), +) -> Entity: if is_person: return entities.Entity(key, plural, label, doc) else: diff --git a/policyengine_core/entities/role.py b/policyengine_core/entities/role.py index 048671c5..d19e3ad4 100644 --- a/policyengine_core/entities/role.py +++ b/policyengine_core/entities/role.py @@ -2,6 +2,9 @@ class Role: + """ + The type of the relation between an entity instance and a group entity instance. + """ def __init__(self, description, entity): self.entity = entity self.key = description["key"] @@ -11,5 +14,5 @@ def __init__(self, description, entity): self.max = description.get("max") self.subroles = None - def __repr__(self): + def __repr__(self) -> str: return "Role({})".format(self.key) diff --git a/policyengine_core/model_api.py b/policyengine_core/model_api.py index b5365997..a71beb68 100644 --- a/policyengine_core/model_api.py +++ b/policyengine_core/model_api.py @@ -9,12 +9,6 @@ where, ) -from policyengine_core.commons import ( - apply_thresholds, - concat, - switch, -) - from policyengine_core.holders import ( set_input_dispatch_by_period, set_input_divide_by_period, diff --git a/policyengine_core/commons/tests/__init__.py b/tests/core/commons/__init__.py similarity index 100% rename from policyengine_core/commons/tests/__init__.py rename to tests/core/commons/__init__.py diff --git a/policyengine_core/commons/tests/test_formulas.py b/tests/core/commons/test_formulas.py similarity index 100% rename from policyengine_core/commons/tests/test_formulas.py rename to tests/core/commons/test_formulas.py diff --git a/policyengine_core/commons/tests/test_rates.py b/tests/core/commons/test_rates.py similarity index 100% rename from policyengine_core/commons/tests/test_rates.py rename to tests/core/commons/test_rates.py From d815a23df1ed35d3ff0cf45ad67cbb168114eb6e Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 00:36:45 +0100 Subject: [PATCH 08/25] Add errors --- docs/_toc.yml | 1 + docs/python_api/errors.md | 75 +++++++++++++++++++ policyengine_core/errors/__init__.py | 25 +------ ...ror.py => variable_name_conflict_error.py} | 0 4 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 docs/python_api/errors.md rename policyengine_core/errors/{variable_name_config_error.py => variable_name_conflict_error.py} (100%) diff --git a/docs/_toc.yml b/docs/_toc.yml index 494286eb..b19f3698 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -7,6 +7,7 @@ parts: - file: python_api/country_template - file: python_api/data_storage - file: python_api/entities + - file: python_api/errors - caption: Contributing chapters: - file: contributing/intro diff --git a/docs/python_api/errors.md b/docs/python_api/errors.md new file mode 100644 index 00000000..a0bdcfe7 --- /dev/null +++ b/docs/python_api/errors.md @@ -0,0 +1,75 @@ +# Errors + +`policyengine_core.errors` contains error definitions specific to simulations. + +## NANCreationError + +```{eval-rst} +.. autoclass:: policyengine_core.errors.nan_creation_error.NaNCreationError + :members: + :undoc-members: + :show-inheritance: +``` + +## ParameterNotFoundError + +```{eval-rst} +.. autoclass:: policyengine_core.errors.parameter_not_found_error.ParameterNotFoundError + :members: + :undoc-members: + :show-inheritance: +``` + +## ParameterParsingError + +```{eval-rst} +.. autoclass:: policyengine_core.errors.parameter_parsing_error.ParameterParsingError + :members: + :undoc-members: + :show-inheritance: +``` + +## PeriodMismatchError + +```{eval-rst} +.. autoclass:: policyengine_core.errors.period_mismatch_error.PeriodMismatchError + :members: + :undoc-members: + :show-inheritance: +``` + +## SituationParsingError + +```{eval-rst} +.. autoclass:: policyengine_core.errors.situation_parsing_error.SituationParsingError + :members: + :undoc-members: + :show-inheritance: +``` + +## SpiralError + +```{eval-rst} +.. autoclass:: policyengine_core.errors.spiral_error.SpiralError + :members: + :undoc-members: + :show-inheritance: +``` + +## VariableNameConflictError + +```{eval-rst} +.. autoclass:: policyengine_core.errors.variable_name_conflict_error.VariableNameConflictError + :members: + :undoc-members: + :show-inheritance: +``` + +## VariableNotFoundError + +```{eval-rst} +.. autoclass:: policyengine_core.errors.variable_not_found_error.VariableNotFoundError + :members: + :undoc-members: + :show-inheritance: +``` \ No newline at end of file diff --git a/policyengine_core/errors/__init__.py b/policyengine_core/errors/__init__.py index e23b5a30..936dfad6 100644 --- a/policyengine_core/errors/__init__.py +++ b/policyengine_core/errors/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from .cycle_error import CycleError from .empty_argument_error import EmptyArgumentError from .nan_creation_error import NaNCreationError @@ -32,7 +9,7 @@ from .period_mismatch_error import PeriodMismatchError from .situation_parsing_error import SituationParsingError from .spiral_error import SpiralError -from .variable_name_config_error import ( +from .variable_name_conflict_error import ( VariableNameConflictError, VariableNameConflictError as VariableNameConflict, ) diff --git a/policyengine_core/errors/variable_name_config_error.py b/policyengine_core/errors/variable_name_conflict_error.py similarity index 100% rename from policyengine_core/errors/variable_name_config_error.py rename to policyengine_core/errors/variable_name_conflict_error.py From dc53536638d37081ebdc436fd1ecff8af8b5dea2 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 00:59:37 +0100 Subject: [PATCH 09/25] Add up to holders --- docs/_toc.yml | 3 ++ docs/python_api/experimental.md | 12 +++++++ docs/python_api/extension_template.md | 5 +++ docs/python_api/holders.md | 18 +++++++++++ .../experimental/memory_config.py | 7 ++-- policyengine_core/holders/helpers.py | 8 +++-- policyengine_core/holders/holder.py | 32 +++++++++++-------- 7 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 docs/python_api/experimental.md create mode 100644 docs/python_api/extension_template.md create mode 100644 docs/python_api/holders.md diff --git a/docs/_toc.yml b/docs/_toc.yml index b19f3698..f40c26b8 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -8,6 +8,9 @@ parts: - file: python_api/data_storage - file: python_api/entities - file: python_api/errors + - file: python_api/experimental + - file: python_api/extension_template + - file: python_api/holders - caption: Contributing chapters: - file: contributing/intro diff --git a/docs/python_api/experimental.md b/docs/python_api/experimental.md new file mode 100644 index 00000000..c751a1dc --- /dev/null +++ b/docs/python_api/experimental.md @@ -0,0 +1,12 @@ +# Experimental features + +`policyengine_core.experimental` houses new features that aren't fully stable yet, and may be removed in future versions. + +## MemoryConfig + +```{eval-rst} +.. autoclass:: policyengine_core.experimental.memory_config.MemoryConfig + :members: + :undoc-members: + :show-inheritance: +``` \ No newline at end of file diff --git a/docs/python_api/extension_template.md b/docs/python_api/extension_template.md new file mode 100644 index 00000000..5086e29a --- /dev/null +++ b/docs/python_api/extension_template.md @@ -0,0 +1,5 @@ +# Extension template + +The `policyengine_core.extension_template` module contains a template for an extension. An extension differs from a country model in that it is intended to be used as a plugin to an existing country model, rather than as a standalone model. To create a new extension, simply copy the contents into a new repo, renaming the `extension_template` folder to the name of your extension, then remove the starter code at will. + +Extensions don't have any specific initialisation (unlike a country package, which must define `CountryTaxBenefitSystem`). \ No newline at end of file diff --git a/docs/python_api/holders.md b/docs/python_api/holders.md new file mode 100644 index 00000000..75b0cdb0 --- /dev/null +++ b/docs/python_api/holders.md @@ -0,0 +1,18 @@ +# Holders + +`policyengine_core.holders` contains the definition of `Holder`, which is a class whose instances represent variable values in a specific time period. + +## Holder + +```{eval-rst} +.. autoclass:: policyengine_core.holders.holder.Holder + :members: + :undoc-members: + :show-inheritance: +``` + +## set_input_dispatch_by_period + +```{eval-rst} +.. autofunction:: policyengine_core.holders.helpers.set_input_dispatch_by_period +``` \ No newline at end of file diff --git a/policyengine_core/experimental/memory_config.py b/policyengine_core/experimental/memory_config.py index 0f8e36f0..a23f25a3 100644 --- a/policyengine_core/experimental/memory_config.py +++ b/policyengine_core/experimental/memory_config.py @@ -1,3 +1,4 @@ +from typing import List import warnings from policyengine_core.warnings import MemoryConfigWarning @@ -6,9 +7,9 @@ class MemoryConfig: def __init__( self, - max_memory_occupation, - priority_variables=None, - variables_to_drop=None, + max_memory_occupation: float, + priority_variables: List[str] = None, + variables_to_drop: List[str] = None, ): message = [ "Memory configuration is a feature that is still currently under experimentation.", diff --git a/policyengine_core/holders/helpers.py b/policyengine_core/holders/helpers.py index 7bbf4c3a..66fe36a2 100644 --- a/policyengine_core/holders/helpers.py +++ b/policyengine_core/holders/helpers.py @@ -1,13 +1,15 @@ import logging import numpy - +from numpy.typing import ArrayLike from policyengine_core import periods +from policyengine_core.holders.holder import Holder +from policyengine_core.periods import Period log = logging.getLogger(__name__) -def set_input_dispatch_by_period(holder, period, array): +def set_input_dispatch_by_period(holder: Holder, period: Period, array: ArrayLike): """ This function can be declared as a ``set_input`` attribute of a variable. @@ -44,7 +46,7 @@ def set_input_dispatch_by_period(holder, period, array): sub_period = sub_period.offset(1) -def set_input_divide_by_period(holder, period, array): +def set_input_divide_by_period(holder: Holder, period: Period, array: ArrayLike): """ This function can be declared as a ``set_input`` attribute of a variable. diff --git a/policyengine_core/holders/holder.py b/policyengine_core/holders/holder.py index 0aa36003..1ab4d33e 100644 --- a/policyengine_core/holders/holder.py +++ b/policyengine_core/holders/holder.py @@ -1,13 +1,17 @@ import os import warnings - +from typing import Any, List, TYPE_CHECKING import numpy import psutil - +from numpy.typing import ArrayLike from policyengine_core import commons, periods, tools from policyengine_core.errors import PeriodMismatchError from policyengine_core.data_storage import InMemoryStorage, OnDiskStorage from policyengine_core.indexed_enums import Enum +from policyengine_core.periods import Period +if TYPE_CHECKING: + from policyengine_core.variables import Variable + from policyengine_core.populations import Population class Holder: @@ -15,7 +19,7 @@ class Holder: A holder keeps tracks of a variable values after they have been calculated, or set as an input. """ - def __init__(self, variable, population): + def __init__(self, variable: "Variable", population: "Population"): self.population = population self.variable = variable self.simulation = population.simulation @@ -40,7 +44,7 @@ def __init__(self, variable, population): ): self._do_not_store = True - def clone(self, population): + def clone(self, population: "Population") -> "Holder": """ Copy the holder just enough to be able to run a new simulation without modifying the original simulation. """ @@ -56,7 +60,7 @@ def clone(self, population): return new - def create_disk_storage(self, directory=None, preserve=False): + def create_disk_storage(self, directory: str = None, preserve: bool = False) -> OnDiskStorage: if directory is None: directory = self.simulation.data_storage_dir storage_dir = os.path.join(directory, self.variable.name) @@ -68,7 +72,7 @@ def create_disk_storage(self, directory=None, preserve=False): preserve_storage_dir=preserve, ) - def delete_arrays(self, period=None): + def delete_arrays(self, period: Period = None) -> None: """ If ``period`` is ``None``, remove all known values of the variable. @@ -79,7 +83,7 @@ def delete_arrays(self, period=None): if self._disk_storage: self._disk_storage.delete(period) - def get_array(self, period): + def get_array(self, period: Period) -> ArrayLike: """ Get the value of the variable for the given period. @@ -93,7 +97,7 @@ def get_array(self, period): if self._disk_storage: return self._disk_storage.get(period) - def get_memory_usage(self): + def get_memory_usage(self) -> dict: """ Get data about the virtual memory usage of the holder. @@ -137,7 +141,7 @@ def get_memory_usage(self): return usage - def get_known_periods(self): + def get_known_periods(self) -> List[Period]: """ Get the list of periods the variable value is known for. """ @@ -150,7 +154,7 @@ def get_known_periods(self): ) ) - def set_input(self, period, array): + def set_input(self, period: Period, array: ArrayLike) -> None: """ Set a variable's value (``array``) for a given period (``period``) @@ -195,7 +199,7 @@ def set_input(self, period, array): return self.variable.set_input(self, period, array) return self._set(period, array) - def _to_array(self, value): + def _to_array(self, value: Any) -> ArrayLike: if not isinstance(value, numpy.ndarray): value = numpy.asarray(value) if value.ndim == 0: @@ -227,7 +231,7 @@ def _to_array(self, value): ) return value - def _set(self, period, value): + def _set(self, period: Period, value: ArrayLike) -> None: value = self._to_array(value) if self.variable.definition_period != periods.ETERNITY: if period is None: @@ -271,7 +275,7 @@ def _set(self, period, value): else: self._memory_storage.put(value, period) - def put_in_cache(self, value, period): + def put_in_cache(self, value: ArrayLike, period: Period) -> None: if self._do_not_store: return @@ -285,7 +289,7 @@ def put_in_cache(self, value, period): self._set(period, value) - def default_array(self): + def default_array(self) -> ArrayLike: """ Return a new array of the appropriate length for the entity, filled with the variable default values. """ From 91a55f8a8598b28ab2481c16aec56960a3ac0c92 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 01:07:14 +0100 Subject: [PATCH 10/25] Add up to enums --- docs/_toc.yml | 1 + docs/python_api/enums.md | 21 +++++++++++++++ docs/python_api/holders.md | 8 +++++- .../country_template/variables/housing.py | 2 +- .../data_storage/on_disk_storage.py | 2 +- policyengine_core/enums/__init__.py | 3 +++ .../{indexed_enums => enums}/config.py | 0 .../{indexed_enums => enums}/enum.py | 0 .../{indexed_enums => enums}/enum_array.py | 2 +- policyengine_core/experimental/__init__.py | 23 ---------------- policyengine_core/holders/__init__.py | 23 ---------------- policyengine_core/holders/holder.py | 2 +- policyengine_core/indexed_enums/__init__.py | 26 ------------------- policyengine_core/model_api.py | 2 +- .../vectorial_parameter_node_at_instant.py | 2 +- .../populations/group_population.py | 2 +- policyengine_core/simulations/simulation.py | 2 +- policyengine_core/tools/__init__.py | 2 +- policyengine_core/tracers/computation_log.py | 2 +- policyengine_core/tracers/flat_trace.py | 2 +- policyengine_core/tracers/trace_node.py | 2 +- policyengine_core/variables/config.py | 6 ++--- policyengine_core/variables/variable.py | 2 +- tests/core/test_simulation_builder.py | 2 +- 24 files changed, 49 insertions(+), 90 deletions(-) create mode 100644 docs/python_api/enums.md create mode 100644 policyengine_core/enums/__init__.py rename policyengine_core/{indexed_enums => enums}/config.py (100%) rename policyengine_core/{indexed_enums => enums}/enum.py (100%) rename policyengine_core/{indexed_enums => enums}/enum_array.py (98%) delete mode 100644 policyengine_core/indexed_enums/__init__.py diff --git a/docs/_toc.yml b/docs/_toc.yml index f40c26b8..21562d87 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -7,6 +7,7 @@ parts: - file: python_api/country_template - file: python_api/data_storage - file: python_api/entities + - file: python_api/enums - file: python_api/errors - file: python_api/experimental - file: python_api/extension_template diff --git a/docs/python_api/enums.md b/docs/python_api/enums.md new file mode 100644 index 00000000..d5aaa8b0 --- /dev/null +++ b/docs/python_api/enums.md @@ -0,0 +1,21 @@ +# Enums + +`policyengine_core.enums` (renamed from `.indexed_enums`) contains definitions for enumerable types, which can be used to specify categorical data types. + +## Enum + +```{eval-rst} +.. autoclass:: policyengine_core.enums.enum.Enum + :members: + :undoc-members: + :show-inheritance: +``` + +## EnumArray + +```{eval-rst} +.. autoclass:: policyengine_core.enums.enum_array.EnumArray + :members: + :undoc-members: + :show-inheritance: +``` diff --git a/docs/python_api/holders.md b/docs/python_api/holders.md index 75b0cdb0..527e3fa8 100644 --- a/docs/python_api/holders.md +++ b/docs/python_api/holders.md @@ -15,4 +15,10 @@ ```{eval-rst} .. autofunction:: policyengine_core.holders.helpers.set_input_dispatch_by_period -``` \ No newline at end of file +``` + +## set_input_divide_by_period + +```{eval-rst} +.. autofunction:: policyengine_core.holders.helpers.set_input_divide_by_period +``` diff --git a/policyengine_core/country_template/variables/housing.py b/policyengine_core/country_template/variables/housing.py index 12b58ad6..c3df3de1 100644 --- a/policyengine_core/country_template/variables/housing.py +++ b/policyengine_core/country_template/variables/housing.py @@ -7,7 +7,7 @@ """ # Import from openfisca-core the Python objects used to code the legislation in OpenFisca -from policyengine_core.indexed_enums import Enum +from policyengine_core.enums import Enum from policyengine_core.periods import MONTH from policyengine_core.variables import Variable diff --git a/policyengine_core/data_storage/on_disk_storage.py b/policyengine_core/data_storage/on_disk_storage.py index 65cf0992..a16416bc 100644 --- a/policyengine_core/data_storage/on_disk_storage.py +++ b/policyengine_core/data_storage/on_disk_storage.py @@ -4,7 +4,7 @@ import numpy from numpy.typing import ArrayLike from policyengine_core import periods -from policyengine_core.indexed_enums import EnumArray +from policyengine_core.enums import EnumArray from policyengine_core.periods import Period diff --git a/policyengine_core/enums/__init__.py b/policyengine_core/enums/__init__.py new file mode 100644 index 00000000..dc2674cd --- /dev/null +++ b/policyengine_core/enums/__init__.py @@ -0,0 +1,3 @@ +from .config import ENUM_ARRAY_DTYPE +from .enum_array import EnumArray +from .enum import Enum diff --git a/policyengine_core/indexed_enums/config.py b/policyengine_core/enums/config.py similarity index 100% rename from policyengine_core/indexed_enums/config.py rename to policyengine_core/enums/config.py diff --git a/policyengine_core/indexed_enums/enum.py b/policyengine_core/enums/enum.py similarity index 100% rename from policyengine_core/indexed_enums/enum.py rename to policyengine_core/enums/enum.py diff --git a/policyengine_core/indexed_enums/enum_array.py b/policyengine_core/enums/enum_array.py similarity index 98% rename from policyengine_core/indexed_enums/enum_array.py rename to policyengine_core/enums/enum_array.py index a5c7a066..1bf5148b 100644 --- a/policyengine_core/indexed_enums/enum_array.py +++ b/policyengine_core/enums/enum_array.py @@ -6,7 +6,7 @@ import numpy if typing.TYPE_CHECKING: - from policyengine_core.indexed_enums import Enum + from policyengine_core.enums import Enum class EnumArray(numpy.ndarray): diff --git a/policyengine_core/experimental/__init__.py b/policyengine_core/experimental/__init__.py index 373fd678..1a80314e 100644 --- a/policyengine_core/experimental/__init__.py +++ b/policyengine_core/experimental/__init__.py @@ -1,24 +1 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from .memory_config import MemoryConfig diff --git a/policyengine_core/holders/__init__.py b/policyengine_core/holders/__init__.py index d471cc24..3017fb6f 100644 --- a/policyengine_core/holders/__init__.py +++ b/policyengine_core/holders/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from .helpers import ( set_input_dispatch_by_period, set_input_divide_by_period, diff --git a/policyengine_core/holders/holder.py b/policyengine_core/holders/holder.py index 1ab4d33e..0195993a 100644 --- a/policyengine_core/holders/holder.py +++ b/policyengine_core/holders/holder.py @@ -7,7 +7,7 @@ from policyengine_core import commons, periods, tools from policyengine_core.errors import PeriodMismatchError from policyengine_core.data_storage import InMemoryStorage, OnDiskStorage -from policyengine_core.indexed_enums import Enum +from policyengine_core.enums import Enum from policyengine_core.periods import Period if TYPE_CHECKING: from policyengine_core.variables import Variable diff --git a/policyengine_core/indexed_enums/__init__.py b/policyengine_core/indexed_enums/__init__.py deleted file mode 100644 index b5a89c7f..00000000 --- a/policyengine_core/indexed_enums/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - -from .config import ENUM_ARRAY_DTYPE -from .enum_array import EnumArray -from .enum import Enum diff --git a/policyengine_core/model_api.py b/policyengine_core/model_api.py index a71beb68..621bc7bb 100644 --- a/policyengine_core/model_api.py +++ b/policyengine_core/model_api.py @@ -14,7 +14,7 @@ set_input_divide_by_period, ) -from policyengine_core.indexed_enums import Enum +from policyengine_core.enums import Enum from policyengine_core.parameters import ( load_parameter_file, diff --git a/policyengine_core/parameters/vectorial_parameter_node_at_instant.py b/policyengine_core/parameters/vectorial_parameter_node_at_instant.py index 98c05509..d6345914 100644 --- a/policyengine_core/parameters/vectorial_parameter_node_at_instant.py +++ b/policyengine_core/parameters/vectorial_parameter_node_at_instant.py @@ -2,7 +2,7 @@ from policyengine_core import parameters from policyengine_core.errors import ParameterNotFoundError -from policyengine_core.indexed_enums import Enum, EnumArray +from policyengine_core.enums import Enum, EnumArray from policyengine_core.parameters import helpers diff --git a/policyengine_core/populations/group_population.py b/policyengine_core/populations/group_population.py index 9a88a8b7..97fa41d1 100644 --- a/policyengine_core/populations/group_population.py +++ b/policyengine_core/populations/group_population.py @@ -4,7 +4,7 @@ from policyengine_core import projectors from policyengine_core.entities import Role -from policyengine_core.indexed_enums import EnumArray +from policyengine_core.enums import EnumArray from policyengine_core.populations import Population diff --git a/policyengine_core/simulations/simulation.py b/policyengine_core/simulations/simulation.py index 1ea7b129..9d2a88d0 100644 --- a/policyengine_core/simulations/simulation.py +++ b/policyengine_core/simulations/simulation.py @@ -5,7 +5,7 @@ from policyengine_core import commons, periods from policyengine_core.errors import CycleError, SpiralError -from policyengine_core.indexed_enums import Enum, EnumArray +from policyengine_core.enums import Enum, EnumArray from policyengine_core.periods import Period from policyengine_core.tracers import ( FullTracer, diff --git a/policyengine_core/tools/__init__.py b/policyengine_core/tools/__init__.py index be67de30..4a8ad4ce 100644 --- a/policyengine_core/tools/__init__.py +++ b/policyengine_core/tools/__init__.py @@ -5,7 +5,7 @@ import numexpr -from policyengine_core.indexed_enums import EnumArray +from policyengine_core.enums import EnumArray def assert_near( diff --git a/policyengine_core/tracers/computation_log.py b/policyengine_core/tracers/computation_log.py index 593fe04a..6a2f02be 100644 --- a/policyengine_core/tracers/computation_log.py +++ b/policyengine_core/tracers/computation_log.py @@ -6,7 +6,7 @@ import numpy from .. import tracers -from policyengine_core.indexed_enums import EnumArray +from policyengine_core.enums import EnumArray if typing.TYPE_CHECKING: from numpy.typing import ArrayLike diff --git a/policyengine_core/tracers/flat_trace.py b/policyengine_core/tracers/flat_trace.py index 38346585..72336e35 100644 --- a/policyengine_core/tracers/flat_trace.py +++ b/policyengine_core/tracers/flat_trace.py @@ -6,7 +6,7 @@ import numpy from policyengine_core import tracers -from policyengine_core.indexed_enums import EnumArray +from policyengine_core.enums import EnumArray if typing.TYPE_CHECKING: from numpy.typing import ArrayLike diff --git a/policyengine_core/tracers/trace_node.py b/policyengine_core/tracers/trace_node.py index 6ba608ea..6e722415 100644 --- a/policyengine_core/tracers/trace_node.py +++ b/policyengine_core/tracers/trace_node.py @@ -6,7 +6,7 @@ if typing.TYPE_CHECKING: import numpy - from policyengine_core.indexed_enums import EnumArray + from policyengine_core.enums import EnumArray from policyengine_core.periods import Period Array = typing.Union[EnumArray, numpy.typing.ArrayLike] diff --git a/policyengine_core/variables/config.py b/policyengine_core/variables/config.py index 43d8491e..97807e7b 100644 --- a/policyengine_core/variables/config.py +++ b/policyengine_core/variables/config.py @@ -2,8 +2,8 @@ import numpy -from policyengine_core import indexed_enums -from policyengine_core.indexed_enums import Enum +from policyengine_core import enums +from policyengine_core.enums import Enum VALUE_TYPES = { @@ -36,7 +36,7 @@ "is_period_size_independent": True, }, Enum: { - "dtype": indexed_enums.ENUM_ARRAY_DTYPE, + "dtype": enums.ENUM_ARRAY_DTYPE, "json_type": "string", "formatted_value_type": "String", "is_period_size_independent": True, diff --git a/policyengine_core/variables/variable.py b/policyengine_core/variables/variable.py index a137bfce..d9de2571 100644 --- a/policyengine_core/variables/variable.py +++ b/policyengine_core/variables/variable.py @@ -8,7 +8,7 @@ from policyengine_core import periods, tools from policyengine_core.entities import Entity -from policyengine_core.indexed_enums import Enum, EnumArray +from policyengine_core.enums import Enum, EnumArray from policyengine_core.periods import Period from . import config, helpers diff --git a/tests/core/test_simulation_builder.py b/tests/core/test_simulation_builder.py index a266aef1..17f62a02 100644 --- a/tests/core/test_simulation_builder.py +++ b/tests/core/test_simulation_builder.py @@ -7,7 +7,7 @@ from policyengine_core import periods, tools from policyengine_core.errors import SituationParsingError -from policyengine_core.indexed_enums import Enum +from policyengine_core.enums import Enum from policyengine_core.populations import Population from policyengine_core.simulations import Simulation, SimulationBuilder from policyengine_core.tools import test_runner From 3ed5561fabea98bd12adc5ad09ccc2d2d1097772 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 03:14:50 +0100 Subject: [PATCH 11/25] Fix parameters module --- .coverage | Bin 544768 -> 602112 bytes docs/_toc.yml | 1 + docs/python_api/parameters.md | 87 ++++++++ policyengine_core/errors/__init__.py | 3 - .../local_town/child_allowance/amount.yaml | 3 +- policyengine_core/model_api.py | 5 +- policyengine_core/parameters/__init__.py | 28 +-- .../parameters/at_instant_like.py | 6 +- policyengine_core/parameters/config.py | 4 +- policyengine_core/parameters/parameter.py | 1 - .../parameters/parameter_at_instant.py | 13 +- .../parameters/parameter_node.py | 17 +- .../parameters/parameter_node_at_instant.py | 16 +- .../parameters/parameter_scale.py | 16 +- .../parameters/values_history.py | 9 - .../vectorial_parameter_node_at_instant.py | 15 +- .../taxbenefitsystems/__init__.py | 4 +- policyengine_core/tools/test_runner.py | 8 +- .../filesystem_hierarchy/node1/param.yaml | 5 +- .../yaml_hierarchy/node1.yaml | 4 +- .../test_fancy_indexing.py | 4 +- .../test_marginal_amount_tax_scale.py | 2 +- .../test_single_amount_tax_scale.py | 6 +- tests/core/test_parameters.py | 6 +- tests/core/test_reforms.py | 192 +----------------- .../tools/test_runner/test_yaml_runner.py | 4 +- 26 files changed, 164 insertions(+), 295 deletions(-) create mode 100644 docs/python_api/parameters.md delete mode 100644 policyengine_core/parameters/values_history.py diff --git a/.coverage b/.coverage index 1e1f95ee3c5a87ef6b6af5103f666863373ddd83..f1082db621f115b0606152ea5099ecf28fd4d0fa 100644 GIT binary patch delta 60616 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 SlHKLnAmz@4^KV0Y?EGJzwIM+O delta 20417 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 HUHyLm>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from policyengine_core.errors import ( - ParameterNotFound, + ParameterNotFoundError, ParameterParsingError, ) @@ -46,10 +23,7 @@ from .parameter_node import ParameterNode from .parameter_scale import ( ParameterScale, - ParameterScale as Scale, ) from .parameter_scale_bracket import ( ParameterScaleBracket, - ParameterScaleBracket as Bracket, ) -from .values_history import ValuesHistory diff --git a/policyengine_core/parameters/at_instant_like.py b/policyengine_core/parameters/at_instant_like.py index 66dcb8f6..503b2359 100644 --- a/policyengine_core/parameters/at_instant_like.py +++ b/policyengine_core/parameters/at_instant_like.py @@ -1,6 +1,8 @@ import abc +from typing import Any from policyengine_core import periods +from policyengine_core.periods import Instant class AtInstantLike(abc.ABC): @@ -8,10 +10,10 @@ class AtInstantLike(abc.ABC): Base class for various types of parameters implementing the at instant protocol. """ - def __call__(self, instant): + def __call__(self, instant: Instant) -> Any: return self.get_at_instant(instant) - def get_at_instant(self, instant): + def get_at_instant(self, instant: Instant) -> Any: instant = str(periods.instant(instant)) return self._get_at_instant(instant) diff --git a/policyengine_core/parameters/config.py b/policyengine_core/parameters/config.py index 2a7f08f8..4f9143ed 100644 --- a/policyengine_core/parameters/config.py +++ b/policyengine_core/parameters/config.py @@ -17,10 +17,8 @@ warnings.warn(" ".join(message), LibYAMLWarning) from yaml import Loader # type: ignore # (see https://github.com/python/mypy/issues/1153#issuecomment-455802270) -# 'unit' and 'reference' are only listed here for backward compatibility. -# It is now recommended to include them in metadata, until a common consensus emerges. ALLOWED_PARAM_TYPES = (float, int, bool, type(None), typing.List) -COMMON_KEYS = {"description", "metadata", "unit", "reference", "documentation"} +COMMON_KEYS = {"description", "metadata", "documentation"} FILE_EXTENSIONS = {".yaml", ".yml"} diff --git a/policyengine_core/parameters/parameter.py b/policyengine_core/parameters/parameter.py index a9773ca5..b8cb1706 100644 --- a/policyengine_core/parameters/parameter.py +++ b/policyengine_core/parameters/parameter.py @@ -57,7 +57,6 @@ def __init__( self.description: Optional[str] = None self.metadata: Dict = {} self.documentation: Optional[str] = None - self.values_history = self # Only for backward compatibility # Normal parameter declaration: the values are declared under the 'values' key: parse the description and metadata. if data.get("values"): diff --git a/policyengine_core/parameters/parameter_at_instant.py b/policyengine_core/parameters/parameter_at_instant.py index 951c2685..94fdfcbb 100644 --- a/policyengine_core/parameters/parameter_at_instant.py +++ b/policyengine_core/parameters/parameter_at_instant.py @@ -11,11 +11,10 @@ class ParameterAtInstant: A value of a parameter at a given instant. """ - # 'unit' and 'reference' are only listed here for backward compatibility - _allowed_keys = set(["value", "metadata", "unit", "reference"]) + _allowed_keys = set(["value", "metadata"]) def __init__( - self, name, instant_str, data=None, file_path=None, metadata=None + self, name: str, instant_str: str, data: dict = None, file_path: str = None, metadata: dict = None ): """ :param str name: name of the parameter, e.g. "taxes.some_tax.some_param" @@ -42,7 +41,7 @@ def __init__( helpers._set_backward_compatibility_metadata(self, data) self.metadata.update(data.get("metadata", {})) - def validate(self, data): + def validate(self, data: dict) -> None: helpers._validate_parameter( self, data, data_type=dict, allowed_keys=self._allowed_keys ) @@ -61,17 +60,17 @@ def validate(self, data): self.file_path, ) - def __eq__(self, other): + def __eq__(self, other) -> bool: return ( (self.name == other.name) and (self.instant_str == other.instant_str) and (self.value == other.value) ) - def __repr__(self): + def __repr__(self) -> str: return "ParameterAtInstant({})".format({self.instant_str: self.value}) - def clone(self): + def clone(self) -> "ParameterAtInstant": clone = commons.empty_clone(self) clone.__dict__ = self.__dict__.copy() clone.metadata = copy.deepcopy(self.metadata) diff --git a/policyengine_core/parameters/parameter_node.py b/policyengine_core/parameters/parameter_node.py index e5bd8c79..3d3b27f7 100644 --- a/policyengine_core/parameters/parameter_node.py +++ b/policyengine_core/parameters/parameter_node.py @@ -3,8 +3,9 @@ import copy import os import typing - +from typing import Iterable, Union from policyengine_core import commons, parameters, tools +from policyengine_core.periods.instant_ import Instant from . import config, helpers, AtInstantLike, Parameter, ParameterNodeAtInstant @@ -18,7 +19,7 @@ class ParameterNode(AtInstantLike): ] = None # By default, no restriction on the keys def __init__( - self, name="", directory_path=None, data=None, file_path=None + self, name: str = "", directory_path: str = None, data: dict = None, file_path: str = None ): """ Instantiate a ParameterNode either from a dict, (using `data`), or from a directory containing YAML files (using `directory_path`). @@ -121,7 +122,7 @@ def __init__( ) self.add_child(child_name, child) - def merge(self, other): + def merge(self, other: "ParameterNode") -> None: """ Merges another ParameterNode into the current node. @@ -130,7 +131,7 @@ def merge(self, other): for child_name, child in other.children.items(): self.add_child(child_name, child) - def add_child(self, name, child): + def add_child(self, name: str, child: Union[ParameterNode, Parameter]): """ Add a new child to the node. @@ -154,7 +155,7 @@ def add_child(self, name, child): self.children[name] = child setattr(self, name, child) - def __repr__(self): + def __repr__(self) -> str: result = os.linesep.join( [ os.linesep.join(["{}:", "{}"]).format( @@ -165,7 +166,7 @@ def __repr__(self): ) return result - def get_descendants(self): + def get_descendants(self) -> Iterable[Union[ParameterNode, Parameter]]: """ Return a generator containing all the parameters and nodes recursively contained in this `ParameterNode` """ @@ -173,7 +174,7 @@ def get_descendants(self): yield child yield from child.get_descendants() - def clone(self): + def clone(self) -> "ParameterNode": clone = commons.empty_clone(self) clone.__dict__ = self.__dict__.copy() @@ -186,5 +187,5 @@ def clone(self): return clone - def _get_at_instant(self, instant): + def _get_at_instant(self, instant: Instant) -> ParameterNodeAtInstant: return ParameterNodeAtInstant(self.name, self, instant) diff --git a/policyengine_core/parameters/parameter_node_at_instant.py b/policyengine_core/parameters/parameter_node_at_instant.py index efa091ae..77eb66d8 100644 --- a/policyengine_core/parameters/parameter_node_at_instant.py +++ b/policyengine_core/parameters/parameter_node_at_instant.py @@ -1,11 +1,15 @@ import os import sys +from typing import Iterable, Union, TYPE_CHECKING import numpy from policyengine_core import parameters, tools from policyengine_core.errors import ParameterNotFoundError from policyengine_core.parameters import helpers +if TYPE_CHECKING: + from policyengine_core.parameters.parameter_node import ParameterNode +from policyengine_core.parameters.vectorial_parameter_node_at_instant import VectorialParameterNodeAtInstant class ParameterNodeAtInstant: @@ -13,7 +17,7 @@ class ParameterNodeAtInstant: Parameter node of the legislation, at a given instant. """ - def __init__(self, name, node, instant_str): + def __init__(self, name: str, node: "ParameterNode", instant_str: str): """ :param name: Name of the node. :param node: Original :any:`ParameterNode` instance. @@ -30,15 +34,15 @@ def __init__(self, name, node, instant_str): if child_at_instant is not None: self.add_child(child_name, child_at_instant) - def add_child(self, child_name, child_at_instant): + def add_child(self, child_name: str, child_at_instant: "ParameterNodeAtInstant"): self._children[child_name] = child_at_instant setattr(self, child_name, child_at_instant) - def __getattr__(self, key): + def __getattr__(self, key: str): param_name = helpers._compose_name(self._name, item_name=key) raise ParameterNotFoundError(param_name, self._instant_str) - def __getitem__(self, key): + def __getitem__(self, key: str) -> Union["ParameterNodeAtInstant", VectorialParameterNodeAtInstant]: # If fancy indexing is used, cast to a vectorial node if isinstance(key, numpy.ndarray): return parameters.VectorialParameterNodeAtInstant.build_from_node( @@ -46,10 +50,10 @@ def __getitem__(self, key): )[key] return self._children[key] - def __iter__(self): + def __iter__(self) -> Iterable: return iter(self._children) - def __repr__(self): + def __repr__(self) -> str: result = os.linesep.join( [ os.linesep.join(["{}:", "{}"]).format( diff --git a/policyengine_core/parameters/parameter_scale.py b/policyengine_core/parameters/parameter_scale.py index 9b655d16..2989c954 100644 --- a/policyengine_core/parameters/parameter_scale.py +++ b/policyengine_core/parameters/parameter_scale.py @@ -1,15 +1,17 @@ import copy import os import typing - +from typing import Any, Iterable from policyengine_core import commons, parameters, tools from policyengine_core.errors import ParameterParsingError from policyengine_core.parameters import config, helpers, AtInstantLike +from policyengine_core.periods.instant_ import Instant from policyengine_core.taxscales import ( LinearAverageRateTaxScale, MarginalAmountTaxScale, MarginalRateTaxScale, SingleAmountTaxScale, + TaxScaleLike, ) @@ -21,7 +23,7 @@ class ParameterScale(AtInstantLike): # 'unit' and 'reference' are only listed here for backward compatibility _allowed_keys = config.COMMON_KEYS.union({"brackets"}) - def __init__(self, name, data, file_path): + def __init__(self, name: str, data: dict, file_path: str): """ :param name: name of the scale, eg "taxes.some_scale" :param data: Data loaded from a YAML file. In case of a reform, the data can also be created dynamically. @@ -54,13 +56,13 @@ def __init__(self, name, data, file_path): brackets.append(bracket) self.brackets: typing.List[parameters.ParameterScaleBracket] = brackets - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: if isinstance(key, int) and key < len(self.brackets): return self.brackets[key] else: raise KeyError(key) - def __repr__(self): + def __repr__(self) -> str: return os.linesep.join( ["brackets:"] + [ @@ -69,10 +71,10 @@ def __repr__(self): ] ) - def get_descendants(self): + def get_descendants(self) -> Iterable: return iter(()) - def clone(self): + def clone(self) -> "ParameterScale": clone = commons.empty_clone(self) clone.__dict__ = self.__dict__.copy() @@ -81,7 +83,7 @@ def clone(self): return clone - def _get_at_instant(self, instant): + def _get_at_instant(self, instant: Instant) -> TaxScaleLike: brackets = [ bracket.get_at_instant(instant) for bracket in self.brackets ] diff --git a/policyengine_core/parameters/values_history.py b/policyengine_core/parameters/values_history.py deleted file mode 100644 index 0d0f87bc..00000000 --- a/policyengine_core/parameters/values_history.py +++ /dev/null @@ -1,9 +0,0 @@ -from policyengine_core.parameters import Parameter - - -class ValuesHistory(Parameter): - """ - Only for backward compatibility. - """ - - pass diff --git a/policyengine_core/parameters/vectorial_parameter_node_at_instant.py b/policyengine_core/parameters/vectorial_parameter_node_at_instant.py index d6345914..c273d681 100644 --- a/policyengine_core/parameters/vectorial_parameter_node_at_instant.py +++ b/policyengine_core/parameters/vectorial_parameter_node_at_instant.py @@ -1,9 +1,12 @@ +from typing import Any, TYPE_CHECKING import numpy - +from numpy.typing import ArrayLike from policyengine_core import parameters from policyengine_core.errors import ParameterNotFoundError from policyengine_core.enums import Enum, EnumArray from policyengine_core.parameters import helpers +if TYPE_CHECKING: + from policyengine_core.parameters.parameter_node import ParameterNode class VectorialParameterNodeAtInstant: @@ -13,7 +16,7 @@ class VectorialParameterNodeAtInstant: """ @staticmethod - def build_from_node(node): + def build_from_node(node: "ParameterNode") -> "VectorialParameterNodeAtInstant": VectorialParameterNodeAtInstant.check_node_vectorisable(node) subnodes_name = node._children.keys() # Recursively vectorize the children of the node @@ -51,7 +54,7 @@ def build_from_node(node): ) @staticmethod - def check_node_vectorisable(node): + def check_node_vectorisable(node: "ParameterNode") -> None: """ Check that a node can be casted to a vectorial node, in order to be able to use fancy indexing. """ @@ -163,19 +166,19 @@ def check_nodes_homogeneous(named_nodes): check_nodes_homogeneous(extract_named_children(node)) - def __init__(self, name, vector, instant_str): + def __init__(self, name: str, vector: ArrayLike, instant_str: str): self.vector = vector self._name = name self._instant_str = instant_str - def __getattr__(self, attribute): + def __getattr__(self, attribute: str) -> Any: result = getattr(self.vector, attribute) if isinstance(result, numpy.recarray): return VectorialParameterNodeAtInstant(result) return result - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: # If the key is a string, just get the subnode if isinstance(key, str): return self.__getattr__(key) diff --git a/policyengine_core/taxbenefitsystems/__init__.py b/policyengine_core/taxbenefitsystems/__init__.py index 5cb34ba1..a8803708 100644 --- a/policyengine_core/taxbenefitsystems/__init__.py +++ b/policyengine_core/taxbenefitsystems/__init__.py @@ -22,8 +22,8 @@ # See: https://www.python.org/dev/peps/pep-0008/#imports from policyengine_core.errors import ( - VariableNameConflict, - VariableNotFound, + VariableNameConflictError, + VariableNotFoundError, ) from .tax_benefit_system import TaxBenefitSystem diff --git a/policyengine_core/tools/test_runner.py b/policyengine_core/tools/test_runner.py index fe0b0f61..4e1a3706 100644 --- a/policyengine_core/tools/test_runner.py +++ b/policyengine_core/tools/test_runner.py @@ -11,7 +11,7 @@ from policyengine_core.tools import assert_near from policyengine_core.simulations import SimulationBuilder -from policyengine_core.errors import SituationParsingError, VariableNotFound +from policyengine_core.errors import SituationParsingError, VariableNotFoundError from policyengine_core.warnings import LibYAMLWarning @@ -193,7 +193,7 @@ def runtest(self): self.simulation = builder.build_from_dict( self.tax_benefit_system, input ) - except (VariableNotFound, SituationParsingError): + except (VariableNotFoundError, SituationParsingError): raise except Exception as e: error_message = os.linesep.join( @@ -266,7 +266,7 @@ def check_output(self): entity_index, ) else: - raise VariableNotFound(key, self.tax_benefit_system) + raise VariableNotFoundError(key, self.tax_benefit_system) def check_variable( self, variable_name, expected_value, period, entity_index=None @@ -313,7 +313,7 @@ def should_ignore_variable(self, variable_name): def repr_failure(self, excinfo): if not isinstance( excinfo.value, - (AssertionError, VariableNotFound, SituationParsingError), + (AssertionError, VariableNotFoundError, SituationParsingError), ): return super(YamlItem, self).repr_failure(excinfo) diff --git a/tests/core/parameter_validation/filesystem_hierarchy/node1/param.yaml b/tests/core/parameter_validation/filesystem_hierarchy/node1/param.yaml index 2d444472..9e9117b3 100644 --- a/tests/core/parameter_validation/filesystem_hierarchy/node1/param.yaml +++ b/tests/core/parameter_validation/filesystem_hierarchy/node1/param.yaml @@ -1,6 +1,7 @@ description: Some parameter -unit: /1 -reference: http://legifrance.fr/rsa +metadata: + unit: /1 + reference: http://legifrance.fr/rsa values: 2015-12-01: value: 1.0 diff --git a/tests/core/parameter_validation/yaml_hierarchy/node1.yaml b/tests/core/parameter_validation/yaml_hierarchy/node1.yaml index f4097785..178717e9 100644 --- a/tests/core/parameter_validation/yaml_hierarchy/node1.yaml +++ b/tests/core/parameter_validation/yaml_hierarchy/node1.yaml @@ -1,7 +1,7 @@ param: description : A dummy description - reference: A dummy reference values: 2015-12-01: value: 1.0 - reference: A dummy reference + metadata: + reference: A dummy reference diff --git a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py index dcc881aa..4bc71f5b 100644 --- a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py +++ b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py @@ -11,7 +11,7 @@ from policyengine_core.parameters import ( ParameterNode, Parameter, - ParameterNotFound, + ParameterNotFoundError, ) from policyengine_core.model_api import * @@ -94,7 +94,7 @@ def test_triple_fancy_indexing(): def test_wrong_key(): zone = np.asarray(["z1", "z2", "z2", "toto"]) - with pytest.raises(ParameterNotFound) as e: + with pytest.raises(ParameterNotFoundError) as e: P.single.owner[zone] assert "'rate.single.owner.toto' was not found" in get_message(e.value) diff --git a/tests/core/tax_scales/test_marginal_amount_tax_scale.py b/tests/core/tax_scales/test_marginal_amount_tax_scale.py index a166f0f9..74497a0d 100644 --- a/tests/core/tax_scales/test_marginal_amount_tax_scale.py +++ b/tests/core/tax_scales/test_marginal_amount_tax_scale.py @@ -37,7 +37,7 @@ def test_calc(): # TODO: move, as we're testing Scale, not MarginalAmountTaxScale def test_dispatch_scale_type_on_creation(data): - scale = parameters.Scale("amount_scale", data, "") + scale = parameters.ParameterScale("amount_scale", data, "") first_jan = periods.Instant((2017, 11, 1)) result = scale.get_at_instant(first_jan) diff --git a/tests/core/tax_scales/test_single_amount_tax_scale.py b/tests/core/tax_scales/test_single_amount_tax_scale.py index febc9143..c69a2fe0 100644 --- a/tests/core/tax_scales/test_single_amount_tax_scale.py +++ b/tests/core/tax_scales/test_single_amount_tax_scale.py @@ -51,7 +51,7 @@ def test_to_dict(): # TODO: move, as we're testing Scale, not SingleAmountTaxScale def test_assign_thresholds_on_creation(data): - scale = parameters.Scale("amount_scale", data, "") + scale = parameters.ParameterScale("amount_scale", data, "") first_jan = periods.Instant((2017, 11, 1)) scale_at_instant = scale.get_at_instant(first_jan) @@ -62,7 +62,7 @@ def test_assign_thresholds_on_creation(data): # TODO: move, as we're testing Scale, not SingleAmountTaxScale def test_assign_amounts_on_creation(data): - scale = parameters.Scale("amount_scale", data, "") + scale = parameters.ParameterScale("amount_scale", data, "") first_jan = periods.Instant((2017, 11, 1)) scale_at_instant = scale.get_at_instant(first_jan) @@ -73,7 +73,7 @@ def test_assign_amounts_on_creation(data): # TODO: move, as we're testing Scale, not SingleAmountTaxScale def test_dispatch_scale_type_on_creation(data): - scale = parameters.Scale("amount_scale", data, "") + scale = parameters.ParameterScale("amount_scale", data, "") first_jan = periods.Instant((2017, 11, 1)) result = scale.get_at_instant(first_jan) diff --git a/tests/core/test_parameters.py b/tests/core/test_parameters.py index e6dc1b03..58ec7766 100644 --- a/tests/core/test_parameters.py +++ b/tests/core/test_parameters.py @@ -3,7 +3,7 @@ import pytest from policyengine_core.parameters import ( - ParameterNotFound, + ParameterNotFoundError, ParameterNode, ParameterNodeAtInstant, load_parameter_file, @@ -39,7 +39,7 @@ def test_param_values(tax_benefit_system): def test_param_before_it_is_defined(tax_benefit_system): - with pytest.raises(ParameterNotFound): + with pytest.raises(ParameterNotFoundError): tax_benefit_system.get_parameters_at_instant( "1997-12-31" ).taxes.income_tax_rate @@ -65,7 +65,7 @@ def test_stopped_parameter_before_end_value(tax_benefit_system): def test_stopped_parameter_after_end_value(tax_benefit_system): - with pytest.raises(ParameterNotFound): + with pytest.raises(ParameterNotFoundError): tax_benefit_system.get_parameters_at_instant( "2016-12-01" ).benefits.housing_allowance diff --git a/tests/core/test_reforms.py b/tests/core/test_reforms.py index 784146dd..70a9643c 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -5,7 +5,7 @@ from policyengine_core import periods from policyengine_core.periods import Instant from policyengine_core.tools import assert_near -from policyengine_core.parameters import ValuesHistory, ParameterNode +from policyengine_core.parameters import ParameterNode from policyengine_core.country_template.entities import Household, Person from policyengine_core.model_api import * @@ -141,196 +141,6 @@ def apply(self): assert str(reform_simulation.calculate("birth", None)[0]) == "1970-01-01" -def test_update_items(): - def check_update_items( - description, - value_history, - start_instant, - stop_instant, - value, - expected_items, - ): - value_history.update( - period=None, start=start_instant, stop=stop_instant, value=value - ) - assert value_history == expected_items - - check_update_items( - "Replace an item by a new item", - ValuesHistory( - "dummy_name", - {"2013-01-01": {"value": 0.0}, "2014-01-01": {"value": None}}, - ), - periods.period(2013).start, - periods.period(2013).stop, - 1.0, - ValuesHistory( - "dummy_name", - {"2013-01-01": {"value": 1.0}, "2014-01-01": {"value": None}}, - ), - ) - check_update_items( - "Replace an item by a new item in a list of items, the last being open", - ValuesHistory( - "dummy_name", - { - "2014-01-01": {"value": 9.53}, - "2015-01-01": {"value": 9.61}, - "2016-01-01": {"value": 9.67}, - }, - ), - periods.period(2015).start, - periods.period(2015).stop, - 1.0, - ValuesHistory( - "dummy_name", - { - "2014-01-01": {"value": 9.53}, - "2015-01-01": {"value": 1.0}, - "2016-01-01": {"value": 9.67}, - }, - ), - ) - check_update_items( - "Open the stop instant to the future", - ValuesHistory( - "dummy_name", - {"2013-01-01": {"value": 0.0}, "2014-01-01": {"value": None}}, - ), - periods.period(2013).start, - None, # stop instant - 1.0, - ValuesHistory("dummy_name", {"2013-01-01": {"value": 1.0}}), - ) - check_update_items( - "Insert a new item in the middle of an existing item", - ValuesHistory( - "dummy_name", - {"2010-01-01": {"value": 0.0}, "2014-01-01": {"value": None}}, - ), - periods.period(2011).start, - periods.period(2011).stop, - 1.0, - ValuesHistory( - "dummy_name", - { - "2010-01-01": {"value": 0.0}, - "2011-01-01": {"value": 1.0}, - "2012-01-01": {"value": 0.0}, - "2014-01-01": {"value": None}, - }, - ), - ) - check_update_items( - "Insert a new open item coming after the last open item", - ValuesHistory( - "dummy_name", - {"2006-01-01": {"value": 0.055}, "2014-01-01": {"value": 0.14}}, - ), - periods.period(2015).start, - None, # stop instant - 1.0, - ValuesHistory( - "dummy_name", - { - "2006-01-01": {"value": 0.055}, - "2014-01-01": {"value": 0.14}, - "2015-01-01": {"value": 1.0}, - }, - ), - ) - check_update_items( - "Insert a new item starting at the same date than the last open item", - ValuesHistory( - "dummy_name", - {"2006-01-01": {"value": 0.055}, "2014-01-01": {"value": 0.14}}, - ), - periods.period(2014).start, - periods.period(2014).stop, - 1.0, - ValuesHistory( - "dummy_name", - { - "2006-01-01": {"value": 0.055}, - "2014-01-01": {"value": 1.0}, - "2015-01-01": {"value": 0.14}, - }, - ), - ) - check_update_items( - "Insert a new open item starting at the same date than the last open item", - ValuesHistory( - "dummy_name", - {"2006-01-01": {"value": 0.055}, "2014-01-01": {"value": 0.14}}, - ), - periods.period(2014).start, - None, # stop instant - 1.0, - ValuesHistory( - "dummy_name", - {"2006-01-01": {"value": 0.055}, "2014-01-01": {"value": 1.0}}, - ), - ) - check_update_items( - "Insert a new item coming before the first item", - ValuesHistory( - "dummy_name", - {"2006-01-01": {"value": 0.055}, "2014-01-01": {"value": 0.14}}, - ), - periods.period(2005).start, - periods.period(2005).stop, - 1.0, - ValuesHistory( - "dummy_name", - { - "2005-01-01": {"value": 1.0}, - "2006-01-01": {"value": 0.055}, - "2014-01-01": {"value": 0.14}, - }, - ), - ) - check_update_items( - "Insert a new item coming before the first item with a hole", - ValuesHistory( - "dummy_name", - {"2006-01-01": {"value": 0.055}, "2014-01-01": {"value": 0.14}}, - ), - periods.period(2003).start, - periods.period(2003).stop, - 1.0, - ValuesHistory( - "dummy_name", - { - "2003-01-01": {"value": 1.0}, - "2004-01-01": {"value": None}, - "2006-01-01": {"value": 0.055}, - "2014-01-01": {"value": 0.14}, - }, - ), - ) - check_update_items( - "Insert a new open item starting before the start date of the first item", - ValuesHistory( - "dummy_name", - {"2006-01-01": {"value": 0.055}, "2014-01-01": {"value": 0.14}}, - ), - periods.period(2005).start, - None, # stop instant - 1.0, - ValuesHistory("dummy_name", {"2005-01-01": {"value": 1.0}}), - ) - check_update_items( - "Insert a new open item starting at the same date than the first item", - ValuesHistory( - "dummy_name", - {"2006-01-01": {"value": 0.055}, "2014-01-01": {"value": 0.14}}, - ), - periods.period(2006).start, - None, # stop instant - 1.0, - ValuesHistory("dummy_name", {"2006-01-01": {"value": 1.0}}), - ) - def test_add_variable(make_simulation, tax_benefit_system): class new_variable(Variable): diff --git a/tests/core/tools/test_runner/test_yaml_runner.py b/tests/core/tools/test_runner/test_yaml_runner.py index c32c5395..78236653 100644 --- a/tests/core/tools/test_runner/test_yaml_runner.py +++ b/tests/core/tools/test_runner/test_yaml_runner.py @@ -9,7 +9,7 @@ YamlItem, YamlFile, ) -from policyengine_core.errors import VariableNotFound +from policyengine_core.errors import VariableNotFoundError from policyengine_core.variables import Variable from policyengine_core.populations import Population from policyengine_core.entities import Entity @@ -89,7 +89,7 @@ def __init__(self): def test_variable_not_found(): test = {"output": {"unknown_variable": 0}} - with pytest.raises(VariableNotFound) as excinfo: + with pytest.raises(VariableNotFoundError) as excinfo: test_item = TestItem(test) test_item.check_output() assert excinfo.value.variable_name == "unknown_variable" From 0052bfe29e8617e20f0a35fa789b10bd2848c55d Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 03:27:15 +0100 Subject: [PATCH 12/25] Up to periods --- docs/_toc.yml | 1 + docs/python_api/periods.md | 33 +++++++++++++++++++ policyengine_core/periods/__init__.py | 23 ------------- policyengine_core/periods/instant_.py | 16 +++++----- policyengine_core/periods/period_.py | 39 ++++++++++++----------- policyengine_core/populations/__init__.py | 23 ------------- 6 files changed, 63 insertions(+), 72 deletions(-) create mode 100644 docs/python_api/periods.md diff --git a/docs/_toc.yml b/docs/_toc.yml index db5eb25a..e42731d7 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -13,6 +13,7 @@ parts: - file: python_api/extension_template - file: python_api/holders - file: python_api/parameters + - file: python_api/periods - caption: Contributing chapters: - file: contributing/intro diff --git a/docs/python_api/periods.md b/docs/python_api/periods.md new file mode 100644 index 00000000..e10f39c0 --- /dev/null +++ b/docs/python_api/periods.md @@ -0,0 +1,33 @@ +# Periods + +The `policyengine_core.periods` module contains classes defining `Instant` (a point in time) and `Period` (an interval of time). + +## Instant + +```{eval-rst} +.. autoclass:: policyengine_core.periods.instant_.Instant + :members: + :undoc-members: + :show-inheritance: +``` + +## Period + +```{eval-rst} +.. autoclass:: policyengine_core.periods.period_.Period + :members: + :undoc-members: + :show-inheritance: +``` + +## instant + +```{eval-rst} +.. autofunction:: policyengine_core.periods.helpers.instant +``` + +## period + +```{eval-rst} +.. autofunction:: policyengine_core.periods.helpers.period +``` diff --git a/policyengine_core/periods/__init__.py b/policyengine_core/periods/__init__.py index 27e4209e..ba415e62 100644 --- a/policyengine_core/periods/__init__.py +++ b/policyengine_core/periods/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from .config import ( DAY, MONTH, diff --git a/policyengine_core/periods/instant_.py b/policyengine_core/periods/instant_.py index 2a190787..d226c558 100644 --- a/policyengine_core/periods/instant_.py +++ b/policyengine_core/periods/instant_.py @@ -6,7 +6,7 @@ class Instant(tuple): - def __repr__(self): + def __repr__(self) -> str: """ Transform instant to to its Python representation as a string. @@ -21,7 +21,7 @@ def __repr__(self): self.__class__.__name__, super(Instant, self).__repr__() ) - def __str__(self): + def __str__(self) -> str: """ Transform instant to a string. @@ -41,7 +41,7 @@ def __str__(self): return instant_str @property - def date(self): + def date(self) -> datetime.date: """ Convert instant to a date. @@ -60,7 +60,7 @@ def date(self): return instant_date @property - def day(self): + def day(self) -> int: """ Extract day from instant. @@ -74,7 +74,7 @@ def day(self): return self[2] @property - def month(self): + def month(self) -> int: """ Extract month from instant. @@ -87,7 +87,7 @@ def month(self): """ return self[1] - def period(self, unit, size=1): + def period(self, unit: str, size: int = 1): """ Create a new period starting at instant. @@ -108,7 +108,7 @@ def period(self, unit, size=1): ), "Invalid size: {} of type {}".format(size, type(size)) return periods.Period((unit, self, size)) - def offset(self, offset, unit): + def offset(self, offset: int, unit: str) -> "Instant": """ Increment (or decrement) the given instant with offset units. @@ -252,7 +252,7 @@ def offset(self, offset, unit): return self.__class__((year, month, day)) @property - def year(self): + def year(self) -> int: """ Extract year from instant. diff --git a/policyengine_core/periods/period_.py b/policyengine_core/periods/period_.py index e224e7a9..6310d72f 100644 --- a/policyengine_core/periods/period_.py +++ b/policyengine_core/periods/period_.py @@ -1,9 +1,12 @@ from __future__ import annotations import calendar +from datetime import datetime +from typing import List from policyengine_core import periods from policyengine_core.periods import config, helpers +from policyengine_core.periods.instant_ import Instant class Period(tuple): @@ -16,7 +19,7 @@ class Period(tuple): Since a period is a triple it can be used as a dictionary key. """ - def __repr__(self): + def __repr__(self) -> str: """ Transform period to to its Python representation as a string. @@ -31,7 +34,7 @@ def __repr__(self): self.__class__.__name__, super(Period, self).__repr__() ) - def __str__(self): + def __str__(self) -> str: """ Transform period to a string. @@ -95,14 +98,14 @@ def __str__(self): return "{}:{}-{:02d}:{}".format(unit, year, month, size) @property - def date(self): + def date(self) -> datetime.date: assert ( self.size == 1 ), '"date" is undefined for a period of size > 1: {}'.format(self) return self.start.date @property - def days(self): + def days(self) -> int: """ Count the number of days in period. @@ -129,7 +132,7 @@ def days(self): """ return (self.stop.date - self.start.date).days + 1 - def intersection(self, start, stop): + def intersection(self, start: Instant, stop: Instant): if start is None and stop is None: return self period_start = self[1] @@ -187,7 +190,7 @@ def intersection(self, start, stop): ) ) - def get_subperiods(self, unit): + def get_subperiods(self, unit: str) -> List["Period"]: """ Return the list of all the periods of unit ``unit`` contained in self. @@ -221,7 +224,7 @@ def get_subperiods(self, unit): for i in range(self.size_in_days) ] - def offset(self, offset, unit=None): + def offset(self, offset: int, unit: str = None) -> "Period": """ Increment (or decrement) the given period with offset units. @@ -371,7 +374,7 @@ def contains(self, other: Period) -> bool: return self.start <= other.start and self.stop >= other.stop @property - def size(self): + def size(self) -> int: """ Return the size of the period. @@ -381,7 +384,7 @@ def size(self): return self[2] @property - def size_in_months(self): + def size_in_months(self) -> int: """ Return the size of the period in months. @@ -399,7 +402,7 @@ def size_in_months(self): ) @property - def size_in_days(self): + def size_in_days(self) -> int: """ Return the size of the period in days. @@ -498,35 +501,35 @@ def stop(self) -> periods.Instant: return periods.Instant((year, month, day)) @property - def unit(self): + def unit(self) -> str: return self[0] # Reference periods @property - def last_3_months(self): + def last_3_months(self) -> "Period": return self.first_month.start.period("month", 3).offset(-3) @property - def last_month(self): + def last_month(self) -> "Period": return self.first_month.offset(-1) @property - def last_year(self): + def last_year(self) -> "Period": return self.start.offset("first-of", "year").period("year").offset(-1) @property - def n_2(self): + def n_2(self) -> "Period": return self.start.offset("first-of", "year").period("year").offset(-2) @property - def this_year(self): + def this_year(self) -> "Period": return self.start.offset("first-of", "year").period("year") @property - def first_month(self): + def first_month(self) -> "Period": return self.start.offset("first-of", "month").period("month") @property - def first_day(self): + def first_day(self) -> "Period": return self.start.period("day") diff --git a/policyengine_core/populations/__init__.py b/policyengine_core/populations/__init__.py index 9b3f827a..eaf4b2c9 100644 --- a/policyengine_core/populations/__init__.py +++ b/policyengine_core/populations/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from policyengine_core.projectors import ( Projector, EntityToPersonProjector, From bd877e77f0f5bfb35abe4e21a364efd6b8f3986f Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 03:43:23 +0100 Subject: [PATCH 13/25] Add populations --- docs/_toc.yml | 1 + docs/python_api/populations.md | 21 ++++++++ .../populations/group_population.py | 49 ++++++++++--------- policyengine_core/populations/population.py | 37 +++++++------- 4 files changed, 67 insertions(+), 41 deletions(-) create mode 100644 docs/python_api/populations.md diff --git a/docs/_toc.yml b/docs/_toc.yml index e42731d7..041b9e69 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -14,6 +14,7 @@ parts: - file: python_api/holders - file: python_api/parameters - file: python_api/periods + - file: python_api/populations - caption: Contributing chapters: - file: contributing/intro diff --git a/docs/python_api/populations.md b/docs/python_api/populations.md new file mode 100644 index 00000000..f44bb88d --- /dev/null +++ b/docs/python_api/populations.md @@ -0,0 +1,21 @@ +# Populations + +`policyengine_core.populations` contains classes defining `Population` (a set of people) and `GroupPopulation` (a set of groups of people). + +## Population + +```{eval-rst} +.. autoclass:: policyengine_core.populations.population.Population + :members: + :undoc-members: + :show-inheritance: +``` + +## GroupPopulation + +```{eval-rst} +.. autoclass:: policyengine_core.populations.group_population.GroupPopulation + :members: + :undoc-members: + :show-inheritance: +``` diff --git a/policyengine_core/populations/group_population.py b/policyengine_core/populations/group_population.py index 97fa41d1..63e43f06 100644 --- a/policyengine_core/populations/group_population.py +++ b/policyengine_core/populations/group_population.py @@ -1,15 +1,16 @@ import typing - +from typing import Callable, Any import numpy - +from numpy.typing import ArrayLike from policyengine_core import projectors -from policyengine_core.entities import Role +from policyengine_core.entities import Role, Entity from policyengine_core.enums import EnumArray from policyengine_core.populations import Population +from policyengine_core.simulations.simulation import Simulation class GroupPopulation(Population): - def __init__(self, entity, members): + def __init__(self, entity: Entity, members: Population): super().__init__(entity) self.members = members self._members_entity_id = None @@ -17,7 +18,7 @@ def __init__(self, entity, members): self._members_position = None self._ordered_members_map = None - def clone(self, simulation): + def clone(self, simulation: Simulation) -> "GroupPopulation": result = GroupPopulation(self.entity, self.members) result.simulation = simulation result._holders = { @@ -33,7 +34,7 @@ def clone(self, simulation): return result @property - def members_position(self): + def members_position(self) -> ArrayLike: if ( self._members_position is None and self.members_entity_id is not None @@ -51,19 +52,19 @@ def members_position(self): return self._members_position @members_position.setter - def members_position(self, members_position): + def members_position(self, members_position: ArrayLike) -> None: self._members_position = members_position @property - def members_entity_id(self): + def members_entity_id(self) -> ArrayLike: return self._members_entity_id @members_entity_id.setter - def members_entity_id(self, members_entity_id): + def members_entity_id(self, members_entity_id: ArrayLike) -> None: self._members_entity_id = members_entity_id @property - def members_role(self): + def members_role(self) -> ArrayLike: if self._members_role is None: default_role = self.entity.flattened_roles[0] self._members_role = numpy.repeat( @@ -72,12 +73,12 @@ def members_role(self): return self._members_role @members_role.setter - def members_role(self, members_role: typing.Iterable[Role]): + def members_role(self, members_role: ArrayLike): if members_role is not None: self._members_role = numpy.array(list(members_role)) @property - def ordered_members_map(self): + def ordered_members_map(self) -> ArrayLike: """ Mask to group the persons by entity This function only caches the map value, to see what the map is used for, see value_nth_person method. @@ -86,7 +87,7 @@ def ordered_members_map(self): self._ordered_members_map = numpy.argsort(self.members_entity_id) return self._ordered_members_map - def get_role(self, role_name): + def get_role(self, role_name: str) -> Role: return next( ( role @@ -99,7 +100,7 @@ def get_role(self, role_name): # Aggregation persons -> entity @projectors.projectable - def sum(self, array, role=None): + def sum(self, array: ArrayLike, role: Role = None) -> ArrayLike: """ Return the sum of ``array`` for the members of the entity. @@ -126,7 +127,7 @@ def sum(self, array, role=None): return numpy.bincount(self.members_entity_id, weights=array) @projectors.projectable - def any(self, array, role=None): + def any(self, array: ArrayLike, role: Role = None) -> ArrayLike: """ Return ``True`` if ``array`` is ``True`` for any members of the entity. @@ -144,7 +145,7 @@ def any(self, array, role=None): return sum_in_entity > 0 @projectors.projectable - def reduce(self, array, reducer, neutral_element, role=None): + def reduce(self, array: ArrayLike, reducer: Callable, neutral_element: Any, role: Role = None) -> ArrayLike: self.members.check_array_compatible_with_entity(array) self.entity.check_role_validity(role) position_in_entity = self.members_position @@ -168,7 +169,7 @@ def reduce(self, array, reducer, neutral_element, role=None): return result @projectors.projectable - def all(self, array, role=None): + def all(self, array: ArrayLike, role: Role = None) -> ArrayLike: """ Return ``True`` if ``array`` is ``True`` for all members of the entity. @@ -187,7 +188,7 @@ def all(self, array, role=None): ) @projectors.projectable - def max(self, array, role=None): + def max(self, array: ArrayLike, role: Role = None) -> ArrayLike: """ Return the maximum value of ``array`` for the entity members. @@ -209,7 +210,7 @@ def max(self, array, role=None): ) @projectors.projectable - def min(self, array, role=None): + def min(self, array: ArrayLike, role: Role = None) -> ArrayLike: """ Return the minimum value of ``array`` for the entity members. @@ -233,7 +234,7 @@ def min(self, array, role=None): ) @projectors.projectable - def nb_persons(self, role=None): + def nb_persons(self, role: Role = None) -> ArrayLike: """ Returns the number of persons contained in the entity. @@ -253,7 +254,7 @@ def nb_persons(self, role=None): # Projection person -> entity @projectors.projectable - def value_from_person(self, array, role, default=0): + def value_from_person(self, array: ArrayLike, role: Role, default: Any = 0) -> ArrayLike: """ Get the value of ``array`` for the person with the unique role ``role``. @@ -283,7 +284,7 @@ def value_from_person(self, array, role, default=0): return result @projectors.projectable - def value_nth_person(self, n, array, default=0): + def value_nth_person(self, n: int, array: ArrayLike, default: Any = 0) -> ArrayLike: """ Get the value of array for the person whose position in the entity is n. @@ -310,12 +311,12 @@ def value_nth_person(self, n, array, default=0): return result @projectors.projectable - def value_from_first_person(self, array): + def value_from_first_person(self, array: ArrayLike): return self.value_nth_person(0, array) # Projection entity -> person(s) - def project(self, array, role=None): + def project(self, array: ArrayLike, role: Role = None) -> ArrayLike: self.check_array_compatible_with_entity(array) self.entity.check_role_validity(role) if role is None: diff --git a/policyengine_core/populations/population.py b/policyengine_core/populations/population.py index 9427a004..f83fb703 100644 --- a/policyengine_core/populations/population.py +++ b/policyengine_core/populations/population.py @@ -1,22 +1,25 @@ import traceback +from typing import Any, List import numpy - +from numpy.typing import ArrayLike from policyengine_core import projectors from policyengine_core.holders import Holder +from policyengine_core.periods.period_ import Period from policyengine_core.populations import config from policyengine_core.projectors import Projector - +from policyengine_core.entities import Entity, Role +from policyengine_core.simulations import Simulation class Population: - def __init__(self, entity): - self.simulation = None + def __init__(self, entity: Entity): + self.simulation: Simulation = None self.entity = entity self._holders = {} self.count = 0 self.ids = [] - def clone(self, simulation): + def clone(self, simulation: Simulation) -> "Population": result = Population(self.entity) result.simulation = simulation result._holders = { @@ -27,13 +30,13 @@ def clone(self, simulation): result.ids = self.ids return result - def empty_array(self): + def empty_array(self) -> numpy.ndarray: return numpy.zeros(self.count) - def filled_array(self, value, dtype=None): + def filled_array(self, value: Any, dtype: Any = None) -> numpy.ndarray: return numpy.full(self.count, value, dtype) - def __getattr__(self, attribute): + def __getattr__(self, attribute: str) -> Any: projector = projectors.get_projector_from_shortcut(self, attribute) if not projector: raise AttributeError( @@ -43,12 +46,12 @@ def __getattr__(self, attribute): ) return projector - def get_index(self, id): + def get_index(self, id: str) -> int: return self.ids.index(id) # Calculations - def check_array_compatible_with_entity(self, array): + def check_array_compatible_with_entity(self, array: numpy.ndarray) -> None: if not self.count == array.size: raise ValueError( "Input {} is not a valid value for the entity {} (size = {} != {} = count)".format( @@ -56,7 +59,7 @@ def check_array_compatible_with_entity(self, array): ) ) - def check_period_validity(self, variable_name, period): + def check_period_validity(self, variable_name: str, period: Period) -> None: if period is None: stack = traceback.extract_stack() filename, line_number, function_name, line_of_code = stack[-3] @@ -72,7 +75,7 @@ def check_period_validity(self, variable_name, period): ) ) - def __call__(self, variable_name, period=None, options=None): + def __call__(self, variable_name: str, period: Period = None, options: dict = None): """ Calculate the variable ``variable_name`` for the entity and the period ``period``, using the variable formula if it exists. @@ -106,7 +109,7 @@ def __call__(self, variable_name, period=None, options=None): # Helpers - def get_holder(self, variable_name): + def get_holder(self, variable_name: str) -> Holder: self.entity.check_variable_defined_for_entity(variable_name) holder = self._holders.get(variable_name) if holder: @@ -115,7 +118,7 @@ def get_holder(self, variable_name): self._holders[variable_name] = holder = Holder(variable, self) return holder - def get_memory_usage(self, variables=None): + def get_memory_usage(self, variables: List[str] = None): holders_memory_usage = { variable_name: holder.get_memory_usage() for variable_name, holder in self._holders.items() @@ -132,7 +135,7 @@ def get_memory_usage(self, variables=None): ) @projectors.projectable - def has_role(self, role): + def has_role(self, role: Role) -> ArrayLike: """ Check if a person has a given role within its `GroupEntity` @@ -154,7 +157,7 @@ def has_role(self, role): return group_population.members_role == role @projectors.projectable - def value_from_partner(self, array, entity, role): + def value_from_partner(self, array: ArrayLike, entity: Entity, role: Role) -> ArrayLike: self.check_array_compatible_with_entity(array) self.entity.check_role_validity(role) @@ -173,7 +176,7 @@ def value_from_partner(self, array, entity, role): ) @projectors.projectable - def get_rank(self, entity, criteria, condition=True): + def get_rank(self, entity: Entity, criteria: ArrayLike, condition: ArrayLike = True) -> ArrayLike: """ Get the rank of a person within an entity according to a criteria. The person with rank 0 has the minimum value of criteria. From b795cadbb1e8cac1a9059dec632f575e93667f60 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 03:43:44 +0100 Subject: [PATCH 14/25] Apply formatting --- policyengine_core/__init__.py | 2 +- .../data_storage/in_memory_storage.py | 1 + .../data_storage/on_disk_storage.py | 5 ++++- policyengine_core/entities/group_entity.py | 10 +++++++++- policyengine_core/entities/helpers.py | 3 ++- policyengine_core/entities/role.py | 1 + policyengine_core/holders/helpers.py | 8 ++++++-- policyengine_core/holders/holder.py | 5 ++++- .../parameters/parameter_at_instant.py | 7 ++++++- policyengine_core/parameters/parameter_node.py | 6 +++++- .../parameters/parameter_node_at_instant.py | 13 ++++++++++--- .../vectorial_parameter_node_at_instant.py | 5 ++++- .../populations/group_population.py | 16 +++++++++++++--- policyengine_core/populations/population.py | 17 +++++++++++++---- .../taxbenefitsystems/tax_benefit_system.py | 2 +- policyengine_core/tools/test_runner.py | 7 +++++-- policyengine_core/variables/variable.py | 9 +++++++-- tests/core/test_reforms.py | 1 - tests/core/test_yaml.py | 8 +++++++- 19 files changed, 99 insertions(+), 27 deletions(-) diff --git a/policyengine_core/__init__.py b/policyengine_core/__init__.py index 7220358c..7699255b 100644 --- a/policyengine_core/__init__.py +++ b/policyengine_core/__init__.py @@ -1,2 +1,2 @@ from policyengine_core.simulations import SimulationBuilder -from policyengine_core.taxbenefitsystems import TaxBenefitSystem \ No newline at end of file +from policyengine_core.taxbenefitsystems import TaxBenefitSystem diff --git a/policyengine_core/data_storage/in_memory_storage.py b/policyengine_core/data_storage/in_memory_storage.py index d4efacaf..7216b84b 100644 --- a/policyengine_core/data_storage/in_memory_storage.py +++ b/policyengine_core/data_storage/in_memory_storage.py @@ -9,6 +9,7 @@ class InMemoryStorage: """ Low-level class responsible for storing and retrieving calculated vectors in memory """ + _arrays: Dict[Period, ArrayLike] is_eternal: bool diff --git a/policyengine_core/data_storage/on_disk_storage.py b/policyengine_core/data_storage/on_disk_storage.py index a16416bc..a692da4b 100644 --- a/policyengine_core/data_storage/on_disk_storage.py +++ b/policyengine_core/data_storage/on_disk_storage.py @@ -14,7 +14,10 @@ class OnDiskStorage: """ def __init__( - self, storage_dir: str, is_eternal: bool = False, preserve_storage_dir: bool = False + self, + storage_dir: str, + is_eternal: bool = False, + preserve_storage_dir: bool = False, ): self._files = {} self._enums = {} diff --git a/policyengine_core/entities/group_entity.py b/policyengine_core/entities/group_entity.py index d90554c1..43d46f52 100644 --- a/policyengine_core/entities/group_entity.py +++ b/policyengine_core/entities/group_entity.py @@ -25,7 +25,15 @@ class GroupEntity(Entity): """ - def __init__(self, key: str, plural: str, label: str, doc: str, roles: List[str], containing_entities: List[str] = ()): + def __init__( + self, + key: str, + plural: str, + label: str, + doc: str, + roles: List[str], + containing_entities: List[str] = (), + ): super().__init__(key, plural, label, doc) self.roles_description = roles self.roles = [] diff --git a/policyengine_core/entities/helpers.py b/policyengine_core/entities/helpers.py index 42ec22f1..a7a0245a 100644 --- a/policyengine_core/entities/helpers.py +++ b/policyengine_core/entities/helpers.py @@ -2,11 +2,12 @@ from policyengine_core import entities from policyengine_core.entities.entity import Entity + def build_entity( key: str, plural: str, label: str, - doc: str= "", + doc: str = "", roles: List[str] = None, is_person: bool = False, containing_entities: List[str] = (), diff --git a/policyengine_core/entities/role.py b/policyengine_core/entities/role.py index d19e3ad4..4b569f5a 100644 --- a/policyengine_core/entities/role.py +++ b/policyengine_core/entities/role.py @@ -5,6 +5,7 @@ class Role: """ The type of the relation between an entity instance and a group entity instance. """ + def __init__(self, description, entity): self.entity = entity self.key = description["key"] diff --git a/policyengine_core/holders/helpers.py b/policyengine_core/holders/helpers.py index 66fe36a2..30cf724a 100644 --- a/policyengine_core/holders/helpers.py +++ b/policyengine_core/holders/helpers.py @@ -9,7 +9,9 @@ log = logging.getLogger(__name__) -def set_input_dispatch_by_period(holder: Holder, period: Period, array: ArrayLike): +def set_input_dispatch_by_period( + holder: Holder, period: Period, array: ArrayLike +): """ This function can be declared as a ``set_input`` attribute of a variable. @@ -46,7 +48,9 @@ def set_input_dispatch_by_period(holder: Holder, period: Period, array: ArrayLik sub_period = sub_period.offset(1) -def set_input_divide_by_period(holder: Holder, period: Period, array: ArrayLike): +def set_input_divide_by_period( + holder: Holder, period: Period, array: ArrayLike +): """ This function can be declared as a ``set_input`` attribute of a variable. diff --git a/policyengine_core/holders/holder.py b/policyengine_core/holders/holder.py index 0195993a..a2b21dd1 100644 --- a/policyengine_core/holders/holder.py +++ b/policyengine_core/holders/holder.py @@ -9,6 +9,7 @@ from policyengine_core.data_storage import InMemoryStorage, OnDiskStorage from policyengine_core.enums import Enum from policyengine_core.periods import Period + if TYPE_CHECKING: from policyengine_core.variables import Variable from policyengine_core.populations import Population @@ -60,7 +61,9 @@ def clone(self, population: "Population") -> "Holder": return new - def create_disk_storage(self, directory: str = None, preserve: bool = False) -> OnDiskStorage: + def create_disk_storage( + self, directory: str = None, preserve: bool = False + ) -> OnDiskStorage: if directory is None: directory = self.simulation.data_storage_dir storage_dir = os.path.join(directory, self.variable.name) diff --git a/policyengine_core/parameters/parameter_at_instant.py b/policyengine_core/parameters/parameter_at_instant.py index 94fdfcbb..e1dd319d 100644 --- a/policyengine_core/parameters/parameter_at_instant.py +++ b/policyengine_core/parameters/parameter_at_instant.py @@ -14,7 +14,12 @@ class ParameterAtInstant: _allowed_keys = set(["value", "metadata"]) def __init__( - self, name: str, instant_str: str, data: dict = None, file_path: str = None, metadata: dict = None + self, + name: str, + instant_str: str, + data: dict = None, + file_path: str = None, + metadata: dict = None, ): """ :param str name: name of the parameter, e.g. "taxes.some_tax.some_param" diff --git a/policyengine_core/parameters/parameter_node.py b/policyengine_core/parameters/parameter_node.py index 3d3b27f7..66e469ea 100644 --- a/policyengine_core/parameters/parameter_node.py +++ b/policyengine_core/parameters/parameter_node.py @@ -19,7 +19,11 @@ class ParameterNode(AtInstantLike): ] = None # By default, no restriction on the keys def __init__( - self, name: str = "", directory_path: str = None, data: dict = None, file_path: str = None + self, + name: str = "", + directory_path: str = None, + data: dict = None, + file_path: str = None, ): """ Instantiate a ParameterNode either from a dict, (using `data`), or from a directory containing YAML files (using `directory_path`). diff --git a/policyengine_core/parameters/parameter_node_at_instant.py b/policyengine_core/parameters/parameter_node_at_instant.py index 77eb66d8..c8316a65 100644 --- a/policyengine_core/parameters/parameter_node_at_instant.py +++ b/policyengine_core/parameters/parameter_node_at_instant.py @@ -7,9 +7,12 @@ from policyengine_core import parameters, tools from policyengine_core.errors import ParameterNotFoundError from policyengine_core.parameters import helpers + if TYPE_CHECKING: from policyengine_core.parameters.parameter_node import ParameterNode -from policyengine_core.parameters.vectorial_parameter_node_at_instant import VectorialParameterNodeAtInstant +from policyengine_core.parameters.vectorial_parameter_node_at_instant import ( + VectorialParameterNodeAtInstant, +) class ParameterNodeAtInstant: @@ -34,7 +37,9 @@ def __init__(self, name: str, node: "ParameterNode", instant_str: str): if child_at_instant is not None: self.add_child(child_name, child_at_instant) - def add_child(self, child_name: str, child_at_instant: "ParameterNodeAtInstant"): + def add_child( + self, child_name: str, child_at_instant: "ParameterNodeAtInstant" + ): self._children[child_name] = child_at_instant setattr(self, child_name, child_at_instant) @@ -42,7 +47,9 @@ def __getattr__(self, key: str): param_name = helpers._compose_name(self._name, item_name=key) raise ParameterNotFoundError(param_name, self._instant_str) - def __getitem__(self, key: str) -> Union["ParameterNodeAtInstant", VectorialParameterNodeAtInstant]: + def __getitem__( + self, key: str + ) -> Union["ParameterNodeAtInstant", VectorialParameterNodeAtInstant]: # If fancy indexing is used, cast to a vectorial node if isinstance(key, numpy.ndarray): return parameters.VectorialParameterNodeAtInstant.build_from_node( diff --git a/policyengine_core/parameters/vectorial_parameter_node_at_instant.py b/policyengine_core/parameters/vectorial_parameter_node_at_instant.py index c273d681..4a478c03 100644 --- a/policyengine_core/parameters/vectorial_parameter_node_at_instant.py +++ b/policyengine_core/parameters/vectorial_parameter_node_at_instant.py @@ -5,6 +5,7 @@ from policyengine_core.errors import ParameterNotFoundError from policyengine_core.enums import Enum, EnumArray from policyengine_core.parameters import helpers + if TYPE_CHECKING: from policyengine_core.parameters.parameter_node import ParameterNode @@ -16,7 +17,9 @@ class VectorialParameterNodeAtInstant: """ @staticmethod - def build_from_node(node: "ParameterNode") -> "VectorialParameterNodeAtInstant": + def build_from_node( + node: "ParameterNode", + ) -> "VectorialParameterNodeAtInstant": VectorialParameterNodeAtInstant.check_node_vectorisable(node) subnodes_name = node._children.keys() # Recursively vectorize the children of the node diff --git a/policyengine_core/populations/group_population.py b/policyengine_core/populations/group_population.py index 63e43f06..e0482460 100644 --- a/policyengine_core/populations/group_population.py +++ b/policyengine_core/populations/group_population.py @@ -145,7 +145,13 @@ def any(self, array: ArrayLike, role: Role = None) -> ArrayLike: return sum_in_entity > 0 @projectors.projectable - def reduce(self, array: ArrayLike, reducer: Callable, neutral_element: Any, role: Role = None) -> ArrayLike: + def reduce( + self, + array: ArrayLike, + reducer: Callable, + neutral_element: Any, + role: Role = None, + ) -> ArrayLike: self.members.check_array_compatible_with_entity(array) self.entity.check_role_validity(role) position_in_entity = self.members_position @@ -254,7 +260,9 @@ def nb_persons(self, role: Role = None) -> ArrayLike: # Projection person -> entity @projectors.projectable - def value_from_person(self, array: ArrayLike, role: Role, default: Any = 0) -> ArrayLike: + def value_from_person( + self, array: ArrayLike, role: Role, default: Any = 0 + ) -> ArrayLike: """ Get the value of ``array`` for the person with the unique role ``role``. @@ -284,7 +292,9 @@ def value_from_person(self, array: ArrayLike, role: Role, default: Any = 0) -> A return result @projectors.projectable - def value_nth_person(self, n: int, array: ArrayLike, default: Any = 0) -> ArrayLike: + def value_nth_person( + self, n: int, array: ArrayLike, default: Any = 0 + ) -> ArrayLike: """ Get the value of array for the person whose position in the entity is n. diff --git a/policyengine_core/populations/population.py b/policyengine_core/populations/population.py index f83fb703..d52e31c5 100644 --- a/policyengine_core/populations/population.py +++ b/policyengine_core/populations/population.py @@ -11,6 +11,7 @@ from policyengine_core.entities import Entity, Role from policyengine_core.simulations import Simulation + class Population: def __init__(self, entity: Entity): self.simulation: Simulation = None @@ -59,7 +60,9 @@ def check_array_compatible_with_entity(self, array: numpy.ndarray) -> None: ) ) - def check_period_validity(self, variable_name: str, period: Period) -> None: + def check_period_validity( + self, variable_name: str, period: Period + ) -> None: if period is None: stack = traceback.extract_stack() filename, line_number, function_name, line_of_code = stack[-3] @@ -75,7 +78,9 @@ def check_period_validity(self, variable_name: str, period: Period) -> None: ) ) - def __call__(self, variable_name: str, period: Period = None, options: dict = None): + def __call__( + self, variable_name: str, period: Period = None, options: dict = None + ): """ Calculate the variable ``variable_name`` for the entity and the period ``period``, using the variable formula if it exists. @@ -157,7 +162,9 @@ def has_role(self, role: Role) -> ArrayLike: return group_population.members_role == role @projectors.projectable - def value_from_partner(self, array: ArrayLike, entity: Entity, role: Role) -> ArrayLike: + def value_from_partner( + self, array: ArrayLike, entity: Entity, role: Role + ) -> ArrayLike: self.check_array_compatible_with_entity(array) self.entity.check_role_validity(role) @@ -176,7 +183,9 @@ def value_from_partner(self, array: ArrayLike, entity: Entity, role: Role) -> Ar ) @projectors.projectable - def get_rank(self, entity: Entity, criteria: ArrayLike, condition: ArrayLike = True) -> ArrayLike: + def get_rank( + self, entity: Entity, criteria: ArrayLike, condition: ArrayLike = True + ) -> ArrayLike: """ Get the rank of a person within an entity according to a criteria. The person with rank 0 has the minimum value of criteria. diff --git a/policyengine_core/taxbenefitsystems/tax_benefit_system.py b/policyengine_core/taxbenefitsystems/tax_benefit_system.py index 4fed8fed..95689a75 100644 --- a/policyengine_core/taxbenefitsystems/tax_benefit_system.py +++ b/policyengine_core/taxbenefitsystems/tax_benefit_system.py @@ -541,6 +541,6 @@ def entities_plural(self): def entities_by_singular(self): return {entity.key: entity for entity in self.entities} - + def test(self, paths: str, verbose: bool = False) -> None: run_tests(self, paths, options=dict(verbose=verbose)) diff --git a/policyengine_core/tools/test_runner.py b/policyengine_core/tools/test_runner.py index 4e1a3706..c8fada50 100644 --- a/policyengine_core/tools/test_runner.py +++ b/policyengine_core/tools/test_runner.py @@ -11,7 +11,10 @@ from policyengine_core.tools import assert_near from policyengine_core.simulations import SimulationBuilder -from policyengine_core.errors import SituationParsingError, VariableNotFoundError +from policyengine_core.errors import ( + SituationParsingError, + VariableNotFoundError, +) from policyengine_core.warnings import LibYAMLWarning @@ -89,7 +92,7 @@ def run_tests(tax_benefit_system, paths, options=None): if not isinstance(paths, list): paths = [paths] - + if not isinstance(paths[0], str): paths = [str(path) for path in paths] diff --git a/policyengine_core/variables/variable.py b/policyengine_core/variables/variable.py index d9de2571..429df5a0 100644 --- a/policyengine_core/variables/variable.py +++ b/policyengine_core/variables/variable.py @@ -13,6 +13,7 @@ from . import config, helpers + class QuantityType: STOCK = "stock" FLOW = "flow" @@ -97,7 +98,7 @@ class Variable: .. attribute:: documentation Free multilines text field describing the variable context and usage. - + .. attribute:: quantity_type Categorical attribute describing whether the variable is a stock or a flow. @@ -229,7 +230,11 @@ def set( attribute_name, self.name ) ) - if required and allowed_values is not None and value not in allowed_values: + if ( + required + and allowed_values is not None + and value not in allowed_values + ): raise ValueError( "Invalid value '{}' for attribute '{}' in variable '{}'. Allowed values are '{}'.".format( value, attribute_name, self.name, allowed_values diff --git a/tests/core/test_reforms.py b/tests/core/test_reforms.py index 70a9643c..4d731822 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -141,7 +141,6 @@ def apply(self): assert str(reform_simulation.calculate("birth", None)[0]) == "1970-01-01" - def test_add_variable(make_simulation, tax_benefit_system): class new_variable(Variable): value_type = int diff --git a/tests/core/test_yaml.py b/tests/core/test_yaml.py index 3f66e431..287711d4 100644 --- a/tests/core/test_yaml.py +++ b/tests/core/test_yaml.py @@ -122,7 +122,13 @@ def test_shell_script(): def test_failing_shell_script(): yaml_path = os.path.join(yaml_tests_dir, "test_failure.yaml") - command = ["policyengine-core", "test", yaml_path, "-c", "openfisca_dummy_country"] + command = [ + "policyengine-core", + "test", + yaml_path, + "-c", + "openfisca_dummy_country", + ] with open(os.devnull, "wb") as devnull: with pytest.raises(subprocess.CalledProcessError): subprocess.check_call(command, stdout=devnull, stderr=devnull) From 80dad88fcdc9a44f069b1f45405303822b3ce51b Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 22:15:05 +0100 Subject: [PATCH 15/25] Add projectors --- docs/_toc.yml | 8 +++-- docs/python_api/intro.md | 3 -- docs/python_api/projectors.md | 32 +++++++++++++++++++ .../populations/group_population.py | 8 ++--- policyengine_core/projectors/__init__.py | 23 ------------- .../projectors/entity_to_person_projector.py | 8 +++-- .../first_person_to_entity_projector.py | 5 ++- policyengine_core/projectors/projector.py | 11 ++++--- .../unique_role_to_entity_projector.py | 2 +- 9 files changed, 59 insertions(+), 41 deletions(-) delete mode 100644 docs/python_api/intro.md create mode 100644 docs/python_api/projectors.md diff --git a/docs/_toc.yml b/docs/_toc.yml index 041b9e69..816b472c 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -1,6 +1,9 @@ format: jb-book root: intro parts: + - caption: Contributing + chapters: + - file: contributing/intro - caption: Python API chapters: - file: python_api/commons @@ -15,6 +18,5 @@ parts: - file: python_api/parameters - file: python_api/periods - file: python_api/populations - - caption: Contributing - chapters: - - file: contributing/intro + - file: python_api/projectors + diff --git a/docs/python_api/intro.md b/docs/python_api/intro.md deleted file mode 100644 index 8e957979..00000000 --- a/docs/python_api/intro.md +++ /dev/null @@ -1,3 +0,0 @@ -# Using PolicyEngine Core - -This chapter contains documentation for individual modules in the PolicyEngine Core source code. diff --git a/docs/python_api/projectors.md b/docs/python_api/projectors.md new file mode 100644 index 00000000..12d89928 --- /dev/null +++ b/docs/python_api/projectors.md @@ -0,0 +1,32 @@ +# Projectors + +The `policyengine_core.projectors` module contains classes that allow formulas to ask relational questions about entities. For example, a formula for a child benefit for a particular child might require the earnings of said child's parent. + +## Projector + +```{eval-rst} +.. autoclass:: policyengine_core.projectors.Projector + :members: +``` + +## EntityToPersonProjector + +```{eval-rst} +.. autoclass:: policyengine_core.projectors.EntityToPersonProjector + :members: +``` + +## FirstPersonToEntityProjector + +```{eval-rst} +.. autoclass:: policyengine_core.projectors.FirstPersonToEntityProjector + :members: +``` + +## UniqueRoleToEntityProjector + +```{eval-rst} +.. autoclass:: policyengine_core.projectors.UniqueRoleToEntityProjector + :members: +``` + diff --git a/policyengine_core/populations/group_population.py b/policyengine_core/populations/group_population.py index e0482460..f3a3a0f6 100644 --- a/policyengine_core/populations/group_population.py +++ b/policyengine_core/populations/group_population.py @@ -12,10 +12,10 @@ class GroupPopulation(Population): def __init__(self, entity: Entity, members: Population): super().__init__(entity) - self.members = members - self._members_entity_id = None - self._members_role = None - self._members_position = None + self.members: Population = members + self._members_entity_id: ArrayLike = None + self._members_role: ArrayLike = None + self._members_position: ArrayLike = None self._ordered_members_map = None def clone(self, simulation: Simulation) -> "GroupPopulation": diff --git a/policyengine_core/projectors/__init__.py b/policyengine_core/projectors/__init__.py index 898bef76..ac980ff7 100644 --- a/policyengine_core/projectors/__init__.py +++ b/policyengine_core/projectors/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from .helpers import projectable, get_projector_from_shortcut from .projector import Projector from .entity_to_person_projector import EntityToPersonProjector diff --git a/policyengine_core/projectors/entity_to_person_projector.py b/policyengine_core/projectors/entity_to_person_projector.py index a4c47f01..4c3dde46 100644 --- a/policyengine_core/projectors/entity_to_person_projector.py +++ b/policyengine_core/projectors/entity_to_person_projector.py @@ -1,12 +1,16 @@ +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from policyengine_core.populations import Population from policyengine_core.projectors import Projector +from numpy.typing import ArrayLike class EntityToPersonProjector(Projector): """For instance person.family.""" - def __init__(self, entity, parent=None): + def __init__(self, entity: "Population", parent: Projector = None): self.reference_entity = entity self.parent = parent - def transform(self, result): + def transform(self, result: ArrayLike) -> ArrayLike: return self.reference_entity.project(result) diff --git a/policyengine_core/projectors/first_person_to_entity_projector.py b/policyengine_core/projectors/first_person_to_entity_projector.py index ad3d8d4b..365c6454 100644 --- a/policyengine_core/projectors/first_person_to_entity_projector.py +++ b/policyengine_core/projectors/first_person_to_entity_projector.py @@ -1,10 +1,13 @@ +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from policyengine_core.populations import GroupPopulation from policyengine_core.projectors import Projector class FirstPersonToEntityProjector(Projector): """For instance famille.first_person.""" - def __init__(self, entity, parent=None): + def __init__(self, entity: "GroupPopulation", parent: Projector = None): self.target_entity = entity self.reference_entity = entity.members self.parent = parent diff --git a/policyengine_core/projectors/projector.py b/policyengine_core/projectors/projector.py index 130a4e78..89277de1 100644 --- a/policyengine_core/projectors/projector.py +++ b/policyengine_core/projectors/projector.py @@ -1,9 +1,12 @@ from policyengine_core.projectors import helpers - +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from policyengine_core.populations import Population +from numpy.typing import ArrayLike class Projector: - reference_entity = None - parent = None + reference_entity: "Population" = None + parent: "Projector" = None def __getattr__(self, attribute): projector = helpers.get_projector_from_shortcut( @@ -33,5 +36,5 @@ def transform_and_bubble_up(self, result): else: return self.parent.transform_and_bubble_up(transformed_result) - def transform(self, result): + def transform(self, result: ArrayLike): return NotImplementedError() diff --git a/policyengine_core/projectors/unique_role_to_entity_projector.py b/policyengine_core/projectors/unique_role_to_entity_projector.py index b2d2ea94..164a29ae 100644 --- a/policyengine_core/projectors/unique_role_to_entity_projector.py +++ b/policyengine_core/projectors/unique_role_to_entity_projector.py @@ -4,7 +4,7 @@ class UniqueRoleToEntityProjector(Projector): """For instance famille.declarant_principal.""" - def __init__(self, entity, role, parent=None): + def __init__(self, entity, role, parent = None): self.target_entity = entity self.reference_entity = entity.members self.parent = parent From 4ab630e5819f7e044098993badf2e45b19dcfda4 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 22:52:38 +0100 Subject: [PATCH 16/25] Add simulations --- .coverage | Bin 602112 -> 688128 bytes docs/_toc.yml | 4 +- docs/python_api/reforms.md | 10 + docs/python_api/scripts.md | 3 + docs/python_api/simulations.md | 21 ++ .../populations/group_population.py | 8 +- policyengine_core/populations/population.py | 10 +- policyengine_core/reforms/__init__.py | 23 -- policyengine_core/reforms/reform.py | 9 +- policyengine_core/scripts/__init__.py | 2 - .../scripts/find_placeholders.py | 65 ---- .../measure_numpy_condition_notations.py | 143 --------- .../scripts/measure_performances.py | 287 ------------------ .../measure_performances_fancy_indexing.py | 110 ------- .../scripts/migrations/__init__.py | 0 .../migrations/v16_2_to_v17/__init__.py | 0 .../migrations/v16_2_to_v17/legislation.xsd | 102 ------- .../xml_to_yaml_country_template.py | 36 --- .../xml_to_yaml_extension_template.py | 30 -- .../scripts/migrations/v24_to_25.py | 166 ---------- policyengine_core/scripts/remove_fuzzy.py | 217 ------------- .../scripts/simulation_generator.py | 26 -- policyengine_core/simulations/__init__.py | 23 -- policyengine_core/simulations/simulation.py | 89 +++--- .../simulations/simulation_builder.py | 53 ++-- .../taxbenefitsystems/tax_benefit_system.py | 5 +- 26 files changed, 128 insertions(+), 1314 deletions(-) create mode 100644 docs/python_api/reforms.md create mode 100644 docs/python_api/scripts.md create mode 100644 docs/python_api/simulations.md delete mode 100644 policyengine_core/scripts/find_placeholders.py delete mode 100755 policyengine_core/scripts/measure_numpy_condition_notations.py delete mode 100644 policyengine_core/scripts/measure_performances.py delete mode 100644 policyengine_core/scripts/measure_performances_fancy_indexing.py delete mode 100644 policyengine_core/scripts/migrations/__init__.py delete mode 100644 policyengine_core/scripts/migrations/v16_2_to_v17/__init__.py delete mode 100644 policyengine_core/scripts/migrations/v16_2_to_v17/legislation.xsd delete mode 100644 policyengine_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py delete mode 100644 policyengine_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py delete mode 100644 policyengine_core/scripts/migrations/v24_to_25.py delete mode 100755 policyengine_core/scripts/remove_fuzzy.py diff --git a/.coverage b/.coverage index f1082db621f115b0606152ea5099ecf28fd4d0fa..6d16da367d212323e4aac31081184614d9a4ef26 100644 GIT binary patch delta 111653 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

ctH6hw4gjgRvwV{GZkXXkeNs5i-t1X7h&kx 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

s?0gZR8q6 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 diff --git a/docs/_toc.yml b/docs/_toc.yml index 816b472c..14c58b49 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -19,4 +19,6 @@ parts: - file: python_api/periods - file: python_api/populations - file: python_api/projectors - + - file: python_api/reforms + - file: python_api/scripts + - file: python_api/simulations diff --git a/docs/python_api/reforms.md b/docs/python_api/reforms.md new file mode 100644 index 00000000..3a6ff608 --- /dev/null +++ b/docs/python_api/reforms.md @@ -0,0 +1,10 @@ +# Reforms + +The `policyengine_core.reforms` module contains the definition for `Reform`, a class which can be used to edit a `TaxBenefitSystem` instance. + +## Reform + +```{eval-rst} +.. autoclass:: policyengine_core.reforms.Reform + :members: +``` diff --git a/docs/python_api/scripts.md b/docs/python_api/scripts.md new file mode 100644 index 00000000..c83d80e6 --- /dev/null +++ b/docs/python_api/scripts.md @@ -0,0 +1,3 @@ +# Scripts + +The `policyengine_core.scripts` module contains Python functions that power the command line interface, including the test runner. diff --git a/docs/python_api/simulations.md b/docs/python_api/simulations.md new file mode 100644 index 00000000..8e45978c --- /dev/null +++ b/docs/python_api/simulations.md @@ -0,0 +1,21 @@ +# Simulations + +The `policyengine_core.simulations` module contains the definition of `Simulation`, the singular most important class in the repo. `Simulations` combine the logic of a country package with data, and can use the country logic and parameters to calculate the values of unknown variables. The class `SimulationBuilder` can create `Simulation`s from a variety of inputs: JSON descriptions, or dataset arrays. + +## Simulation + +```{eval-rst} +.. autoclass:: policyengine_core.simulations.simulation.Simulation + :members: + :inherited-members: + :show-inheritance: +``` + +## SimulationBuilder + +```{eval-rst} +.. autoclass:: policyengine_core.simulations.simulation_builder.SimulationBuilder + :members: + :inherited-members: + :show-inheritance: +``` diff --git a/policyengine_core/populations/group_population.py b/policyengine_core/populations/group_population.py index f3a3a0f6..1521d860 100644 --- a/policyengine_core/populations/group_population.py +++ b/policyengine_core/populations/group_population.py @@ -1,12 +1,12 @@ -import typing -from typing import Callable, Any +from typing import Callable, Any, TYPE_CHECKING import numpy from numpy.typing import ArrayLike from policyengine_core import projectors from policyengine_core.entities import Role, Entity from policyengine_core.enums import EnumArray from policyengine_core.populations import Population -from policyengine_core.simulations.simulation import Simulation +if TYPE_CHECKING: + from policyengine_core.simulations import Simulation class GroupPopulation(Population): @@ -18,7 +18,7 @@ def __init__(self, entity: Entity, members: Population): self._members_position: ArrayLike = None self._ordered_members_map = None - def clone(self, simulation: Simulation) -> "GroupPopulation": + def clone(self, simulation: "Simulation") -> "GroupPopulation": result = GroupPopulation(self.entity, self.members) result.simulation = simulation result._holders = { diff --git a/policyengine_core/populations/population.py b/policyengine_core/populations/population.py index d52e31c5..34922e02 100644 --- a/policyengine_core/populations/population.py +++ b/policyengine_core/populations/population.py @@ -1,6 +1,5 @@ import traceback -from typing import Any, List - +from typing import Any, List, TYPE_CHECKING import numpy from numpy.typing import ArrayLike from policyengine_core import projectors @@ -9,18 +8,19 @@ from policyengine_core.populations import config from policyengine_core.projectors import Projector from policyengine_core.entities import Entity, Role -from policyengine_core.simulations import Simulation +if TYPE_CHECKING: + from policyengine_core.simulations import Simulation class Population: def __init__(self, entity: Entity): - self.simulation: Simulation = None + self.simulation: "Simulation" = None self.entity = entity self._holders = {} self.count = 0 self.ids = [] - def clone(self, simulation: Simulation) -> "Population": + def clone(self, simulation: "Simulation") -> "Population": result = Population(self.entity) result.simulation = simulation result._holders = { diff --git a/policyengine_core/reforms/__init__.py b/policyengine_core/reforms/__init__.py index e10f6446..abf21732 100644 --- a/policyengine_core/reforms/__init__.py +++ b/policyengine_core/reforms/__init__.py @@ -1,24 +1 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from .reform import Reform diff --git a/policyengine_core/reforms/reform.py b/policyengine_core/reforms/reform.py index 5846164d..d8768058 100644 --- a/policyengine_core/reforms/reform.py +++ b/policyengine_core/reforms/reform.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +from typing import Callable from policyengine_core.parameters import ParameterNode from policyengine_core.taxbenefitsystems import TaxBenefitSystem @@ -35,9 +36,9 @@ class Reform(TaxBenefitSystem): >>> self.modify_parameters(modifier_function = modify_my_parameters) """ - name = None + name: str = None - def __init__(self, baseline): + def __init__(self, baseline: TaxBenefitSystem): """ :param baseline: Baseline TaxBenefitSystem. """ @@ -60,7 +61,7 @@ def __getattr__(self, attribute): return getattr(self.baseline, attribute) @property - def full_key(self): + def full_key(self) -> str: key = self.key assert ( key is not None @@ -70,7 +71,7 @@ def full_key(self): key = ".".join([baseline_full_key, key]) return key - def modify_parameters(self, modifier_function): + def modify_parameters(self, modifier_function: Callable[[ParameterNode], ParameterNode]) -> None: """Make modifications on the parameters of the legislation. Call this function in `apply()` if the reform asks for legislation parameter modifications. diff --git a/policyengine_core/scripts/__init__.py b/policyengine_core/scripts/__init__.py index df8dbbd9..02338192 100644 --- a/policyengine_core/scripts/__init__.py +++ b/policyengine_core/scripts/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import traceback import importlib import logging diff --git a/policyengine_core/scripts/find_placeholders.py b/policyengine_core/scripts/find_placeholders.py deleted file mode 100644 index 2cd31c3c..00000000 --- a/policyengine_core/scripts/find_placeholders.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- -# flake8: noqa T001 - -import os -import fnmatch -import sys - -from bs4 import BeautifulSoup - - -def find_param_files(input_dir): - param_files = [] - for root, dirnames, filenames in os.walk(input_dir): - for filename in fnmatch.filter(filenames, "*.xml"): - param_files.append(os.path.join(root, filename)) - - return param_files - - -def find_placeholders(filename_input): - with open(filename_input, "r") as f: - xml_content = f.read() - - xml_parsed = BeautifulSoup(xml_content, "lxml-xml") - - placeholders = xml_parsed.find_all("PLACEHOLDER") - - output_list = [] - for placeholder in placeholders: - parent_list = list(placeholder.parents)[:-1] - path = ".".join( - [p.attrs["code"] for p in parent_list if "code" in p.attrs][::-1] - ) - - deb = placeholder.attrs["deb"] - - output_list.append((deb, path)) - - output_list = sorted(output_list, key=lambda x: x[0]) - - return output_list - - -if __name__ == "__main__": - print( - """find_placeholders.py : Find nodes PLACEHOLDER in xml parameter files -Usage : - python find_placeholders /dir/to/search -""" - ) - - assert len(sys.argv) == 2 - input_dir = sys.argv[1] - - param_files = find_param_files(input_dir) - - for filename_input in param_files: - output_list = find_placeholders(filename_input) - - print("File {}".format(filename_input)) - - for deb, path in output_list: - print("{} {}".format(deb, path)) - - print("\n") diff --git a/policyengine_core/scripts/measure_numpy_condition_notations.py b/policyengine_core/scripts/measure_numpy_condition_notations.py deleted file mode 100755 index b3ad207c..00000000 --- a/policyengine_core/scripts/measure_numpy_condition_notations.py +++ /dev/null @@ -1,143 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- -# flake8: noqa T001 - - -""" -Measure and compare different vectorial condition notations: -- using multiplication notation: (choice == 1) * choice_1_value + (choice == 2) * choice_2_value -- using np.select: the same than multiplication but more idiomatic like a "switch" control-flow statement -- using np.fromiter: iterates in Python over the array and calculates lazily only the required values - -The aim of this script is to compare the time taken by the calculation of the values -""" -from contextlib import contextmanager -import argparse -import sys -import time - -import numpy as np - - -args = None - - -@contextmanager -def measure_time(title): - t1 = time.time() - yield - t2 = time.time() - print("{}\t: {:.8f} seconds elapsed".format(title, t2 - t1)) - - -def switch_fromiter(conditions, function_by_condition, dtype): - value_by_condition = {} - - def get_or_store_value(condition): - if condition not in value_by_condition: - value = function_by_condition[condition]() - value_by_condition[condition] = value - return value_by_condition[condition] - - return np.fromiter( - (get_or_store_value(condition) for condition in conditions), - dtype, - ) - - -def switch_select(conditions, value_by_condition): - condlist = [ - conditions == condition for condition in value_by_condition.keys() - ] - return np.select(condlist, value_by_condition.values()) - - -def calculate_choice_1_value(): - time.sleep(args.calculate_time) - return 80 - - -def calculate_choice_2_value(): - time.sleep(args.calculate_time) - return 90 - - -def calculate_choice_3_value(): - time.sleep(args.calculate_time) - return 95 - - -def test_multiplication(choice): - choice_1_value = calculate_choice_1_value() - choice_2_value = calculate_choice_2_value() - choice_3_value = calculate_choice_3_value() - result = ( - (choice == 1) * choice_1_value - + (choice == 2) * choice_2_value - + (choice == 3) * choice_3_value - ) - return result - - -def test_switch_fromiter(choice): - result = switch_fromiter( - choice, - { - 1: calculate_choice_1_value, - 2: calculate_choice_2_value, - 3: calculate_choice_3_value, - }, - dtype=np.int, - ) - return result - - -def test_switch_select(choice): - choice_1_value = calculate_choice_1_value() - choice_2_value = calculate_choice_2_value() - choice_3_value = calculate_choice_2_value() - result = switch_select( - choice, - { - 1: choice_1_value, - 2: choice_2_value, - 3: choice_3_value, - }, - ) - return result - - -def test_all_notations(): - # choice is an array with 1 and 2 items like [2, 1, ..., 1, 2] - choice = np.random.randint(2, size=args.array_length) + 1 - - with measure_time("multiplication"): - test_multiplication(choice) - - with measure_time("switch_select"): - test_switch_select(choice) - - with measure_time("switch_fromiter"): - test_switch_fromiter(choice) - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--array-length", default=1000, type=int, help="length of the array" - ) - parser.add_argument( - "--calculate-time", - default=0.1, - type=float, - help="time taken by the calculation in seconds", - ) - global args - args = parser.parse_args() - - print(args) - test_all_notations() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/policyengine_core/scripts/measure_performances.py b/policyengine_core/scripts/measure_performances.py deleted file mode 100644 index 18adcad7..00000000 --- a/policyengine_core/scripts/measure_performances.py +++ /dev/null @@ -1,287 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- -# flake8: noqa T001 - - -"""Measure performances of a basic tax-benefit system to compare to other OpenFisca implementations.""" -import argparse -import logging -import sys -import time - -import numpy as np -from numpy.core.defchararray import startswith - -from policyengine_core import periods, simulations -from policyengine_core.periods import ETERNITY -from policyengine_core.entities import build_entity -from policyengine_core.variables import Variable -from policyengine_core.taxbenefitsystems import TaxBenefitSystem -from policyengine_core.tools import assert_near - - -args = None - - -def timeit(method): - def timed(*args, **kwargs): - start_time = time.time() - result = method(*args, **kwargs) - # print '%r (%r, %r) %2.9f s' % (method.__name__, args, kw, time.time() - start_time) - print("{:2.6f} s".format(time.time() - start_time)) - return result - - return timed - - -# Entities - -Famille = build_entity( - key="famille", - plural="familles", - label="Famille", - roles=[ - { - "key": "parent", - "plural": "parents", - "label": "Parents", - "subroles": ["demandeur", "conjoint"], - }, - { - "key": "enfant", - "plural": "enfants", - "label": "Enfants", - }, - ], -) - - -Individu = build_entity( - key="individu", - plural="individus", - label="Individu", - is_person=True, -) - -# Input variables - - -class age_en_mois(Variable): - value_type = int - entity = Individu - label = "Âge (en nombre de mois)" - - -class birth(Variable): - value_type = "Date" - entity = Individu - label = "Date de naissance" - - -class city_code(Variable): - value_type = "FixedStr" - max_length = 5 - entity = Famille - definition_period = ETERNITY - label = ( - """Code INSEE "city_code" de la commune de résidence de la famille""" - ) - - -class salaire_brut(Variable): - value_type = float - entity = Individu - label = "Salaire brut" - - -# Calculated variables - - -class age(Variable): - value_type = int - entity = Individu - label = "Âge (en nombre d'années)" - - def formula(self, simulation, period): - birth = simulation.get_array("birth", period) - if birth is None: - age_en_mois = simulation.get_array("age_en_mois", period) - if age_en_mois is not None: - return age_en_mois // 12 - birth = simulation.calculate("birth", period) - return (np.datetime64(period.date) - birth).astype("timedelta64[Y]") - - -class dom_tom(Variable): - value_type = "Bool" - entity = Famille - label = "La famille habite-t-elle les DOM-TOM ?" - - def formula(self, simulation, period): - period = period.start.period("year").offset("first-of") - city_code = simulation.calculate("city_code", period) - return np.logical_or( - startswith(city_code, "97"), startswith(city_code, "98") - ) - - -class revenu_disponible(Variable): - value_type = float - entity = Individu - label = "Revenu disponible de l'individu" - - def formula(self, simulation, period): - period = period.start.period("year").offset("first-of") - rsa = simulation.calculate("rsa", period) - salaire_imposable = simulation.calculate("salaire_imposable", period) - return rsa + salaire_imposable * 0.7 - - -class rsa(Variable): - value_type = float - entity = Individu - label = "RSA" - - def formula_2010_01_01(self, simulation, period): - period = period.start.period("month").offset("first-of") - salaire_imposable = simulation.calculate("salaire_imposable", period) - return (salaire_imposable < 500) * 100.0 - - def formula_2011_01_01(self, simulation, period): - period = period.start.period("month").offset("first-of") - salaire_imposable = simulation.calculate("salaire_imposable", period) - return (salaire_imposable < 500) * 200.0 - - def formula_2013_01_01(self, simulation, period): - period = period.start.period("month").offset("first-of") - salaire_imposable = simulation.calculate("salaire_imposable", period) - return (salaire_imposable < 500) * 300 - - -class salaire_imposable(Variable): - value_type = float - entity = Individu - label = "Salaire imposable" - - def formula(individu, period): - period = period.start.period("year").offset("first-of") - dom_tom = individu.famille("dom_tom", period) - salaire_net = individu("salaire_net", period) - return salaire_net * 0.9 - 100 * dom_tom - - -class salaire_net(Variable): - value_type = float - entity = Individu - label = "Salaire net" - - def formula(self, simulation, period): - period = period.start.period("year").offset("first-of") - salaire_brut = simulation.calculate("salaire_brut", period) - return salaire_brut * 0.8 - - -# TaxBenefitSystem instance declared after formulas - - -tax_benefit_system = TaxBenefitSystem([Famille, Individu]) -tax_benefit_system.add_variables( - age_en_mois, - birth, - city_code, - salaire_brut, - age, - dom_tom, - revenu_disponible, - rsa, - salaire_imposable, - salaire_net, -) - - -@timeit -def check_revenu_disponible(year, city_code, expected_revenu_disponible): - simulation = simulations.Simulation( - period=periods.period(year), tax_benefit_system=tax_benefit_system - ) - famille = simulation.populations["famille"] - famille.count = 3 - famille.roles_count = 2 - famille.step_size = 1 - individu = simulation.populations["individu"] - individu.count = 6 - individu.step_size = 2 - simulation.get_or_new_holder("city_code").array = np.array( - [city_code, city_code, city_code] - ) - famille.members_entity_id = np.array([0, 0, 1, 1, 2, 2]) - simulation.get_or_new_holder("salaire_brut").array = np.array( - [0.0, 0.0, 50000.0, 0.0, 100000.0, 0.0] - ) - revenu_disponible = simulation.calculate("revenu_disponible") - assert_near( - revenu_disponible, - expected_revenu_disponible, - absolute_error_margin=0.005, - ) - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "-v", - "--verbose", - action="store_true", - default=False, - help="increase output verbosity", - ) - global args - args = parser.parse_args() - logging.basicConfig( - level=logging.DEBUG if args.verbose else logging.WARNING, - stream=sys.stdout, - ) - - check_revenu_disponible( - 2009, "75101", np.array([0, 0, 25200, 0, 50400, 0]) - ) - check_revenu_disponible( - 2010, "75101", np.array([1200, 1200, 25200, 1200, 50400, 1200]) - ) - check_revenu_disponible( - 2011, "75101", np.array([2400, 2400, 25200, 2400, 50400, 2400]) - ) - check_revenu_disponible( - 2012, "75101", np.array([2400, 2400, 25200, 2400, 50400, 2400]) - ) - check_revenu_disponible( - 2013, "75101", np.array([3600, 3600, 25200, 3600, 50400, 3600]) - ) - - check_revenu_disponible( - 2009, "97123", np.array([-70.0, -70.0, 25130.0, -70.0, 50330.0, -70.0]) - ) - check_revenu_disponible( - 2010, - "97123", - np.array([1130.0, 1130.0, 25130.0, 1130.0, 50330.0, 1130.0]), - ) - check_revenu_disponible( - 2011, - "98456", - np.array([2330.0, 2330.0, 25130.0, 2330.0, 50330.0, 2330.0]), - ) - check_revenu_disponible( - 2012, - "98456", - np.array([2330.0, 2330.0, 25130.0, 2330.0, 50330.0, 2330.0]), - ) - check_revenu_disponible( - 2013, - "98456", - np.array([3530.0, 3530.0, 25130.0, 3530.0, 50330.0, 3530.0]), - ) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/policyengine_core/scripts/measure_performances_fancy_indexing.py b/policyengine_core/scripts/measure_performances_fancy_indexing.py deleted file mode 100644 index 95a96d77..00000000 --- a/policyengine_core/scripts/measure_performances_fancy_indexing.py +++ /dev/null @@ -1,110 +0,0 @@ -# flake8: noqa T001 - -import numpy as np -import timeit -from openfisca_france import CountryTaxBenefitSystem -from policyengine_core.model_api import * - - -tbs = CountryTaxBenefitSystem() -N = 200000 -al_plaf_acc = tbs.get_parameters_at_instant( - "2015-01-01" -).prestations.al_plaf_acc -zone_apl = np.random.choice([1, 2, 3], N) -al_nb_pac = np.random.choice(6, N) -couple = np.random.choice([True, False], N) -formatted_zone = concat( - "plafond_pour_accession_a_la_propriete_zone_", zone_apl -) # zone_apl returns 1, 2 or 3 but the parameters have a long name - - -def formula_with(): - plafonds = al_plaf_acc[formatted_zone] - - result = ( - plafonds.personne_isolee_sans_enfant * not_(couple) * (al_nb_pac == 0) - + plafonds.menage_seul * couple * (al_nb_pac == 0) - + plafonds.menage_ou_isole_avec_1_enfant * (al_nb_pac == 1) - + plafonds.menage_ou_isole_avec_2_enfants * (al_nb_pac == 2) - + plafonds.menage_ou_isole_avec_3_enfants * (al_nb_pac == 3) - + plafonds.menage_ou_isole_avec_4_enfants * (al_nb_pac == 4) - + plafonds.menage_ou_isole_avec_5_enfants * (al_nb_pac >= 5) - + plafonds.menage_ou_isole_par_enfant_en_plus - * (al_nb_pac > 5) - * (al_nb_pac - 5) - ) - - return result - - -def formula_without(): - z1 = al_plaf_acc.plafond_pour_accession_a_la_propriete_zone_1 - z2 = al_plaf_acc.plafond_pour_accession_a_la_propriete_zone_2 - z3 = al_plaf_acc.plafond_pour_accession_a_la_propriete_zone_3 - - return ( - (zone_apl == 1) - * ( - z1.personne_isolee_sans_enfant * not_(couple) * (al_nb_pac == 0) - + z1.menage_seul * couple * (al_nb_pac == 0) - + z1.menage_ou_isole_avec_1_enfant * (al_nb_pac == 1) - + z1.menage_ou_isole_avec_2_enfants * (al_nb_pac == 2) - + z1.menage_ou_isole_avec_3_enfants * (al_nb_pac == 3) - + z1.menage_ou_isole_avec_4_enfants * (al_nb_pac == 4) - + z1.menage_ou_isole_avec_5_enfants * (al_nb_pac >= 5) - + z1.menage_ou_isole_par_enfant_en_plus - * (al_nb_pac > 5) - * (al_nb_pac - 5) - ) - + (zone_apl == 2) - * ( - z2.personne_isolee_sans_enfant * not_(couple) * (al_nb_pac == 0) - + z2.menage_seul * couple * (al_nb_pac == 0) - + z2.menage_ou_isole_avec_1_enfant * (al_nb_pac == 1) - + z2.menage_ou_isole_avec_2_enfants * (al_nb_pac == 2) - + z2.menage_ou_isole_avec_3_enfants * (al_nb_pac == 3) - + z2.menage_ou_isole_avec_4_enfants * (al_nb_pac == 4) - + z2.menage_ou_isole_avec_5_enfants * (al_nb_pac >= 5) - + z2.menage_ou_isole_par_enfant_en_plus - * (al_nb_pac > 5) - * (al_nb_pac - 5) - ) - + (zone_apl == 3) - * ( - z3.personne_isolee_sans_enfant * not_(couple) * (al_nb_pac == 0) - + z3.menage_seul * couple * (al_nb_pac == 0) - + z3.menage_ou_isole_avec_1_enfant * (al_nb_pac == 1) - + z3.menage_ou_isole_avec_2_enfants * (al_nb_pac == 2) - + z3.menage_ou_isole_avec_3_enfants * (al_nb_pac == 3) - + z3.menage_ou_isole_avec_4_enfants * (al_nb_pac == 4) - + z3.menage_ou_isole_avec_5_enfants * (al_nb_pac >= 5) - + z3.menage_ou_isole_par_enfant_en_plus - * (al_nb_pac > 5) - * (al_nb_pac - 5) - ) - ) - - -if __name__ == "__main__": - - time_with = timeit.timeit( - "formula_with()", setup="from __main__ import formula_with", number=50 - ) - time_without = timeit.timeit( - "formula_without()", - setup="from __main__ import formula_without", - number=50, - ) - - print( - "Computing with dynamic legislation computing took {}".format( - time_with - ) - ) - print( - "Computing without dynamic legislation computing took {}".format( - time_without - ) - ) - print("Ratio: {}".format(time_with / time_without)) diff --git a/policyengine_core/scripts/migrations/__init__.py b/policyengine_core/scripts/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/policyengine_core/scripts/migrations/v16_2_to_v17/__init__.py b/policyengine_core/scripts/migrations/v16_2_to_v17/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/policyengine_core/scripts/migrations/v16_2_to_v17/legislation.xsd b/policyengine_core/scripts/migrations/v16_2_to_v17/legislation.xsd deleted file mode 100644 index aab781ff..00000000 --- a/policyengine_core/scripts/migrations/v16_2_to_v17/legislation.xsd +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/policyengine_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py b/policyengine_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py deleted file mode 100644 index 87248b10..00000000 --- a/policyengine_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- - -""" xml_to_yaml_country_template.py : Parse XML parameter files for Country-Template and convert them to YAML files. Comments are NOT transformed. - -Usage : - `python xml_to_yaml_country_template.py output_dir` -or just (output is written in a directory called `yaml_parameters`): - `python xml_to_yaml_country_template.py` -""" -import sys -import os - -from policyengine_core.country_template import ( - CountryTaxBenefitSystem, - COUNTRY_DIR, -) -from . import xml_to_yaml - -tax_benefit_system = CountryTaxBenefitSystem() - -if len(sys.argv) > 1: - target_path = sys.argv[1] -else: - target_path = "yaml_parameters" - -param_dir = os.path.join(COUNTRY_DIR, "parameters") -param_files = [ - "benefits.xml", - "general.xml", - "taxes.xml", -] -legislation_xml_info_list = [ - (os.path.join(param_dir, param_file), []) for param_file in param_files -] - -xml_to_yaml.write_parameters(legislation_xml_info_list, target_path) diff --git a/policyengine_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py b/policyengine_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py deleted file mode 100644 index f31a615f..00000000 --- a/policyengine_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- - -""" xml_to_yaml_extension_template.py : Parse XML parameter files for Extension-Template and convert them to YAML files. Comments are NOT transformed. - -Usage : - `python xml_to_yaml_extension_template.py output_dir` -or just (output is written in a directory called `yaml_parameters`): - `python xml_to_yaml_extension_template.py` -""" - -import sys -import os - -from . import xml_to_yaml -import policyengine_core.extension_template as openfisca_extension_template - -if len(sys.argv) > 1: - target_path = sys.argv[1] -else: - target_path = "yaml_parameters" - -param_dir = os.path.dirname(openfisca_extension_template.__file__) -param_files = [ - "parameters.xml", -] -legislation_xml_info_list = [ - (os.path.join(param_dir, param_file), []) for param_file in param_files -] - -xml_to_yaml.write_parameters(legislation_xml_info_list, target_path) diff --git a/policyengine_core/scripts/migrations/v24_to_25.py b/policyengine_core/scripts/migrations/v24_to_25.py deleted file mode 100644 index e489e492..00000000 --- a/policyengine_core/scripts/migrations/v24_to_25.py +++ /dev/null @@ -1,166 +0,0 @@ -# -*- coding: utf-8 -*- -# flake8: noqa T001 - -import argparse -import os -import glob - -from ruamel.yaml.comments import CommentedSeq - -from policyengine_core.scripts import ( - add_tax_benefit_system_arguments, - build_tax_benefit_system, -) - -from ruamel.yaml import YAML - -yaml = YAML() -yaml.default_flow_style = False -yaml.width = 4096 - -TEST_METADATA = { - "period", - "name", - "reforms", - "only_variables", - "ignore_variables", - "absolute_error_margin", - "relative_error_margin", - "description", - "keywords", -} - - -def build_parser(): - parser = argparse.ArgumentParser() - parser.add_argument( - "path", - help="paths (files or directories) of tests to execute", - nargs="+", - ) - parser = add_tax_benefit_system_arguments(parser) - - return parser - - -class Migrator(object): - def __init__(self, tax_benefit_system): - self.tax_benefit_system = tax_benefit_system - self.entities_by_plural = { - entity.plural: entity - for entity in self.tax_benefit_system.entities - } - - def migrate(self, path): - if isinstance(path, list): - for item in path: - self.migrate(item) - return - - if os.path.isdir(path): - yaml_paths = glob.glob(os.path.join(path, "*.yaml")) - subdirectories = glob.glob(os.path.join(path, "*/")) - - for yaml_path in yaml_paths: - self.migrate(yaml_path) - - for subdirectory in subdirectories: - self.migrate(subdirectory) - - return - - print("Migrating {}.".format(path)) - - with open(path) as yaml_file: - tests = yaml.safe_load(yaml_file) - if isinstance(tests, CommentedSeq): - migrated_tests = [self.convert_test(test) for test in tests] - else: - migrated_tests = self.convert_test(tests) - - with open(path, "w") as yaml_file: - yaml.dump(migrated_tests, yaml_file) - - def convert_test(self, test): - if test.get("output"): - # This test is already converted, ignoring it - return test - result = {} - outputs = test.pop("output_variables") - inputs = test.pop("input_variables", {}) - for key, value in test.items(): - if key in TEST_METADATA: - result[key] = value - else: - inputs[key] = value - result["input"] = self.convert_inputs(inputs) - result["output"] = outputs - return result - - def convert_inputs(self, inputs): - first_key = next(iter(inputs.keys()), None) - if first_key not in self.entities_by_plural: - return inputs - results = {} - for entity_plural, entities_description in inputs.items(): - entity = self.entities_by_plural[entity_plural] - if not isinstance(entities_description, (CommentedSeq, list)): - entities_description = [entities_description] - if not entity.is_person and len(entities_description) == 1: - results[entity.key] = remove_id(entities_description[0]) - continue - results[entity_plural] = self.convert_entities( - entity, entities_description - ) - - results = self.generate_missing_entities(results) - - return results - - def convert_entities(self, entity, entities_description): - return { - entity_description.get( - "id", "{}_{}".format(entity.key, index) - ): remove_id(entity_description) - for index, entity_description in enumerate(entities_description) - } - - def generate_missing_entities(self, inputs): - for entity in self.tax_benefit_system.entities: - if entity.plural in inputs or entity.key in inputs: - continue - persons = inputs[self.tax_benefit_system.person_entity.plural] - if len(persons) == 1: - person_id = next(iter(persons)) - inputs[entity.key] = { - entity.roles[0].plural or entity.roles[0].key: [person_id] - } - else: - inputs[entity.plural] = { - "{}_{}".format(entity.key, index): { - entity.roles[0].plural - or entity.roles[0].key: [person_id] - } - for index, person_id in enumerate(persons.keys()) - } - return inputs - - -def remove_id(input_dict): - return {key: value for (key, value) in input_dict.items() if key != "id"} - - -def main(): - parser = build_parser() - args = parser.parse_args() - paths = [os.path.abspath(path) for path in args.path] - - tax_benefit_system = build_tax_benefit_system( - args.country_package, args.extensions, args.reforms - ) - - Migrator(tax_benefit_system).migrate(paths) - - -if __name__ == "__main__": - main() diff --git a/policyengine_core/scripts/remove_fuzzy.py b/policyengine_core/scripts/remove_fuzzy.py deleted file mode 100755 index 93f45ca6..00000000 --- a/policyengine_core/scripts/remove_fuzzy.py +++ /dev/null @@ -1,217 +0,0 @@ -# remove_fuzzy.py : Remove the fuzzy attribute in xml files and add END tags. -# See https://github.com/openfisca/openfisca-core/issues/437 - -import re -import datetime -import sys -import numpy as np - -assert len(sys.argv) == 2 -filename = sys.argv[1] - -with open(filename, "r") as f: - lines = f.readlines() - - -# Remove fuzzy - -lines_2 = [line.replace(' fuzzy="true"', "") for line in lines] - -regex_indent = r"^(\s*)\n') - lines_3.append(line[:fin_left] + line[fin_right:]) - else: - lines_3.append(line) - - -# Remove useless END tags - -regex_code = "<(CODE|SEUIL|TAUX|ASSIETTE)" -regex_code_end = "\n$' -) - -bool_code = [bool(re.search(regex_code, line)) for line in lines_5] - -bool_code_end = [bool(re.search(regex_code_end, line)) for line in lines_5] - -list_value = [] -for line in lines_5: - m = re.match(regex_value2, line) - if m: - list_value.append(m.groups()[0]) - else: - list_value.append(None) - - -index_code = [j + 1 for j, x in enumerate(bool_code) if x] -index_code_end = [j for j, x in enumerate(bool_code_end) if x] - -assert len(index_code) == len(index_code_end) - -position_code = list(zip(index_code, index_code_end)) - -to_remove = [] -for i in range(len(lines_5) - 1): - if ( - (list_value[i] is not None) - and (list_value[i + 1] is not None) - and (list_value[i] == list_value[i + 1]) - ): - to_remove.append(i) - -to_remove_set = set(to_remove) - -lines_6 = [line for j, line in enumerate(lines_5) if j not in to_remove_set] - -# Write - -with open(filename, "w") as f: - for line in lines_6: - f.write(line) diff --git a/policyengine_core/scripts/simulation_generator.py b/policyengine_core/scripts/simulation_generator.py index 209bf196..45e170fa 100644 --- a/policyengine_core/scripts/simulation_generator.py +++ b/policyengine_core/scripts/simulation_generator.py @@ -46,29 +46,3 @@ def make_simulation(tax_benefit_system, nb_persons, nb_groups, **kwargs): entity.flattened_roles[-1], ) return simulation - - -def randomly_init_variable( - simulation, variable_name, period, max_value, condition=None -): - """ - Initialise a variable with random values (from 0 to max_value) for the given period. - If a condition vector is provided, only set the value of persons or groups for which condition is True. - - Example: - - >>> from policyengine_core.scripts.simulation_generator import make_simulation, randomly_init_variable - >>> from openfisca_france import CountryTaxBenefitSystem - >>> tbs = CountryTaxBenefitSystem() - >>> simulation = make_simulation(tbs, 400, 100) # Create a simulation with 400 persons, spread among 100 families - >>> randomly_init_variable(simulation, 'salaire_net', 2017, max_value = 50000, condition = simulation.persons.has_role(simulation.famille.DEMANDEUR)) # Randomly set a salaire_net for all persons between 0 and 50000? - >>> simulation.calculate('revenu_disponible', 2017) - """ - if condition is None: - condition = True - variable = simulation.tax_benefit_system.get_variable(variable_name) - population = simulation.get_variable_population(variable_name) - value = (np.random.rand(population.count) * max_value * condition).astype( - variable.dtype - ) - simulation.set_input(variable_name, period, value) diff --git a/policyengine_core/simulations/__init__.py b/policyengine_core/simulations/__init__.py index 32f4dcb3..e6d5af9a 100644 --- a/policyengine_core/simulations/__init__.py +++ b/policyengine_core/simulations/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from policyengine_core.errors import ( CycleError, NaNCreationError, diff --git a/policyengine_core/simulations/simulation.py b/policyengine_core/simulations/simulation.py index 9d2a88d0..a9a82c84 100644 --- a/policyengine_core/simulations/simulation.py +++ b/policyengine_core/simulations/simulation.py @@ -1,26 +1,31 @@ import tempfile -import warnings - +from typing import Any, Dict, List, TYPE_CHECKING import numpy - +from numpy.typing import ArrayLike from policyengine_core import commons, periods +from policyengine_core.entities.entity import Entity from policyengine_core.errors import CycleError, SpiralError from policyengine_core.enums import Enum, EnumArray +from policyengine_core.holders.holder import Holder from policyengine_core.periods import Period from policyengine_core.tracers import ( FullTracer, SimpleTracer, TracingParameterNodeAtInstant, ) -from policyengine_core.warnings import TempfileWarning - +if TYPE_CHECKING: + from policyengine_core.taxbenefitsystems import TaxBenefitSystem +from policyengine_core.populations import Population +from policyengine_core.tracers import SimpleTracer +from policyengine_core.experimental import MemoryConfig +from policyengine_core.variables import Variable class Simulation: """ Represents a simulation, and handles the calculation logic """ - def __init__(self, tax_benefit_system, populations): + def __init__(self, tax_benefit_system: "TaxBenefitSystem", populations: Dict[str, Population]): """ This constructor is reserved for internal use; see :any:`SimulationBuilder`, which is the preferred way to obtain a Simulation initialized with a consistent @@ -30,45 +35,45 @@ def __init__(self, tax_benefit_system, populations): assert tax_benefit_system is not None self.populations = populations - self.persons = self.populations[tax_benefit_system.person_entity.key] + self.persons: Population = self.populations[tax_benefit_system.person_entity.key] self.link_to_entities_instances() self.create_shortcuts() self.invalidated_caches = set() - self.debug = False - self.trace = False - self.tracer = SimpleTracer() - self.opt_out_cache = False + self.debug: bool = False + self.trace: bool = False + self.tracer: SimpleTracer = SimpleTracer() + self.opt_out_cache: bool = False # controls the spirals detection; check for performance impact if > 1 - self.max_spiral_loops = 1 - self.memory_config = None - self._data_storage_dir = None + self.max_spiral_loops: int = 1 + self.memory_config: MemoryConfig = None + self._data_storage_dir: str = None @property - def trace(self): + def trace(self) -> bool: return self._trace @trace.setter - def trace(self, trace): + def trace(self, trace: SimpleTracer) -> None: self._trace = trace if trace: self.tracer = FullTracer() else: self.tracer = SimpleTracer() - def link_to_entities_instances(self): + def link_to_entities_instances(self) -> None: for _key, entity_instance in self.populations.items(): entity_instance.simulation = self - def create_shortcuts(self): + def create_shortcuts(self) -> None: for _key, population in self.populations.items(): # create shortcut simulation.person and simulation.household (for instance) setattr(self, population.entity.key, population) @property - def data_storage_dir(self): + def data_storage_dir(self) -> str: """ Temporary folder used to store intermediate calculation data in case the memory is saturated """ @@ -85,7 +90,7 @@ def data_storage_dir(self): # ----- Calculation methods ----- # - def calculate(self, variable_name, period): + def calculate(self, variable_name: str, period: Period) -> ArrayLike: """Calculate ``variable_name`` for ``period``.""" if period is not None and not isinstance(period, Period): @@ -102,7 +107,7 @@ def calculate(self, variable_name, period): self.tracer.record_calculation_end() self.purge_cache_of_invalid_values() - def _calculate(self, variable_name, period: Period): + def _calculate(self, variable_name: str, period: Period) -> ArrayLike: """ Calculate the variable ``variable_name`` for the period ``period``, using the variable formula if it exists. @@ -140,7 +145,7 @@ def _calculate(self, variable_name, period: Period): return array - def purge_cache_of_invalid_values(self): + def purge_cache_of_invalid_values(self) -> None: # We wait for the end of calculate(), signalled by an empty stack, before purging the cache if self.tracer.stack: return @@ -149,7 +154,7 @@ def purge_cache_of_invalid_values(self): holder.delete_arrays(_period) self.invalidated_caches = set() - def calculate_add(self, variable_name, period): + def calculate_add(self, variable_name: str, period: Period) -> ArrayLike: variable = self.tax_benefit_system.get_variable( variable_name, check_existence=True ) @@ -183,7 +188,7 @@ def calculate_add(self, variable_name, period): for sub_period in period.get_subperiods(variable.definition_period) ) - def calculate_divide(self, variable_name, period): + def calculate_divide(self, variable_name: str, period: Period) -> ArrayLike: variable = self.tax_benefit_system.get_variable( variable_name, check_existence=True ) @@ -218,7 +223,7 @@ def calculate_divide(self, variable_name, period): ) ) - def calculate_output(self, variable_name, period): + def calculate_output(self, variable_name: str, period: Period) -> ArrayLike: """ Calculate the value of a variable using the ``calculate_output`` attribute of the variable. """ @@ -238,7 +243,7 @@ def trace_parameters_at_instant(self, formula_period): self.tracer, ) - def _run_formula(self, variable, population, period): + def _run_formula(self, variable: str, population: Population, period: Period) -> ArrayLike: """ Find the ``variable`` formula for the given ``period`` if it exists, and apply it to ``population``. """ @@ -259,7 +264,7 @@ def _run_formula(self, variable, population, period): return array - def _check_period_consistency(self, period, variable): + def _check_period_consistency(self, period: Period, variable: Variable) -> None: """ Check that a period matches the variable definition_period """ @@ -297,7 +302,7 @@ def _check_period_consistency(self, period, variable): ) ) - def _cast_formula_result(self, value, variable): + def _cast_formula_result(self, value: Any, variable: str) -> ArrayLike: if variable.value_type == Enum and not isinstance(value, EnumArray): return variable.possible_values.encode(value) @@ -312,7 +317,7 @@ def _cast_formula_result(self, value, variable): # ----- Handle circular dependencies in a calculation ----- # - def _check_for_cycle(self, variable: str, period): + def _check_for_cycle(self, variable: str, period: Period) -> None: """ Raise an exception in the case of a circular definition, where evaluating a variable for a given period loops around to evaluating the same variable/period pair. Also guards, as @@ -339,10 +344,10 @@ def _check_for_cycle(self, variable: str, period): ) raise SpiralError(message, variable) - def invalidate_cache_entry(self, variable: str, period): + def invalidate_cache_entry(self, variable: str, period: Period) -> None: self.invalidated_caches.add((variable, period)) - def invalidate_spiral_variables(self, variable: str): + def invalidate_spiral_variables(self, variable: str) -> None: # Visit the stack, from the bottom (most recent) up; we know that we'll find # the variable implicated in the spiral (max_spiral_loops+1) times; we keep the # intermediate values computed (to avoid impacting performance) but we mark them @@ -357,7 +362,7 @@ def invalidate_spiral_variables(self, variable: str): # ----- Methods to access stored values ----- # - def get_array(self, variable_name, period): + def get_array(self, variable_name: str, period: Period) -> ArrayLike: """ Return the value of ``variable_name`` for ``period``, if this value is alreay in the cache (if it has been set as an input or previously calculated). @@ -367,7 +372,7 @@ def get_array(self, variable_name, period): period = periods.period(period) return self.get_holder(variable_name).get_array(period) - def get_holder(self, variable_name): + def get_holder(self, variable_name: str) -> Holder: """ Get the :obj:`.Holder` associated with the variable ``variable_name`` for the simulation """ @@ -375,7 +380,7 @@ def get_holder(self, variable_name): variable_name ) - def get_memory_usage(self, variables=None): + def get_memory_usage(self, variables: List[str] = None) -> dict: """ Get data about the virtual memory usage of the simulation """ @@ -388,7 +393,7 @@ def get_memory_usage(self, variables=None): # ----- Misc ----- # - def delete_arrays(self, variable, period=None): + def delete_arrays(self, variable: str, period: Period = None) -> None: """ Delete a variable's value for a given period @@ -417,7 +422,7 @@ def delete_arrays(self, variable, period=None): """ self.get_holder(variable).delete_arrays(period) - def get_known_periods(self, variable): + def get_known_periods(self, variable: str) -> List[Period]: """ Get a list variable's known period, i.e. the periods where a value has been initialized and @@ -434,7 +439,7 @@ def get_known_periods(self, variable): """ return self.get_holder(variable).get_known_periods() - def set_input(self, variable_name, period, value): + def set_input(self, variable_name: str, period: Period, value: ArrayLike) -> None: """ Set a variable's value for a given period @@ -459,13 +464,13 @@ def set_input(self, variable_name, period, value): return self.get_holder(variable_name).set_input(period, value) - def get_variable_population(self, variable_name): + def get_variable_population(self, variable_name: str) -> Population: variable = self.tax_benefit_system.get_variable( variable_name, check_existence=True ) return self.populations[variable.entity.key] - def get_population(self, plural=None): + def get_population(self, plural: str = None) -> Population: return next( ( population @@ -475,17 +480,17 @@ def get_population(self, plural=None): None, ) - def get_entity(self, plural=None): + def get_entity(self, plural: str = None) -> Entity: population = self.get_population(plural) return population and population.entity - def describe_entities(self): + def describe_entities(self) -> dict: return { population.entity.plural: population.ids for population in self.populations.values() } - def clone(self, debug=False, trace=False): + def clone(self, debug: bool = False, trace: bool = False) -> "Simulation": """ Copy the simulation just enough to be able to run the copy without modifying the original simulation """ diff --git a/policyengine_core/simulations/simulation_builder.py b/policyengine_core/simulations/simulation_builder.py index eb477241..d0c728a0 100644 --- a/policyengine_core/simulations/simulation_builder.py +++ b/policyengine_core/simulations/simulation_builder.py @@ -1,25 +1,28 @@ import copy import dpath.util import typing - +from typing import TYPE_CHECKING, Any, List import numpy - +from numpy.typing import ArrayLike from policyengine_core import periods -from policyengine_core.entities import Entity +from policyengine_core.periods import Period +from policyengine_core.entities import Entity, Role from policyengine_core.errors import ( PeriodMismatchError, SituationParsingError, VariableNotFoundError, ) -from policyengine_core.populations import Population +from policyengine_core.populations import Population, GroupPopulation from policyengine_core.simulations import helpers, Simulation +if TYPE_CHECKING: + from policyengine_core.taxbenefitsystems.tax_benefit_system import TaxBenefitSystem from policyengine_core.variables import Variable class SimulationBuilder: def __init__(self): - self.default_period = None # Simulation period used for variables when no period is defined - self.persons_plural = None # Plural name for person entity in current tax and benefits system + self.default_period: Period = None # Simulation period used for variables when no period is defined + self.persons_plural: str = None # Plural name for person entity in current tax and benefits system # JSON input - Memory of known input values. Indexed by variable or axis name. self.input_buffer: typing.Dict[ @@ -45,7 +48,7 @@ def __init__(self): ] = {} self.axes_roles: typing.Dict[Entity.plural, typing.List[int]] = {} - def build_from_dict(self, tax_benefit_system, input_dict): + def build_from_dict(self, tax_benefit_system: "TaxBenefitSystem", input_dict: dict) -> Simulation: """ Build a simulation from ``input_dict`` @@ -163,7 +166,7 @@ def build_from_entities(self, tax_benefit_system, input_dict): return simulation - def build_from_variables(self, tax_benefit_system, input_dict): + def build_from_variables(self, tax_benefit_system: "TaxBenefitSystem", input_dict: dict) -> Simulation: """ Build a simulation from a Python dict ``input_dict`` describing variables values without expliciting entities. @@ -190,7 +193,7 @@ def build_from_variables(self, tax_benefit_system, input_dict): simulation.set_input(variable, period_str, dated_value) return simulation - def build_default_simulation(self, tax_benefit_system, count=1): + def build_default_simulation(self, tax_benefit_system: "TaxBenefitSystem", count: int = 1) -> Simulation: """ Build a simulation where: - There are ``count`` persons @@ -210,33 +213,33 @@ def build_default_simulation(self, tax_benefit_system, count=1): ) # Each person is its own group entity return simulation - def create_entities(self, tax_benefit_system): + def create_entities(self, tax_benefit_system: "TaxBenefitSystem") -> None: self.populations = tax_benefit_system.instantiate_entities() def declare_person_entity( - self, person_singular, persons_ids: typing.Iterable - ): + self, person_singular: str, persons_ids: typing.Iterable + ) -> None: person_instance = self.populations[person_singular] person_instance.ids = numpy.array(list(persons_ids)) person_instance.count = len(person_instance.ids) self.persons_plural = person_instance.entity.plural - def declare_entity(self, entity_singular, entity_ids: typing.Iterable): + def declare_entity(self, entity_singular: str, entity_ids: typing.Iterable) -> Population: entity_instance = self.populations[entity_singular] entity_instance.ids = numpy.array(list(entity_ids)) entity_instance.count = len(entity_instance.ids) return entity_instance - def nb_persons(self, entity_singular, role=None): + def nb_persons(self, entity_singular: str, role: Role = None) -> ArrayLike: return self.populations[entity_singular].nb_persons(role=role) def join_with_persons( self, - group_population, - persons_group_assignment, + group_population: GroupPopulation, + persons_group_assignment: ArrayLike, roles: typing.Iterable[str], - ): + ) -> None: # Maps group's identifiers to a 0-based integer range, for indexing into members_roles (see PR#876) group_sorted_indices = numpy.unique( persons_group_assignment, return_inverse=True @@ -260,10 +263,10 @@ def join_with_persons( flattened_roles, ) - def build(self, tax_benefit_system): + def build(self, tax_benefit_system: "TaxBenefitSystem") -> Simulation: return Simulation(tax_benefit_system, self.populations) - def explicit_singular_entities(self, tax_benefit_system, input_dict): + def explicit_singular_entities(self, tax_benefit_system: "TaxBenefitSystem", input_dict: dict) -> dict: """ Preprocess ``input_dict`` to explicit entities defined using the single-entity shortcut @@ -293,7 +296,7 @@ def explicit_singular_entities(self, tax_benefit_system, input_dict): return result - def add_person_entity(self, entity, instances_json): + def add_person_entity(self, entity: Entity, instances_json: dict) -> List[int]: """ Add the simulation's instances of the persons entity as described in ``instances_json``. """ @@ -313,7 +316,7 @@ def add_person_entity(self, entity, instances_json): return self.get_ids(entity.plural) - def add_default_group_entity(self, persons_ids, entity): + def add_default_group_entity(self, persons_ids: ArrayLike, entity: Entity) -> None: persons_count = len(persons_ids) self.entity_ids[entity.plural] = persons_ids self.entity_counts[entity.plural] = persons_count @@ -325,8 +328,8 @@ def add_default_group_entity(self, persons_ids, entity): ) def add_group_entity( - self, persons_plural, persons_ids, entity, instances_json - ): + self, persons_plural: str, persons_ids: ArrayLike, entity: Entity, instances_json: dict + ) -> None: """ Add all instances of one of the model's entities as described in ``instances_json``. """ @@ -434,11 +437,11 @@ def add_group_entity( entity.plural ].tolist() - def set_default_period(self, period_str): + def set_default_period(self, period_str: str) -> None: if period_str: self.default_period = str(periods.period(period_str)) - def get_input(self, variable, period_str): + def get_input(self, variable: str, period_str: str) -> Any: if variable not in self.input_buffer: self.input_buffer[variable] = {} return self.input_buffer[variable].get(period_str) diff --git a/policyengine_core/taxbenefitsystems/tax_benefit_system.py b/policyengine_core/taxbenefitsystems/tax_benefit_system.py index 95689a75..b0d50c0a 100644 --- a/policyengine_core/taxbenefitsystems/tax_benefit_system.py +++ b/policyengine_core/taxbenefitsystems/tax_benefit_system.py @@ -12,7 +12,7 @@ import sys import traceback import typing - +from typing import TYPE_CHECKING from policyengine_core import commons, periods, variables from policyengine_core.entities import Entity from policyengine_core.errors import ( @@ -22,7 +22,6 @@ from policyengine_core.parameters import ParameterNode from policyengine_core.periods import Instant, Period from policyengine_core.populations import Population, GroupPopulation -from policyengine_core.simulations import SimulationBuilder from policyengine_core.tools.test_runner import run_tests from policyengine_core.variables import Variable @@ -125,7 +124,7 @@ def new_simulation( if baseline is None: break tax_benefit_system = baseline - + from policyengine_core.simulations import SimulationBuilder builder = SimulationBuilder() if self.attributes: variables = self.attributes.get("input_variables") or {} From 78431070b121d973a7113253e3769b43120bbbc7 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 23:01:46 +0100 Subject: [PATCH 17/25] Add tax-benefit systems --- docs/_toc.yml | 1 + docs/python_api/taxbenefitsystems.md | 12 ++ policyengine_core/simulations/simulation.py | 1 - .../taxbenefitsystems/__init__.py | 23 ---- .../taxbenefitsystems/tax_benefit_system.py | 107 ++++-------------- policyengine_core/taxscales/__init__.py | 23 ---- policyengine_core/tracers/__init__.py | 23 ---- policyengine_core/variables/__init__.py | 23 ---- policyengine_core/warnings/__init__.py | 23 ---- 9 files changed, 38 insertions(+), 198 deletions(-) create mode 100644 docs/python_api/taxbenefitsystems.md diff --git a/docs/_toc.yml b/docs/_toc.yml index 14c58b49..cd521493 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -22,3 +22,4 @@ parts: - file: python_api/reforms - file: python_api/scripts - file: python_api/simulations + - file: python_api/taxbenefitsystems diff --git a/docs/python_api/taxbenefitsystems.md b/docs/python_api/taxbenefitsystems.md new file mode 100644 index 00000000..f4016e43 --- /dev/null +++ b/docs/python_api/taxbenefitsystems.md @@ -0,0 +1,12 @@ +# Tax-benefit systems + +The `policyengine_core.taxbenefitsystems` module contains the definition of `TaxBenefitSystem`, the class that represents a country's tax-benefit system: the logic, parameters and entities, not the input data. + +## TaxBenefitSystem + +```{eval-rst} +.. autoclass:: policyengine_core.taxbenefitsystems.tax_benefit_system.TaxBenefitSystem + :members: + :inherited-members: + :show-inheritance: +``` \ No newline at end of file diff --git a/policyengine_core/simulations/simulation.py b/policyengine_core/simulations/simulation.py index a9a82c84..e08e73de 100644 --- a/policyengine_core/simulations/simulation.py +++ b/policyengine_core/simulations/simulation.py @@ -85,7 +85,6 @@ def data_storage_dir(self) -> str: ).format(self._data_storage_dir), "You should remove this directory once you're done with your simulation.", ] - # warnings.warn(" ".join(message), TempfileWarning) # Deprecated warning. return self._data_storage_dir # ----- Calculation methods ----- # diff --git a/policyengine_core/taxbenefitsystems/__init__.py b/policyengine_core/taxbenefitsystems/__init__.py index a8803708..77db125c 100644 --- a/policyengine_core/taxbenefitsystems/__init__.py +++ b/policyengine_core/taxbenefitsystems/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from policyengine_core.errors import ( VariableNameConflictError, VariableNotFoundError, diff --git a/policyengine_core/taxbenefitsystems/tax_benefit_system.py b/policyengine_core/taxbenefitsystems/tax_benefit_system.py index b0d50c0a..6351ca40 100644 --- a/policyengine_core/taxbenefitsystems/tax_benefit_system.py +++ b/policyengine_core/taxbenefitsystems/tax_benefit_system.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import Any, Dict, List, Optional, Sequence, Type, Union import copy import glob @@ -19,7 +19,7 @@ VariableNameConflictError, VariableNotFoundError, ) -from policyengine_core.parameters import ParameterNode +from policyengine_core.parameters import ParameterNode, ParameterNodeAtInstant from policyengine_core.periods import Instant, Period from policyengine_core.populations import Population, GroupPopulation from policyengine_core.tools.test_runner import run_tests @@ -42,11 +42,11 @@ class TaxBenefitSystem: """ - _base_tax_benefit_system = None + _base_tax_benefit_system: "TaxBenefitSystem" = None _parameters_at_instant_cache: Optional[Dict[Any, Any]] = None - person_key_plural = None - preprocess_parameters = None - baseline = None # Baseline tax-benefit system. Used only by reforms. Note: Reforms can be chained. + person_key_plural: str = None + preprocess_parameters: str = None + baseline: "TaxBenefitSystem" = None # Baseline tax-benefit system. Used only by reforms. Note: Reforms can be chained. cache_blacklist = None decomposition_file_path = None @@ -72,7 +72,7 @@ def __init__(self, entities: Sequence[Entity]) -> None: entity.set_tax_benefit_system(self) @property - def base_tax_benefit_system(self): + def base_tax_benefit_system(self) -> "TaxBenefitSystem": base_tax_benefit_system = self._base_tax_benefit_system if base_tax_benefit_system is None: baseline = self.baseline @@ -83,7 +83,7 @@ def base_tax_benefit_system(self): ) = baseline.base_tax_benefit_system return base_tax_benefit_system - def instantiate_entities(self): + def instantiate_entities(self) -> Dict[str, Population]: person = self.person_entity members = Population(person) entities: typing.Dict[Entity.key, Entity] = {person.key: members} @@ -93,64 +93,7 @@ def instantiate_entities(self): return entities - # Deprecated method of constructing simulations, to be phased out in favor of SimulationBuilder - def new_scenario(self): - class ScenarioAdapter(object): - def __init__(self, tax_benefit_system): - self.tax_benefit_system = tax_benefit_system - - def init_from_attributes(self, **attributes): - self.attributes = attributes - return self - - def init_from_dict(self, dict): - self.attributes = None - self.dict = dict - self.period = dict.pop("period") - return self - - def new_simulation( - self, - debug=False, - opt_out_cache=False, - use_baseline=False, - trace=False, - ): - # Legacy from scenarios, used in reforms - tax_benefit_system = self.tax_benefit_system - if use_baseline: - while True: - baseline = tax_benefit_system.baseline - if baseline is None: - break - tax_benefit_system = baseline - from policyengine_core.simulations import SimulationBuilder - builder = SimulationBuilder() - if self.attributes: - variables = self.attributes.get("input_variables") or {} - period = self.attributes.get("period") - builder.set_default_period(period) - simulation = builder.build_from_variables( - tax_benefit_system, variables - ) - else: - builder.set_default_period(self.period) - simulation = builder.build_from_entities( - tax_benefit_system, self.dict - ) - - simulation.trace = trace - simulation.debug = debug - simulation.opt_out_cache = opt_out_cache - - return simulation - - return ScenarioAdapter(self) - - def prefill_cache(self): - pass - - def load_variable(self, variable_class, update=False): + def load_variable(self, variable_class: Type[Variable], update: bool = False) -> Variable: name = variable_class.__name__ # Check if a Variable with the same name is already registered. @@ -179,7 +122,7 @@ def add_variable(self, variable: Variable) -> Variable: """ return self.load_variable(variable, update=False) - def replace_variable(self, variable): + def replace_variable(self, variable: str) -> Variable: """ Replaces an existing OpenFisca variable in the tax and benefit system by a new one. @@ -194,7 +137,7 @@ def replace_variable(self, variable): del self.variables[name] self.load_variable(variable, update=False) - def update_variable(self, variable): + def update_variable(self, variable: str) -> Variable: """ Updates an existing OpenFisca variable in the tax and benefit system. @@ -208,7 +151,7 @@ def update_variable(self, variable): """ return self.load_variable(variable, update=True) - def add_variables_from_file(self, file_path): + def add_variables_from_file(self, file_path: str) -> None: """ Adds all OpenFisca variables contained in a given file to the tax and benefit system. """ @@ -256,7 +199,7 @@ def add_variables_from_file(self, file_path): ) raise - def add_variables_from_directory(self, directory): + def add_variables_from_directory(self, directory: str) -> None: """ Recursively explores a directory, and adds all OpenFisca variables found there to the tax and benefit system. """ @@ -267,7 +210,7 @@ def add_variables_from_directory(self, directory): for subdirectory in subdirectories: self.add_variables_from_directory(subdirectory) - def add_variables(self, *variables): + def add_variables(self, *variables: List[Type[Variable]]): """ Adds a list of OpenFisca Variables to the `TaxBenefitSystem`. @@ -276,7 +219,7 @@ def add_variables(self, *variables): for variable in variables: self.add_variable(variable) - def load_extension(self, extension): + def load_extension(self, extension: str) -> None: """ Loads an extension to the tax and benefit system. @@ -357,7 +300,7 @@ def apply_reform(self, reform_path: str) -> "TaxBenefitSystem": return reform(self) - def get_variable(self, variable_name, check_existence=False): + def get_variable(self, variable_name: str, check_existence: bool = False) -> Variable: """ Get a variable from the tax and benefit system. @@ -370,7 +313,7 @@ def get_variable(self, variable_name, check_existence=False): raise VariableNotFoundError(variable_name, self) return found - def neutralize_variable(self, variable_name): + def neutralize_variable(self, variable_name: str) -> None: """ Neutralizes an OpenFisca variable existing in the tax and benefit system. @@ -389,7 +332,7 @@ def annualize_variable( self.get_variable(variable_name, period) ) - def load_parameters(self, path_to_yaml_dir): + def load_parameters(self, path_to_yaml_dir: str) -> None: """ Loads the legislation parameter for a directory containing YAML parameters files. @@ -407,13 +350,13 @@ def load_parameters(self, path_to_yaml_dir): self.parameters = parameters - def _get_baseline_parameters_at_instant(self, instant): + def _get_baseline_parameters_at_instant(self, instant: Instant) -> ParameterNodeAtInstant: baseline = self.baseline if baseline is None: return self.get_parameters_at_instant(instant) return baseline._get_baseline_parameters_at_instant(instant) - def get_parameters_at_instant(self, instant): + def get_parameters_at_instant(self, instant: Instant) -> ParameterNodeAtInstant: """ Get the parameters of the legislation at a given instant @@ -440,7 +383,7 @@ def get_parameters_at_instant(self, instant): self._parameters_at_instant_cache[instant] = parameters_at_instant return parameters_at_instant - def get_package_metadata(self): + def get_package_metadata(self) -> dict: """ Gets metatada relative to the country package the tax and benefit system is built from. @@ -494,7 +437,7 @@ def get_package_metadata(self): "location": location, } - def get_variables(self, entity=None): + def get_variables(self, entity: Entity = None) -> dict: """ Gets all variables contained in a tax and benefit system. @@ -514,7 +457,7 @@ def get_variables(self, entity=None): if variable.entity.key == entity.key } - def clone(self): + def clone(self) -> "TaxBenefitSystem": new = commons.empty_clone(self) new_dict = new.__dict__ @@ -535,10 +478,10 @@ def clone(self): new_dict["open_api_config"] = self.open_api_config.copy() return new - def entities_plural(self): + def entities_plural(self) -> dict: return {entity.plural for entity in self.entities} - def entities_by_singular(self): + def entities_by_singular(self) -> dict: return {entity.key: entity for entity in self.entities} def test(self, paths: str, verbose: bool = False) -> None: diff --git a/policyengine_core/taxscales/__init__.py b/policyengine_core/taxscales/__init__.py index eb709cf2..fb2a971c 100644 --- a/policyengine_core/taxscales/__init__.py +++ b/policyengine_core/taxscales/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from policyengine_core.errors import EmptyArgumentError from .helpers import combine_tax_scales diff --git a/policyengine_core/tracers/__init__.py b/policyengine_core/tracers/__init__.py index d157a2f7..e5985fde 100644 --- a/policyengine_core/tracers/__init__.py +++ b/policyengine_core/tracers/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core import module -# >>> module.Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from .computation_log import ComputationLog from .flat_trace import FlatTrace from .full_tracer import FullTracer diff --git a/policyengine_core/variables/__init__.py b/policyengine_core/variables/__init__.py index e2a8b292..95417a15 100644 --- a/policyengine_core/variables/__init__.py +++ b/policyengine_core/variables/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from .config import VALUE_TYPES, FORMULA_NAME_PREFIX from .helpers import ( get_annualized_variable, diff --git a/policyengine_core/warnings/__init__.py b/policyengine_core/warnings/__init__.py index d8a6c4b4..b136953f 100644 --- a/policyengine_core/warnings/__init__.py +++ b/policyengine_core/warnings/__init__.py @@ -1,26 +1,3 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from policyengine_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from policyengine_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from policyengine_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - from .libyaml_warning import LibYAMLWarning from .memory_warning import MemoryConfigWarning from .tempfile_warning import TempfileWarning From 823524c0eb47310020f6ad436b432d3719f93ff7 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 23:13:17 +0100 Subject: [PATCH 18/25] Add tax scales --- docs/_toc.yml | 1 + docs/python_api/taxscales.md | 84 ++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 docs/python_api/taxscales.md diff --git a/docs/_toc.yml b/docs/_toc.yml index cd521493..c2e56ea9 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -23,3 +23,4 @@ parts: - file: python_api/scripts - file: python_api/simulations - file: python_api/taxbenefitsystems + - file: python_api/taxscales diff --git a/docs/python_api/taxscales.md b/docs/python_api/taxscales.md new file mode 100644 index 00000000..c41ad0d2 --- /dev/null +++ b/docs/python_api/taxscales.md @@ -0,0 +1,84 @@ +# Tax scales + +The `policyengine_core.taxscales` module contains classes used to represent tax scales (parameters that are numeric functions of a variable). + +## TaxScaleLike + +```{eval-rst} +.. autoclass:: policyengine_core.taxscales.tax_scale_like.TaxScaleLike + :members: + :inherited-members: + :show-inheritance: +``` + +## RateTaxScaleLike + +```{eval-rst} +.. autoclass:: policyengine_core.taxscales.rate_tax_scale_like.RateTaxScaleLike + :members: + :inherited-members: + :show-inheritance: +``` + +## AbstractTaxScale + +```{eval-rst} +.. autoclass:: policyengine_core.taxscales.abstract_tax_scale.AbstractTaxScale + :members: + :inherited-members: + :show-inheritance: +``` + +## AbstractRateTaxScale + +```{eval-rst} +.. autoclass:: policyengine_core.taxscales.abstract_rate_tax_scale.AbstractRateTaxScale + :members: + :inherited-members: + :show-inheritance: +``` + +## AmountTaxScaleLike + +```{eval-rst} +.. autoclass:: policyengine_core.taxscales.amount_tax_scale_like.AmountTaxScaleLike + :members: + :inherited-members: + :show-inheritance: +``` + +## SingleAmountTaxScale + +```{eval-rst} +.. autoclass:: policyengine_core.taxscales.single_amount_tax_scale.SingleAmountTaxScale + :members: + :inherited-members: + :show-inheritance: +``` + +## MarginalRateTaxScale + +```{eval-rst} +.. autoclass:: policyengine_core.taxscales.marginal_rate_tax_scale.MarginalRateTaxScale + :members: + :inherited-members: + :show-inheritance: +``` + +## MarginalAmountTaxScale + +```{eval-rst} +.. autoclass:: policyengine_core.taxscales.marginal_amount_tax_scale.MarginalAmountTaxScale + :members: + :inherited-members: + :show-inheritance: +``` + +## LinearAverageRateTaxScale + +```{eval-rst} +.. autoclass:: policyengine_core.taxscales.linear_average_rate_tax_scale.LinearAverageRateTaxScale + :members: + :inherited-members: + :show-inheritance: +``` From 86cce26fc12129a0efe46781e32b9efea48e6210 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 23:16:16 +0100 Subject: [PATCH 19/25] Add tools --- docs/_toc.yml | 1 + docs/python_api/tools.md | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 docs/python_api/tools.md diff --git a/docs/_toc.yml b/docs/_toc.yml index c2e56ea9..807b1a3b 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -24,3 +24,4 @@ parts: - file: python_api/simulations - file: python_api/taxbenefitsystems - file: python_api/taxscales + - file: python_api/tools diff --git a/docs/python_api/tools.md b/docs/python_api/tools.md new file mode 100644 index 00000000..4c8473ab --- /dev/null +++ b/docs/python_api/tools.md @@ -0,0 +1,21 @@ +# Tools + +The `policyengine_core.tools` module contains miscellaneous utility functions, including for running YAML tests. + +## run_tests + +```{eval-rst} +.. autofunction:: policyengine_core.tools.test_runner.run_tests +``` + +## dump_simulation + +```{eval-rst} +.. autofunction:: policyengine_core.tools.simulation_dumper.dump_simulation +``` + +## restore_simulation + +```{eval-rst} +.. autofunction:: policyengine_core.tools.simulation_dumper.restore_simulation +``` From 6b89a58945c7b974d14002237071986565783552 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 23:20:49 +0100 Subject: [PATCH 20/25] Add tracers --- docs/_toc.yml | 1 + docs/python_api/tracers.md | 66 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 docs/python_api/tracers.md diff --git a/docs/_toc.yml b/docs/_toc.yml index 807b1a3b..91f4a247 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -25,3 +25,4 @@ parts: - file: python_api/taxbenefitsystems - file: python_api/taxscales - file: python_api/tools + - file: python_api/tracers diff --git a/docs/python_api/tracers.md b/docs/python_api/tracers.md new file mode 100644 index 00000000..5ab9b2e0 --- /dev/null +++ b/docs/python_api/tracers.md @@ -0,0 +1,66 @@ +# Tracers + +The `policyengine_core.tracers` module contains classes used to represent tracers, which are used to track the computation involved in calculating variables. + +## ComputationLog + +```{eval-rst} +.. autoclass:: policyengine_core.tracers.computation_log.ComputationLog + :members: + :inherited-members: + :show-inheritance: +``` + +## FlatTrace + +```{eval-rst} +.. autoclass:: policyengine_core.tracers.flat_trace.FlatTrace + :members: + :inherited-members: + :show-inheritance: +``` + +## FullTracer + +```{eval-rst} +.. autoclass:: policyengine_core.tracers.full_tracer.FullTracer + :members: + :inherited-members: + :show-inheritance: +``` + +## PerformanceLog + +```{eval-rst} +.. autoclass:: policyengine_core.tracers.performance_log.PerformanceLog + :members: + :inherited-members: + :show-inheritance: +``` + +## SimpleTracer + +```{eval-rst} +.. autoclass:: policyengine_core.tracers.simple_tracer.SimpleTracer + :members: + :inherited-members: + :show-inheritance: +``` + +## TraceNode + +```{eval-rst} +.. autoclass:: policyengine_core.tracers.trace_node.TraceNode + :members: + :inherited-members: + :show-inheritance: +``` + +## TracingParameterNodeAtInstant + +```{eval-rst} +.. autoclass:: policyengine_core.tracers.tracing_parameter_node_at_instant.TracingParameterNodeAtInstant + :members: + :inherited-members: + :show-inheritance: +``` From 3058c0bf84469eace1c039bef885735279697da7 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 23:21:18 +0100 Subject: [PATCH 21/25] Add types --- docs/_toc.yml | 1 + docs/python_api/types.md | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 docs/python_api/types.md diff --git a/docs/_toc.yml b/docs/_toc.yml index 91f4a247..63313cc5 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -26,3 +26,4 @@ parts: - file: python_api/taxscales - file: python_api/tools - file: python_api/tracers + - file: python_api/types diff --git a/docs/python_api/types.md b/docs/python_api/types.md new file mode 100644 index 00000000..07e0a266 --- /dev/null +++ b/docs/python_api/types.md @@ -0,0 +1,3 @@ +# Types + +The `policyengine_core.types` module contains classes used to represent types of variables. From 7957185096a2dd55f03ad4ae375dcba7628c64bf Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 23:27:03 +0100 Subject: [PATCH 22/25] Complete documentation --- docs/_toc.yml | 3 ++ docs/python_api/variables.md | 12 ++++++++ docs/python_api/warnings.md | 30 +++++++++++++++++++ policyengine_core/warnings/__init__.py | 2 +- ...ry_warning.py => memory_config_warning.py} | 0 5 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 docs/python_api/variables.md create mode 100644 docs/python_api/warnings.md rename policyengine_core/warnings/{memory_warning.py => memory_config_warning.py} (100%) diff --git a/docs/_toc.yml b/docs/_toc.yml index 63313cc5..d40128ab 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -27,3 +27,6 @@ parts: - file: python_api/tools - file: python_api/tracers - file: python_api/types + - file: python_api/variables + - file: python_api/warnings + diff --git a/docs/python_api/variables.md b/docs/python_api/variables.md new file mode 100644 index 00000000..3d655f6c --- /dev/null +++ b/docs/python_api/variables.md @@ -0,0 +1,12 @@ +# Variables + +The `policyengine_core.variables` module contains the definition of `Variable`, which can have a value for each entity instance in a simulation, for each period in a simulation, dependent on the parameters in that period, as well as other entity instances. + +## Variable + +```{eval-rst} +.. autoclass:: policyengine_core.variables.variable.Variable + :members: + :inherited-members: + :show-inheritance: +``` diff --git a/docs/python_api/warnings.md b/docs/python_api/warnings.md new file mode 100644 index 00000000..d7fa2412 --- /dev/null +++ b/docs/python_api/warnings.md @@ -0,0 +1,30 @@ +# Warnings + +The `policyengine_core.warnings` module contains specific definitions for individual warnings raised by other classes. + +## LibYAMLWarning + +```{eval-rst} +.. autoclass:: policyengine_core.warnings.LibYAMLWarning + :members: + :inherited-members: + :show-inheritance: +``` + +## MemoryConfigWarning + +```{eval-rst} +.. autoclass:: policyengine_core.warnings.MemoryConfigWarning + :members: + :inherited-members: + :show-inheritance: +``` + +## TempfileWarning + +```{eval-rst} +.. autoclass:: policyengine_core.warnings.TempfileWarning + :members: + :inherited-members: + :show-inheritance: +``` diff --git a/policyengine_core/warnings/__init__.py b/policyengine_core/warnings/__init__.py index b136953f..e1e6ff12 100644 --- a/policyengine_core/warnings/__init__.py +++ b/policyengine_core/warnings/__init__.py @@ -1,3 +1,3 @@ from .libyaml_warning import LibYAMLWarning -from .memory_warning import MemoryConfigWarning +from .memory_config_warning import MemoryConfigWarning from .tempfile_warning import TempfileWarning diff --git a/policyengine_core/warnings/memory_warning.py b/policyengine_core/warnings/memory_config_warning.py similarity index 100% rename from policyengine_core/warnings/memory_warning.py rename to policyengine_core/warnings/memory_config_warning.py From 0cd6ec12a853f194b899f265e5e5a16361890475 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 23:28:18 +0100 Subject: [PATCH 23/25] Add documentation build action step --- .github/workflows/push.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 7adaf2f9..e6154f50 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -58,6 +58,14 @@ jobs: - name: Run tests run: make test - uses: codecov/codecov-action@v3 + - name: Generate documentation + run: make documentation + - name: Deploy documentation + uses: JamesIves/github-pages-deploy-action@releases/v3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages # The branch the action should deploy to. + FOLDER: docs/_build/html # The folder the action should deploy. Publish: runs-on: ubuntu-latest if: | From 01a81b56840aec6e3cf97fcfea3ec3bee5f8abe6 Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 23:29:51 +0100 Subject: [PATCH 24/25] Update versioning --- changelog_entry.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29b..9c1fca49 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,5 @@ +- bump: patch + changes: + added: + - Type hints to most functions and classes. + - Jupyter book documentation for all modules. From e58f734afb4a4377d5d466e52f476b268f71678d Mon Sep 17 00:00:00 2001 From: Nikhil Woodruff Date: Sun, 9 Oct 2022 23:29:58 +0100 Subject: [PATCH 25/25] Apply formatting --- .../populations/group_population.py | 1 + policyengine_core/populations/population.py | 1 + .../projectors/entity_to_person_projector.py | 1 + .../first_person_to_entity_projector.py | 1 + policyengine_core/projectors/projector.py | 2 + .../unique_role_to_entity_projector.py | 2 +- policyengine_core/reforms/reform.py | 4 +- policyengine_core/simulations/simulation.py | 32 +++++++++++---- .../simulations/simulation_builder.py | 39 ++++++++++++++----- .../taxbenefitsystems/tax_benefit_system.py | 16 ++++++-- 10 files changed, 77 insertions(+), 22 deletions(-) diff --git a/policyengine_core/populations/group_population.py b/policyengine_core/populations/group_population.py index 1521d860..ba89e36c 100644 --- a/policyengine_core/populations/group_population.py +++ b/policyengine_core/populations/group_population.py @@ -5,6 +5,7 @@ from policyengine_core.entities import Role, Entity from policyengine_core.enums import EnumArray from policyengine_core.populations import Population + if TYPE_CHECKING: from policyengine_core.simulations import Simulation diff --git a/policyengine_core/populations/population.py b/policyengine_core/populations/population.py index 34922e02..c6523ac2 100644 --- a/policyengine_core/populations/population.py +++ b/policyengine_core/populations/population.py @@ -8,6 +8,7 @@ from policyengine_core.populations import config from policyengine_core.projectors import Projector from policyengine_core.entities import Entity, Role + if TYPE_CHECKING: from policyengine_core.simulations import Simulation diff --git a/policyengine_core/projectors/entity_to_person_projector.py b/policyengine_core/projectors/entity_to_person_projector.py index 4c3dde46..5a954cae 100644 --- a/policyengine_core/projectors/entity_to_person_projector.py +++ b/policyengine_core/projectors/entity_to_person_projector.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING + if TYPE_CHECKING: from policyengine_core.populations import Population from policyengine_core.projectors import Projector diff --git a/policyengine_core/projectors/first_person_to_entity_projector.py b/policyengine_core/projectors/first_person_to_entity_projector.py index 365c6454..99a7dd8b 100644 --- a/policyengine_core/projectors/first_person_to_entity_projector.py +++ b/policyengine_core/projectors/first_person_to_entity_projector.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING + if TYPE_CHECKING: from policyengine_core.populations import GroupPopulation from policyengine_core.projectors import Projector diff --git a/policyengine_core/projectors/projector.py b/policyengine_core/projectors/projector.py index 89277de1..16149531 100644 --- a/policyengine_core/projectors/projector.py +++ b/policyengine_core/projectors/projector.py @@ -1,9 +1,11 @@ from policyengine_core.projectors import helpers from typing import TYPE_CHECKING + if TYPE_CHECKING: from policyengine_core.populations import Population from numpy.typing import ArrayLike + class Projector: reference_entity: "Population" = None parent: "Projector" = None diff --git a/policyengine_core/projectors/unique_role_to_entity_projector.py b/policyengine_core/projectors/unique_role_to_entity_projector.py index 164a29ae..b2d2ea94 100644 --- a/policyengine_core/projectors/unique_role_to_entity_projector.py +++ b/policyengine_core/projectors/unique_role_to_entity_projector.py @@ -4,7 +4,7 @@ class UniqueRoleToEntityProjector(Projector): """For instance famille.declarant_principal.""" - def __init__(self, entity, role, parent = None): + def __init__(self, entity, role, parent=None): self.target_entity = entity self.reference_entity = entity.members self.parent = parent diff --git a/policyengine_core/reforms/reform.py b/policyengine_core/reforms/reform.py index d8768058..978665f3 100644 --- a/policyengine_core/reforms/reform.py +++ b/policyengine_core/reforms/reform.py @@ -71,7 +71,9 @@ def full_key(self) -> str: key = ".".join([baseline_full_key, key]) return key - def modify_parameters(self, modifier_function: Callable[[ParameterNode], ParameterNode]) -> None: + def modify_parameters( + self, modifier_function: Callable[[ParameterNode], ParameterNode] + ) -> None: """Make modifications on the parameters of the legislation. Call this function in `apply()` if the reform asks for legislation parameter modifications. diff --git a/policyengine_core/simulations/simulation.py b/policyengine_core/simulations/simulation.py index e08e73de..f0a4f556 100644 --- a/policyengine_core/simulations/simulation.py +++ b/policyengine_core/simulations/simulation.py @@ -13,6 +13,7 @@ SimpleTracer, TracingParameterNodeAtInstant, ) + if TYPE_CHECKING: from policyengine_core.taxbenefitsystems import TaxBenefitSystem from policyengine_core.populations import Population @@ -20,12 +21,17 @@ from policyengine_core.experimental import MemoryConfig from policyengine_core.variables import Variable + class Simulation: """ Represents a simulation, and handles the calculation logic """ - def __init__(self, tax_benefit_system: "TaxBenefitSystem", populations: Dict[str, Population]): + def __init__( + self, + tax_benefit_system: "TaxBenefitSystem", + populations: Dict[str, Population], + ): """ This constructor is reserved for internal use; see :any:`SimulationBuilder`, which is the preferred way to obtain a Simulation initialized with a consistent @@ -35,7 +41,9 @@ def __init__(self, tax_benefit_system: "TaxBenefitSystem", populations: Dict[str assert tax_benefit_system is not None self.populations = populations - self.persons: Population = self.populations[tax_benefit_system.person_entity.key] + self.persons: Population = self.populations[ + tax_benefit_system.person_entity.key + ] self.link_to_entities_instances() self.create_shortcuts() @@ -187,7 +195,9 @@ def calculate_add(self, variable_name: str, period: Period) -> ArrayLike: for sub_period in period.get_subperiods(variable.definition_period) ) - def calculate_divide(self, variable_name: str, period: Period) -> ArrayLike: + def calculate_divide( + self, variable_name: str, period: Period + ) -> ArrayLike: variable = self.tax_benefit_system.get_variable( variable_name, check_existence=True ) @@ -222,7 +232,9 @@ def calculate_divide(self, variable_name: str, period: Period) -> ArrayLike: ) ) - def calculate_output(self, variable_name: str, period: Period) -> ArrayLike: + def calculate_output( + self, variable_name: str, period: Period + ) -> ArrayLike: """ Calculate the value of a variable using the ``calculate_output`` attribute of the variable. """ @@ -242,7 +254,9 @@ def trace_parameters_at_instant(self, formula_period): self.tracer, ) - def _run_formula(self, variable: str, population: Population, period: Period) -> ArrayLike: + def _run_formula( + self, variable: str, population: Population, period: Period + ) -> ArrayLike: """ Find the ``variable`` formula for the given ``period`` if it exists, and apply it to ``population``. """ @@ -263,7 +277,9 @@ def _run_formula(self, variable: str, population: Population, period: Period) -> return array - def _check_period_consistency(self, period: Period, variable: Variable) -> None: + def _check_period_consistency( + self, period: Period, variable: Variable + ) -> None: """ Check that a period matches the variable definition_period """ @@ -438,7 +454,9 @@ def get_known_periods(self, variable: str) -> List[Period]: """ return self.get_holder(variable).get_known_periods() - def set_input(self, variable_name: str, period: Period, value: ArrayLike) -> None: + def set_input( + self, variable_name: str, period: Period, value: ArrayLike + ) -> None: """ Set a variable's value for a given period diff --git a/policyengine_core/simulations/simulation_builder.py b/policyengine_core/simulations/simulation_builder.py index d0c728a0..f9c97cfc 100644 --- a/policyengine_core/simulations/simulation_builder.py +++ b/policyengine_core/simulations/simulation_builder.py @@ -14,8 +14,11 @@ ) from policyengine_core.populations import Population, GroupPopulation from policyengine_core.simulations import helpers, Simulation + if TYPE_CHECKING: - from policyengine_core.taxbenefitsystems.tax_benefit_system import TaxBenefitSystem + from policyengine_core.taxbenefitsystems.tax_benefit_system import ( + TaxBenefitSystem, + ) from policyengine_core.variables import Variable @@ -48,7 +51,9 @@ def __init__(self): ] = {} self.axes_roles: typing.Dict[Entity.plural, typing.List[int]] = {} - def build_from_dict(self, tax_benefit_system: "TaxBenefitSystem", input_dict: dict) -> Simulation: + def build_from_dict( + self, tax_benefit_system: "TaxBenefitSystem", input_dict: dict + ) -> Simulation: """ Build a simulation from ``input_dict`` @@ -166,7 +171,9 @@ def build_from_entities(self, tax_benefit_system, input_dict): return simulation - def build_from_variables(self, tax_benefit_system: "TaxBenefitSystem", input_dict: dict) -> Simulation: + def build_from_variables( + self, tax_benefit_system: "TaxBenefitSystem", input_dict: dict + ) -> Simulation: """ Build a simulation from a Python dict ``input_dict`` describing variables values without expliciting entities. @@ -193,7 +200,9 @@ def build_from_variables(self, tax_benefit_system: "TaxBenefitSystem", input_dic simulation.set_input(variable, period_str, dated_value) return simulation - def build_default_simulation(self, tax_benefit_system: "TaxBenefitSystem", count: int = 1) -> Simulation: + def build_default_simulation( + self, tax_benefit_system: "TaxBenefitSystem", count: int = 1 + ) -> Simulation: """ Build a simulation where: - There are ``count`` persons @@ -225,7 +234,9 @@ def declare_person_entity( self.persons_plural = person_instance.entity.plural - def declare_entity(self, entity_singular: str, entity_ids: typing.Iterable) -> Population: + def declare_entity( + self, entity_singular: str, entity_ids: typing.Iterable + ) -> Population: entity_instance = self.populations[entity_singular] entity_instance.ids = numpy.array(list(entity_ids)) entity_instance.count = len(entity_instance.ids) @@ -266,7 +277,9 @@ def join_with_persons( def build(self, tax_benefit_system: "TaxBenefitSystem") -> Simulation: return Simulation(tax_benefit_system, self.populations) - def explicit_singular_entities(self, tax_benefit_system: "TaxBenefitSystem", input_dict: dict) -> dict: + def explicit_singular_entities( + self, tax_benefit_system: "TaxBenefitSystem", input_dict: dict + ) -> dict: """ Preprocess ``input_dict`` to explicit entities defined using the single-entity shortcut @@ -296,7 +309,9 @@ def explicit_singular_entities(self, tax_benefit_system: "TaxBenefitSystem", inp return result - def add_person_entity(self, entity: Entity, instances_json: dict) -> List[int]: + def add_person_entity( + self, entity: Entity, instances_json: dict + ) -> List[int]: """ Add the simulation's instances of the persons entity as described in ``instances_json``. """ @@ -316,7 +331,9 @@ def add_person_entity(self, entity: Entity, instances_json: dict) -> List[int]: return self.get_ids(entity.plural) - def add_default_group_entity(self, persons_ids: ArrayLike, entity: Entity) -> None: + def add_default_group_entity( + self, persons_ids: ArrayLike, entity: Entity + ) -> None: persons_count = len(persons_ids) self.entity_ids[entity.plural] = persons_ids self.entity_counts[entity.plural] = persons_count @@ -328,7 +345,11 @@ def add_default_group_entity(self, persons_ids: ArrayLike, entity: Entity) -> No ) def add_group_entity( - self, persons_plural: str, persons_ids: ArrayLike, entity: Entity, instances_json: dict + self, + persons_plural: str, + persons_ids: ArrayLike, + entity: Entity, + instances_json: dict, ) -> None: """ Add all instances of one of the model's entities as described in ``instances_json``. diff --git a/policyengine_core/taxbenefitsystems/tax_benefit_system.py b/policyengine_core/taxbenefitsystems/tax_benefit_system.py index 6351ca40..2d41a363 100644 --- a/policyengine_core/taxbenefitsystems/tax_benefit_system.py +++ b/policyengine_core/taxbenefitsystems/tax_benefit_system.py @@ -93,7 +93,9 @@ def instantiate_entities(self) -> Dict[str, Population]: return entities - def load_variable(self, variable_class: Type[Variable], update: bool = False) -> Variable: + def load_variable( + self, variable_class: Type[Variable], update: bool = False + ) -> Variable: name = variable_class.__name__ # Check if a Variable with the same name is already registered. @@ -300,7 +302,9 @@ def apply_reform(self, reform_path: str) -> "TaxBenefitSystem": return reform(self) - def get_variable(self, variable_name: str, check_existence: bool = False) -> Variable: + def get_variable( + self, variable_name: str, check_existence: bool = False + ) -> Variable: """ Get a variable from the tax and benefit system. @@ -350,13 +354,17 @@ def load_parameters(self, path_to_yaml_dir: str) -> None: self.parameters = parameters - def _get_baseline_parameters_at_instant(self, instant: Instant) -> ParameterNodeAtInstant: + def _get_baseline_parameters_at_instant( + self, instant: Instant + ) -> ParameterNodeAtInstant: baseline = self.baseline if baseline is None: return self.get_parameters_at_instant(instant) return baseline._get_baseline_parameters_at_instant(instant) - def get_parameters_at_instant(self, instant: Instant) -> ParameterNodeAtInstant: + def get_parameters_at_instant( + self, instant: Instant + ) -> ParameterNodeAtInstant: """ Get the parameters of the legislation at a given instant