├── .dockerignore ├── .github ├── changelog_template.md ├── get-changelog-diff.sh ├── has-functional-changes.sh ├── is-version-number-acceptable.sh ├── publish-git-tag.sh ├── pull_request_template.md └── workflows │ ├── pr.yml │ └── push.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── app.yaml ├── changelog.yaml ├── changelog_entry.yaml ├── debug_server.py ├── docs ├── _config.yml ├── _toc.yml └── index.ipynb ├── policyengine-client ├── .gitignore ├── .npmrc ├── README.md ├── craco.config.js ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── main_logo.png │ ├── manifest.json │ ├── social_preview.png │ └── social_preview │ │ ├── global.png │ │ ├── uk.png │ │ └── us.png ├── src │ ├── App.jsx │ ├── constants.jsx │ ├── countries │ │ ├── country.jsx │ │ ├── index.jsx │ │ ├── uk │ │ │ ├── components │ │ │ │ ├── autoUBI.jsx │ │ │ │ ├── countrySpecific.jsx │ │ │ │ ├── extraBand.jsx │ │ │ │ ├── faq.md │ │ │ │ └── timeTravel.jsx │ │ │ ├── index.jsx │ │ │ └── uk.jsx │ │ ├── us │ │ │ ├── components │ │ │ │ └── stateSpecific.jsx │ │ │ ├── index.jsx │ │ │ └── us.jsx │ │ └── utils │ │ │ └── timePeriod.jsx │ ├── fof.jsx │ ├── images │ │ ├── help │ │ │ ├── uk │ │ │ │ ├── householdHelper.gif │ │ │ │ ├── impactHelper.gif │ │ │ │ └── policyHelper.gif │ │ │ └── us │ │ │ │ ├── householdHelper.gif │ │ │ │ └── policyHelper.gif │ │ ├── logos │ │ │ ├── black.png │ │ │ ├── blue.png │ │ │ ├── cgo.png │ │ │ ├── cgo_wide.png │ │ │ ├── facebook_logo.png │ │ │ ├── linkedin_logo.png │ │ │ ├── partnership_icon.png │ │ │ ├── sponsorship_logo.png │ │ │ └── white.png │ │ └── parameter-icons │ │ │ ├── clock.png │ │ │ ├── clock.webp │ │ │ ├── misc.png │ │ │ ├── misc.webp │ │ │ ├── simulation.png │ │ │ ├── simulation.webp │ │ │ ├── third-party.png │ │ │ ├── ubi-center.png │ │ │ ├── ubi-center.webp │ │ │ ├── uk │ │ │ ├── third-party │ │ │ │ ├── green-party.png │ │ │ │ ├── green-party.webp │ │ │ │ ├── smf.png │ │ │ │ └── smf.webp │ │ │ ├── uk.png │ │ │ └── uk.webp │ │ │ └── us │ │ │ ├── state-governments.png │ │ │ ├── state-governments │ │ │ ├── ma.png │ │ │ ├── md.jpeg │ │ │ ├── ny.webp │ │ │ ├── or.png │ │ │ ├── pa.webp │ │ │ └── wa.png │ │ │ ├── third-party │ │ │ └── congress.svg.png │ │ │ ├── us-government │ │ │ ├── doe.png │ │ │ ├── fcc.png │ │ │ ├── hhs.webp │ │ │ ├── hud.svg │ │ │ ├── irs.png │ │ │ ├── ssa.png │ │ │ └── usda.png │ │ │ ├── us.png │ │ │ └── us.webp │ ├── index.jsx │ ├── landing.jsx │ ├── markdown │ │ ├── about.md │ │ ├── contact.md │ │ ├── uk │ │ │ └── faq.md │ │ └── us │ │ │ └── faq.md │ ├── policyengine │ │ ├── footer │ │ │ ├── footer.jsx │ │ │ └── index.jsx │ │ ├── general │ │ │ ├── centered.jsx │ │ │ ├── help.jsx │ │ │ ├── navigationButton.jsx │ │ │ ├── radioButton.jsx │ │ │ └── spinner.jsx │ │ ├── header │ │ │ ├── index.jsx │ │ │ ├── mainNavigation.jsx │ │ │ ├── socialLinks.jsx │ │ │ └── title.jsx │ │ ├── index.jsx │ │ ├── layout │ │ │ └── general.jsx │ │ ├── pages │ │ │ ├── apiExplorer.jsx │ │ │ ├── household │ │ │ │ ├── accountingTable.jsx │ │ │ │ ├── earningsCharts.jsx │ │ │ │ ├── household.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── inputPane.jsx │ │ │ │ ├── menu.jsx │ │ │ │ └── variable.jsx │ │ │ ├── markdown.jsx │ │ │ ├── policy │ │ │ │ ├── index.jsx │ │ │ │ ├── menu.jsx │ │ │ │ ├── overview.jsx │ │ │ │ ├── parameter │ │ │ │ │ ├── index.js │ │ │ │ │ ├── numericParameterControl.jsx │ │ │ │ │ └── parameter.jsx │ │ │ │ └── policy.jsx │ │ │ └── populationImpact │ │ │ │ ├── ageChart.jsx │ │ │ │ ├── breakdown.jsx │ │ │ │ ├── chart.jsx │ │ │ │ ├── figure.jsx │ │ │ │ ├── index.jsx │ │ │ │ └── populationImpact.jsx │ │ ├── policyengine.jsx │ │ └── tools │ │ │ ├── translation.jsx │ │ │ └── url.jsx │ └── style │ │ ├── google-fonts.css │ │ └── policyengine.less ├── tailwind.config.js └── vite.config.js ├── policyengine ├── __init__.py ├── country │ ├── __init__.py │ ├── country.py │ ├── openfisca │ │ ├── __init__.py │ │ ├── computation_trees.py │ │ ├── entities.py │ │ ├── parameters.py │ │ ├── reforms.py │ │ └── variables.py │ ├── results_config.py │ ├── uk │ │ ├── __init__.py │ │ ├── additional_parameters.yaml │ │ ├── default_reform.py │ │ ├── results_config.py │ │ └── uk.py │ └── us │ │ ├── __init__.py │ │ ├── additional_parameters.yaml │ │ ├── default_reform.py │ │ ├── results_config.py │ │ └── us.py ├── impact │ ├── __init__.py │ ├── household │ │ ├── __init__.py │ │ ├── charts │ │ │ ├── __init__.py │ │ │ ├── budget.py │ │ │ └── marginal_tax_rate.py │ │ └── earnings_impact.py │ ├── population │ │ ├── __init__.py │ │ ├── by_provision.py │ │ ├── charts │ │ │ ├── __init__.py │ │ │ ├── age.py │ │ │ ├── budgetary_impact.py │ │ │ ├── decile.py │ │ │ ├── inequality.py │ │ │ ├── intra_decile.py │ │ │ └── poverty.py │ │ └── metrics.py │ └── utils │ │ ├── __init__.py │ │ ├── numeric.py │ │ ├── plotly.py │ │ └── text.py ├── package.py ├── policyengine.py ├── server.py ├── tests │ ├── basic.yaml │ ├── test_api.py │ ├── test_code_health.py │ └── us │ │ └── state_specific.yaml └── web_server │ ├── __init__.py │ ├── cache.py │ ├── cors.py │ ├── logging.py │ ├── social_card.py │ └── static_site.py ├── requirements.txt └── setup.py /.dockerignore: -------------------------------------------------------------------------------- 1 | policyengine-client/* 2 | -------------------------------------------------------------------------------- /.github/changelog_template.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | This repo consists of two packages - the React client and the Python server. A change to either repo should trigger an update in the versions for both to ensure a consistent changelog in this repo. 9 | 10 | {{changelog}} -------------------------------------------------------------------------------- /.github/get-changelog-diff.sh: -------------------------------------------------------------------------------- 1 | last_tagged_commit=`git describe --tags --abbrev=0 --first-parent` 2 | git --no-pager diff $last_tagged_commit -- CHANGELOG.md -------------------------------------------------------------------------------- /.github/has-functional-changes.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | IGNORE_DIFF_ON="README.md CONTRIBUTING.md Makefile docs/* .gitignore LICENSE* .github/*" 4 | 5 | last_tagged_commit=`git describe --tags --abbrev=0 --first-parent` # --first-parent ensures we don't follow tags not published in master through an unlikely intermediary merge commit 6 | 7 | if git diff-index --name-only --exit-code $last_tagged_commit -- . `echo " $IGNORE_DIFF_ON" | sed 's/ / :(exclude)/g'` # Check if any file that has not be listed in IGNORE_DIFF_ON has changed since the last tag was published. 8 | then 9 | echo "No functional changes detected." 10 | exit 1 11 | else echo "The functional files above were changed." 12 | fi 13 | -------------------------------------------------------------------------------- /.github/is-version-number-acceptable.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | if [[ ${GITHUB_REF#refs/heads/} == master ]] 4 | then 5 | echo "No need for a version check on master." 6 | exit 0 7 | fi 8 | 9 | if ! $(dirname "$BASH_SOURCE")/has-functional-changes.sh 10 | then 11 | echo "No need for a version update." 12 | exit 0 13 | fi 14 | 15 | current_version=`python setup.py --version` 16 | 17 | if git rev-parse --verify --quiet $current_version 18 | then 19 | echo "Version $current_version already exists in commit:" 20 | git --no-pager log -1 $current_version 21 | echo 22 | echo "Update the version number in setup.py before merging this branch into master." 23 | echo "Look at the CONTRIBUTING.md file to learn how the version number should be updated." 24 | exit 1 25 | fi 26 | 27 | if ! $(dirname "$BASH_SOURCE")/has-functional-changes.sh | grep --quiet changelog.yaml 28 | then 29 | echo "changelog.yaml has not been modified, while functional changes were made." 30 | echo "Explain what you changed before merging this branch into master." 31 | echo "Look at the CONTRIBUTING.md file to learn how to write the changelog." 32 | exit 2 33 | fi 34 | -------------------------------------------------------------------------------- /.github/publish-git-tag.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | git tag `python setup.py --version` 4 | git push --tags # update the repository version 5 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # New feature/Improvement/Bug fix: Title 2 | 3 | Short description of the changes made. 4 | 5 | - [ ] Added a new entry to `changelog.yaml` (undated). 6 | 7 | ### Status 8 | - [ ] Cosmetic change 9 | - [ ] Screenshots attached 10 | - [ ] Back-end change 11 | - [ ] Unintuitive logic commented 12 | - [ ] Version change 13 | - [ ] Major 14 | - [ ] Minor 15 | - [ ] Patch 16 | 17 | ### Issues fixed 18 | 19 | List any issues fixed here. 20 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Pull request 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | lint: 7 | name: Lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-python@v2 12 | - uses: psf/black@stable 13 | with: 14 | options: ". -l 79 --check" 15 | check-version: 16 | name: Check version 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | repository: ${{ github.event.pull_request.head.repo.full_name }} 23 | ref: ${{ github.event.pull_request.head.ref }} 24 | - name: Set up Python 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: 3.7 28 | - name: Build changelog 29 | run: pip install yaml-changelog && make changelog 30 | - name: Preview changelog update 31 | run: ".github/get-changelog-diff.sh" 32 | - name: Check version number has been properly updated 33 | run: ".github/is-version-number-acceptable.sh" 34 | test: 35 | name: Test 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: Checkout repo 39 | uses: actions/checkout@v2 40 | - name: Setup Python 41 | uses: actions/setup-python@v2 42 | with: 43 | python-version: 3.7 44 | cache: 'pip' 45 | - name: Setup Python Cache 46 | uses: actions/cache@v2 47 | with: 48 | path: ${{ env.pythonLocation }} 49 | key: ${{ env.pythonLocation }}-${{ hashFiles('setup.py') }} 50 | - name: Setup pnpm 51 | id: pnpm-install 52 | uses: pnpm/action-setup@v2.0.1 53 | with: 54 | version: 7 55 | run_install: false 56 | - name: Setup Node 57 | uses: actions/setup-node@v3 58 | with: 59 | node-version: 14 60 | cache: 'pnpm' 61 | cache-dependency-path: '**/pnpm-lock.yaml' 62 | - name: Set up Cloud SDK 63 | uses: google-github-actions/setup-gcloud@v0 64 | with: 65 | project_id: uk-policy-engine 66 | service_account_key: ${{ secrets.GCP_SA_KEY }} 67 | export_default_credentials: true 68 | - name: Install dependencies 69 | run: make install 70 | - name: Prepare test environment 71 | run: make build 72 | - name: Run the main tests 73 | run: make test 74 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | lint: 10 | name: Lint 11 | runs-on: ubuntu-latest 12 | if: | 13 | (github.repository == 'PolicyEngine/policyengine') 14 | && (github.event.head_commit.message == 'Update PolicyEngine') 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-python@v2 18 | - uses: psf/black@stable 19 | with: 20 | options: ". -l 79 --check" 21 | test: 22 | name: Test and Deploy 23 | runs-on: ubuntu-latest 24 | if: | 25 | (github.repository == 'PolicyEngine/policyengine') 26 | && (github.event.head_commit.message == 'Update PolicyEngine') 27 | steps: 28 | - name: Checkout repo 29 | uses: actions/checkout@v2 30 | - name: Setup Python 31 | uses: actions/setup-python@v2 32 | with: 33 | python-version: 3.7 34 | cache: 'pip' 35 | - name: Setup Python Cache 36 | uses: actions/cache@v2 37 | with: 38 | path: ${{ env.pythonLocation }} 39 | key: ${{ env.pythonLocation }}-${{ hashFiles('setup.py') }} 40 | - name: Setup pnpm 41 | id: pnpm-install 42 | uses: pnpm/action-setup@v2.0.1 43 | with: 44 | version: 7 45 | run_install: false 46 | - name: Setup Node 47 | uses: actions/setup-node@v3 48 | with: 49 | node-version: 14 50 | cache: 'pnpm' 51 | cache-dependency-path: '**/pnpm-lock.yaml' 52 | - name: Set up Cloud SDK 53 | uses: google-github-actions/setup-gcloud@v0 54 | with: 55 | project_id: uk-policy-engine 56 | service_account_key: ${{ secrets.GCP_SA_KEY }} 57 | export_default_credentials: true 58 | - name: Install dependencies 59 | run: make install 60 | - name: Deploy 61 | run: make deploy 62 | versioning: 63 | name: Update versioning 64 | if: | 65 | (github.repository == 'PolicyEngine/policyengine') 66 | && !(github.event.head_commit.message == 'Update PolicyEngine') 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Checkout repo 70 | uses: actions/checkout@v2 71 | with: 72 | repository: ${{ github.event.pull_request.head.repo.full_name }} 73 | ref: ${{ github.event.pull_request.head.ref }} 74 | token: ${{ secrets.POLICYENGINE_GITHUB }} 75 | - name: Setup Python 76 | uses: actions/setup-python@v2 77 | with: 78 | python-version: 3.7 79 | - name: Build changelog 80 | run: pip install yaml-changelog && make changelog 81 | - name: Preview changelog update 82 | run: ".github/get-changelog-diff.sh" 83 | - name: Update changelog 84 | uses: EndBug/add-and-commit@v8 85 | with: 86 | add: "." 87 | committer_name: Github Actions[bot] 88 | author_name: Github Actions[bot] 89 | message: Update PolicyEngine 90 | publish: 91 | name: Publish packages 92 | if: | 93 | (github.repository == 'PolicyEngine/policyengine') 94 | && (github.event.head_commit.message == 'Update PolicyEngine') 95 | runs-on: ubuntu-latest 96 | steps: 97 | - name: Checkout repo 98 | uses: actions/checkout@v2 99 | with: 100 | repository: ${{ github.event.pull_request.head.repo.full_name }} 101 | ref: ${{ github.event.pull_request.head.ref }} 102 | - name: Setup Python 103 | uses: actions/setup-python@v2 104 | with: 105 | python-version: 3.7 106 | cache: 'pip' 107 | - name: Setup Python Cache 108 | uses: actions/cache@v2 109 | with: 110 | path: ${{ env.pythonLocation }} 111 | key: ${{ env.pythonLocation }}-${{ hashFiles('setup.py') }} 112 | - name: Publish a git tag 113 | run: ".github/publish-git-tag.sh" 114 | - name: Setup pnpm 115 | id: pnpm-install 116 | uses: pnpm/action-setup@v2.0.1 117 | with: 118 | version: 7 119 | run_install: false 120 | - name: Setup Node 121 | uses: actions/setup-node@v3 122 | with: 123 | node-version: 14 124 | cache: 'pnpm' 125 | cache-dependency-path: '**/pnpm-lock.yaml' 126 | - name: Set up Cloud SDK 127 | uses: google-github-actions/setup-gcloud@v0 128 | with: 129 | project_id: uk-policy-engine 130 | service_account_key: ${{ secrets.GCP_SA_KEY }} 131 | export_default_credentials: true 132 | - name: Install dependencies 133 | run: make install 134 | - name: Build server package 135 | run: make build 136 | - name: Publish a Python distribution to PyPI 137 | uses: pypa/gh-action-pypi-publish@release/v1 138 | with: 139 | user: __token__ 140 | password: ${{ secrets.PYPI }} 141 | skip_existing: true 142 | packages_dir: dist/ 143 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | **/node_modules 3 | **/package-lock.json 4 | **/build 5 | **/*.log* 6 | **/dist 7 | **/pytest_cache 8 | **/__pycache__ 9 | **/*.egg-info 10 | **/.vscode 11 | **/static/* 12 | policyengine-client/src/countries/debug.jsx 13 | docs/_build/ 14 | policyengine/web_server/logs.yaml 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gcr.io/google-appengine/python 2 | 3 | # Create a virtualenv for dependencies. This isolates these packages from 4 | # system-level packages. 5 | # Use -p python3 or -p python3.7 to select python version. Default is version 2. 6 | RUN virtualenv /env -p python3.7 7 | 8 | # Setting these environment variables are the same as running 9 | # source /env/bin/activate. 10 | ENV VIRTUAL_ENV /env 11 | ENV PATH /env/bin:$PATH 12 | ENV GOOGLE_APPLICATION_CREDENTIALS .gac.json 13 | 14 | # Copy the application's requirements.txt and run pip to install all 15 | # dependencies into the virtualenv. 16 | 17 | # Add the application source code. 18 | ADD . /app 19 | 20 | RUN python -m pip install --upgrade pip 21 | 22 | RUN cd /app && make server 23 | 24 | # Run a WSGI server to serve the application. gunicorn must be declared as 25 | # a dependency in requirements.txt. 26 | CMD gunicorn -b :$PORT policyengine.server:app --timeout 240 27 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include policyengine * 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: install build format test 2 | install: install-client install-server 3 | install-client: 4 | cd policyengine-client; pnpm install 5 | install-server: 6 | pip install -e . 7 | build: build-client build-server 8 | build-client: 9 | cd policyengine-client; pnpm run build 10 | rm -rf policyengine/static 11 | cp -r policyengine-client/build policyengine/static 12 | build-server: 13 | rm -rf build/ dist/ policyengine.egg-info; python setup.py sdist bdist_wheel 14 | publish: publish-client publish-server 15 | publish-server: policyengine 16 | twine upload policyengine/dist/* --skip-existing 17 | publish-client: 18 | cd policyengine-client; pnpm publish 19 | debug-server: 20 | POLICYENGINE_DEBUG=1 FLASK_APP=policyengine.server:app FLASK_DEBUG=1 flask run 21 | debug-client: 22 | cd policyengine-client; pnpm start 23 | format: 24 | autopep8 policyengine -r -i 25 | autopep8 setup.py -i 26 | black policyengine -l 79 27 | black . -l 79 28 | test: 29 | pytest policyengine/tests -vv 30 | deploy: build-client 31 | cat $(GOOGLE_APPLICATION_CREDENTIALS) > .gac.json 32 | gcloud config set app/cloud_build_timeout 6000 33 | y | gcloud app deploy 34 | rm .gac.json 35 | deploy-beta: 36 | cat $(GOOGLE_APPLICATION_CREDENTIALS) > .gac.json 37 | gcloud config set app/cloud_build_timeout 6000 38 | y | gcloud app deploy --version beta --no-promote 39 | rm .gac.json 40 | server: install-server test 41 | changelog: 42 | build-changelog changelog.yaml --output changelog.yaml --update-last-date --start-from 1.4.1 --append-file changelog_entry.yaml 43 | build-changelog changelog.yaml --org PolicyEngine --repo policyengine --output CHANGELOG.md --template .github/changelog_template.md 44 | bump-version changelog.yaml policyengine-client/package.json setup.py policyengine/policyengine.py 45 | rm changelog_entry.yaml || true 46 | touch changelog_entry.yaml 47 | documentation: 48 | jb clean docs 49 | jb build docs -W 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PolicyEngine 2 | 3 | This repository contains the core infrastructure for [policyengine.org](https://policyengine.org). 4 | Namely: 5 | * `policyengine`, a Python package which contains the server-side implementations, and 6 | * `policyengine-client`, a React library containing high-level components to build the client-side interface. 7 | 8 | ## Development 9 | 10 | *NOTE:* requires Python 3.7 11 | 12 | First, ensure you have `pnpm` installed: https://pnpm.io/installation. 13 | 14 | Then, install using `make install`. Then, to debug the client, run `make debug-client`, or to debug the server, run `make debug-server`. 15 | 16 | If your changes involve the server, change `useLocalServer = false;` to `useLocalServer = true;` in `policyengine-client/src/countries/country.jsx`. 17 | Otherwise, change `usePolicyEngineOrgServer = false;` to `usePolicyEngineOrgServer = true;` in `policyengine-client/src/countries/country.jsx`. 18 | 19 | If you don't have access to the UK Family Resources Survey, you can still run the UK population-wide calculator on an anonymised version. To do that, instead of running `make debug-server`, run `UK_SYNTHETIC=1 make debug-server` 20 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: custom 2 | env: flex 3 | resources: 4 | cpu: 2 5 | memory_gb: 6 6 | disk_size_gb: 32 7 | automatic_scaling: 8 | min_num_instances: 1 9 | max_num_instances: 2 10 | cool_down_period_sec: 180 11 | cpu_utilization: 12 | target_utilization: 0.9 -------------------------------------------------------------------------------- /changelog_entry.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/changelog_entry.yaml -------------------------------------------------------------------------------- /debug_server.py: -------------------------------------------------------------------------------- 1 | from policyengine import PolicyEngine 2 | 3 | app = PolicyEngine(debug=True).app 4 | app.run(debug=True) 5 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: PolicyEngine documentation 2 | author: PolicyEngine 3 | copyright : '2022' 4 | html: 5 | use_edit_page_button : true # Whether to add an "edit this page" button to pages. If `true`, repository information in repository: must be filled in 6 | use_repository_button : true # Whether to add a link to your repository button 7 | use_issues_button : true # Whether to add an "open an issue" button 8 | google_analytics_id : "G-TYHPWJGXW8" # A GA id that can be used to track book views. 9 | home_page_in_navbar : true # Whether to include your home page in the left Navigation Bar 10 | notebook_interface : 'notebook' 11 | binder: 12 | binderhub_url : 'https://mybinder.org' 13 | text : 'Launch binder' 14 | launch_buttons: 15 | notebook_interface : 'classic' 16 | repository: 17 | url: https://github.com/PolicyEngine/policyengine 18 | branch: main 19 | path_to_book: docs/ 20 | # This enables plotly's JavaScript 21 | # See https://github.com/executablebooks/jupyter-book/issues/954#issuecomment-688220038 22 | sphinx: 23 | extra_extensions: [ 24 | 'sphinx.ext.autodoc', 25 | 'sphinx.ext.mathjax', 26 | 'sphinx.ext.viewcode', 27 | 'sphinx.ext.napoleon', 28 | 'alabaster' 29 | ] 30 | config: 31 | html_js_files: 32 | - https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js 33 | execute: 34 | execute_notebooks: 'force' 35 | jupyter_execute_notebooks: 'force' 36 | timeout: -1 37 | -------------------------------------------------------------------------------- /docs/_toc.yml: -------------------------------------------------------------------------------- 1 | format: jb-book 2 | root: index 3 | -------------------------------------------------------------------------------- /docs/index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# PolicyEngine documentation\n", 8 | "\n", 9 | "`policyengine` is a Python package that powers the interactive PolicyEngine site, allowing users additional flexibility in exploring UK and US tax-benefit systems. It builds on the OpenFisca UK and OpenFisca US tax-benefit models (which are also open-source Python packages) by providing a standardised interface for analysing policy reforms to both systems.\n", 10 | "\n", 11 | "## Example 1: microsimulation analysis" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 4, 17 | "metadata": {}, 18 | "outputs": [ 19 | { 20 | "data": { 21 | "text/markdown": [ 22 | "From this code snippet above, PolicyEngine estimates that revenue loss from lowering the US first tax rate from 10\\% to 5\\% is **$76.2bn**." 23 | ], 24 | "text/plain": [ 25 | "" 26 | ] 27 | }, 28 | "execution_count": 4, 29 | "metadata": {}, 30 | "output_type": "execute_result" 31 | } 32 | ], 33 | "source": [ 34 | "from policyengine import PolicyEngineUS\n", 35 | "from IPython.display import Markdown\n", 36 | "\n", 37 | "baseline, reformed = PolicyEngineUS().create_microsimulations(\n", 38 | " dict(\n", 39 | " gov_irs_income_bracket_rates_1=0.05, # Set the first tax rate to 5% (currently 10%)\n", 40 | " )\n", 41 | ")\n", 42 | "\n", 43 | "gain = (\n", 44 | " reformed.calc(\"spm_unit_net_income\").sum()\n", 45 | " - baseline.calc(\"spm_unit_net_income\").sum()\n", 46 | ")\n", 47 | "Markdown(\n", 48 | " f\"From this code snippet above, PolicyEngine estimates that revenue loss from lowering the US first tax rate from 10\\% to 5\\% is **${gain.sum() / 1e9:.1f}bn**.\"\n", 49 | ")" 50 | ] 51 | } 52 | ], 53 | "metadata": { 54 | "kernelspec": { 55 | "display_name": "Python 3.7.11 ('policyengine')", 56 | "language": "python", 57 | "name": "python3" 58 | }, 59 | "language_info": { 60 | "codemirror_mode": { 61 | "name": "ipython", 62 | "version": 3 63 | }, 64 | "file_extension": ".py", 65 | "mimetype": "text/x-python", 66 | "name": "python", 67 | "nbconvert_exporter": "python", 68 | "pygments_lexer": "ipython3", 69 | "version": "3.7.11" 70 | }, 71 | "orig_nbformat": 4, 72 | "vscode": { 73 | "interpreter": { 74 | "hash": "8dfc7b25af29ff50af05ad7ef5344155df419318e539ca710572e6dd4f9023a3" 75 | } 76 | } 77 | }, 78 | "nbformat": 4, 79 | "nbformat_minor": 2 80 | } 81 | -------------------------------------------------------------------------------- /policyengine-client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /policyengine-client/.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /policyengine-client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /policyengine-client/craco.config.js: -------------------------------------------------------------------------------- 1 | const CracoLessPlugin = require("craco-less"); 2 | 3 | module.exports = { 4 | plugins: [ 5 | { 6 | plugin: CracoLessPlugin, 7 | options: { 8 | lessLoaderOptions: { 9 | lessOptions: { 10 | modifyVars: { 11 | "primary-color": "#2c6496", 12 | "primary-1": "#fff", 13 | "link-color": "#002766", 14 | "success-color": "#0DD078", 15 | "border-radius-base": "40px", 16 | }, 17 | javascriptEnabled: true, 18 | }, 19 | }, 20 | }, 21 | }, 22 | ]}; 23 | -------------------------------------------------------------------------------- /policyengine-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | PolicyEngine 18 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /policyengine-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "policyengine-client", 3 | "version": "1.126.1", 4 | "dependencies": { 5 | "@ant-design/icons": "^4.7.0", 6 | "@emotion/react": "^11.9.3", 7 | "@emotion/styled": "^11.9.3", 8 | "@material-ui/core": "^4.12.4", 9 | "@mui/material": "^5.8.7", 10 | "@testing-library/jest-dom": "^5.16.4", 11 | "@testing-library/react": "^13.3.0", 12 | "@testing-library/user-event": "^14.2.1", 13 | "@vitejs/plugin-react": "^1.3.2", 14 | "@vitejs/plugin-react-refresh": "^1.3.6", 15 | "antd": "^4.21.5", 16 | "bootstrap": "^5.1.3", 17 | "framer-motion": "^6.3.16", 18 | "fuzzysort": "^2.0.1", 19 | "ga-4-react": "^0.1.281", 20 | "grommet": "^2.25.0", 21 | "moment": "^2.29.3", 22 | "plotly.js-basic-dist-min": "^2.12.1", 23 | "polished": "^4.2.2", 24 | "pretty-ms": "^8.0.0", 25 | "react": "^18.2.0", 26 | "react-bootstrap": "^2.4.0", 27 | "react-dom": "^18.2.0", 28 | "react-markdown": "^8.0.3", 29 | "react-plotly.js": "^2.5.1", 30 | "react-router": "^6.3.0", 31 | "react-router-dom": "6.3.0", 32 | "react-scripts": "5.0.1", 33 | "react-share": "^4.4.0", 34 | "rehype-raw": "^6.1.1", 35 | "source-map-explorer": "^2.5.2", 36 | "styled-components": "^5.3.5", 37 | "vite": "^2.9.13", 38 | "vite-plugin-svgr": "^2.2.0", 39 | "web-vitals": "^2.1.4" 40 | }, 41 | "scripts": { 42 | "start": "vite", 43 | "build": "vite build", 44 | "serve": "vite preview", 45 | "test": "craco test", 46 | "eject": "react-scripts eject", 47 | "analyze": "source-map-explorer 'build/static/js/*.js'" 48 | }, 49 | "browserslist": { 50 | "production": [ 51 | ">0.2%", 52 | "not dead", 53 | "not op_mini all" 54 | ], 55 | "development": [ 56 | "last 1 chrome version", 57 | "last 1 firefox version", 58 | "last 1 safari version" 59 | ] 60 | }, 61 | "devDependencies": { 62 | "@babel/core": "^7.18.6", 63 | "@babel/plugin-syntax-flow": "^7.18.6", 64 | "@babel/plugin-transform-react-jsx": "^7.18.6", 65 | "@popperjs/core": "^2.11.5", 66 | "@testing-library/dom": "^8.14.0", 67 | "@types/react": "^18.0.14", 68 | "@vitejs/plugin-react-refresh": "^1.3.1", 69 | "autoprefixer": "^10.4.7", 70 | "less": "^4.1.3", 71 | "plotly.js": "^2.12.1", 72 | "postcss": "^8.4.14", 73 | "react-is": "^18.2.0", 74 | "tailwindcss": "^3.1.5", 75 | "typescript": "^4.7.4", 76 | "vite": "^2.3.2", 77 | "vite-plugin-markdown": "^2.0.2", 78 | "vite-plugin-svgr": "^0.3.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /policyengine-client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /policyengine-client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/public/favicon.ico -------------------------------------------------------------------------------- /policyengine-client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/public/logo192.png -------------------------------------------------------------------------------- /policyengine-client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/public/logo512.png -------------------------------------------------------------------------------- /policyengine-client/public/main_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/public/main_logo.png -------------------------------------------------------------------------------- /policyengine-client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "PolicyEngine", 3 | "name": "PolicyEngine", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#002766", 24 | "background_color": "#002766" 25 | } 26 | -------------------------------------------------------------------------------- /policyengine-client/public/social_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/public/social_preview.png -------------------------------------------------------------------------------- /policyengine-client/public/social_preview/global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/public/social_preview/global.png -------------------------------------------------------------------------------- /policyengine-client/public/social_preview/uk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/public/social_preview/uk.png -------------------------------------------------------------------------------- /policyengine-client/public/social_preview/us.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/public/social_preview/us.png -------------------------------------------------------------------------------- /policyengine-client/src/App.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains the top-level logic: directing as per the URL 3 | * up to the /country page. 4 | */ 5 | 6 | import React from "react"; 7 | import { 8 | BrowserRouter as Router, 9 | Routes, 10 | Route, 11 | Navigate, 12 | } from "react-router-dom"; 13 | import PolicyEngine from "./policyengine/policyengine"; 14 | import { UK, US } from "./countries"; 15 | import MarkdownPage from "./policyengine/pages/markdown"; 16 | import LandingPage from "./landing"; 17 | import FOF from "./fof"; 18 | 19 | // Markdown files 20 | 21 | import { html as UK_FAQ } from "./markdown/uk/faq.md"; 22 | import { html as ABOUT } from "./markdown/about.md"; 23 | import { html as CONTACT } from "./markdown/contact.md"; 24 | // Import other markdown files here 25 | 26 | const markdownPages = [ 27 | { content: UK_FAQ, path: "/uk/faq", title: "FAQ" }, 28 | { content: ABOUT, path: "/about", title: "About" }, 29 | { content: CONTACT, path: "/contact", title: "Contact" }, 30 | // Add other pages here 31 | ]; 32 | 33 | export default function App(props) { 34 | // Redirect http to https 35 | if ( 36 | !window.location.hostname.includes("localhost") && 37 | window.location.protocol !== "https:" 38 | ) { 39 | window.location.href = 40 | "https:" + 41 | window.location.href.substring(window.location.protocol.length); 42 | } 43 | const uk = new UK(); 44 | const us = new US(); 45 | 46 | const pages = ["policy", "population-impact", "household"]; 47 | for (let page of pages) { 48 | for (let country of [uk, us]) { 49 | for (let url of Object.keys(country.namedPolicies)) { 50 | if(window.location.href.includes(`/${country.name}/${page}${url}`)) { 51 | window.history.pushState({}, "", `/${country.name}/${page}?${country.namedPolicies[url]}`); 52 | return 53 | } 54 | } 55 | } 56 | } 57 | 58 | return ( 59 | 60 | 61 | {markdownPages.map((page) => ( 62 | 72 | } 73 | /> 74 | ))} 75 | } /> 76 | } /> 77 | } /> 78 | 82 | 83 | 84 | } 85 | /> 86 | 90 | 91 | 92 | } 93 | /> 94 | } 97 | /> 98 | 99 | 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /policyengine-client/src/constants.jsx: -------------------------------------------------------------------------------- 1 | export const POLICYENGINE_LIGHT_BLUE = "#5091cc"; 2 | export const POLICYENGINE_MEDIUM_BLUE = "#2c6496"; 3 | export const POLICYENGINE_DARK_BLUE = "#002766"; 4 | -------------------------------------------------------------------------------- /policyengine-client/src/countries/country.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains static data for each country. 3 | */ 4 | import { createContext } from "react"; 5 | 6 | export default class Country { 7 | stateHolder = null 8 | populationImpactResults = null 9 | populationImpactBreakdownResults = null 10 | ageChartResult = null; 11 | editingReform = true; 12 | year = 2022 13 | showDatePicker = false; 14 | parameterRenames = {}; 15 | 16 | updatePolicy(name, value) { 17 | // Update a parameter - validate, then update the state 18 | let oldPolicy = this.policy; 19 | const targetKey = this.editingReform ? "value" : "baselineValue"; 20 | oldPolicy[name][targetKey] = value; 21 | let { policy, policyValid } = this.validatePolicy(oldPolicy); 22 | this.stateHolder.setCountryState({ 23 | policy: policy, 24 | policyValid: policyValid, 25 | populationImpactIsOutdated: true, 26 | populationBreakdownIsOutdated: true, 27 | baselineSituationImpactIsOutdated: !this.editingReform, 28 | reformSituationImpactIsOutdated: this.editingReform, 29 | situationVariationImpactIsOutdated: true, 30 | ageChartIsOutdated: true, 31 | }); 32 | } 33 | 34 | updateEntirePolicy(policy) { 35 | this.stateHolder.setCountryState({ 36 | policy: policy, 37 | populationImpactIsOutdated: true, 38 | populationBreakdownIsOutdated: true, 39 | ageChartIsOutdated: true, 40 | reformSituationImpactIsOutdated: true, 41 | situationVariationImpactIsOutdated: true 42 | }); 43 | } 44 | 45 | getPolicyJSONPayload() { 46 | const submission = {}; 47 | for (const key in this.policy) { 48 | if (this.policy[key].value !== this.policy[key].baselineValue) { 49 | submission[key] = this.policy[key].value; 50 | } 51 | if (this.policy[key].baselineValue !== this.policy[key].defaultValue) { 52 | submission["baseline_" + key] = this.policy[key].baselineValue; 53 | } 54 | } 55 | return submission; 56 | } 57 | 58 | setState(object, callback) { 59 | this.stateHolder.setCountryState(object, callback); 60 | } 61 | 62 | validatePolicy = policy => { return { policy: policy, valid: true } }; 63 | 64 | parameterHierarchy = {}; 65 | 66 | validateSituation(situation) { 67 | let metadata; 68 | let entity; 69 | let value; 70 | for (let variable of this.inputVariables) { 71 | metadata = this.variables[variable]; 72 | if (!metadata) { 73 | throw new Error(`Failed to load ${variable}.`) 74 | } 75 | value = metadata.valueType === "Enum" ? metadata.defaultValue.key : metadata.defaultValue; 76 | entity = this.entities[metadata.entity]; 77 | for (let entityInstance of Object.keys(situation[entity.plural])) { 78 | if (!Object.keys(situation[entity.plural][entityInstance]).includes(variable)) { 79 | let entry = {}; 80 | entry[this.year] = value; 81 | situation[entity.plural][entityInstance][variable] = entry; 82 | } 83 | } 84 | } 85 | for (let variable of this.outputVariables) { 86 | metadata = this.variables[variable]; 87 | if (!metadata) { 88 | throw new Error(`Failed to load ${variable}.`) 89 | } 90 | entity = this.entities[metadata.entity]; 91 | for (let entityInstance of Object.keys(situation[entity.plural])) { 92 | if (!Object.keys(situation[entity.plural][entityInstance]).includes(variable)) { 93 | situation[entity.plural][entityInstance][variable] = { [this.year]: null }; 94 | } 95 | } 96 | } 97 | return { situation: situation, valid: true } 98 | } 99 | 100 | policyIsOutdated = true; 101 | situationIsOutdated = true; 102 | populationImpactIsOutdated = true; 103 | populationBreakdownIsOutdated = true; 104 | ageChartIsOutdated = true; 105 | baselineSituationImpactIsOutdated = true; 106 | reformSituationImpactIsOutdated = true; 107 | situationVariationImpactIsOutdated = true; 108 | 109 | computedBaselineSituation = null; 110 | computedReformSituation = null; 111 | 112 | updateSituationValue(entityType, entityName, variable, value) { 113 | let situation = this.situation; 114 | let entry = {}; 115 | entry[this.year] = value; 116 | situation[this.entities[entityType].plural][entityName][variable] = entry; 117 | this.setState({ 118 | situation: situation, 119 | baselineSituationImpactIsOutdated: true, 120 | situationVariationImpactIsOutdated: true, 121 | reformSituationImpactIsOutdated: true, 122 | }); 123 | } 124 | 125 | useLocalServer = false; 126 | usePolicyEngineOrgServer = false; 127 | 128 | waitingOnPopulationImpact = false; 129 | waitingOnAgeChart = false; 130 | waitingOnAccountingTableBaseline = false; 131 | waitingOnAccountingTableReform = false; 132 | waitingOnEarningsCharts = false; 133 | waitingOnPopulationBreakdown = false; 134 | showSnapShot = true 135 | 136 | getParameterList() { 137 | function getLeafList(node) { 138 | if (Array.isArray(node)) { 139 | return node; 140 | } else { 141 | let list = []; 142 | for (let key in node) { 143 | list = list.concat(getLeafList(node[key])); 144 | } 145 | return list; 146 | } 147 | } 148 | return getLeafList(this.parameterHierarchy).concat(this.extraParameterListNames); 149 | } 150 | 151 | extraParameterListNames = []; 152 | } 153 | 154 | export const CountryContext = createContext({ name: "uk" }); -------------------------------------------------------------------------------- /policyengine-client/src/countries/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains static data for each country. 3 | */ 4 | 5 | export { UK } from "./uk"; 6 | export { US } from "./us"; 7 | export { CountryContext } from "./country"; -------------------------------------------------------------------------------- /policyengine-client/src/countries/uk/components/autoUBI.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, Divider, Alert, Spin, message } from "antd"; 3 | import { LoadingOutlined } from "@ant-design/icons"; 4 | import { CountryContext } from "../../country"; 5 | 6 | function computeUBI(countryContext, callBack) { 7 | // This function sends an API call to the AutoUBI endpoint, 8 | // passing in the current policy as a JSON payload. It then 9 | // checks the results every 2 seconds until it gets a non-loading 10 | // response. Upon completion, it calls the callback function with 11 | // the UBI amount. 12 | const submission = countryContext.getPolicyJSONPayload(); 13 | let url = new URL(countryContext.apiURL + "/auto-ubi"); 14 | url.search = new URLSearchParams(submission).toString(); 15 | const roundAndWeeklyise = (x) => Math.round(x / 52 * 100) / 100; 16 | return fetch(url) 17 | .then((response) => response.json()) 18 | .then((data) => { 19 | if(data.status === "completed") { 20 | callBack(roundAndWeeklyise(data.UBI)); 21 | } else { 22 | let checker = setInterval(() => { 23 | fetch(url).then(res => res.json()).then(data => { 24 | if (data.status === "completed") { 25 | clearInterval(checker); 26 | callBack(roundAndWeeklyise(data.UBI)) 27 | } 28 | }) 29 | }, 1000 * 2); 30 | } 31 | }); 32 | } 33 | 34 | function updatePolicyWithUBI(countryContext, UBI) { 35 | // This function updates the policy with the UBI amount. 36 | let newPolicy = countryContext.policy; 37 | newPolicy.child_bi.value = UBI; 38 | newPolicy.adult_bi.value = UBI; 39 | newPolicy.senior_bi.value = UBI; 40 | countryContext.updateEntirePolicy(newPolicy); 41 | } 42 | 43 | export default function AutoUBI(props) { 44 | const [waiting, setWaiting] = React.useState(false); 45 | const [ubiAmount, setUBIAmount] = React.useState(0); 46 | const country = React.useContext(CountryContext); 47 | let loadingMessageIfNeeded; 48 | 49 | if(waiting) { 50 | loadingMessageIfNeeded = This reform would fund a UBI of £}/>/week 52 | } />; 53 | } else if(ubiAmount > 0) { 54 | loadingMessageIfNeeded = This reform would fund a UBI of £{ubiAmount}/week 56 | } /> ; 57 | } 58 | 59 | const onButtonClick = () => { 60 | setWaiting(true); 61 | computeUBI(country, (amount) => { 62 | updatePolicyWithUBI(country, amount); 63 | setUBIAmount(amount); 64 | setWaiting(false); 65 | }); 66 | }; 67 | 68 | return ( 69 | <> 70 | AutoUBI 71 | 74 | {loadingMessageIfNeeded} 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /policyengine-client/src/countries/uk/components/countrySpecific.jsx: -------------------------------------------------------------------------------- 1 | import { Select } from "antd"; 2 | import { useContext } from "react"; 3 | import { CountryContext } from "../../country"; 4 | 5 | const { Option } = Select; 6 | 7 | export default function CountrySpecific(props) { 8 | const country = useContext(CountryContext); 9 | const parameter = country.policy.country_specific; 10 | return <> 11 |
{parameter.label}
12 |

{parameter.description}

13 | 33 | 34 | } -------------------------------------------------------------------------------- /policyengine-client/src/countries/uk/components/extraBand.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Parameter from "../../../policyengine/pages/policy/parameter"; 3 | import { Checkbox } from "antd"; 4 | import { CountryContext } from "../../country"; 5 | 6 | export default class ExtraBand extends React.Component { 7 | static contextType = CountryContext; 8 | 9 | constructor(props, context) { 10 | super(props); 11 | const rate_param = context.policy[this.props.rate_parameter]; 12 | const threshold_param = context.policy[this.props.threshold_parameter]; 13 | this.state = { 14 | shown: (rate_param.value !== rate_param.defaultValue) || (threshold_param.value !== threshold_param.defaultValue) 15 | }; 16 | } 17 | 18 | updateChecked(checked) { 19 | if(checked) { 20 | // Force showing of extra band 21 | this.setState({shown: true}); 22 | } else { 23 | // Reset band on uncheck 24 | this.context.updatePolicy(this.props.rate_parameter, this.context.policy[this.props.rate_parameter].defaultValue); 25 | this.context.updatePolicy(this.props.threshold_parameter, this.context.policy[this.props.threshold_parameter].defaultValue); 26 | this.setState({shown: false}); 27 | } 28 | } 29 | 30 | render() { 31 | return ( 32 | <> 33 | this.updateChecked(e.target.checked)}>Add extra band 34 | { 35 | this.state.shown && 36 | <> 37 | 38 | 39 | 40 | } 41 | 42 | ); 43 | } 44 | } -------------------------------------------------------------------------------- /policyengine-client/src/countries/uk/index.jsx: -------------------------------------------------------------------------------- 1 | export { UK } from "./uk" 2 | -------------------------------------------------------------------------------- /policyengine-client/src/countries/us/components/stateSpecific.jsx: -------------------------------------------------------------------------------- 1 | import { Select } from "antd"; 2 | import { useContext } from "react"; 3 | import { CountryContext } from "../../country"; 4 | 5 | const { Option } = Select; 6 | 7 | export default function StateSpecific(props) { 8 | const country = useContext(CountryContext); 9 | const parameter = country.policy.state_specific; 10 | return <> 11 |
{parameter.label}
12 |

{parameter.description}

13 | 33 | 34 | } -------------------------------------------------------------------------------- /policyengine-client/src/countries/us/index.jsx: -------------------------------------------------------------------------------- 1 | export { US } from "./us" -------------------------------------------------------------------------------- /policyengine-client/src/countries/utils/timePeriod.jsx: -------------------------------------------------------------------------------- 1 | export default function translateTimePeriod(oldSituation, fromYear, toYear) { 2 | // Find every occurrence of {fromYear: ...} in the situation and replace it with 3 | // {toYear: ...}, removing the original entry. 4 | if(fromYear === toYear) { 5 | return oldSituation; 6 | } 7 | let situation = JSON.parse(JSON.stringify(oldSituation)); 8 | for (let entity of Object.keys(situation)) { 9 | for (let entityInstance of Object.keys(situation[entity])) { 10 | for (let variable of Object.keys(situation[entity][entityInstance])) { 11 | if (variable !== "members") { 12 | situation[entity][entityInstance][variable][toYear] = situation[entity][entityInstance][variable][fromYear]; 13 | delete situation[entity][entityInstance][variable][fromYear]; 14 | } 15 | } 16 | } 17 | } 18 | return situation; 19 | } -------------------------------------------------------------------------------- /policyengine-client/src/fof.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Header } from "./policyengine/header"; 3 | 4 | export default function FOF() { 5 | return ( 6 |
7 |
8 |

This page does not exist, please navigate home.

9 |
10 | ) 11 | } -------------------------------------------------------------------------------- /policyengine-client/src/images/help/uk/householdHelper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/help/uk/householdHelper.gif -------------------------------------------------------------------------------- /policyengine-client/src/images/help/uk/impactHelper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/help/uk/impactHelper.gif -------------------------------------------------------------------------------- /policyengine-client/src/images/help/uk/policyHelper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/help/uk/policyHelper.gif -------------------------------------------------------------------------------- /policyengine-client/src/images/help/us/householdHelper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/help/us/householdHelper.gif -------------------------------------------------------------------------------- /policyengine-client/src/images/help/us/policyHelper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/help/us/policyHelper.gif -------------------------------------------------------------------------------- /policyengine-client/src/images/logos/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/logos/black.png -------------------------------------------------------------------------------- /policyengine-client/src/images/logos/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/logos/blue.png -------------------------------------------------------------------------------- /policyengine-client/src/images/logos/cgo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/logos/cgo.png -------------------------------------------------------------------------------- /policyengine-client/src/images/logos/cgo_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/logos/cgo_wide.png -------------------------------------------------------------------------------- /policyengine-client/src/images/logos/facebook_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/logos/facebook_logo.png -------------------------------------------------------------------------------- /policyengine-client/src/images/logos/linkedin_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/logos/linkedin_logo.png -------------------------------------------------------------------------------- /policyengine-client/src/images/logos/partnership_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/logos/partnership_icon.png -------------------------------------------------------------------------------- /policyengine-client/src/images/logos/sponsorship_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/logos/sponsorship_logo.png -------------------------------------------------------------------------------- /policyengine-client/src/images/logos/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/logos/white.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/clock.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/clock.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/clock.webp -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/misc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/misc.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/misc.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/misc.webp -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/simulation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/simulation.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/simulation.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/simulation.webp -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/third-party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/third-party.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/ubi-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/ubi-center.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/ubi-center.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/ubi-center.webp -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/uk/third-party/green-party.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/uk/third-party/green-party.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/uk/third-party/green-party.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/uk/third-party/green-party.webp -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/uk/third-party/smf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/uk/third-party/smf.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/uk/third-party/smf.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/uk/third-party/smf.webp -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/uk/uk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/uk/uk.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/uk/uk.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/uk/uk.webp -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/state-governments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/state-governments.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/state-governments/ma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/state-governments/ma.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/state-governments/md.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/state-governments/md.jpeg -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/state-governments/ny.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/state-governments/ny.webp -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/state-governments/or.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/state-governments/or.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/state-governments/pa.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/state-governments/pa.webp -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/state-governments/wa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/state-governments/wa.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/third-party/congress.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/third-party/congress.svg.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/us-government/doe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/us-government/doe.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/us-government/fcc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/us-government/fcc.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/us-government/hhs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/us-government/hhs.webp -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/us-government/irs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/us-government/irs.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/us-government/ssa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/us-government/ssa.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/us-government/usda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/us-government/usda.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/us.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/us.png -------------------------------------------------------------------------------- /policyengine-client/src/images/parameter-icons/us/us.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine-client/src/images/parameter-icons/us/us.webp -------------------------------------------------------------------------------- /policyengine-client/src/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * This is the first entry point of the app. No UI code is here; 3 | * it is only responsible for initializing the app. 4 | */ 5 | 6 | import React from "react"; 7 | import ReactDOM from "react-dom/client"; 8 | import App from "./App.jsx"; 9 | import GA4React from "ga-4-react"; 10 | 11 | import "bootstrap/dist/css/bootstrap.min.css"; 12 | import "./style/policyengine.less"; 13 | 14 | const ga4react = new GA4React("G-QL2XFHB7B4"); 15 | (async (_) => { 16 | await ga4react 17 | .initialize() 18 | .catch((err) => console.log("Analytics failed to load")) 19 | .then((analytics) => { 20 | ReactDOM.createRoot(document.getElementById("root")).render( 21 | 22 | , 23 | 24 | ); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /policyengine-client/src/markdown/about.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | ## Leadership 4 | 5 | **Max Ghenis** is the co-founder and CEO of PolicyEngine. 6 | He is also the founder and president of the UBI Center, a think tank researching universal basic income policies, and was previously a data scientist at Google. 7 | Max has a master's degree in Data, Economics, and Development Policy from MIT and a bachelor's degree in operations research from UC Berkeley. 8 | 9 | **Nikhil Woodruff** is the co-founder and CTO of PolicyEngine. 10 | He is also the UK Research Director at the UBI Center, a think tank researching universal basic income policies, and was previously a data scientist at Caspian, where he worked in improving anti-money laundering investigations. 11 | Nikhil is currently on leave from Durham University's Computer Science program. 12 | 13 | ## Advisors 14 | 15 | **Jason M. DeBacker** is an associate professor in the Department of Economics at the Darla Moore School of Business and president of the PSL Foundation. 16 | His research interests lie in the areas of public finance and macroeconomics. 17 | He has published papers on these topics in the Journal of Financial Economics, the Journal of Law and Economics, the Journal of Public Economics, the Brookings Papers on Economic Activity and other outlets. 18 | From 2009 to 2012, he worked as a financial economist in the Office of Tax Analysis at the U.S. Department of the Treasury. 19 | Jason holds a bachelor's degree in Management Information Systems from the University of Georgia and a Ph.D. in Economics from the University of Texas at Austin. 20 | 21 | **Richard W. Evans** is Advisory Board Visiting Fellow at Rice University’s Baker Institute for Public Policy, Director of the Open Source Economics Laboratory, nonresident fellow at the Tax Policy Center, and President of the Open Research Group. 22 | His research focuses on macroeconomics, fiscal policy, and computational modeling. 23 | Richard received a B.A. in economics and M.A. in public policy from Brigham Young University and a Ph.D. in economics from the University of Texas at Austin. 24 | 25 | **Jesse Horwitz** is the co-founder of Hubble Contacts. 26 | He previously worked as a policy advisor to Andrew Yang's mayoral campaign and as an investment analyst. 27 | Jesse holds a bachelor's degree in Economics-Mathematics from Columbia University and attended Harvard Law School. 28 | 29 | **Matt Jensen** is the founding director of the Open Source Policy Center at the American Enterprise Institute. 30 | He is also a co-founder of the Open Research Group, the Policy Simulation Library, and Compute Tooling. 31 | Jensen is a graduate of Pomona College with a degree in math. 32 | 33 | **Ben Ogorek** is the chief data scientist at Spencer Health Solutions. 34 | He previous worked in data science roles at Nousot, CUNA Mutual Group, Google, and Nationwide. 35 | Ben holds a B.A., M.A., and Ph.D. in Statistics from North Carolina State University. 36 | 37 | ## PSL Foundation Board of Directors 38 | 39 | PolicyEngine is fiscally sponsored by the [PSL Foundation](https://psl-foundation.org/), a US nonprofit with the following directors: 40 | 41 | **Jason DeBacker,** president of the PSL Foundation and associate professor of economics at the University of South Carolina's Darla Moore School of Business. 42 | 43 | **Linda Gibbs,** principal at Bloomberg Associates. Linda previously served New York City as the Deputy Mayor for Health and Human Services and Commissioner of the Department of Homeless Services. 44 | 45 | **Glenn Hubbard,** Dean Emeritus and Professor of Finance and Economics at Columbia University and nonresident senior fellow at the American Enterprise Institute. Glenn previously served as Chairman of the Council of Economic Advisers and Deputy Assistant Treasury Secretary. 46 | -------------------------------------------------------------------------------- /policyengine-client/src/markdown/contact.md: -------------------------------------------------------------------------------- 1 | # Contact us 2 | 3 | ## Email 4 | 5 | You can reach us [by email](mailto:hello@policyengine.org). 6 | 7 | ## Feedback 8 | 9 | You can send us feedback about our app on [here](https://zej8fnylwn9.typeform.com/to/XFFu15Xq). 10 | -------------------------------------------------------------------------------- /policyengine-client/src/markdown/uk/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## General 4 | 5 | ### What is PolicyEngine? 6 | 7 | PolicyEngine is a web app that calculates taxes and benefits for society and households under current policy and customisable policy reforms. 8 | With PolicyEngine, you can design simple or complex tax and benefit reforms, and see how they affect the UK budget, poverty, and inequality, as well as how they affect your own finances. 9 | 10 | ### How do I use PolicyEngine? 11 | 12 | See the help button in the bottom-left for a demo. 13 | 14 | ### When will PolicyEngine be available in my country? 15 | 16 | Currently, PolicyEngine is only available in the UK, but we're working on bringing it to the US. 17 | Want to see PolicyEngine in your country? 18 | [Let us know!](https://zej8fnylwn9.typeform.com/to/XFFu15Xq) 19 | 20 | ### Where can I learn more about how PolicyEngine works? 21 | 22 | The code for [PolicyEngine](http://github.com/PolicyEngine/policyengine) and for [OpenFisca-UK](https://github.com/PolicyEngine/openfisca-uk), the microsimulation model that underlies it, is publicly available on GitHub. 23 | We've also answered some common questions below. 24 | 25 | ### How can I help? 26 | 27 | Using and sharing PolicyEngine is already a great help to us! 28 | If you'd like to support our work and computing fees, please consider [making a donation](https://opencollective.com/psl) through our fiscal sponsor, the PSL Foundation (tax-deductible in the US). 29 | We're also entirely open source, and welcome contributions from developers on [GitHub](http://github.com/PolicyEngine/policyengine). 30 | 31 | ## Policy page 32 | 33 | ### Do you only simulate policies listed on this page? 34 | 35 | No, we simulate essentially the entire tax and benefit system, except for [capital gains tax](https://github.com/PolicyEngine/openfisca-uk/issues/40) and [council tax benefit](https://github.com/PolicyEngine/openfisca-uk/issues/150). 36 | For example, while we don't yet expose parameters on Income Support, we do simulate it for those who are eligible. 37 | 38 | ### Will you be adding more policy parameters? 39 | 40 | Yes, we're planning to add more tax and benefit parameter options. 41 | What would you like to simulate? 42 | [Let us know!](https://zej8fnylwn9.typeform.com/to/XFFu15Xq) 43 | 44 | ### How can I reset a policy parameter to its current value? 45 | 46 | For now, you'll have to change it back using the slider or text box, or reload the page to reset all policy parameters. 47 | We're working on a [better way](https://github.com/PolicyEngine/policyengine/issues/107). 48 | 49 | ### As of what date are policy parameters set? 50 | 51 | We use policy parameters from today's date, and backdate them to the start of the year. 52 | You can adjust the snapshot date with the **Snapshot** menu item from the Policy screen. 53 | 54 | ### How is this model validated? 55 | 56 | PolicyEngine uses the OpenFisca UK microsimulation model of the UK tax and benefit system. 57 | We cite legislation or government sites for all policy parameters, and validate against various gov.uk sites, reports from other microsimulation models, and external benefits calculators. 58 | See the [OpenFisca UK validation page](https://PolicyEngine.github.io/openfisca-uk/validation.html) for more information. 59 | 60 | ### Does abolishing legacy benefits move claimants to Universal Credit, and vice versa? 61 | 62 | No; PolicyEngine treats Universal Credit enrolment as fixed, so claimants are not moved between it and legacy benefits. 63 | We suggest abolishing Universal Credit and legacy benefits together if abolishing either. 64 | 65 | ## UK impact page 66 | 67 | ### What data do you use to estimate UK-wide impacts? 68 | 69 | We use the most recent Family Resources Survey (FRS), which covers the 2019-2020 fiscal year. 70 | The FRS is the UK's standard survey for estimating the distribution of income. 71 | We then extrapolate the FRS to 2022 using growth factors published by the Office of National Statistics and Office for Budget Responsibility. 72 | We also [adjust FRS weights](https://policyengine.github.io/openfisca-uk/model/reweighting) to minimize discrepancies against over 1,500 aggregates published by the UK government. 73 | 74 | ### What behavioural or macroeconomic assumptions do you make? 75 | 76 | None; PolicyEngine is a "static model" only. 77 | For example, it assumes that changing marginal tax rates will not affect labour supply. 78 | 79 | ### How does PolicyEngine define poverty? 80 | 81 | PolicyEngine reports the change to the absolute poverty rate before housing costs. 82 | Because we adjust data to more closely match administrative statistics, our baseline poverty estimate may differ from the government's. 83 | 84 | _[Learn more about poverty measurement in the UK.](https://osr.statisticsauthority.gov.uk/the-trouble-with-measuring-poverty/)_ 85 | 86 | ### How are the age groups in the poverty chart defined? 87 | 88 | Child poverty refers to poverty among people aged 0 to 17. 89 | Working age adults are people at least 18 years of age but younger than State Pension age. 90 | Retired people are people State Pension age or older. 91 | 92 | ## Your household page 93 | 94 | ### Do you store data about my household? 95 | 96 | No, we don't track any household-level information provided by users. 97 | 98 | ## Household impact page 99 | 100 | ### What are marginal tax rates and how are they calculated? 101 | 102 | Marginal tax rates are the share of an additional pound of income that the state takes, either through reduced benefit payments or through taxes. 103 | The baseline tax system has only three marginal rates—the 20% Basic Rate, the 40% Higher Rate, and the 45% Additional Rate—but due to the withdrawal of Universal Credit, the Child Benefit's High Income Tax Charge, the withdrawal of the Personal Allowance, and other features, marginal tax rate schedules are not strictly monotonic. 104 | PolicyEngine calculates marginal tax rates with respect to the employment income of the household head ("You" in the _Your household_ page). 105 | -------------------------------------------------------------------------------- /policyengine-client/src/markdown/us/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## General 4 | 5 | ### What is PolicyEngine? 6 | 7 | PolicyEngine is a web app that calculates taxes and benefits under current policy and customizable policy reforms. 8 | 9 | ### When will PolicyEngine be available in my state? 10 | 11 | Currently, PolicyEngine is only available in California, but we're working on bringing it to other states. 12 | Want to see PolicyEngine in your state? 13 | [Let us know!](https://zej8fnylwn9.typeform.com/to/XFFu15Xq) 14 | 15 | ### Where can I learn more about how PolicyEngine works? 16 | 17 | The code for [PolicyEngine](http://github.com/PolicyEngine/policyengine) and for [OpenFisca US](https://github.com/PolicyEngine/openfisca-us), the microsimulation model that underlies it, is publicly available on GitHub. 18 | We've also answered some common questions below. 19 | 20 | ### How can I help? 21 | 22 | Using and sharing PolicyEngine is already a great help to us! 23 | If you'd like to support our work and computing fees, please consider [making a donation](https://opencollective.com/psl) through our fiscal sponsor, the PSL Foundation (tax-deductible in the US). 24 | We're also entirely open source, and welcome contributions from developers on [GitHub](http://github.com/PolicyEngine/policyengine). 25 | 26 | ## Policy page 27 | 28 | ### Do you only simulate policies listed on this page? 29 | 30 | No, we simulate essentially the entire tax system and many benefits. 31 | For example, while we don't yet expose parameters on the Child Tax Credit, we do simulate it. 32 | 33 | ### Will you be adding more policy parameters? 34 | 35 | Yes, we're planning to add more tax and benefit parameter options. 36 | What would you like to simulate? 37 | [Let us know!](https://zej8fnylwn9.typeform.com/to/XFFu15Xq) 38 | 39 | ### How can I reset a policy parameter to its current value? 40 | 41 | For now, you'll have to change it back using the slider or text box, or reload the page to reset all policy parameters. 42 | We're working on a [better way](https://github.com/PolicyEngine/policyengine/issues/107). 43 | 44 | ### As of what date are policy parameters set? 45 | 46 | We use policy parameters from today's date, and backdate them to the start of the year. 47 | You can adjust the snapshot date with the **Snapshot** menu item from the Policy screen. 48 | 49 | ### How is this model validated? 50 | 51 | PolicyEngine uses the OpenFisca US microsimulation model of the UK tax and benefit system. 52 | We cite legislation or government sites for all policy parameters, and validate against government websites, reports from other microsimulation models, and external benefits calculators. 53 | 54 | We are still in beta and will validate against more sources before fully launching. 55 | 56 | ## Your household page 57 | 58 | ### Do you store data about my household? 59 | 60 | No, we don't track any household-level information provided by users. 61 | -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/footer/footer.jsx: -------------------------------------------------------------------------------- 1 | import SocialLinks from "../header/socialLinks"; 2 | import { useContext } from "react"; 3 | import { CountryContext } from "../../countries"; 4 | import HelpButton from "../general/help"; 5 | 6 | export default function Footer(props) { 7 | const country = useContext(CountryContext); 8 | return ( 9 |
22 |
23 | PolicyEngine © 2022 |{" "} 24 | About | |{" "} 25 | FAQ |{" "} 26 | Blog |{" "} 27 | Feedback |{" "} 28 | Donate 29 |
30 |
34 | 35 |
36 |
37 |
40 | PolicyEngine © 2022 | 41 | About | 42 | | FAQ | 43 | Blog | 44 | Feedback | 45 | Donate 46 |
47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/footer/index.jsx: -------------------------------------------------------------------------------- 1 | export { default as Footer } from "./footer"; -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/general/centered.jsx: -------------------------------------------------------------------------------- 1 | import { Empty } from "antd" 2 | 3 | export default function Centered(props) { 4 | return 5 | {props.children} 6 | 7 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/general/navigationButton.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from "antd"; 2 | import { useContext } from "react"; 3 | import { Link } from "react-router-dom"; 4 | import { CountryContext } from "../../countries"; 5 | import { policyToURL } from "../tools/url"; 6 | 7 | export default function SimulateButton(props) { 8 | const country = useContext(CountryContext); 9 | const url = policyToURL(`/${country.name}/${props.target}`, country.policy); 10 | let button = < 11 | Button 12 | disabled={props.disabled} 13 | type={props.primary ? "primary" : null} 14 | onClick={props.onClick} 15 | block 16 | >{props.text} 17 | ; 18 | if (props.target) { 19 | button = {button}; 20 | } 21 | return ( 22 |
23 | {button} 24 |
25 | ); 26 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/general/radioButton.jsx: -------------------------------------------------------------------------------- 1 | import { Radio } from "antd"; 2 | 3 | 4 | export default function RadioButton(props) { 5 | return ( 6 |
7 | props.onChange(e.target.value)} > 8 | {props.options.map(option => {option})} 9 | 10 |
11 | ); 12 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/general/spinner.jsx: -------------------------------------------------------------------------------- 1 | import { LoadingOutlined } from "@ant-design/icons"; 2 | import { Spin } from "antd"; 3 | 4 | export default function Spinner(props) { 5 | return }/>; 6 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/header/index.jsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from "react-router-dom"; 2 | import MainNavigation from "./mainNavigation"; 3 | 4 | export function Header(props) { 5 | let navigation; 6 | if (props.title || props.noTabs) { 7 | navigation = ; 8 | } else { 9 | navigation = ( 10 | 11 | } /> 12 | } 15 | /> 16 | } 19 | /> 20 | } /> 21 | } 24 | /> 25 | 26 | ); 27 | } 28 | return ( 29 |
38 | {navigation} 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/header/mainNavigation.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Components for the main tab-based navigation. 3 | */ 4 | 5 | import { Tabs } from "antd"; 6 | import { useNavigate } from "react-router-dom"; 7 | import { Row, Col } from "react-bootstrap"; 8 | import { policyToURL } from "../tools/url"; 9 | import SocialLinks from "./socialLinks"; 10 | import Title from "./title"; 11 | import { useContext } from "react"; 12 | import { CountryContext } from "../../countries"; 13 | 14 | const { TabPane } = Tabs; 15 | 16 | export default function MainNavigation(props) { 17 | const navigate = useNavigate(); 18 | const country = useContext(CountryContext); 19 | let middleColumn; 20 | if (props.title || props.noTabs) { 21 | middleColumn = ( 22 | 23 | 24 | 25 | ); 26 | } else { 27 | const onTabClick = (key) => { 28 | navigate(policyToURL(`/${country.name}/${key}`, country.policy)); 29 | }; 30 | middleColumn = ( 31 | 38 | {country.showPolicy && } 39 | {country.showPopulationImpact && ( 40 | 41 | )} 42 | {country.showHousehold && ( 43 | 44 | )} 45 | 46 | ); 47 | } 48 | return ( 49 |
57 | 58 | 59 | 60 | </Col> 61 | <Col 62 | lg={8} 63 | className="d-flex align-items-center justify-content-center" 64 | style={{ paddingLeft: 25, paddingRight: 25 }} 65 | > 66 | {middleColumn} 67 | </Col> 68 | <Col 69 | lg={2} 70 | className="d-none d-lg-flex align-items-center justify-content-right" 71 | > 72 | <SocialLinks color="white" /> 73 | </Col> 74 | </Row> 75 | </div> 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/header/socialLinks.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Social links component - the social media icons. 3 | */ 4 | 5 | import { 6 | FacebookOutlined, 7 | InstagramOutlined, 8 | LinkedinOutlined, 9 | TwitterOutlined, 10 | GithubOutlined, 11 | } from '@ant-design/icons'; 12 | 13 | import LinkedInLogo from "../../images/logos/linkedin_logo.png" 14 | import FacebookLogo from "../../images/logos/facebook_logo.png" 15 | 16 | 17 | export default function SocialLinks(props) { 18 | const iconStyle = { marginLeft: 15, fontSize: 15, color: props.color }; 19 | const importedIconStyle = { height: "14px", marginLeft: 15, marginTop: 4, filter: props.color === "black" ? "" : "invert(1)" }; 20 | return ( 21 | <div className="d-flex justify-content-center"> 22 | <a href="https://twitter.com/ThePolicyEngine"> 23 | <TwitterOutlined style={iconStyle} /> 24 | </a> 25 | <a href="https://facebook.com/PolicyEngine"> 26 | <img src={FacebookLogo} style={importedIconStyle}/> 27 | </a> 28 | <a href="https://linkedin.com/company/ThePolicyEngine"> 29 | <img src={LinkedInLogo} style={importedIconStyle}/> 30 | </a> 31 | <a href="https://instagram.com/PolicyEngine"> 32 | <InstagramOutlined style={iconStyle} /> 33 | </a> 34 | <a href="https://github.com/PolicyEngine"> 35 | <GithubOutlined style={iconStyle} /> 36 | </a> 37 | </div> 38 | ); 39 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/header/title.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Component for the top-left title. 3 | */ 4 | 5 | import { Tag, PageHeader, Image } from "antd"; 6 | import { useContext } from "react"; 7 | import { CountryContext } from "../../countries"; 8 | import MainLogo from "../../images/logos/white.png"; 9 | 10 | export default function Title(props) { 11 | const country = useContext(CountryContext) || {}; 12 | const betaTag = country && (country.beta ? [<Tag key="beta" color="#002766">BETA</Tag>] : null); 13 | const title = ( 14 | <a href={props.link || "/"}> 15 | <Image 16 | src={MainLogo} 17 | preview={false} 18 | height={50} 19 | width={100} 20 | style={{ padding: 0, margin: 0 }} 21 | /> 22 | </a> 23 | ); 24 | return ( 25 | <div style={{ minWidth: 200 }}> 26 | <div className="d-none d-lg-flex align-items-center "> 27 | <PageHeader 28 | title={title} 29 | style={{ minHeight: 30, padding: 0, margin: 0 }} 30 | tags={betaTag} 31 | /> 32 | </div> 33 | <div className="d-lg-none"> 34 | <div className="d-flex align-items-center justify-content-center"> 35 | <PageHeader 36 | title={title} 37 | tags={betaTag} 38 | style={{ 39 | paddingBottom: 8, 40 | padding: 10, 41 | paddingLeft: betaTag ? 54 : 0, 42 | }} 43 | /> 44 | </div> 45 | </div> 46 | </div> 47 | ); 48 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/index.jsx: -------------------------------------------------------------------------------- 1 | export { default as PolicyEngine } from "./policyengine"; -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/layout/general.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Components for general spacing/layout. 3 | */ 4 | 5 | import React from "react"; 6 | import { useContext } from "react"; 7 | import { Container } from "react-bootstrap"; 8 | import { Navigate } from "react-router"; 9 | import { CountryContext } from "../../countries"; 10 | 11 | export function BodyWrapper(props) { 12 | return ( 13 | <> 14 | <div className="d-none d-lg-block"> 15 | <Container 16 | fluid 17 | style={{ 18 | height: "calc(100vh - 4.2em)", 19 | overflow: props.scroll && "scroll", 20 | position: "fixed", 21 | top: 60, 22 | }} 23 | > 24 | {props.children} 25 | </Container> 26 | </div> 27 | <div className="d-block d-lg-none"> 28 | <Container 29 | style={{ 30 | maxHeight: "calc(100vh - 4.2em)", 31 | overflow: props.scroll && "scroll", 32 | paddingTop: 120, 33 | }} 34 | > 35 | {props.children} 36 | </Container> 37 | </div> 38 | </> 39 | ); 40 | } 41 | 42 | export function PolicyEngineWrapper(props) { 43 | return ( 44 | <Container style={{ padding: 0 }} fluid> 45 | {props.children} 46 | </Container> 47 | ); 48 | } 49 | 50 | export function NamedPolicyRedirects(props) { 51 | const country = useContext(CountryContext); 52 | return Object.keys(country.namedPolicies).map((name) => ( 53 | <Navigate 54 | from={`/${country.name}${name}`} 55 | to={`/${country.name}${props.page}?${country.namedPolicies[name]}`} 56 | /> 57 | )); 58 | } 59 | 60 | export function Spacing() { 61 | return <div style={{ paddingTop: 15 }} />; 62 | } 63 | -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/household/earningsCharts.jsx: -------------------------------------------------------------------------------- 1 | import { Row, Radio } from "antd"; 2 | import React from "react"; 3 | import { CountryContext } from "../../../countries"; 4 | import Centered from "../../general/centered"; 5 | import Spinner from "../../general/spinner"; 6 | import { Chart } from "../populationImpact/chart"; 7 | import { Spacing } from "../../layout/general"; 8 | 9 | 10 | export default class AccountingTable extends React.Component { 11 | static contextType = CountryContext; 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | error: false, 16 | show_difference: false, 17 | } 18 | this.updateCharts = this.updateCharts.bind(this); 19 | } 20 | 21 | updateCharts() { 22 | this.context.setState({ waitingOnEarningsCharts: true, }, () => { 23 | const submission = this.context.getPolicyJSONPayload(); 24 | const reformExists = Object.keys(submission).length > 1; 25 | const eta = this.context["endpoint-runtimes"][reformExists ? "household_variation_reform_and_baseline" : "household_variation_baseline_only"]; 26 | let url = new URL(this.context.apiURL + "/household-variation"); 27 | url.search = new URLSearchParams(submission).toString(); 28 | const requestOptions = { 29 | method: 'POST', 30 | headers: { 31 | 'Accept': 'application/json', 32 | 'Content-Type': 'application/json' 33 | }, 34 | body: JSON.stringify({ "household": this.context.situation }) 35 | }; 36 | fetch(url, requestOptions).then((res) => { 37 | if (res.ok) { 38 | let checker = setInterval(() => { 39 | fetch(url, requestOptions).then(res => res.json()).then((data) => { 40 | if(data.status === "completed") { 41 | clearInterval(checker); 42 | if(data.error) { 43 | throw new Error(data.error); 44 | } 45 | this.context.setState({ computedSituationVariationCharts: data, situationVariationImpactIsOutdated: false, waitingOnEarningsCharts: false }, () => { 46 | this.setState({ error: false }); 47 | }); 48 | } 49 | }).catch(e => { 50 | this.context.setState({ waitingOnEarningsCharts: false }); 51 | this.setState({ error: true, }); 52 | }); 53 | }, 1000 * eta * 0.2); 54 | } else { 55 | throw res; 56 | } 57 | }) 58 | }); 59 | } 60 | 61 | componentDidMount() { 62 | if (this.context.situationVariationImpactIsOutdated) { 63 | this.updateCharts(); 64 | } 65 | } 66 | 67 | render() { 68 | // Update situations where necessary (re-using where not) 69 | // If the policy changes, we need to update only the reform 70 | // situation. If the situation changes, we need to update both. 71 | const reformExists = Object.keys(this.context.getPolicyJSONPayload()).length > +(this.context.year != 2022); 72 | const eta = this.context["endpoint-runtimes"][reformExists ? "household_variation_reform_and_baseline" : "household_variation_baseline_only"]; 73 | if (!this.context.computedSituationVariationCharts || this.context.waitingOnEarningsCharts) { 74 | const message = <><p>{`Calculating tax-benefit responses to your income...`}</p><p>{`(this usually takes around ${Math.round(eta / 15) * 15} seconds)`}</p></>; 75 | return <Centered><Spinner rightSpacing={10} />{message}</Centered> 76 | } else if (this.state.error) { 77 | return <Centered>Something went wrong.</Centered> 78 | } 79 | if (reformExists) { 80 | return ( 81 | <> 82 | <Spacing /> 83 | <div className="justify-content-center d-flex" style={{marginBottom: 10}}> 84 | <Radio.Group defaultValue={true} buttonStyle="solid" onChange={() => this.setState({ show_difference: !this.state.show_difference })} > 85 | <Radio.Button value={true}>Baseline and reform</Radio.Button> 86 | <Radio.Button value={false}>Difference</Radio.Button> 87 | </Radio.Group> 88 | </div> 89 | <Row> 90 | <Chart plot={this.state.show_difference ? this.context.computedSituationVariationCharts.budget_difference_chart : this.context.computedSituationVariationCharts.budget_chart} /> 91 | </Row> 92 | <Row> 93 | <Chart plot={this.state.show_difference ? this.context.computedSituationVariationCharts.mtr_difference_chart : this.context.computedSituationVariationCharts.mtr_chart} /> 94 | </Row> 95 | </> 96 | ); 97 | } 98 | else { 99 | return ( 100 | <> 101 | <Spacing /> 102 | <Row> 103 | <Chart plot={this.context.computedSituationVariationCharts.budget_chart} /> 104 | </Row> 105 | <Row> 106 | <Chart plot={this.context.computedSituationVariationCharts.mtr_chart} /> 107 | </Row> 108 | </> 109 | ); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/household/index.jsx: -------------------------------------------------------------------------------- 1 | export { Household } from "./household"; -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/household/inputPane.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { CountryContext } from "../../../countries"; 3 | import Variable, { Spacing } from "./variable"; 4 | import { Radio, Select } from "antd"; 5 | 6 | 7 | function HouseholdSetup(props) { 8 | const country = useContext(CountryContext); 9 | return <> 10 | <Spacing /> 11 | <h5>Marital status</h5> 12 | <Spacing /> 13 | <Radio.Group defaultValue={country.getHouseholdMaritalStatus()} onChange={e => country.setHouseholdMaritalStatus(e.target.value)}> 14 | {country.householdMaritalOptions.map(i => <Radio.Button key={i} value={i}>{i}</Radio.Button>)} 15 | </Radio.Group> 16 | <Spacing /> 17 | <h5>{ 18 | country.name === "us" ? 19 | "Dependents" : 20 | "Children" 21 | }</h5> 22 | <Spacing /> 23 | <Radio.Group defaultValue={country.getNumChildren()} onChange={e => country.setNumChildren(e.target.value)}> 24 | {[...Array(6).keys()].map(i => <Radio.Button key={i} value={i}>{i}</Radio.Button>)} 25 | </Radio.Group> 26 | </> 27 | } 28 | 29 | const { Option } = Select; 30 | 31 | export class VariableControlPane extends React.Component { 32 | static contextType = CountryContext; 33 | 34 | constructor(props) { 35 | super(props); 36 | this.state = {selectedGroup: null, selectedName: null}; 37 | } 38 | 39 | render() { 40 | if((this.props.variables.length > 0) && this.props.variables.includes("setup")) { 41 | // Show variables controlling the structure of the household 42 | return <HouseholdSetup /> 43 | } 44 | if(this.props.variables.length === 0) { 45 | return <></> 46 | } 47 | const entity = this.context.entities[this.context.variables[this.props.variables[0]].entity]; 48 | const instances = Object.keys(this.context.situation[entity.plural]); 49 | let selectedName; 50 | if(this.props.selected !== this.state.selectedGroup) { 51 | this.setState({selectedGroup: this.props.selected, selectedName: instances[0]}); 52 | selectedName = instances[0]; 53 | } else { 54 | selectedName = this.state.selectedName; 55 | } 56 | let entitySelector = null; 57 | if(instances.length > 1) { 58 | entitySelector = <> 59 | <Spacing /> 60 | <Select 61 | bordered={false} 62 | defaultValue={selectedName} 63 | style={{ width: 200, float: "right" }} 64 | onChange={e => this.setState({selectedName: e})}> 65 | {instances.map(name => <Option key={name} value={name}>{name}</Option>)} 66 | </Select> 67 | </>; 68 | } 69 | const controls = this.props.variables.map(variable => <Variable entityName={selectedName} key={variable} name={variable} />); 70 | return <> 71 | {entitySelector} 72 | {controls} 73 | </>; 74 | } 75 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/household/menu.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * The parameter menu component. 3 | */ 4 | 5 | import { Divider, Menu as AntMenu } from "antd"; 6 | import React from "react"; 7 | import { useContext } from "react"; 8 | import { CountryContext } from "../../../countries"; 9 | 10 | const { SubMenu } = AntMenu; 11 | 12 | export default function Menu(props) { 13 | const country = useContext(CountryContext); 14 | function addMenuEntry(variableGroup, parent) { 15 | // This function is needed because the menu entries need 16 | // to be direct descendents of the menu component. 17 | let children = []; 18 | for(let child in variableGroup) { 19 | const name = parent + "/" + child; 20 | if(Array.isArray(variableGroup[child])) { 21 | children.push(<AntMenu.Item key={name}>{child}</AntMenu.Item>); 22 | } else { 23 | children.push(<SubMenu key={name} title={child}>{addMenuEntry(variableGroup[child], name)}</SubMenu>); 24 | } 25 | } 26 | return children; 27 | } 28 | return ( 29 | <AntMenu 30 | onClick={(e) => {props.selectVariableGroup(e.key);}} 31 | mode="inline" 32 | defaultOpenKeys={country.defaultOpenVariableGroups} 33 | defaultSelectedKeys={[country.defaultSelectedVariableGroup]} 34 | style={{fontSize: 18, paddingBottom: 20}} 35 | > 36 | {addMenuEntry(country.inputVariableHierarchy, "")} 37 | </AntMenu> 38 | ) 39 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/household/variable.jsx: -------------------------------------------------------------------------------- 1 | import { getTranslators } from "../../tools/translation"; 2 | import React, { useState } from "react"; 3 | import { CloseCircleFilled, EditOutlined } from "@ant-design/icons"; 4 | import { 5 | Switch, Slider, Select, 6 | Alert, Input, 7 | DatePicker 8 | } from "antd"; 9 | import Spinner from "../../general/spinner"; 10 | import { CountryContext } from "../../../countries/country"; 11 | 12 | const { Option } = Select; 13 | 14 | function Error(props) { 15 | return <Alert 16 | type="error" 17 | message={props.message} 18 | style={{marginBottom: 10}} 19 | showIcon icon={<CloseCircleFilled style={{marginTop: 5}} color="red" />} 20 | /> 21 | } 22 | 23 | function BooleanParameterControl(props) { 24 | return <Switch 25 | onChange={props.onChange} 26 | checked={props.metadata.value} 27 | /> 28 | } 29 | 30 | function CategoricalParameterControl(props) { 31 | return <Select 32 | style={{minWidth: 200, marginLeft: 0, paddingLeft: 10, border: "1px solid black", borderRadius: 20}} 33 | showSearch 34 | placeholder={props.metadata.defaultValue.value} 35 | value={props.metadata.value} 36 | disabled={props.metadata.disabled} 37 | bordered={false} 38 | dropdownStyle={{borderRadius:20}} 39 | onSelect={props.onChange}> 40 | {props.metadata.possibleValues.map(value => ( 41 | <Option 42 | key={value.key} 43 | value={value.key} 44 | > 45 | {value.value} 46 | </Option> 47 | ))} 48 | </Select> 49 | } 50 | 51 | function StringParameterControl(props) { 52 | return <Input 53 | onPressEnter={(e) => {props.onChange(e.target.value)}} 54 | defaultValue={props.metadata.value} 55 | /> 56 | } 57 | 58 | function DateParameterControl(props) { 59 | return <DatePicker 60 | allowClear={false} 61 | format="YYYY-MM-DD" 62 | value={getTranslators(props.metadata.name).parser(props.metadata.value)} 63 | onChange={(_, dateStr) => { 64 | props.onChange(+(dateStr.replace("-", "").replace("-", ""))) 65 | }} 66 | /> 67 | } 68 | 69 | function NumericParameterControl(props) { 70 | let [focused, setFocused] = useState(false); 71 | let { formatter, min, max } = getTranslators(props.metadata); 72 | let marks = {[max]: formatter(max)}; 73 | if(min) { 74 | marks[min] = formatter(min); 75 | } 76 | const multiplier = props.metadata.unit === "/1" ? 100 : 1; 77 | let formattedValue = formatter(props.metadata.value); 78 | formattedValue = props.metadata.value === null ? <Spinner /> : formattedValue; 79 | return ( 80 | <> 81 | <Slider 82 | value={props.metadata.value} 83 | style={{marginLeft: min ? 30 : 0, marginRight: 30}} 84 | min={min} 85 | max={max} 86 | marks={marks} 87 | onChange={props.onChange} 88 | step={props.metadata.unit === "/1" ? 0.01 : 1} 89 | tooltipVisible={false} 90 | disabled={props.disabled} 91 | /> 92 | { 93 | focused ? 94 | <Input.Search 95 | enterButton="Enter" 96 | style={{maxWidth: 300}} 97 | placeholder={multiplier * props.metadata.value} 98 | onSearch={value => { 99 | setFocused(false); 100 | props.onChange(value / multiplier); 101 | }} /> : 102 | <div> 103 | {formattedValue} 104 | <EditOutlined 105 | style={{marginLeft: 5}} 106 | onClick={() => setFocused(true)} 107 | /> 108 | </div> 109 | } 110 | </> 111 | ); 112 | } 113 | 114 | export function Spacing() { 115 | return <div style={{paddingTop: 15}}/>; 116 | } 117 | 118 | export default class Variable extends React.Component { 119 | static contextType = CountryContext; 120 | 121 | constructor(props, context) { 122 | super(props); 123 | this.state = { 124 | focused: false, 125 | }; 126 | } 127 | 128 | render() { 129 | if(!this.context.fullyLoaded) { 130 | return <></>; 131 | } 132 | let metadata; 133 | try { 134 | metadata = this.context.variables[this.props.name]; 135 | metadata.value = this.context.situation[this.context.entities[metadata.entity].plural][this.props.entityName][metadata.name][this.context.year]; 136 | } catch { 137 | console.log("Variable not found: " + this.props.name); 138 | return null; 139 | } 140 | if (!metadata) { 141 | return null; 142 | } 143 | const onChange = value => { 144 | if(value !== "") { 145 | this.context.updateSituationValue(metadata.entity, this.props.entityName, this.props.name, value) 146 | } 147 | }; 148 | const control = { 149 | "bool": <BooleanParameterControl onChange={onChange} metadata={metadata} />, 150 | "Enum": <CategoricalParameterControl onChange={onChange} metadata={metadata} />, 151 | "string": <StringParameterControl onChange={onChange} metadata={metadata} />, 152 | "date": <DateParameterControl onChange={onChange} metadata={metadata} />, 153 | }[metadata.valueType] || <NumericParameterControl onChange={onChange} metadata={metadata} />; 154 | return ( 155 | <> 156 | <h6 style={{marginTop: 20}}>{metadata.label}</h6> 157 | {metadata.error ? <Error message={metadata.error} /> : null} 158 | <p>{metadata.description}</p> 159 | {control} 160 | <div style={{paddingBottom: 20}} /> 161 | </> 162 | ); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/markdown.jsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from "react-markdown"; 2 | import { Row, Col } from "react-bootstrap"; 3 | import { Divider } from "antd"; 4 | import React from "react"; 5 | import rehypeRaw from "rehype-raw"; 6 | import { CountryContext } from "../../countries/country"; 7 | import { BodyWrapper } from "../layout/general"; 8 | import MainNavigation from "../header/mainNavigation"; 9 | import { Footer } from "../footer"; 10 | 11 | function Header(props) { 12 | return ( 13 | <> 14 | <h1>{props.children}</h1> 15 | </> 16 | ); 17 | } 18 | 19 | function Subheader(props) { 20 | return ( 21 | <> 22 | <h3 style={{ paddingTop: 30 }}>{props.children}</h3> 23 | <Divider /> 24 | </> 25 | ); 26 | } 27 | 28 | function Subsubheader(props) { 29 | return ( 30 | <> 31 | <h5> 32 | <i>{props.children}</i> 33 | </h5> 34 | </> 35 | ); 36 | } 37 | 38 | export class MarkdownPage extends React.Component { 39 | static contextType = CountryContext; 40 | constructor(props) { 41 | super(props); 42 | this.state = { text: props.content }; 43 | } 44 | 45 | render() { 46 | const components = { h1: Header, h2: Subheader, h3: Subsubheader }; 47 | return ( 48 | <> 49 | <MainNavigation 50 | title={ 51 | !["Home", "About", "Contact"].includes(this.props.title) && 52 | this.props.title 53 | } 54 | /> 55 | <div className="mx-auto max-w-screen-md pt-24 py-16"> 56 | <ReactMarkdown rehypePlugins={[rehypeRaw]} components={components}> 57 | {this.state.text} 58 | </ReactMarkdown> 59 | </div> 60 | <Footer /> 61 | </> 62 | ); 63 | } 64 | } 65 | 66 | export default MarkdownPage; 67 | -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/policy/index.jsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | export { default as Policy } from "./policy"; -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/policy/menu.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * The parameter menu component. 3 | */ 4 | 5 | import { Menu as AntMenu, Image } from "antd"; 6 | import React from "react"; 7 | import { useContext } from "react"; 8 | import { CountryContext } from "../../../countries"; 9 | 10 | const { SubMenu } = AntMenu; 11 | 12 | export default function Menu(props) { 13 | const country = useContext(CountryContext); 14 | function addMenuEntry(parameter, parent) { 15 | // This function is needed because the menu entries need 16 | // to be direct descendents of the menu component. 17 | let children = []; 18 | for (let child in parameter) { 19 | const name = parent + "/" + child; 20 | let logo; 21 | if (country.organisations && child in country.organisations) { 22 | logo = ( 23 | <Image 24 | src={country.organisations[child].logo} 25 | preview={false} 26 | height={30} 27 | width={30} 28 | /> 29 | ); 30 | } else { 31 | logo = null; 32 | } 33 | if (Array.isArray(parameter[child])) { 34 | children.push( 35 | <AntMenu.Item icon={logo} key={name}> 36 | { 37 | logo 38 | ? <div style={{ paddingLeft: 10 }}>{child}</div> 39 | : <div style={{lineHeight: "20px", whiteSpace: "normal", height: "auto"}}>{child}</div> 40 | } 41 | </AntMenu.Item> 42 | ); 43 | } else { 44 | children.push( 45 | <SubMenu 46 | icon={logo} 47 | key={name} 48 | title={ 49 | logo 50 | ? <div style={{ paddingLeft: 10 }}>{child}</div> 51 | : <div style={{lineHeight: "20px", whiteSpace: "normal", height: "auto"}}>{child}</div> 52 | } 53 | > 54 | {addMenuEntry(parameter[child], name)} 55 | </SubMenu> 56 | ); 57 | } 58 | } 59 | return children; 60 | } 61 | return ( 62 | <AntMenu 63 | onClick={(e) => { 64 | props.selectParameterGroup(e.key); 65 | }} 66 | mode="inline" 67 | defaultOpenKeys={country.defaultOpenParameterGroups} 68 | defaultSelectedKeys={[country.defaultSelectedParameterGroup]} 69 | style={{ paddingBottom: 20 }} 70 | > 71 | {addMenuEntry(country.parameterHierarchy, "")} 72 | </AntMenu> 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/policy/overview.jsx: -------------------------------------------------------------------------------- 1 | import { Pagination, Steps, Divider, Empty, Button, message, Tooltip } from "antd"; 2 | import { CheckCircleOutlined, LinkOutlined, TranslationOutlined, TwitterOutlined } from "@ant-design/icons"; 3 | import { TwitterShareButton } from "react-share"; 4 | import React, { useContext, useState } from "react"; 5 | import { policyToURL } from "../../tools/url"; 6 | import { getTranslators } from "../../tools/translation"; 7 | import { CountryContext } from "../../../countries"; 8 | import RadioButton from "../../general/radioButton"; 9 | 10 | const { Step } = Steps; 11 | 12 | function generateStepFromParameter(parameter, editingReform, country, page) { 13 | const comparisonKey = editingReform ? "baselineValue" : "defaultValue"; 14 | const targetKey = editingReform ? "value" : "baselineValue"; 15 | let populationSimCheckbox = null; 16 | let hide = false; 17 | let shouldShowTags = ( 18 | (country.showPopulationImpact | (page === "population-impact")) && 19 | country.notAllParametersPopulationSimulatable && 20 | ((page === "population-impact") | (page === "policy")) 21 | ); 22 | if(shouldShowTags) { 23 | populationSimCheckbox = country.populationSimulatableParameters.includes(parameter.name) ? 24 | <Tooltip title="This parameter will affect the country-wide simulation" overlayInnerStyle={{padding: 20, paddingRight: 0}}><CheckCircleOutlined /></Tooltip> : 25 | null; 26 | if(country.populationSimulatableParameters.includes(parameter.name)) { 27 | hide = false; 28 | } else { 29 | hide = true; 30 | } 31 | } 32 | if((parameter[targetKey] !== parameter[comparisonKey]) && (!hide | (page !== "population-impact"))) { 33 | const formatter = getTranslators(parameter).formatter; 34 | const changeLabel = (!isNaN(parameter[targetKey]) && (typeof parameter[targetKey] !== "boolean")) ? 35 | (parameter[targetKey] > parameter[comparisonKey] ? "Increase" : "Decrease") : 36 | "Change"; 37 | const description = `${changeLabel} from ${formatter(parameter[comparisonKey])} to ${formatter(parameter[targetKey])}` 38 | return <> 39 | <Step 40 | key={parameter.name} 41 | status="finish" 42 | title={ <><h6><b>{parameter.label}</b></h6><> {populationSimCheckbox}</></>} 43 | description={description} 44 | /> 45 | <br/> 46 | </> 47 | } else { 48 | return null; 49 | } 50 | } 51 | 52 | export function OverviewHolder(props) { 53 | return ( 54 | <> 55 | <div className="d-block d-lg-none" style={{backgroundColor: "#fafafa"}}> 56 | {props.children} 57 | </div> 58 | <div className="d-none d-lg-block" style={{backgroundColor: "#fafafa", height: "100%", textAlign: "center"}}> 59 | {props.children} 60 | </div> 61 | </> 62 | ); 63 | } 64 | 65 | export function PolicyOverview(props) { 66 | const country = useContext(CountryContext); 67 | const plan = Object.values(country.policy).map(step => generateStepFromParameter(step, country.editingReform, country, props.page)).filter(step => step != null); 68 | const isEmpty = plan.length === 0; 69 | const pageSize = props.pageSize || 4; 70 | let [page, setPage] = useState(1); 71 | const emptyMessage = country.editingReform ? 72 | "You haven't created a reform yet." : 73 | "Your reform will be compared against current policy." 74 | const emptyComponent = <> 75 | <div className="d-flex d-lg-none justify-content-center"> 76 | <p>{emptyMessage}</p> 77 | </div> 78 | <div className="d-none d-lg-block"> 79 | <Empty description={emptyMessage} /> 80 | </div> 81 | </> 82 | return ( 83 | <> 84 | <RadioButton style={{paddingTop: 15, paddingBottom: 20}} options={["Baseline", "Reform"]} selected={country.editingReform ? "Reform" : "Baseline"} onChange={option => {country.setState({editingReform: option === "Reform"})}} /> 85 | {!isEmpty ? 86 | <> 87 | <div style={{ padding: "0 10%" }}> 88 | {plan.slice((page - 1) * pageSize, page * pageSize)} 89 | </div> 90 | { 91 | (plan.length > pageSize) && <Pagination 92 | pageSize={pageSize} 93 | defaultCurrent={page} 94 | controlled 95 | onChange={setPage} 96 | total={plan.length} 97 | style={{ textAlign: "center" }} 98 | /> 99 | } 100 | </> : 101 | emptyComponent 102 | } </> 103 | ); 104 | } 105 | 106 | export function SharePolicyLinks(props) { 107 | const country = useContext(CountryContext); 108 | const url = policyToURL(`https://policyengine.org/${country.name}/${props.page}`, country.policy); 109 | return ( 110 | <> 111 | <Divider><Button style={{marginRight: 20, border: 0}} onClick={() => {navigator.clipboard.writeText(url); message.info("Link copied!");}}><LinkOutlined /></Button><TwitterShareButton style={{width: "44px", height: "32px", border: 0, backgroundColor: "white", borderRadius: "16px"}} title="I just simulated a reform to the UK tax and benefit system with @ThePolicyEngine. Check it out or make your own!" url={url}><TwitterOutlined /></TwitterShareButton></Divider> 112 | </> 113 | ); 114 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/policy/parameter/index.js: -------------------------------------------------------------------------------- 1 | import Parameter from "./parameter"; 2 | export default Parameter; -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/policy/parameter/numericParameterControl.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | import { CountryContext } from "../../../../countries/country"; 3 | import { getTranslators } from "../../../tools/translation"; 4 | import { EditOutlined } from "@ant-design/icons"; 5 | import { 6 | Slider, 7 | Alert, 8 | Input, 9 | } from "antd"; 10 | import Spinner from "../../../general/spinner"; 11 | 12 | const parsePotentiallyInfiniteNumber = (number) => { 13 | if (number == "inf") { 14 | return Infinity; 15 | } else if (number == "-inf") { 16 | return -Infinity; 17 | } else { 18 | return number; 19 | } 20 | }; 21 | 22 | const checkParameterInRange = (value, min, max) => { 23 | // min and max may be null, in which case they are ignored. 24 | if (min !== null && value < min) { 25 | return false; 26 | } 27 | if (max !== null && value > max) { 28 | return false; 29 | } 30 | return true; 31 | }; 32 | 33 | const NumericParameterControlSlider = (props) => { 34 | let {formatter, min, max} = getTranslators(props.metadata); 35 | let marks = {[max]: formatter(max)}; 36 | if(min) { 37 | marks[min] = formatter(min); 38 | } 39 | let formattedValue = formatter(props.metadata[props.targetKey]); 40 | formattedValue = props.metadata[props.targetKey] === null ? <Spinner /> : formattedValue; 41 | return <Slider 42 | value={props.metadata[props.targetKey]} 43 | style={{marginLeft: min ? 30 : 0, marginRight: 30}} 44 | min={min} 45 | max={max} 46 | marks={marks} 47 | onChange={props.onChange} 48 | step={0.01} 49 | tooltipVisible={false} 50 | disabled={props.disabled} 51 | paddingRight={15} 52 | /> 53 | } 54 | 55 | const NumericParameterControl = (props) => { 56 | const country = useContext(CountryContext); 57 | const targetKey = country.editingReform ? "value" : "baselineValue"; 58 | // focused==true means the user is currently editing the value. 59 | let [focused, setFocused] = useState(false); 60 | // error messages are shown when attempting to set an invalid value. 61 | let [errorMessage, setErrorMessage] = useState(null); 62 | 63 | let {formatter, min, max} = getTranslators(props.metadata); 64 | let formattedValue = formatter(props.metadata[targetKey]); 65 | 66 | let { hardMin, hardMax } = props; 67 | hardMin = parsePotentiallyInfiniteNumber(hardMin); 68 | hardMax = parsePotentiallyInfiniteNumber(hardMax); 69 | const multiplier = props.metadata.unit === "/1" ? 100 : 1; 70 | const slider = <NumericParameterControlSlider 71 | metadata={props.metadata} 72 | onChange={props.onChange} 73 | disabled={props.disabled} 74 | targetKey={targetKey} 75 | /> 76 | return ( 77 | <> 78 | {!props.noSlider && slider} 79 | { 80 | errorMessage && <Alert style={{marginBottom: 5}} message={errorMessage} type="error" showIcon /> 81 | } 82 | { 83 | focused & !props.displayOnly ? 84 | <Input.Search 85 | enterButton="Enter" 86 | style={{maxWidth: 300}} 87 | placeholder={multiplier * props.metadata[targetKey]} 88 | onSearch={value => { 89 | if(checkParameterInRange(value / multiplier, hardMin, hardMax)) { 90 | setFocused(false); 91 | props.onChange(value / multiplier); 92 | setErrorMessage(null); 93 | } else { 94 | setErrorMessage(`Value must be between ${min} and ${max}`); 95 | } 96 | }} /> : 97 | <div> 98 | {props.displayOnly || formattedValue} 99 | { 100 | !props.displayOnly && <EditOutlined 101 | style={{marginLeft: 5}} 102 | onClick={() => setFocused(true)} 103 | /> 104 | } 105 | </div> 106 | } 107 | </> 108 | ); 109 | } 110 | 111 | export default NumericParameterControl; -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/populationImpact/ageChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Collapse, Alert, Table, Tooltip, Radio } from 'antd'; 3 | import { Chart } from "./chart"; 4 | import prettyMilliseconds from "pretty-ms"; 5 | import { Row } from "react-bootstrap"; 6 | import { CountryContext } from '../../../countries'; 7 | import Spinner from '../../general/spinner'; 8 | 9 | const { Panel } = Collapse; 10 | 11 | export default class AgeChart extends React.Component { 12 | // The age chart is an optional microsimulation output, showing the effect 13 | // of a given reform on each age group. 14 | static contextType = CountryContext; 15 | 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | error: false, 20 | } 21 | this.fetchResults = this.fetchResults.bind(this); 22 | } 23 | 24 | fetchResults() { 25 | let url = new URL(`${this.context.apiURL}/age-chart`); 26 | const eta = this.context["endpoint-runtimes"]["age_chart"]; 27 | const submission = this.context.getPolicyJSONPayload(); 28 | const requestOptions = { 29 | method: "POST", 30 | headers: { 31 | "Content-Type": "application/json" 32 | }, 33 | body: JSON.stringify(submission) 34 | }; 35 | this.context.setState({ waitingOnAgeChart: true }, () => { 36 | fetch(url, requestOptions) 37 | .then((res) => { 38 | if(res.ok) { 39 | let checker = setInterval(() => { 40 | fetch(url, requestOptions).then(res => res.json()).then(data => { 41 | if(data.status === "completed") { 42 | clearInterval(checker); 43 | if(data.error) { 44 | throw new Error(data.error); 45 | } 46 | this.setState({ error: false }); 47 | this.context.setState({ageChartResult: data, waitingOnAgeChart: false, ageChartIsOutdated: false}); 48 | } 49 | }).catch(e => { 50 | this.context.setState({ waitingOnAgeChart: false}); 51 | this.setState({ error: true }); 52 | }); 53 | }, 1000 * eta * 0.2); 54 | } 55 | }); 56 | }); 57 | } 58 | 59 | render() { 60 | const results = this.context.ageChartResult; 61 | return ( 62 | <Collapse ghost onChange={open => {if(open && (!results || this.context.ageChartIsOutdated)) { this.fetchResults(); }}}> 63 | <Panel header="Impact by age" key="1"> 64 | { 65 | (this.context.waitingOnAgeChart || (!this.state.error && !results)) ? 66 | <Spinner /> : 67 | this.state.error ? 68 | <Alert type="error" message="Something went wrong." /> : 69 | <> 70 | <Row> 71 | <Chart plot={results.age_chart} md={12}/> 72 | </Row> 73 | </> 74 | } 75 | </Panel> 76 | </Collapse> 77 | ); 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/populationImpact/breakdown.jsx: -------------------------------------------------------------------------------- 1 | import { Collapse, Alert, Table, Tooltip, Radio } from 'antd'; 2 | import { Chart } from "./chart"; 3 | import React from 'react'; 4 | import prettyMilliseconds from "pretty-ms"; 5 | import { Row } from "react-bootstrap"; 6 | import { CountryContext } from '../../../countries'; 7 | import Spinner from '../../general/spinner'; 8 | 9 | const { Panel } = Collapse; 10 | 11 | export class BreakdownTable extends React.Component { 12 | static contextType = CountryContext; 13 | constructor(props) { 14 | super(props); 15 | this.state = {error: false, showAbsDecile: false}; 16 | this.fetchResults = this.fetchResults.bind(this); 17 | } 18 | 19 | fetchResults() { 20 | let url = new URL(`${this.context.apiURL}/population-breakdown`); 21 | const requestOptions = { 22 | method: "POST", 23 | headers: { 24 | "Content-Type": "application/json" 25 | }, 26 | body: JSON.stringify(this.context.getPolicyJSONPayload()) 27 | }; 28 | this.context.setState({ waitingOnPopulationBreakdown: true }, () => { 29 | fetch(url, requestOptions).then((res) => { 30 | if (res.ok) { 31 | let checker = setInterval(() => { 32 | fetch(url, requestOptions).then(res => res.json()).then(data => { 33 | if(data.status === "completed") { 34 | clearInterval(checker); 35 | if(data.error) { 36 | throw new Error(data.error); 37 | } 38 | this.setState({ error: false }); 39 | this.context.setState({populationImpactBreakdownResults: data, waitingOnPopulationBreakdown: false}); 40 | } 41 | }).catch(e => { 42 | this.context.setState({ waitingOnPopulationBreakdown: false}); 43 | this.setState({ error: true }); 44 | }); 45 | }, 5000); 46 | } else { 47 | throw res; 48 | } 49 | }); 50 | }); 51 | } 52 | 53 | render() { 54 | const results = this.context.populationImpactBreakdownResults; 55 | return ( 56 | <Collapse ghost onChange={open => {if(open && (!results || this.context.populationBreakdownIsOutdated)) { this.fetchResults(); }}}> 57 | <Panel header={<Tooltip title={`Estimated to take around ${prettyMilliseconds(2400 + Object.values(this.props.policy).filter(x => x.value !== x.baselineValue).length * 1600, {compact: true})}`}>See a breakdown of the changes (may take longer)</Tooltip>} key="1"> 58 | { 59 | (this.context.waitingOnPopulationBreakdown || (!this.state.error && !results)) ? 60 | <Spinner /> : 61 | this.state.error ? 62 | <Alert type="error" message="Something went wrong." /> : 63 | <> 64 | <BreakdownTableContent results={results} /> 65 | <Row> 66 | { 67 | this.state.showAbsDecile ? 68 | <Chart plot={results.avg_decile_chart} md={12}/> : 69 | <Chart plot={results.rel_decile_chart} md={12}/> 70 | } 71 | </Row> 72 | <Row> 73 | <div className="justify-content-center d-flex"> 74 | <Radio.Group defaultValue={true} buttonStyle="solid" onChange={() => this.setState({showAbsDecile: !this.state.showAbsDecile})} > 75 | <Radio.Button value={true}>Relative change</Radio.Button> 76 | <Radio.Button value={false}>Absolute change</Radio.Button> 77 | </Radio.Group> 78 | </div> 79 | </Row> 80 | </> 81 | } 82 | </Panel> 83 | </Collapse> 84 | ); 85 | } 86 | } 87 | 88 | function BreakdownTableContent(props) { 89 | const columns = [ 90 | { 91 | title: "Provision", 92 | dataIndex: "provision", 93 | key: "provision", 94 | }, 95 | { 96 | title: "Additional spending (£bn)", 97 | dataIndex: "additional_spending", 98 | key: "additional_spending", 99 | }, 100 | { 101 | title: "Cumulative spending (£bn)", 102 | dataIndex: "cumulative_spending", 103 | key: "cumulative_spending", 104 | }, 105 | ]; 106 | let data = []; 107 | for(let i = 0; i < props.results.spending.length; i++) { 108 | data.push({ 109 | key: props.results.provisions[i], 110 | provision: props.results.provisions[i], 111 | additional_spending: props.results.spending[i], 112 | cumulative_spending: props.results.cumulative_spending[i], 113 | }); 114 | } 115 | return <Table columns={columns} dataSource={data} />; 116 | } 117 | -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/populationImpact/chart.jsx: -------------------------------------------------------------------------------- 1 | import Plotly from "plotly.js-basic-dist-min"; 2 | import createPlotlyComponent from "react-plotly.js/factory"; 3 | import { Col } from "react-bootstrap"; 4 | import React from "react"; 5 | 6 | const Plot = createPlotlyComponent(Plotly); 7 | 8 | export function Chart(props) { 9 | return ( 10 | <> 11 | <Col> 12 | <Plot 13 | data={props.plot.data} 14 | layout={props.plot.layout} 15 | config={{ displayModeBar: false }} 16 | frames={props.plot.frames} 17 | style={{ width: "100%" }} 18 | /> 19 | </Col> 20 | </> 21 | ); 22 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/populationImpact/figure.jsx: -------------------------------------------------------------------------------- 1 | import { Col } from "react-bootstrap"; 2 | import React from "react"; 3 | 4 | export function TakeAway(props) { 5 | return <Col> 6 | <div style={{padding: 10}} className="d-flex justify-content-center align-items-center"> 7 | <div style={{fontSize: 20, color: "gray"}}>{props.children}</div> 8 | </div> 9 | </Col>; 10 | } 11 | -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/pages/populationImpact/index.jsx: -------------------------------------------------------------------------------- 1 | export { default as PopulationImpact } from "./populationImpact"; -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/tools/translation.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Helper functions to translate to and from OpenFisca metadata to 3 | * PolicyEngine descriptions and units 4 | */ 5 | 6 | import moment from "moment"; 7 | 8 | export function getTranslators(parameter) { 9 | let period = parameter.period || parameter.definitionPeriod; 10 | if (parameter.quantityType === "stock") { 11 | period = null; 12 | } 13 | const CURRENCY_SYMBOLS = { 14 | "currency-GBP": "£", 15 | "currency-USD": "$", 16 | } 17 | let result; 18 | let minMax = 1; 19 | if (parameter.unit === "kWh") { 20 | result = { 21 | formatter: value => `${value} kWh`, 22 | parser: value => value.replace("kWh", "").trim(), 23 | }; 24 | } else if (parameter.unit === "/1") { 25 | result = { 26 | formatter: value => `${parseFloat((value * 100).toFixed(2))}%`, 27 | } 28 | } else if (parameter.unit === "year") { 29 | result = { 30 | formatter: value => value + " year" + (value !== 1 ? "s" : ""), 31 | } 32 | minMax = 100; 33 | } else if (parameter.unit === "child") { 34 | result = { 35 | formatter: value => value + " child" + (value !== 1 ? "ren" : ""), 36 | } 37 | } else if (parameter.unit === "tonne CO2") { 38 | result = { 39 | formatter: value => `${value} tonnes CO2`, 40 | } 41 | minMax = 100; 42 | } else if (parameter.valueType === "bool") { 43 | result = { 44 | formatter: value => value ? "true" : "false", 45 | } 46 | } else if (parameter.unit === "hour") { 47 | result = { 48 | formatter: value => `${value} hour${value !== 1 ? "s" : ""}`, 49 | } 50 | minMax = 80; 51 | } else if (Object.keys(CURRENCY_SYMBOLS).includes(parameter.unit)) { 52 | for (let currency in CURRENCY_SYMBOLS) { 53 | if (parameter.unit === currency) { 54 | const round = value => parseFloat(Number(Math.abs(Math.round(value * (10 ** (parameter.precision || 0))) / (10 ** (parameter.precision || 0))))); 55 | result = { 56 | formatter: (value, noPeriod) => `${value < 0 ? "- " : ""}${CURRENCY_SYMBOLS[currency]}${round(value).toLocaleString(undefined, { maximumFractionDigits: parameter.precision })}${period && !noPeriod ? ("/" + period) : ""}`, 57 | } 58 | minMax = { year: 100_000, month: 1000, week: 100, null: 100 }[period || "year"]; 59 | } 60 | } 61 | } else if (parameter.valueType === "date") { 62 | const dateIntToMoment = value => moment(value.toString().slice(0, 4) + "-" + value.toString().slice(4, 6) + "-" + value.toString().slice(6, 8), "YYYY-MM-DD"); 63 | result = { 64 | formatter: value => dateIntToMoment(value).format("LL"), 65 | parser: dateIntToMoment, 66 | } 67 | } else if (parameter.valueType === "Enum") { 68 | result = { 69 | formatter: value => parameter.possibleValues.filter(x => x.key === value)[0].value, 70 | parser: value => parameter.possibleValues.filter(x => x.value === value)[0].key, 71 | } 72 | } else { 73 | result = { 74 | formatter: value => +value, 75 | parser: value => +value, 76 | } 77 | } 78 | return { 79 | formatter: result.formatter, 80 | parser: result.parser, 81 | min: 0, 82 | max: Math.max(parameter.max || minMax, Math.pow(10, Math.ceil(Math.log10(Math.max(parameter.defaultValue, parameter.value))))), 83 | } 84 | } -------------------------------------------------------------------------------- /policyengine-client/src/policyengine/tools/url.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Helper functions for translating between URLs and policies 3 | */ 4 | 5 | import { useContext } from "react"; 6 | import { CountryContext } from "../../countries"; 7 | 8 | export function policyToURL(targetPage, policy) { 9 | let searchParams = new URLSearchParams(window.location.search); 10 | for (const editingReform of [true, false]) { 11 | const targetKey = editingReform ? "value" : "baselineValue"; 12 | const comparisonKey = editingReform ? "baselineValue" : "defaultValue"; 13 | for (const key in policy) { 14 | 15 | if (policy[key][targetKey] !== policy[key][comparisonKey]) { 16 | let value; 17 | if(policy[key].unit === "/1") { 18 | value = parseFloat((policy[key][targetKey] * 100).toFixed(2)).toString().replace(".", "_"); 19 | } else if(policy[key].valueType === "Enum") { 20 | value = policy[key][targetKey]; 21 | } else { 22 | try { 23 | value = +parseFloat(policy[key][targetKey].toFixed(2)); 24 | } catch { 25 | value = +policy[key][targetKey]; 26 | } 27 | value = value.toString().replace(".", "_"); 28 | } 29 | searchParams.set(editingReform ? key : `baseline_${key}`, value); 30 | } else { 31 | searchParams.delete(editingReform ? key : `baseline_${key}`); 32 | } 33 | } 34 | } 35 | const url = `${targetPage}?${searchParams.toString()}`; 36 | return url; 37 | } 38 | 39 | export function urlToPolicy(defaultPolicy, policyRenames) { 40 | let plan = JSON.parse(JSON.stringify(defaultPolicy)); 41 | const { searchParams } = new URL(document.location); 42 | // Sort search params keys so `baseline_` keys are processed first 43 | const searchParamsKeys = Array.from(searchParams.keys()).sort((a, b) => { 44 | if (a.startsWith("baseline_")) { 45 | return -1; 46 | } else if (b.startsWith("baseline_")) { 47 | return 1; 48 | } else { 49 | return 0; 50 | }}); 51 | for (const parameter in plan) { 52 | plan[parameter].defaultValue = defaultPolicy[parameter].value; 53 | if (plan[parameter].baselineValue === undefined) { 54 | plan[parameter].baselineValue = defaultPolicy[parameter].value; 55 | } 56 | } 57 | if(policyRenames) { 58 | for (const key in policyRenames) { 59 | if (searchParams.has(key)) { 60 | searchParams.set(policyRenames[key], searchParams.get(key)); 61 | searchParams.delete(key) 62 | } 63 | } 64 | } 65 | for (const key of searchParamsKeys) { 66 | const target = key.includes("baseline_") ? "baselineValue" : "value"; 67 | const parameterName = key.replace("baseline_", ""); 68 | try { 69 | if(plan[parameterName].valueType === "Enum") { 70 | plan[parameterName][target] = searchParams.get(key); 71 | if((target === "baselineValue") && !Object.keys(searchParams).includes(parameterName)) { 72 | plan[parameterName].value = searchParams.get(key); 73 | } 74 | } else { 75 | plan[parameterName][target] = +searchParams.get(key).replace("_", ".") / (defaultPolicy[parameterName].unit === "/1" ? 100 : 1); 76 | if((target === "baselineValue") && !Object.keys(searchParams).includes(parameterName)) { 77 | plan[parameterName].value = +searchParams.get(key).replace("_", ".") / (defaultPolicy[parameterName].unit === "/1" ? 100 : 1); 78 | } 79 | } 80 | } catch(e) { 81 | // Bad parameter, do nothing 82 | console.log(e) 83 | } 84 | } 85 | return plan; 86 | } -------------------------------------------------------------------------------- /policyengine-client/src/style/google-fonts.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Ubuntu'); -------------------------------------------------------------------------------- /policyengine-client/src/style/policyengine.less: -------------------------------------------------------------------------------- 1 | /* 2 | * This less file contains the modifications to 3 | * styling for PolicyEngine's look. 4 | */ 5 | 6 | @import (inline) "./google-fonts.css"; 7 | @import "../../node_modules/antd/dist/antd.less"; 8 | 9 | // @tailwind base; 10 | @tailwind components; 11 | @tailwind utilities; 12 | 13 | img, 14 | svg, 15 | video, 16 | canvas, 17 | audio, 18 | iframe, 19 | embed, 20 | object { 21 | display: block; 22 | } 23 | 24 | .footer { 25 | display: flex; 26 | justify-content: center; 27 | margin: auto; 28 | column-gap: 10px; 29 | } 30 | 31 | .ant-table-cell { 32 | padding-right: 0 !important; 33 | padding-left: 0 !important; 34 | } 35 | 36 | .ant-tabs-top>.ant-tabs-nav::before { 37 | border-bottom: none; 38 | } 39 | 40 | .ant-tabs-ink-bar { 41 | background: #FFF; 42 | } 43 | 44 | .ant-tabs-tab { 45 | padding-bottom: 5px; 46 | padding-top: 5px; 47 | } 48 | 49 | .ant-tabs-tab div { 50 | color: #d9d9d9 !important; 51 | } 52 | 53 | .ant-tabs-tab.ant-tabs-tab-disabled div { 54 | color: #8c8c8c !important; 55 | } 56 | 57 | .ant-tabs-tab.ant-tabs-tab-active div { 58 | color: #fff !important; 59 | } 60 | 61 | svg { 62 | margin-bottom: 5px; 63 | } 64 | 65 | .ant-switch-checked.switch-red { 66 | background-color: #ee0000 !important; 67 | } 68 | 69 | body { 70 | font-family: "Ubuntu", sans-serif; 71 | -webkit-font-smoothing: antialiased; 72 | -moz-osx-font-smoothing: grayscale; 73 | } 74 | 75 | ::-webkit-scrollbar { 76 | display: none; 77 | } 78 | 79 | .ant-page-header-heading-title { 80 | font-size: 25px !important; 81 | } 82 | 83 | .ant-page-header-heading-sub-title { 84 | font-size: 20px !important; 85 | } 86 | 87 | .svg-container .user-select-none { 88 | margin: 0px !important; 89 | } 90 | 91 | .ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item-description { 92 | max-width: 200px !important; 93 | } 94 | 95 | .ant-table-tbody>tr>td>.ant-table-wrapper:only-child .ant-table, 96 | .ant-table-tbody>tr>td>.ant-table-expanded-row-fixed>.ant-table-wrapper:only-child .ant-table { 97 | margin-left: 0px !important; 98 | } 99 | 100 | .ant-table-expanded-row>.ant-table-cell { 101 | padding-left: 0px !important; 102 | } 103 | 104 | .ant-tabs-top>.ant-tabs-nav { 105 | margin: 0 !important; 106 | } 107 | 108 | .ant-page-header-heading-title { 109 | line-height: 0px; 110 | } 111 | 112 | h1 { 113 | color: #002766; 114 | font-weight: bold; 115 | } 116 | 117 | h2 { 118 | color: #002766; 119 | font-weight: bold; 120 | } 121 | 122 | h4 { 123 | color: #0b5394; 124 | line-height: 1.8; 125 | } 126 | 127 | div.ant-select-selector { 128 | padding-left: 0px !important; 129 | } 130 | 131 | .ant-page-header { 132 | padding-left: 0px; 133 | } -------------------------------------------------------------------------------- /policyengine-client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /policyengine-client/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import svgrPlugin from "vite-plugin-svgr"; 3 | import mdPlugin from "vite-plugin-markdown"; 4 | import react from "@vitejs/plugin-react"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | // This changes the out put dir from dist to build 9 | // comment this out if that isn't relevant for your project 10 | build: { 11 | outDir: "build", 12 | }, 13 | css: { 14 | preprocessorOptions: { 15 | less: { 16 | javascriptEnabled: true, 17 | modifyVars: { 18 | "primary-color": "#2c6496", 19 | "primary-1": "#fff", 20 | "link-color": "#002766", 21 | "success-color": "#0DD078", 22 | "border-radius-base": "40px", 23 | }, 24 | additionalData: "@root-entry-name: default;", 25 | }, 26 | }, 27 | }, 28 | plugins: [ 29 | mdPlugin({ 30 | mode: "html", 31 | markdown: (body) => body, 32 | }), 33 | react(), 34 | svgrPlugin({ 35 | svgrOptions: { 36 | icon: true, 37 | // ...svgr options (https://react-svgr.com/docs/options/) 38 | }, 39 | }), 40 | ], 41 | }); 42 | -------------------------------------------------------------------------------- /policyengine/__init__.py: -------------------------------------------------------------------------------- 1 | from policyengine.policyengine import PolicyEngine 2 | from policyengine.country import US as PolicyEngineUS, UK as PolicyEngineUK 3 | -------------------------------------------------------------------------------- /policyengine/country/__init__.py: -------------------------------------------------------------------------------- 1 | from .country import PolicyEngineCountry 2 | from .uk import UK 3 | from .us import US 4 | -------------------------------------------------------------------------------- /policyengine/country/openfisca/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine/country/openfisca/__init__.py -------------------------------------------------------------------------------- /policyengine/country/openfisca/computation_trees.py: -------------------------------------------------------------------------------- 1 | from policyengine_core.tracers import FullTracer 2 | import dpath 3 | 4 | 5 | def get_computation_trees_json(simulation, params): 6 | simulation.trace = True 7 | simulation.tracer = FullTracer() 8 | 9 | requested_computations = dpath.util.search( 10 | params["household"], 11 | "*/*/*/*", 12 | afilter=lambda t: t is None, 13 | yielded=True, 14 | ) 15 | 16 | for computation in requested_computations: 17 | path = computation[0] 18 | entity_plural, entity_id, variable_name, period = path.split("/") 19 | result = simulation.calculate(variable_name, period) 20 | 21 | def trace_node_to_dict(node): 22 | try: 23 | value = [float(value) for value in node.value] 24 | except: 25 | try: 26 | value = [str(value) for value in node.value] 27 | except: 28 | value = None 29 | return { 30 | "name": node.name, 31 | "value": value, 32 | "children": [trace_node_to_dict(child) for child in node.children], 33 | } 34 | 35 | return { 36 | "computation_trees": [ 37 | trace_node_to_dict(tree) for tree in simulation.tracer.trees 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /policyengine/country/openfisca/entities.py: -------------------------------------------------------------------------------- 1 | from policyengine_core.entities.role import Role 2 | from policyengine_core.taxbenefitsystems import TaxBenefitSystem 3 | from policyengine_core.entities import Entity 4 | 5 | 6 | def build_entities(tax_benefit_system: TaxBenefitSystem) -> dict: 7 | entities = { 8 | entity.key: build_entity(entity) 9 | for entity in tax_benefit_system.entities 10 | } 11 | return entities 12 | 13 | 14 | def build_entity(entity: Entity) -> dict: 15 | formatted_doc = entity.doc.strip() 16 | 17 | formatted_entity = { 18 | "key": entity.key, 19 | "label": entity.label, 20 | "is_group": not entity.is_person, 21 | "plural": entity.plural, 22 | "description": entity.label, 23 | "documentation": formatted_doc, 24 | } 25 | if not entity.is_person: 26 | formatted_entity["roles"] = { 27 | role.key: build_role(role) for role in entity.roles 28 | } 29 | return formatted_entity 30 | 31 | 32 | def build_role(role: Role) -> dict: 33 | formatted_role = { 34 | "key": role.key, 35 | "label": role.label, 36 | "plural": role.plural, 37 | "description": role.doc, 38 | } 39 | 40 | if role.max: 41 | formatted_role["max"] = role.max 42 | if role.subroles: 43 | formatted_role["max"] = len(role.subroles) 44 | 45 | return formatted_role 46 | -------------------------------------------------------------------------------- /policyengine/country/openfisca/variables.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Dict, List, Tuple 2 | from policyengine_core.taxbenefitsystems import TaxBenefitSystem 3 | from policyengine_core.model_api import Enum, Variable 4 | from openfisca_tools.model_api import FLOW 5 | from openfisca_tools import IndividualSim 6 | from collections import Sequence 7 | 8 | 9 | class PolicyEngineVariable: 10 | PROPERTIES: List[str] = ( 11 | "name", 12 | "unit", 13 | "label", 14 | "description", 15 | "valueType", 16 | "defaultValue", 17 | "definitionPeriod", 18 | "entity", 19 | "possibleValues", 20 | "reference", 21 | "quantityType", 22 | ) 23 | 24 | def __init__(self, openfisca_variable: Variable): 25 | self.openfisca_variable = openfisca_variable 26 | 27 | @property 28 | def reference(self): 29 | try: 30 | if isinstance(self.openfisca_variable.reference, Sequence): 31 | return {v: v for v in self.openfisca_variable.reference} 32 | elif self.openfisca_variable.reference is None: 33 | return {} 34 | elif isinstance(reference, dict): 35 | reference = [reference] 36 | else: 37 | return { 38 | self.openfisca_variable.reference: self.openfisca_variable.reference 39 | } 40 | except: 41 | return {} 42 | 43 | @property 44 | def description(self): 45 | if self.openfisca_variable.documentation is not None: 46 | description = self.openfisca_variable.documentation 47 | if description[-1] != ".": 48 | description += "." 49 | else: 50 | description = None 51 | return description 52 | 53 | @property 54 | def possibleValues(self): 55 | if self.openfisca_variable.value_type == Enum: 56 | if len(self.openfisca_variable.possible_values) > 200: 57 | raise ValueError( 58 | f"Enums with more than 100 values are not supported ({self.openfisca_variable.name} has {len(self.openfisca_variable.possible_values)})." 59 | ) 60 | return [ 61 | dict(key=enum.name, value=enum.value) 62 | for enum in self.openfisca_variable.possible_values 63 | ] 64 | else: 65 | return None 66 | 67 | @property 68 | def defaultValue(self): 69 | if self.openfisca_variable.value_type == Enum: 70 | return dict( 71 | key=self.openfisca_variable.default_value.name, 72 | value=self.openfisca_variable.default_value.value, 73 | ) 74 | else: 75 | return self.openfisca_variable.default_value 76 | 77 | @property 78 | def quantityType(self): 79 | if hasattr(self.openfisca_variable, "quantity_type"): 80 | return self.openfisca_variable.quantity_type.lower() 81 | else: 82 | return FLOW.lower() 83 | 84 | @property 85 | def valueType(self): 86 | return self.openfisca_variable.value_type.__name__ 87 | 88 | @property 89 | def entity(self): 90 | return self.openfisca_variable.entity.key 91 | 92 | @property 93 | def definitionPeriod(self): 94 | return self.openfisca_variable.definition_period 95 | 96 | def __getattr__(self, key: str): 97 | if hasattr(self.openfisca_variable, key): 98 | return getattr(self.openfisca_variable, key) 99 | else: 100 | raise ValueError(f"Property {key} not found.") 101 | 102 | def to_dict(self) -> dict: 103 | """Return a dictionary representation of the variable. 104 | 105 | Raises: 106 | ValueError: If a required property is not found. This will likely be due to a change in the OpenFisca API. 107 | 108 | Returns: 109 | dict: The variable metadata. 110 | """ 111 | data = {} 112 | for prop in self.PROPERTIES: 113 | try: 114 | if hasattr(self, prop): 115 | data[prop] = getattr(self, prop) 116 | else: 117 | raise ValueError(f"Property {prop} not found.") 118 | except: 119 | # In the case of an exception, abandon the entire variable. 120 | return None 121 | return data 122 | 123 | 124 | def build_variables(system: TaxBenefitSystem) -> Dict[str, dict]: 125 | """Extracts PolicyEngine parameters from OpenFisca parameter metadata. 126 | 127 | Args: 128 | system (TaxBenefitSystem): The tax-benefit system to extract from. 129 | 130 | Returns: 131 | Dict[str, dict]: The parameter metadata. 132 | """ 133 | variables = system.variables.values() 134 | variable_metadata = {} 135 | for variable in variables: 136 | processed_variable = PolicyEngineVariable(variable) 137 | data = processed_variable.to_dict() 138 | if data is not None: 139 | variable_metadata[variable.name] = data 140 | return variable_metadata 141 | -------------------------------------------------------------------------------- /policyengine/country/results_config.py: -------------------------------------------------------------------------------- 1 | class PolicyEngineResultsConfig: 2 | """Configuration class for calculating and displaying policy impacts using an OpenFisca country model.""" 3 | 4 | in_poverty_variable: str 5 | in_deep_poverty_variable: str 6 | 7 | household_net_income_variable: str 8 | household_wealth_variable: str 9 | equiv_household_net_income_variable: str 10 | 11 | child_variable: str 12 | working_age_variable: str 13 | senior_variable: str 14 | person_variable: str 15 | 16 | tax_variable: str 17 | benefit_variable: str 18 | employment_income_variable: str 19 | self_employment_income_variable: str 20 | total_income_variable: str 21 | 22 | currency: str 23 | household_entity: str 24 | region_variable: str 25 | -------------------------------------------------------------------------------- /policyengine/country/uk/__init__.py: -------------------------------------------------------------------------------- 1 | from .uk import UK 2 | -------------------------------------------------------------------------------- /policyengine/country/uk/results_config.py: -------------------------------------------------------------------------------- 1 | from policyengine.country.results_config import PolicyEngineResultsConfig 2 | 3 | 4 | class UKResultsConfig(PolicyEngineResultsConfig): 5 | in_poverty_variable = "in_poverty_bhc" 6 | in_deep_poverty_variable = "in_deep_poverty_bhc" 7 | 8 | household_net_income_variable = "household_net_income" 9 | household_wealth_variable = "total_wealth" 10 | equiv_household_net_income_variable = "equiv_household_net_income" 11 | 12 | child_variable = "is_child" 13 | working_age_variable = "is_WA_adult" 14 | senior_variable = "is_SP_age" 15 | person_variable = "people" 16 | 17 | tax_variable = "household_tax" 18 | benefit_variable = "household_benefits" 19 | employment_income_variable = "employment_income" 20 | self_employment_income_variable = "self_employment_income" 21 | total_income_variable = "total_income" 22 | 23 | currency = "£" 24 | household_entity = "household" 25 | region_variable = "region" 26 | -------------------------------------------------------------------------------- /policyengine/country/uk/uk.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from policyengine.country.uk.default_reform import create_default_reform 3 | from policyengine.country.uk.results_config import UKResultsConfig 4 | from .. import PolicyEngineCountry 5 | import policyengine_uk 6 | from policyengine_uk import EnhancedFRS 7 | 8 | 9 | class UK(PolicyEngineCountry): 10 | openfisca_country_model = policyengine_uk 11 | default_reform = create_default_reform() 12 | results_config = UKResultsConfig 13 | dataset = EnhancedFRS 14 | dataset_year = 2022 15 | 16 | def __init__(self, *args, **kwargs): 17 | if 2022 not in EnhancedFRS.years: 18 | EnhancedFRS.download(2022) 19 | super().__init__(*args, **kwargs) 20 | 21 | def create_microsimulations(self, parameters): 22 | filtered_country = parameters.get("baseline_country_specific") 23 | if filtered_country is not None: 24 | baseline, reformed = super().create_microsimulations( 25 | parameters, 26 | force_refresh_baseline=True, 27 | do_not_cache=True, 28 | ) 29 | # Specific country selected: filter out other countries. 30 | household_weights = baseline.calc("household_weight") 31 | country = baseline.calc("country") 32 | baseline.set_input( 33 | "household_weight", 34 | baseline.default_calculation_period, 35 | np.where(country == filtered_country, household_weights, 0), 36 | ) 37 | reformed.set_input( 38 | "household_weight", 39 | reformed.default_calculation_period, 40 | np.where(country == filtered_country, household_weights, 0), 41 | ) 42 | person_weights = baseline.calc("person_weight") 43 | person_country = baseline.calc("country", map_to="person") 44 | baseline.set_input( 45 | "person_weight", 46 | baseline.default_calculation_period, 47 | np.where( 48 | person_country == filtered_country, person_weights, 0 49 | ), 50 | ) 51 | reformed.set_input( 52 | "person_weight", 53 | reformed.default_calculation_period, 54 | np.where( 55 | person_country == filtered_country, person_weights, 0 56 | ), 57 | ) 58 | else: 59 | baseline, reformed = super().create_microsimulations(parameters) 60 | 61 | policy_date = parameters.get("policy_date") 62 | if policy_date is not None: 63 | year = int(str(policy_date)[:4]) 64 | baseline.default_calculation_period = year 65 | reformed.default_calculation_period = year 66 | 67 | return baseline, reformed 68 | -------------------------------------------------------------------------------- /policyengine/country/us/__init__.py: -------------------------------------------------------------------------------- 1 | from .us import US 2 | -------------------------------------------------------------------------------- /policyengine/country/us/default_reform.py: -------------------------------------------------------------------------------- 1 | from policyengine_us.variables.household.income.spm_unit.spm_unit_net_income import ( 2 | spm_unit_net_income as original_spm_unit_net_income, 3 | ) 4 | from policyengine_us.variables.household.income.spm_unit.spm_unit_benefits import ( 5 | spm_unit_benefits as original_spm_unit_benefits, 6 | ) 7 | from policyengine_us.model_api import * 8 | from ..openfisca.reforms import add_parameter_file, use_current_parameters 9 | 10 | 11 | def create_default_reform(): 12 | class spm_unit_benefits(original_spm_unit_benefits): 13 | def formula(spm_unit, period, parameters): 14 | original_income = original_spm_unit_benefits.formula( 15 | spm_unit, period, parameters 16 | ) 17 | if parameters(period).reforms.abolition.exempt_ptc_from_flat_tax: 18 | ptc = add(spm_unit, period, ["premium_tax_credit"]) 19 | return original_income + ptc 20 | else: 21 | return original_income 22 | 23 | class us_default_reform(Reform): 24 | def apply(self): 25 | self.neutralize_variable("spm_unit_net_income_reported") 26 | add_parameter_file( 27 | Path(__file__).parent / "additional_parameters.yaml" 28 | ).apply(self) 29 | self.update_variable(spm_unit_benefits) 30 | 31 | return us_default_reform, use_current_parameters() 32 | -------------------------------------------------------------------------------- /policyengine/country/us/results_config.py: -------------------------------------------------------------------------------- 1 | from policyengine.country.results_config import PolicyEngineResultsConfig 2 | 3 | 4 | class USResultsConfig(PolicyEngineResultsConfig): 5 | in_poverty_variable = "spm_unit_is_in_spm_poverty" 6 | in_deep_poverty_variable = "spm_unit_is_in_deep_spm_poverty" 7 | 8 | household_net_income_variable = "spm_unit_net_income" 9 | # Placeholder until we implement wealth data in OpenFisca US. 10 | household_wealth_variable = "spm_unit_net_income" 11 | equiv_household_net_income_variable = "spm_unit_oecd_equiv_net_income" 12 | 13 | child_variable = "is_child" 14 | working_age_variable = "is_wa_adult" 15 | senior_variable = "is_senior" 16 | person_variable = "people" 17 | 18 | tax_variable = "spm_unit_taxes" 19 | benefit_variable = "spm_unit_benefits" 20 | employment_income_variable = "employment_income" 21 | self_employment_income_variable = "self_employment_income" 22 | total_income_variable = "spm_unit_market_income" 23 | 24 | currency = "$" 25 | household_entity = "spm_unit" 26 | region_variable = "state" 27 | -------------------------------------------------------------------------------- /policyengine/country/us/us.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from policyengine.country.us.default_reform import create_default_reform 3 | from policyengine.country.us.results_config import USResultsConfig 4 | from .. import PolicyEngineCountry 5 | import policyengine_us 6 | from policyengine_us.data import CPS 7 | 8 | 9 | class US(PolicyEngineCountry): 10 | openfisca_country_model = policyengine_us 11 | default_reform = create_default_reform() 12 | results_config = USResultsConfig 13 | dataset = CPS 14 | dataset_year = 2021 15 | 16 | def __init__(self, *args, **kwargs): 17 | if self.dataset_year not in self.dataset.years: 18 | self.dataset.download(self.dataset_year) 19 | super().__init__(*args, **kwargs) 20 | 21 | def create_microsimulations(self, parameters): 22 | filtered_state = parameters.get("baseline_state_specific") 23 | if filtered_state is None: 24 | baseline, reformed = super().create_microsimulations(parameters) 25 | # US-wide analyses hold State tax policy fixed. 26 | for sim in baseline, reformed: 27 | reported_state_tax = sim.calc( 28 | "spm_unit_state_tax_reported" 29 | ).values 30 | sim.set_input( 31 | "spm_unit_state_tax", 32 | 2022, 33 | reported_state_tax, 34 | ) 35 | else: 36 | baseline, reformed = super().create_microsimulations( 37 | parameters, force_refresh_baseline=True, do_not_cache=True 38 | ) 39 | # Specific State selected: filter out other States. 40 | household_weights = baseline.calc("household_weight") 41 | state = baseline.calc("state_code_str") 42 | baseline.set_input( 43 | "household_weight", 44 | baseline.default_calculation_period, 45 | np.where(state == filtered_state, household_weights, 0), 46 | ) 47 | reformed.set_input( 48 | "household_weight", 49 | reformed.default_calculation_period, 50 | np.where(state == filtered_state, household_weights, 0), 51 | ) 52 | person_weights = baseline.calc("person_weight") 53 | person_state = baseline.calc("state_code_str", map_to="person") 54 | baseline.set_input( 55 | "person_weight", 56 | baseline.default_calculation_period, 57 | np.where(person_state == filtered_state, person_weights, 0), 58 | ) 59 | reformed.set_input( 60 | "person_weight", 61 | reformed.default_calculation_period, 62 | np.where(person_state == filtered_state, person_weights, 0), 63 | ) 64 | 65 | for subgroup in ("tax_unit", "family", "spm_unit"): 66 | subgroup_in_state = ( 67 | baseline.populations[subgroup].household( 68 | "state_code_str", baseline.default_calculation_period 69 | ) 70 | == filtered_state 71 | ) 72 | weight = baseline.calc(f"{subgroup}_weight") 73 | baseline.set_input( 74 | f"{subgroup}_weight", 75 | baseline.default_calculation_period, 76 | np.where(subgroup_in_state, weight, 0), 77 | ) 78 | reformed.set_input( 79 | f"{subgroup}_weight", 80 | baseline.default_calculation_period, 81 | np.where(subgroup_in_state, weight, 0), 82 | ) 83 | 84 | policy_date = parameters.get("policy_date") 85 | if policy_date is not None: 86 | year = int(str(policy_date)[:4]) 87 | baseline.default_calculation_period = year 88 | reformed.default_calculation_period = year 89 | 90 | return baseline, reformed 91 | -------------------------------------------------------------------------------- /policyengine/impact/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine/impact/__init__.py -------------------------------------------------------------------------------- /policyengine/impact/household/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine/impact/household/__init__.py -------------------------------------------------------------------------------- /policyengine/impact/household/charts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine/impact/household/charts/__init__.py -------------------------------------------------------------------------------- /policyengine/impact/household/earnings_impact.py: -------------------------------------------------------------------------------- 1 | from policyengine_core.simulations import IndividualSim 2 | from policyengine.country.results_config import PolicyEngineResultsConfig 3 | from policyengine.impact.household.charts.budget import budget_chart 4 | from policyengine.impact.household.charts.marginal_tax_rate import mtr_chart 5 | 6 | 7 | def earnings_impact( 8 | baseline: IndividualSim, 9 | reformed: IndividualSim, 10 | config: PolicyEngineResultsConfig, 11 | ) -> dict: 12 | """Calculates the impact of a reform on household earnings. 13 | 14 | Args: 15 | baseline (IndividualSim): The baseline simulation. 16 | reformed (IndividualSim): The reformed simulation. 17 | config (PolicyEngineResultsConfig): The results configuration. 18 | """ 19 | employment_income = baseline.calc(config.employment_income_variable).sum() 20 | self_employment_income = baseline.calc( 21 | config.self_employment_income_variable 22 | ).sum() 23 | earnings_variable = ( 24 | config.employment_income_variable 25 | if employment_income >= self_employment_income 26 | else config.self_employment_income_variable 27 | ) 28 | earnings = max(employment_income, self_employment_income) 29 | total_income = baseline.calc(config.total_income_variable).sum() 30 | benefits = baseline.calc(config.benefit_variable).sum() 31 | tax = baseline.calc(config.tax_variable).sum() 32 | vary_max = max(200_000, earnings * 1.5) 33 | baseline.vary( 34 | earnings_variable, 35 | step=100, 36 | max=vary_max, 37 | ) 38 | if reformed is not None: 39 | reformed.vary( 40 | earnings_variable, 41 | step=100, 42 | max=vary_max, 43 | ) 44 | budget = budget_chart( 45 | baseline, 46 | reformed, 47 | False, 48 | config, 49 | reformed is not None, 50 | total_income, 51 | tax, 52 | benefits, 53 | ) 54 | budget_difference = budget_chart( 55 | baseline, 56 | reformed, 57 | True, 58 | config, 59 | reformed is not None, 60 | total_income, 61 | tax, 62 | benefits, 63 | ) 64 | mtr = mtr_chart( 65 | baseline, 66 | reformed, 67 | False, 68 | config, 69 | reformed is not None, 70 | total_income, 71 | ) 72 | mtr_difference = mtr_chart( 73 | baseline, 74 | reformed, 75 | True, 76 | config, 77 | reformed is not None, 78 | total_income, 79 | ) 80 | return dict( 81 | budget_chart=budget, 82 | budget_difference_chart=budget_difference, 83 | mtr_chart=mtr, 84 | mtr_difference_chart=mtr_difference, 85 | ) 86 | -------------------------------------------------------------------------------- /policyengine/impact/population/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine/impact/population/__init__.py -------------------------------------------------------------------------------- /policyengine/impact/population/charts/__init__.py: -------------------------------------------------------------------------------- 1 | from .budgetary_impact import waterfall_chart 2 | from .inequality import inequality_chart 3 | from .decile import decile_chart 4 | from .poverty import poverty_chart 5 | from .intra_decile import intra_decile_chart 6 | -------------------------------------------------------------------------------- /policyengine/impact/population/charts/age.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | import plotly.express as px 3 | import numpy as np 4 | from openfisca_tools import Microsimulation 5 | import pandas as pd 6 | from policyengine.impact.utils import * 7 | from policyengine.country.results_config import PolicyEngineResultsConfig 8 | 9 | 10 | def age_chart( 11 | baseline: Microsimulation, 12 | reformed: Microsimulation, 13 | config: Type[PolicyEngineResultsConfig], 14 | ) -> dict: 15 | """Generates a bar chart showing the impact of a reform by age. 16 | 17 | Args: 18 | baseline (Microsimulation): The baseline simulation. 19 | reformed (Microsimulation): The reformed simulation. 20 | config (Type[PolicyEngineResultsConfig]): The country metadata. 21 | 22 | Returns: 23 | dict: The Plotly JSON. 24 | """ 25 | 26 | baseline_household_net_income = baseline.calc( 27 | config.household_net_income_variable, 28 | map_to="person", 29 | ) 30 | reform_household_net_income = reformed.calc( 31 | config.household_net_income_variable, 32 | map_to="person", 33 | ) 34 | age = baseline.calc("age") 35 | gain = reform_household_net_income - baseline_household_net_income 36 | gain_by_age = gain.groupby(age).sum() / gain.groupby(age).count() 37 | df = pd.DataFrame( 38 | { 39 | "Age": gain_by_age.index, 40 | "Average increase": gain_by_age.values, 41 | } 42 | ) 43 | hover_labels = [] 44 | for age, gain in zip(df.Age, df["Average increase"]): 45 | hover_labels += [ 46 | f"<b>{int(age)}-year olds</b> see their household's net income <br>{'rise' if gain >= 0 else 'fall '} by <b>{config.currency}{abs(gain):,.0f}</b> on average." 47 | ] 48 | df["Label"] = hover_labels 49 | fig = ( 50 | px.bar( 51 | df, 52 | x="Age", 53 | y="Average increase", 54 | custom_data=["Label"], 55 | ) 56 | .update_layout( 57 | title="Average net income increase by age", 58 | yaxis_tickformat=f",.0f", 59 | yaxis_tickprefix=config.currency, 60 | xaxis_tickvals=list(range(0, 100, 10)), 61 | ) 62 | .update_traces( 63 | marker_color=np.where( 64 | df["Average increase"] > 0, DARK_GREEN, GRAY 65 | ), 66 | hovertemplate="%{customdata[0]}", 67 | ) 68 | ) 69 | add_zero_line(fig) 70 | return formatted_fig_json(fig) 71 | -------------------------------------------------------------------------------- /policyengine/impact/population/charts/decile.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Type 2 | import plotly.express as px 3 | import numpy as np 4 | from openfisca_tools import Microsimulation 5 | import pandas as pd 6 | from policyengine.impact.utils import * 7 | from policyengine.country.results_config import PolicyEngineResultsConfig 8 | 9 | 10 | def individual_decile_chart( 11 | df: pd.DataFrame, 12 | metric: str, 13 | config: Type[PolicyEngineResultsConfig], 14 | decile_type: str, 15 | ) -> dict: 16 | """Chart of average or relative net effect of a reform by income decile. 17 | 18 | :param df: DataFrame with columns for Decile, Relative change, and Average change. 19 | :type df: pd.DataFrame 20 | :param metric: "Relative change" or "Average change". 21 | :type metric: str 22 | :return: Decile chart (relative or absolute) as a JSON representation of a Plotly chart. 23 | :rtype: dict 24 | """ 25 | fig = ( 26 | px.bar(df, x="Decile", y=metric) 27 | .update_layout( 28 | title=f"Change to net income by {decile_type} decile", 29 | xaxis_title=f"{'Equivalised disposable income' if decile_type == 'income' else 'Wealth'} decile", 30 | yaxis_title="Change to household net income", 31 | yaxis_tickformat=",~%" if metric == "Relative change" else ",", 32 | yaxis_tickprefix="" 33 | if metric == "Relative change" 34 | else config.currency, 35 | showlegend=False, 36 | xaxis_tickvals=list(range(1, 11)), 37 | ) 38 | .update_traces(marker_color=np.where(df[metric] > 0, DARK_GREEN, GRAY)) 39 | ) 40 | add_zero_line(fig) 41 | return formatted_fig_json(fig) 42 | 43 | 44 | def decile_chart( 45 | baseline: Microsimulation, 46 | reformed: Microsimulation, 47 | config: Type[PolicyEngineResultsConfig], 48 | decile_type: str = "income", 49 | ) -> Tuple[dict, dict]: 50 | """Chart of average net effect of a reform by income decile. 51 | 52 | :param baseline: Baseline microsimulation. 53 | :type baseline: Microsimulation 54 | :param reformed: Reform microsimulation. 55 | :type reformed: Microsimulation 56 | :return: Decile charts (relative and absolute) as JSON representations of Plotly charts. 57 | :rtype: Tuple[dict, dict] 58 | """ 59 | baseline_household_net_income = baseline.calc( 60 | config.household_net_income_variable 61 | ) 62 | baseline_household_equiv_income = baseline.calc( 63 | config.equiv_household_net_income_variable 64 | if decile_type == "income" 65 | else config.household_wealth_variable 66 | ) 67 | reform_household_net_income = reformed.calc( 68 | config.household_net_income_variable 69 | ) 70 | household_gain = ( 71 | reform_household_net_income - baseline_household_net_income 72 | ) 73 | household_size = baseline.calc("people", map_to=config.household_entity) 74 | # Group households in decile such that each decile has the same 75 | # number of people 76 | baseline_household_equiv_income.weights *= household_size 77 | household_decile = baseline_household_equiv_income.decile_rank() 78 | agg_gain_by_decile = household_gain.groupby(household_decile).sum() 79 | households_by_decile = baseline_household_net_income.groupby( 80 | household_decile 81 | ).count() 82 | baseline_agg_income_by_decile = baseline_household_net_income.groupby( 83 | household_decile 84 | ).sum() 85 | reform_agg_income_by_decile = reform_household_net_income.groupby( 86 | household_decile 87 | ).sum() 88 | baseline_mean_income_by_decile = ( 89 | baseline_agg_income_by_decile / households_by_decile 90 | ) 91 | reform_mean_income_by_decile = ( 92 | reform_agg_income_by_decile / households_by_decile 93 | ) 94 | # Total decile gain / total decile income. 95 | rel_agg_changes = ( 96 | (agg_gain_by_decile / baseline_agg_income_by_decile) 97 | .round(3) 98 | .astype(float) 99 | ) 100 | # Total gain / number of households by decile. 101 | mean_gain_by_decile = (agg_gain_by_decile / households_by_decile).round() 102 | # Write out hovercard. 103 | decile_number = rel_agg_changes.index 104 | verb = np.where( 105 | mean_gain_by_decile > 0, 106 | "rise", 107 | np.where(mean_gain_by_decile < 0, "fall", "remain"), 108 | ) 109 | label_prefix = ( 110 | "<b>Household incomes in the " 111 | + pd.Series(decile_number) 112 | .astype(int) 113 | .reset_index(drop=True) 114 | .apply(ordinal) 115 | + " decile <br>" 116 | + pd.Series(verb).reset_index(drop=True) 117 | + " by an average of " 118 | ) 119 | label_value_abs = ( 120 | pd.Series(np.abs(mean_gain_by_decile)) 121 | .apply(lambda x: f"{config.currency}{x:,.0f}") 122 | .reset_index(drop=True) 123 | ) 124 | label_value_rel = ( 125 | pd.Series(rel_agg_changes) 126 | .apply(lambda x: f"{x:.1%}") 127 | .reset_index(drop=True) 128 | ) 129 | label_suffix = ( 130 | f"</b><br>from {config.currency}" 131 | + pd.Series(baseline_mean_income_by_decile) 132 | .apply(lambda x: f"{x:,.0f}") 133 | .reset_index(drop=True) 134 | + f" to {config.currency}" 135 | + pd.Series(reform_mean_income_by_decile) 136 | .apply(lambda x: f"{x:,.0f}") 137 | .reset_index(drop=True) 138 | + " per year" 139 | ) 140 | label_rel = label_prefix + label_value_rel + label_suffix 141 | label_abs = label_prefix + label_value_abs + label_suffix 142 | """ 143 | Examples: 144 | - Household incomes in the 1st decile rise by an average of $1, from $1,000 to $1,001 per year 145 | - Household incomes in the 2nd decile fall by an average of $1, from $1,000 to $999 per year 146 | - Household incomes in the 3rd decile remain at $1,000 per year 147 | """ 148 | df = pd.DataFrame( 149 | { 150 | "Decile": decile_number, 151 | "Relative change": rel_agg_changes.values, 152 | "Average change": mean_gain_by_decile.values, 153 | "label_rel": label_rel, 154 | "label_abs": label_abs, 155 | } 156 | ) 157 | return ( 158 | individual_decile_chart(df, "Relative change", config, decile_type), 159 | individual_decile_chart(df, "Average change", config, decile_type), 160 | ) 161 | -------------------------------------------------------------------------------- /policyengine/impact/population/charts/inequality.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Type 2 | import plotly.express as px 3 | import numpy as np 4 | from openfisca_tools import Microsimulation 5 | import pandas as pd 6 | from policyengine.impact.utils import * 7 | from policyengine.country.results_config import PolicyEngineResultsConfig 8 | 9 | 10 | def inequality_chart( 11 | baseline: Microsimulation, 12 | reformed: Microsimulation, 13 | config: Type[PolicyEngineResultsConfig], 14 | ) -> dict: 15 | equiv_income = baseline.calc( 16 | config.equiv_household_net_income_variable, map_to="person" 17 | ) 18 | reform_equiv_income = reformed.calc( 19 | config.equiv_household_net_income_variable, map_to="person" 20 | ) 21 | baseline_gini = equiv_income.gini() 22 | reform_gini = reform_equiv_income.gini() 23 | gini_change = reform_gini / baseline_gini - 1 24 | baseline_top_ten_pct_share = ( 25 | equiv_income[equiv_income.decile_rank() == 10].sum() 26 | / equiv_income.sum() 27 | ) 28 | reform_top_ten_pct_share = ( 29 | reform_equiv_income[reform_equiv_income.decile_rank() == 10].sum() 30 | / reform_equiv_income.sum() 31 | ) 32 | top_ten_pct_share_change = ( 33 | reform_top_ten_pct_share / baseline_top_ten_pct_share - 1 34 | ) 35 | baseline_top_one_pct_share = ( 36 | equiv_income[equiv_income.percentile_rank() == 100].sum() 37 | / equiv_income.sum() 38 | ) 39 | reform_top_one_pct_share = ( 40 | reform_equiv_income[reform_equiv_income.percentile_rank() == 100].sum() 41 | / reform_equiv_income.sum() 42 | ) 43 | top_one_pct_share_change = ( 44 | reform_top_one_pct_share / baseline_top_one_pct_share - 1 45 | ) 46 | df = pd.DataFrame( 47 | { 48 | "Metric": ["Gini index", f"Top 10% share", f"Top 1% share"], 49 | "Percent change": [ 50 | gini_change, 51 | top_ten_pct_share_change, 52 | top_one_pct_share_change, 53 | ], 54 | "Baseline": [ 55 | baseline_gini, 56 | baseline_top_ten_pct_share, 57 | baseline_top_one_pct_share, 58 | ], 59 | "Reform": [ 60 | reform_gini, 61 | reform_top_ten_pct_share, 62 | reform_top_one_pct_share, 63 | ], 64 | } 65 | ) 66 | df["pct_change_str"] = df["Percent change"].abs().map("{:.1%}".format) 67 | df["label"] = ( 68 | "<b>" 69 | + df.Metric 70 | + " " 71 | + np.where( 72 | df.pct_change_str == "0.0%", 73 | "does not change", 74 | ( 75 | np.where(df["Percent change"] < 0, "falls ", "rises ") 76 | + df.pct_change_str.astype(str) 77 | ), 78 | ) 79 | + "</b><br> from " 80 | + np.where( 81 | df.Metric == "Gini index", 82 | df.Baseline.map("{:.3}".format).astype(str), 83 | df.Baseline.map("{:.1%}".format).astype(str), 84 | ) 85 | + " to " 86 | + np.where( 87 | df.Metric == "Gini index", 88 | df.Reform.map("{:.3}".format).astype(str), 89 | df.Reform.map("{:.1%}".format).astype(str), 90 | ) 91 | ) 92 | fig = ( 93 | px.bar(df, x="Metric", y="Percent change", custom_data=["label"]) 94 | .update_layout( 95 | title="Income inequality impact", 96 | xaxis_title=None, 97 | yaxis_title="Percent change", 98 | yaxis_tickformat="~%", 99 | ) 100 | .update_traces( 101 | marker_color=np.where(df["Percent change"] < 0, DARK_GREEN, GRAY) 102 | ) 103 | ) 104 | add_zero_line(fig) 105 | add_custom_hovercard(fig) 106 | return formatted_fig_json(fig) 107 | -------------------------------------------------------------------------------- /policyengine/impact/population/charts/poverty.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Type 2 | import plotly.express as px 3 | import numpy as np 4 | from openfisca_tools import Microsimulation 5 | import pandas as pd 6 | from policyengine.impact.utils import * 7 | from policyengine.impact.population.metrics import ( 8 | deep_poverty_rate, 9 | poverty_rate, 10 | ) 11 | from policyengine.country.results_config import PolicyEngineResultsConfig 12 | 13 | 14 | def pov_chg( 15 | baseline: Microsimulation, 16 | reformed: Microsimulation, 17 | criterion: str, 18 | config: Type[PolicyEngineResultsConfig], 19 | ) -> float: 20 | """Calculate change in poverty rates. 21 | 22 | :param baseline: Baseline simulation. 23 | :type baseline: Microsimulation 24 | :param reform: Reform simulation. 25 | :type reform: Microsimulation 26 | :param criterion: Filter for each simulation. 27 | :type criterion: str 28 | :return: Percentage (not percentage point) difference in poverty rates. 29 | :rtype: float 30 | """ 31 | return pct_change( 32 | poverty_rate(baseline, criterion, config), 33 | poverty_rate(reformed, criterion, config), 34 | ) 35 | 36 | 37 | def deep_pov_chg( 38 | baseline: Microsimulation, 39 | reformed: Microsimulation, 40 | criterion: str, 41 | config: Type[PolicyEngineResultsConfig], 42 | ) -> float: 43 | """Calculate change in poverty rates. 44 | 45 | :param baseline: Baseline simulation. 46 | :type baseline: Microsimulation 47 | :param reform: Reform simulation. 48 | :type reform: Microsimulation 49 | :param criterion: Filter for each simulation. 50 | :type criterion: str 51 | :return: Percentage (not percentage point) difference in poverty rates. 52 | :rtype: float 53 | """ 54 | return pct_change( 55 | deep_poverty_rate(baseline, criterion, config), 56 | deep_poverty_rate(reformed, criterion, config), 57 | ) 58 | 59 | 60 | def poverty_chart( 61 | baseline: Microsimulation, 62 | reformed: Microsimulation, 63 | is_deep: bool, 64 | config: Type[PolicyEngineResultsConfig], 65 | ) -> dict: 66 | """Chart of poverty impact by age group and overall. 67 | 68 | :param baseline: Baseline microsimulation. 69 | :type baseline: Microsimulation 70 | :param reformed: Reform microsimulation. 71 | :type reformed: Microsimulation 72 | :return: JSON representation of Plotly chart with poverty impact for: 73 | - Children (under 18) 74 | - Working age adults (18 to State Pension age) 75 | - Pensioners (State Pension age and above) 76 | - Overall 77 | :rtype: dict 78 | """ 79 | if is_deep: 80 | f_pov_chg = deep_pov_chg 81 | f_poverty_rate = deep_poverty_rate 82 | metric_name = "Deep poverty" 83 | else: 84 | f_pov_chg = pov_chg 85 | f_poverty_rate = poverty_rate 86 | metric_name = "Poverty" 87 | df = pd.DataFrame( 88 | { 89 | "group": ["Child", "Working-age", "Senior", "All"], 90 | "pov_chg": [ 91 | f_pov_chg(baseline, reformed, i, config) 92 | for i in [ 93 | config.child_variable, 94 | config.working_age_variable, 95 | config.senior_variable, 96 | config.person_variable, 97 | ] 98 | ], 99 | "baseline": [ 100 | f_poverty_rate(baseline, i, config) 101 | for i in [ 102 | config.child_variable, 103 | config.working_age_variable, 104 | config.senior_variable, 105 | config.person_variable, 106 | ] 107 | ], 108 | "reformed": [ 109 | f_poverty_rate(reformed, i, config) 110 | for i in [ 111 | config.child_variable, 112 | config.working_age_variable, 113 | config.senior_variable, 114 | config.person_variable, 115 | ] 116 | ], 117 | } 118 | ) 119 | df["abs_chg_str"] = df.pov_chg.abs().map("{:.1%}".format) 120 | df["label"] = ( 121 | "<b>" 122 | + np.where(df.group == "All", "Total", df.group) 123 | + " " 124 | + metric_name.lower() 125 | + " " 126 | + np.where( 127 | df.abs_chg_str == "0.0%", 128 | "does not change", 129 | ( 130 | np.where(df.pov_chg < 0, "falls ", "rises ") 131 | + df.abs_chg_str 132 | + "</b><br> from " 133 | + df.baseline.map("{:.1%}".format) 134 | + " to " 135 | + df.reformed.map("{:.1%}".format) 136 | ), 137 | ) 138 | ) 139 | fig = px.bar( 140 | df, 141 | x="group", 142 | y="pov_chg", 143 | custom_data=["label"], 144 | labels={"group": "Group", "pov_chg": metric_name + " rate change"}, 145 | ) 146 | fig.update_layout( 147 | title=metric_name + " impact by age group", 148 | xaxis_title=None, 149 | yaxis=dict(title="Percent change", tickformat=",~%"), 150 | ) 151 | fig.update_traces(marker_color=np.where(df.pov_chg < 0, DARK_GREEN, GRAY)) 152 | add_custom_hovercard(fig) 153 | add_zero_line(fig) 154 | return formatted_fig_json(fig) 155 | -------------------------------------------------------------------------------- /policyengine/impact/population/metrics.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions generating aggregates and other numerical outputs from microsimulation results. 3 | """ 4 | from typing import Type 5 | from policyengine.country.results_config import PolicyEngineResultsConfig 6 | from openfisca_tools import Microsimulation 7 | from policyengine.impact.utils import * 8 | 9 | 10 | def poverty_rate( 11 | sim: Microsimulation, 12 | population: str, 13 | config: Type[PolicyEngineResultsConfig], 14 | ) -> float: 15 | return sim.calc(config.in_poverty_variable, map_to="person")[ 16 | sim.calc(population) > 0 17 | ].mean() 18 | 19 | 20 | def deep_poverty_rate( 21 | sim: Microsimulation, 22 | population: str, 23 | config: Type[PolicyEngineResultsConfig], 24 | ) -> float: 25 | return sim.calc(config.in_deep_poverty_variable, map_to="person")[ 26 | sim.calc(population) > 0 27 | ].mean() 28 | 29 | 30 | def headline_metrics( 31 | baseline: Microsimulation, 32 | reformed: Microsimulation, 33 | config: Type[PolicyEngineResultsConfig], 34 | ) -> dict: 35 | """Compute headline society-wide metrics. 36 | 37 | :param baseline: Baseline simulation. 38 | :type baseline: Microsimulation 39 | :param reformed: Reform simulation. 40 | :type reformed: Microsimulation 41 | :return: Dictionary with net_cost, poverty_change, winner_share, 42 | loser_share, and gini_change. 43 | :rtype: dict 44 | """ 45 | new_income = reformed.calc( 46 | config.household_net_income_variable, map_to="person" 47 | ) 48 | old_income = baseline.calc( 49 | config.household_net_income_variable, map_to="person" 50 | ) 51 | gain = new_income - old_income 52 | net_cost = ( 53 | reformed.calc(config.household_net_income_variable).sum() 54 | - baseline.calc(config.household_net_income_variable).sum() 55 | ) 56 | poverty_change = pct_change( 57 | baseline.calc(config.in_poverty_variable, map_to="person").mean(), 58 | reformed.calc(config.in_poverty_variable, map_to="person").mean(), 59 | ) 60 | winner_share = (gain > 0).mean() 61 | loser_share = (gain < 0).mean() 62 | return dict( 63 | budgetary_impact_str=f'{"-" if net_cost < 0 else ""}{config.currency}{num(abs(net_cost))}', 64 | budgetary_impact=float(net_cost), 65 | poverty_change=float(poverty_change), 66 | winner_share=float(winner_share), 67 | loser_share=float(loser_share), 68 | ) 69 | 70 | 71 | def spending( 72 | baseline: Microsimulation, 73 | reformed: Microsimulation, 74 | config: Type[PolicyEngineResultsConfig], 75 | ) -> float: 76 | """Budgetary impact of a reform (difference in net income). 77 | 78 | :param baseline: Baseline microsimulation. 79 | :type baseline: Microsimulation 80 | :param reformed: Reform microsimulation. 81 | :type reformed: Microsimulation 82 | :return: Reform net income minus baseline net income. 83 | :rtype: float 84 | """ 85 | return ( 86 | reformed.calc(config.household_net_income_variable).sum() 87 | - baseline.calc(config.household_net_income_variable).sum() 88 | ) 89 | -------------------------------------------------------------------------------- /policyengine/impact/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .numeric import num, ordinal, pct_change, describe_change 2 | from .plotly import ( 3 | plotly_json_to_fig, 4 | WHITE, 5 | BLUE, 6 | GRAY, 7 | DARK_GRAY, 8 | LIGHT_GRAY, 9 | LIGHT_GREEN, 10 | DARK_GREEN, 11 | formatted_fig_json, 12 | add_custom_hovercard, 13 | add_zero_line, 14 | ) 15 | -------------------------------------------------------------------------------- /policyengine/impact/utils/numeric.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for handling numeric operations. 3 | """ 4 | 5 | 6 | from typing import Callable 7 | 8 | 9 | def pct_change(x: float, y: float) -> float: 10 | return (y - x) / x 11 | 12 | 13 | def num(x: float) -> str: 14 | """Converts a number to a human-readable string, using the k/m/bn/tr suffixes after rounding to 2 significant figures.""" 15 | 16 | if x < 0: 17 | return "-" + num(-x) 18 | if x < 1e3: 19 | return f"{x:.2f}" 20 | if x < 1e6: 21 | return f"{x / 1e3:.0f}k" 22 | if x < 1e9: 23 | return f"{x / 1e6:.0f}m" 24 | if x < 1e10: 25 | return f"{x / 1e9:.2f}bn" 26 | if x < 1e12: 27 | return f"{x / 1e9:.1f}bn" 28 | return f"{x / 1e12:.2f}tr" 29 | 30 | 31 | def ordinal(n: int) -> str: 32 | """Create an ordinal number (1st, 2nd, etc.) from an integer. 33 | 34 | Source: https://stackoverflow.com/a/20007730/1840471 35 | 36 | :param n: Number. 37 | :type n: int 38 | :return: Ordinal number (1st, 2nd, etc.). 39 | :rtype: str 40 | """ 41 | return "%d%s" % ( 42 | n, 43 | "tsnrhtdd"[(n // 10 % 10 != 1) * (n % 10 < 4) * n % 10 :: 4], 44 | ) 45 | 46 | 47 | def describe_change( 48 | x: float, 49 | y: float, 50 | formatter: Callable = lambda x: x, 51 | change_formatter=lambda x: x, 52 | plural: bool = False, 53 | ) -> str: 54 | s = "" if plural else "s" 55 | if y > x: 56 | return f"rise{s} from {formatter(x)} to {formatter(y)} (+{change_formatter(y - x)})" 57 | elif y == x: 58 | return f"remain{s} at {formatter(x)}" 59 | else: 60 | return f"fall{s} from {formatter(x)} to {formatter(y)} (-{change_formatter(x - y)})" 61 | -------------------------------------------------------------------------------- /policyengine/impact/utils/plotly.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for formatting charts. 3 | """ 4 | import plotly.graph_objects as go 5 | import json 6 | 7 | 8 | def plotly_json_to_fig(json): 9 | """Converts a JSON dict to a plotly figure. 10 | 11 | :param json: JSON dict. 12 | :type json: dict 13 | :return: Plotly figure. 14 | :rtype: go.Figure 15 | """ 16 | return go.Figure(data=json["data"], layout=json["layout"]) 17 | 18 | 19 | WHITE = "#FFF" 20 | BLUE = "#5091cc" 21 | GRAY = "#BDBDBD" 22 | DARK_GRAY = "#616161" 23 | LIGHT_GRAY = "#F5F5F5" 24 | LIGHT_GREEN = "#C5E1A5" 25 | DARK_GREEN = "#558B2F" 26 | 27 | 28 | def formatted_fig_json(fig: go.Figure) -> dict: 29 | """Formats figure with styling and returns as JSON. 30 | 31 | :param fig: Plotly figure. 32 | :type fig: go.Figure 33 | :return: Formatted plotly figure as a JSON dict. 34 | :rtype: dict 35 | """ 36 | fig.update_xaxes( 37 | title_font=dict(size=16, color="black"), tickfont={"size": 14} 38 | ) 39 | fig.update_yaxes( 40 | title_font=dict(size=16, color="black"), tickfont={"size": 14} 41 | ) 42 | fig.update_layout( 43 | hoverlabel_align="right", 44 | font_family="Ubuntu", 45 | font_color="Black", 46 | title_font_size=20, 47 | plot_bgcolor="white", 48 | paper_bgcolor="white", 49 | hoverlabel=dict(font_family="Ubuntu"), 50 | ) 51 | return json.loads(fig.to_json()) 52 | 53 | 54 | def add_custom_hovercard(fig: go.Figure) -> None: 55 | """Add a custom hovercard to the figure based on the first element of 56 | customdata, without the title to the right. 57 | 58 | :param fig: Plotly figure. 59 | :type fig: go.Figure 60 | """ 61 | # Per https://stackoverflow.com/a/69430974/1840471. 62 | fig.update_traces(hovertemplate="%{customdata[0]}<extra></extra>") 63 | 64 | 65 | def add_zero_line(fig: go.Figure) -> None: 66 | """Add a solid line across y=0. 67 | 68 | :param fig: Plotly figure. 69 | :type fig: go.Figure 70 | """ 71 | fig.add_shape( 72 | type="line", 73 | xref="paper", 74 | yref="y", 75 | x0=0, 76 | y0=0, 77 | x1=1, 78 | y1=0, 79 | line=dict(color="grey", width=1), 80 | ) 81 | -------------------------------------------------------------------------------- /policyengine/impact/utils/text.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | def format_summary_of_parameter_value(metadata: dict, value: Any) -> str: 5 | """Formats a parameter value for display. 6 | 7 | Args: 8 | metadata (dict): The parameter metadata. 9 | value (Any): The parameter value. 10 | 11 | Returns: 12 | str: The formatted parameter value. 13 | """ 14 | unit = metadata.get("unit") 15 | if unit == "currency-GBP": 16 | prefix = "£" 17 | elif unit == "currency-USD": 18 | prefix = "$" 19 | else: 20 | prefix = "" 21 | 22 | period = metadata.get("period") 23 | if period == "year": 24 | suffix = "/year" 25 | elif period == "month": 26 | suffix = "/month" 27 | elif period == "week": 28 | suffix = "/week" 29 | else: 30 | suffix = "" 31 | label = metadata.get("label") 32 | uncapitalised_label = f"{label.lower()[0]}{label[1:]}" 33 | if unit == "abolition": 34 | return f"Abolish {metadata.get('variable')}" 35 | elif (unit == "bool") and value: 36 | return metadata.get("label") 37 | elif unit == "bool": 38 | return f"Revoke {uncapitalised_label}" 39 | elif unit == "Enum": 40 | return f"Set {uncapitalised_label} to {value}" 41 | elif unit == "/1": 42 | return f"Set {uncapitalised_label} to {value:.1%}" 43 | else: 44 | return f"Set {uncapitalised_label} to {prefix}{value:,.2f}{suffix}" 45 | -------------------------------------------------------------------------------- /policyengine/package.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | POLICYENGINE_PACKAGE_PATH = Path(__file__).parent 4 | -------------------------------------------------------------------------------- /policyengine/policyengine.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Tuple, Type 3 | from flask import Flask 4 | from flask_cors import CORS 5 | from policyengine.web_server.cache import ( 6 | DisabledCache, 7 | LocalCache, 8 | PolicyEngineCache, 9 | add_params_and_caching, 10 | ) 11 | from policyengine.web_server.logging import PolicyEngineLogger, logged_endpoint 12 | from policyengine.web_server.cors import after_request_func 13 | from policyengine.web_server.static_site import add_static_site_handling 14 | from policyengine.package import POLICYENGINE_PACKAGE_PATH 15 | from .country import PolicyEngineCountry, UK, US 16 | 17 | 18 | class PolicyEngine: 19 | """Class initialising and running the PolicyEngine API.""" 20 | 21 | version: str = "1.126.1" 22 | """The version of the PolicyEngine API, used to identify the API version in the cache. 23 | """ 24 | 25 | cache_bucket_name: str = "uk-policy-engine.appspot.com" 26 | """The name of the Google Cloud Storage bucket used to cache results. 27 | """ 28 | 29 | countries: Tuple[Type[PolicyEngineCountry]] = (UK, US) 30 | """The country models supported by the PolicyEngine API. 31 | """ 32 | 33 | app: Flask 34 | """The Flask application handling the web server.""" 35 | 36 | @property 37 | def debug_mode(self): 38 | """Whether the PolicyEngine API is running in debug mode.""" 39 | return bool(os.environ.get("POLICYENGINE_DEBUG")) or self._debug 40 | 41 | def log(self, message: str): 42 | """Log a message to the PolicyEngine API logger as a general server message.""" 43 | self.logger.log(event="general_server_message", message=message) 44 | 45 | def __init__(self, debug: bool = False): 46 | self._debug = debug 47 | self._init_logger() 48 | self.log("Initialising server.") 49 | self._init_countries() 50 | self._init_cache() 51 | self._init_flask() 52 | self._init_routes() 53 | self.log("Initialisation complete.") 54 | 55 | def _init_countries(self): 56 | """Initialise the country models.""" 57 | self.countries = tuple(map(lambda country: country(), self.countries)) 58 | 59 | def _init_cache(self): 60 | """Initialise the cache for load-intensive endpoint results.""" 61 | if self.cache_bucket_name is not None and not self.debug_mode: 62 | print("Initialising cache.") 63 | self.cache = PolicyEngineCache( 64 | self.version, self.cache_bucket_name 65 | ) 66 | else: 67 | self.cache = LocalCache(self.version) 68 | 69 | def _init_flask(self): 70 | """Initialise the Flask application.""" 71 | self.app = Flask( 72 | type(self).__name__, 73 | static_url_path="", 74 | static_folder=str( 75 | (POLICYENGINE_PACKAGE_PATH / "static").absolute() 76 | ), 77 | ) 78 | CORS(self.app) 79 | 80 | def _init_routes(self): 81 | """Initialise Flask routing to direct non-API requests to a static assets folder.""" 82 | add_static_site_handling(self.app) 83 | for country in self.countries: 84 | for endpoint, endpoint_fn in country.api_endpoints.items(): 85 | endpoint_fn = add_params_and_caching( 86 | endpoint_fn, self.cache, self.logger 87 | ) 88 | endpoint_fn = logged_endpoint(endpoint_fn, self.logger) 89 | self.app.route( 90 | f"/{country.name}/api/{endpoint.replace('_', '-')}", 91 | methods=["GET", "POST"], 92 | endpoint=f"{country.name}_{endpoint}", 93 | )(endpoint_fn) 94 | setattr(self, endpoint_fn.__name__, endpoint_fn) 95 | self.after_request_func = self.app.after_request(after_request_func) 96 | 97 | def _init_logger(self): 98 | """Initialise the logger for the PolicyEngine API.""" 99 | self.logger = PolicyEngineLogger(local=self.debug_mode) 100 | -------------------------------------------------------------------------------- /policyengine/server.py: -------------------------------------------------------------------------------- 1 | from policyengine import PolicyEngine 2 | 3 | app = PolicyEngine().app 4 | -------------------------------------------------------------------------------- /policyengine/tests/basic.yaml: -------------------------------------------------------------------------------- 1 | - label: Basic UK calculation endpoint 2 | endpoint: /uk/api/calculate 3 | input: 4 | household: 5 | people: 6 | adult: 7 | age: 8 | "2022": 45 9 | is_WA_adult: 10 | "2022": null 11 | output: 12 | people: 13 | adult: 14 | is_WA_adult: 15 | "2022": true 16 | 17 | - label: Basic US calculation endpoint 18 | endpoint: /us/api/calculate 19 | input: 20 | household: 21 | people: 22 | adult: 23 | age: 24 | "2022": 45 25 | is_adult: 26 | "2022": null 27 | output: 28 | people: 29 | adult: 30 | is_adult: 31 | "2022": true 32 | 33 | - label: Basic UK tax reform (basic rate rise) 34 | endpoint: /uk/api/population-reform 35 | async: true 36 | input: 37 | basic_rate: 0.22 38 | output: 39 | budgetary_impact: -12e9 < x < -8e9 40 | 41 | - label: Basic US tax reform (CTC abolition) 42 | endpoint: /us/api/population-reform 43 | async: true 44 | input: 45 | abolish_refundable_ctc: 1 46 | output: 47 | budgetary_impact: -30e9 < x < 15e9 48 | -------------------------------------------------------------------------------- /policyengine/tests/test_api.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pathlib import Path 3 | import yaml 4 | from policyengine import PolicyEngine 5 | from time import sleep, time 6 | import warnings 7 | 8 | warnings.filterwarnings("ignore") 9 | 10 | # Initialise the API 11 | test_client = PolicyEngine(debug=True).app.test_client() 12 | 13 | # Each YAML file in this directory has the structure: 14 | # - endpoint: /uk/policy 15 | # - input: {} 16 | # - output: {} 17 | 18 | # This function will test each endpoint with the given input and check that the 19 | # output contains the the data given in the output object. It uses PyTest's parametric 20 | # testing feature to run each test case. 21 | 22 | files = Path(__file__).parent.glob("**/*.yaml") 23 | tests = [] 24 | for test_file in files: 25 | with open(test_file, "r") as f: 26 | cases = yaml.safe_load(f) 27 | i = 0 28 | for case in cases: 29 | i += 1 30 | tests.append( 31 | ( 32 | case.get("label", f"{test_file}-{i}"), 33 | case.get("async", False), 34 | case["endpoint"], 35 | case["input"], 36 | case["output"], 37 | case.get("time"), 38 | ) 39 | ) 40 | 41 | 42 | def match_value(actual, target): 43 | if isinstance(target, str): 44 | # Decode Python 45 | assert eval( 46 | target, dict(x=actual) 47 | ), f"{actual} does not match {target}" 48 | else: 49 | assert actual == target 50 | 51 | 52 | def match_object(actual, target): 53 | if isinstance(target, dict): 54 | for key in target.keys(): 55 | if key not in actual: 56 | raise ValueError( 57 | f"Key {key} not found in the response: {actual}, but it is in the target: {target}" 58 | ) 59 | match_object(actual[key], target[key]) 60 | else: 61 | match_value(actual, target) 62 | 63 | 64 | @pytest.mark.parametrize( 65 | "label,asynchronous,endpoint,input,output,time_limit", 66 | tests, 67 | ids=[t[0] for t in tests], 68 | ) 69 | def test_endpoint(label, asynchronous, endpoint, input, output, time_limit): 70 | start_time = time() 71 | response = test_client.post(endpoint, json=input) 72 | if response.status_code != 200: 73 | raise ValueError( 74 | f"Endpoint {endpoint} failed with status code {response.status_code}. The full result is {response.json}" 75 | ) 76 | if asynchronous: 77 | # Keep trying every 5 seconds until we get a response with data. 78 | while response.json is not None and response.json.get("status") in ( 79 | "queued", 80 | "in_progress", 81 | ): 82 | sleep(5) 83 | response = test_client.post(endpoint, json=input) 84 | match_object(response.json, output) 85 | t = time() - start_time 86 | if time_limit is not None: 87 | if isinstance(time_limit, str): 88 | assert eval( 89 | time_limit, 90 | dict(t=t, s=1, m=60, h=60 * 60, ms=1 / 1000), 91 | ) 92 | else: 93 | assert t < time_limit 94 | -------------------------------------------------------------------------------- /policyengine/tests/test_code_health.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | REPO = Path(__file__).parents[1] 4 | 5 | 6 | def in_text_files(folder: Path, text: str): 7 | for child in folder.iterdir(): 8 | if child.is_dir(): 9 | if in_text_files(child, text): 10 | return True, child 11 | else: 12 | with open(child, encoding="utf-8") as f: 13 | try: 14 | contents = f.read() 15 | if text in contents: 16 | return True, child 17 | except: 18 | pass 19 | 20 | 21 | def test_localhost_included(): 22 | if (REPO / "policyengine-client" / "src").exists(): 23 | assert not in_text_files( 24 | REPO / "policyengine-client" / "src", "useLocalServer = true" 25 | ) 26 | -------------------------------------------------------------------------------- /policyengine/tests/us/state_specific.yaml: -------------------------------------------------------------------------------- 1 | - label: Changing US State doesn't cause budgetary impacts. 2 | endpoint: /us/api/population-reform 3 | async: true 4 | input: 5 | baseline_state_specific: MD 6 | output: 7 | budgetary_impact: 0 8 | -------------------------------------------------------------------------------- /policyengine/web_server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PolicyEngine/policyengine/755fa5168caf3b7e443302db4821d22c034e1733/policyengine/web_server/__init__.py -------------------------------------------------------------------------------- /policyengine/web_server/cors.py: -------------------------------------------------------------------------------- 1 | from flask import request, make_response 2 | 3 | 4 | def after_request_func(response): 5 | """Adds CORS headers to the API response.""" 6 | origin = request.headers.get("Origin") 7 | if request.method == "OPTIONS": 8 | response = make_response() 9 | response.headers.add("Access-Control-Allow-Credentials", "true") 10 | response.headers.add("Access-Control-Allow-Headers", "Content-Type") 11 | response.headers.add("Access-Control-Allow-Headers", "x-csrf-token") 12 | response.headers.add( 13 | "Access-Control-Allow-Methods", 14 | "GET, POST, OPTIONS, PUT, PATCH, DELETE", 15 | ) 16 | if origin: 17 | response.headers.add("Access-Control-Allow-Origin", origin) 18 | else: 19 | response.headers.add("Access-Control-Allow-Credentials", "true") 20 | if origin: 21 | response.headers.add("Access-Control-Allow-Origin", origin) 22 | response.headers[ 23 | "Cache-Control" 24 | ] = "no-cache, no-store, must-revalidate, public, max-age=0" 25 | response.headers["Pragma"] = "no-cache" 26 | response.headers["Expires"] = "0" 27 | 28 | return response 29 | -------------------------------------------------------------------------------- /policyengine/web_server/logging.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pathlib import Path 3 | from time import time 4 | from typing import Callable 5 | import yaml 6 | from policyengine.package import POLICYENGINE_PACKAGE_PATH 7 | 8 | # Imports the Cloud Logging client library 9 | import logging 10 | import google.cloud.logging 11 | 12 | 13 | class PolicyEngineLogger: 14 | """Class managing PolicyEngine server logs.""" 15 | 16 | local: bool = True 17 | """Whether to log to a local YAML file or to Google Cloud. 18 | """ 19 | 20 | print_to_console: bool = True 21 | """Whether to print log messages to the console as well as to a log file. 22 | """ 23 | 24 | def __init__(self, local: bool = True, print_to_console: bool = True): 25 | self.local = local 26 | if not local: 27 | # Instantiates a client 28 | client = google.cloud.logging.Client() 29 | 30 | # Retrieves a Cloud Logging handler based on the environment 31 | # you're running in and integrates the handler with the 32 | # Python logging module. By default this captures all logs 33 | # at INFO level and higher 34 | client.setup_logging() 35 | self.print_to_console = print_to_console 36 | 37 | def log(self, *messages, **data: dict): 38 | """Log a message to the PolicyEngine server logs. 39 | 40 | Args: 41 | data (dict): The data to log. 42 | """ 43 | if len(messages) > 0: 44 | data["message"] = "\n".join([str(x) for x in messages]) 45 | if self.local: 46 | with open(Path(__file__).parent / "logs.yaml", "a") as f: 47 | yaml.dump( 48 | [ 49 | { 50 | "time": datetime.now().isoformat(), 51 | **data, 52 | } 53 | ], 54 | f, 55 | ) 56 | else: 57 | if data.get("status") == "error": 58 | log = logging.error 59 | else: 60 | log = logging.info 61 | log( 62 | data.get("message"), 63 | extra={k: v for k, v in data.items() if k != "message"}, 64 | ) 65 | 66 | if self.print_to_console: 67 | print( 68 | f"[{datetime.now().isoformat()}]" 69 | + "".join([f" {k}: {v}" for k, v in data.items()]) 70 | ) 71 | 72 | 73 | def logged_endpoint(fn: Callable, logger: PolicyEngineLogger) -> Callable: 74 | """Decorator for logging the start and end of an endpoint. 75 | 76 | Args: 77 | fn (Callable): The endpoint function. 78 | logger (PolicyEngineLogger): The logger to use. 79 | 80 | Returns: 81 | Callable: The decorated function. 82 | """ 83 | 84 | def new_fn(*args, **kwargs): 85 | start_time = time() 86 | result = fn(*args, **kwargs, logger=logger) 87 | duration = time() - start_time 88 | logger.log( 89 | event="endpoint_call", 90 | endpoint=fn.__name__, 91 | time=duration, 92 | ) 93 | return result 94 | 95 | new_fn.__name__ = "timed_" + fn.__name__ 96 | return new_fn 97 | -------------------------------------------------------------------------------- /policyengine/web_server/social_card.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | 4 | def add_social_card_metadata(path: str, html: str) -> str: 5 | """Modifies index.html to include social card metadata. 6 | 7 | Args: 8 | path (str): The path requested in the URL. 9 | html (str): The HTML to modify. 10 | 11 | Returns: 12 | str: The modified HTML. 13 | """ 14 | 15 | if "/uk" in path: 16 | country = "UK" 17 | elif "/us" in path: 18 | country = "US" 19 | else: 20 | country = None 21 | 22 | image_url = { 23 | "UK": "/social_preview/uk.png", 24 | "US": "/social_preview/us.png", 25 | None: "/social_preview/global.png", 26 | }[country] 27 | 28 | image_url = request.host_url + image_url 29 | 30 | title = { 31 | "UK": "PolicyEngine UK", 32 | "US": "PolicyEngine US", 33 | None: "PolicyEngine", 34 | }[country] 35 | 36 | description = { 37 | "UK": "Compute the impacts of public policy for the UK and your household", 38 | "US": "Compute the impacts of public policy for your household.", 39 | None: "Compute the impacts of public policy.", 40 | }[country] 41 | 42 | placeholder = "<title>PolicyEngine" 43 | replacement = f"""{title} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | """ 60 | 61 | return html.replace(placeholder, replacement) 62 | -------------------------------------------------------------------------------- /policyengine/web_server/static_site.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, send_from_directory 2 | from .social_card import add_social_card_metadata 3 | from pathlib import Path 4 | from policyengine.package import POLICYENGINE_PACKAGE_PATH 5 | 6 | 7 | def add_static_site_handling(app: Flask): 8 | """Add a handling route to a Flask app, directing 404 results to index.html, adding country-specific social card metadata. 9 | 10 | Args: 11 | app (Flask): A Flask application. 12 | """ 13 | 14 | def static_site(e): 15 | with open( 16 | str(POLICYENGINE_PACKAGE_PATH / "static" / "index.html") 17 | ) as f: 18 | text = f.read() 19 | modified = add_social_card_metadata(request.path, text) 20 | with open( 21 | str(POLICYENGINE_PACKAGE_PATH / "static" / "index_mod.html"), "w" 22 | ) as f: 23 | f.write(modified) 24 | response = send_from_directory( 25 | str(POLICYENGINE_PACKAGE_PATH / "static"), "index_mod.html" 26 | ) 27 | return response 28 | 29 | app.errorhandler(404)(static_site) 30 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # What's this file for? 2 | # Mostly so that Github Actions CI caching works! (it looks for requirements.txt by default) 3 | # 4 | # If you have written a fully working setup.py for your package already, 5 | # and your dependencies are mostly external, you could consider having a simple 6 | # requirements.txt with only the following: 7 | # -e . 8 | 9 | # This means that running 10 | # pip install -r requirements.txt 11 | # will be the same as 12 | # pip install -e . 13 | # 14 | -e . -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="PolicyEngine", 5 | version="1.126.1", 6 | author="PolicyEngine", 7 | license="http://www.fsf.org/licensing/licenses/agpl-3.0.html", 8 | url="https://github.com/policyengine/policyengine", 9 | install_requires=[ 10 | "dpath<2.0.0,>=1.5.0", 11 | "flask", 12 | "flask_cors", 13 | "google-cloud-storage>=1.42.0", 14 | "google-cloud-logging", 15 | "gunicorn", 16 | "itsdangerous==2.0.1", 17 | "Jinja2==3.0.3", 18 | "kaleido", 19 | "microdf_python", 20 | "numpy", 21 | "OpenFisca-Tools", 22 | "PolicyEngine-Core>=1.9.0", 23 | "PolicyEngine-UK==0.35.0", 24 | "PolicyEngine-US==0.177.0", 25 | "pandas", 26 | "plotly", 27 | "pytest", 28 | "rdbl", 29 | "tables", 30 | "wheel", 31 | "yaml-changelog>=0.2.0", 32 | ], 33 | packages=find_packages(), 34 | ) 35 | --------------------------------------------------------------------------------