taxcalc 4.2.1__py3-none-any.whl → 4.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- taxcalc/__init__.py +1 -1
- taxcalc/assumptions/ASSUMPTIONS.md +53 -0
- taxcalc/assumptions/README.md +17 -0
- taxcalc/assumptions/economic_assumptions_template.json +77 -0
- taxcalc/calcfunctions.py +7 -4
- taxcalc/data.py +10 -5
- taxcalc/policy_current_law.json +2033 -184
- taxcalc/reforms/2017_law.json +125 -0
- taxcalc/reforms/2017_law.out.csv +10 -0
- taxcalc/reforms/ARPA.json +78 -0
- taxcalc/reforms/ARPA.out.csv +10 -0
- taxcalc/reforms/BrownKhanna.json +23 -0
- taxcalc/reforms/BrownKhanna.out.csv +10 -0
- taxcalc/reforms/CARES.json +40 -0
- taxcalc/reforms/CARES.out.csv +10 -0
- taxcalc/reforms/ConsolidatedAppropriationsAct2021.json +15 -0
- taxcalc/reforms/ConsolidatedAppropriationsAct2021.out.csv +10 -0
- taxcalc/reforms/Larson2019.json +36 -0
- taxcalc/reforms/Larson2019.out.csv +10 -0
- taxcalc/reforms/README.md +22 -0
- taxcalc/reforms/REFORMS.md +92 -0
- taxcalc/reforms/Renacci.json +61 -0
- taxcalc/reforms/Renacci.out.csv +10 -0
- taxcalc/reforms/SandersDeFazio.json +15 -0
- taxcalc/reforms/SandersDeFazio.out.csv +10 -0
- taxcalc/reforms/TCJA.json +160 -0
- taxcalc/reforms/TCJA.md +48 -0
- taxcalc/reforms/TCJA.out.csv +10 -0
- taxcalc/reforms/Trump2016.json +71 -0
- taxcalc/reforms/Trump2016.out.csv +10 -0
- taxcalc/reforms/Trump2017.json +51 -0
- taxcalc/reforms/Trump2017.out.csv +10 -0
- taxcalc/reforms/archive/Clinton2016.json +56 -0
- taxcalc/reforms/archive/RyanBrady.json +104 -0
- taxcalc/reforms/archive/TCJA_House.json +144 -0
- taxcalc/reforms/archive/TCJA_House_Amended.json +152 -0
- taxcalc/reforms/archive/TCJA_Reconciliation.json +187 -0
- taxcalc/reforms/archive/TCJA_Senate.json +116 -0
- taxcalc/reforms/archive/TCJA_Senate_111417.json +169 -0
- taxcalc/reforms/archive/TCJA_Senate_120117.json +174 -0
- taxcalc/reforms/cases.csv +10 -0
- taxcalc/reforms/clp.out.csv +10 -0
- taxcalc/reforms/ext.json +59 -0
- taxcalc/reforms/growfactors_ext.csv +65 -0
- taxcalc/reforms/ptaxes0.json +37 -0
- taxcalc/reforms/ptaxes0.out.csv +10 -0
- taxcalc/reforms/ptaxes1.json +21 -0
- taxcalc/reforms/ptaxes1.out.csv +10 -0
- taxcalc/reforms/ptaxes2.json +18 -0
- taxcalc/reforms/ptaxes2.out.csv +10 -0
- taxcalc/reforms/ptaxes3.json +28 -0
- taxcalc/reforms/ptaxes3.out.csv +10 -0
- taxcalc/reforms/rounding2022.json +153 -0
- taxcalc/reforms/rounding2022.out.csv +10 -0
- taxcalc/tests/benefits_expect.csv +169 -0
- taxcalc/tests/cmpi_cps_expect.txt +132 -0
- taxcalc/tests/cmpi_puf_expect.txt +132 -0
- taxcalc/tests/conftest.py +143 -0
- taxcalc/tests/cpscsv_agg_expect.csv +26 -0
- taxcalc/tests/puf_var_correl_coeffs_2016.csv +80 -0
- taxcalc/tests/puf_var_wght_means_by_year.csv +80 -0
- taxcalc/tests/pufcsv_agg_expect.csv +26 -0
- taxcalc/tests/pufcsv_mtr_expect.txt +63 -0
- taxcalc/tests/reforms.json +649 -0
- taxcalc/tests/reforms_expect.csv +65 -0
- taxcalc/tests/test_4package.py +67 -0
- taxcalc/tests/test_benefits.py +86 -0
- taxcalc/tests/test_calcfunctions.py +871 -0
- taxcalc/tests/test_calculator.py +1021 -0
- taxcalc/tests/test_compare.py +336 -0
- taxcalc/tests/test_compatible_data.py +338 -0
- taxcalc/tests/test_consumption.py +144 -0
- taxcalc/tests/test_cpscsv.py +163 -0
- taxcalc/tests/test_data.py +133 -0
- taxcalc/tests/test_decorators.py +332 -0
- taxcalc/tests/test_growdiff.py +102 -0
- taxcalc/tests/test_growfactors.py +94 -0
- taxcalc/tests/test_parameters.py +617 -0
- taxcalc/tests/test_policy.py +1575 -0
- taxcalc/tests/test_puf_var_stats.py +194 -0
- taxcalc/tests/test_pufcsv.py +385 -0
- taxcalc/tests/test_records.py +234 -0
- taxcalc/tests/test_reforms.py +385 -0
- taxcalc/tests/test_responses.py +41 -0
- taxcalc/tests/test_taxcalcio.py +755 -0
- taxcalc/tests/test_tmdcsv.py +38 -0
- taxcalc/tests/test_utils.py +792 -0
- taxcalc/tmd_growfactors.csv +54 -54
- taxcalc/tmd_weights.csv.gz +0 -0
- taxcalc/validation/CSV_INPUT_VARS.md +29 -0
- taxcalc/validation/CSV_OUTPUT_VARS.md +63 -0
- taxcalc/validation/README.md +68 -0
- taxcalc/validation/taxsim35/Differences_Explained.md +54 -0
- taxcalc/validation/taxsim35/README.md +139 -0
- taxcalc/validation/taxsim35/expected_differences/a17-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/a18-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/a19-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/a20-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/a21-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/b17-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/b18-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/b19-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/b20-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/b21-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/c17-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/c18-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/expected_differences/c19-taxdiffs-expect.csv +25 -0
- taxcalc/validation/taxsim35/input_setup.py +67 -0
- taxcalc/validation/taxsim35/main_comparison.py +183 -0
- taxcalc/validation/taxsim35/prepare_taxcalc_input.py +161 -0
- taxcalc/validation/taxsim35/process_taxcalc_output.py +140 -0
- taxcalc/validation/taxsim35/taxsim_emulation.json +49 -0
- taxcalc/validation/taxsim35/taxsim_input.py +321 -0
- taxcalc/validation/taxsim35/tc_sims.py +98 -0
- taxcalc/validation/taxsim35/tests_35.py +80 -0
- taxcalc/validation/tests_35.sh +13 -0
- {taxcalc-4.2.1.dist-info → taxcalc-4.2.2.dist-info}/METADATA +3 -4
- taxcalc-4.2.2.dist-info/RECORD +144 -0
- {taxcalc-4.2.1.dist-info → taxcalc-4.2.2.dist-info}/WHEEL +1 -1
- taxcalc-4.2.1.dist-info/RECORD +0 -34
- {taxcalc-4.2.1.dist-info → taxcalc-4.2.2.dist-info}/LICENSE +0 -0
- {taxcalc-4.2.1.dist-info → taxcalc-4.2.2.dist-info}/entry_points.txt +0 -0
- {taxcalc-4.2.1.dist-info → taxcalc-4.2.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1575 @@
|
|
1
|
+
"""
|
2
|
+
Test Policy class and its methods.
|
3
|
+
"""
|
4
|
+
# CODING-STYLE CHECKS:
|
5
|
+
# pycodestyle test_policy.py
|
6
|
+
# pylint --disable=locally-disabled test_policy.py
|
7
|
+
#
|
8
|
+
# pylint: disable=too-many-lines
|
9
|
+
|
10
|
+
import copy
|
11
|
+
import os
|
12
|
+
import json
|
13
|
+
import numpy as np
|
14
|
+
import pytest
|
15
|
+
import paramtools as pt
|
16
|
+
# pylint: disable=import-error
|
17
|
+
from taxcalc import Policy
|
18
|
+
|
19
|
+
|
20
|
+
def cmp_policy_objs(pol1, pol2, year_range=None, exclude=None):
|
21
|
+
"""
|
22
|
+
Compare parameter values two policy objects.
|
23
|
+
|
24
|
+
year_range: years over which to compare values.
|
25
|
+
exclude: list of parameters to exclude from comparison.
|
26
|
+
"""
|
27
|
+
if year_range is not None:
|
28
|
+
pol1.set_state(year=list(year_range))
|
29
|
+
pol2.set_state(year=list(year_range))
|
30
|
+
else:
|
31
|
+
pol1.clear_state()
|
32
|
+
pol2.clear_state()
|
33
|
+
for param in pol1._data:
|
34
|
+
if exclude and param in exclude:
|
35
|
+
continue
|
36
|
+
v1 = getattr(pol1, param)
|
37
|
+
v2 = getattr(pol2, param)
|
38
|
+
np.testing.assert_allclose(v1, v2)
|
39
|
+
|
40
|
+
|
41
|
+
def test_incorrect_class_instantiation():
|
42
|
+
"""
|
43
|
+
Test incorrect instantiation of Policy class object.
|
44
|
+
"""
|
45
|
+
with pytest.raises(ValueError):
|
46
|
+
Policy(gfactors=list())
|
47
|
+
|
48
|
+
|
49
|
+
def test_correct_class_instantiation():
|
50
|
+
"""
|
51
|
+
Test correct instantiation of Policy class object.
|
52
|
+
"""
|
53
|
+
pol = Policy()
|
54
|
+
assert pol
|
55
|
+
pol.implement_reform({})
|
56
|
+
with pytest.raises(pt.ValidationError):
|
57
|
+
pol.implement_reform(list())
|
58
|
+
with pytest.raises(pt.ValidationError):
|
59
|
+
pol.implement_reform({2099: {'II_em': 99000}})
|
60
|
+
pol.set_year(2019)
|
61
|
+
with pytest.raises(pt.ValidationError):
|
62
|
+
pol.implement_reform({2018: {'II_em': 99000}})
|
63
|
+
with pytest.raises(pt.ValidationError):
|
64
|
+
pol.implement_reform({2020: {'II_em': -1000}})
|
65
|
+
|
66
|
+
|
67
|
+
def test_json_reform_url():
|
68
|
+
"""
|
69
|
+
Test reading a JSON reform from a URL. Results from the URL are expected
|
70
|
+
to match the results from the string.
|
71
|
+
"""
|
72
|
+
reform_str = """
|
73
|
+
{
|
74
|
+
// raise FICA payroll tax rate in 2018 and 2020
|
75
|
+
"FICA_ss_trt_employer": {
|
76
|
+
"2018": 0.065,
|
77
|
+
"2020": 0.070
|
78
|
+
},
|
79
|
+
"FICA_ss_trt_employee": {
|
80
|
+
"2018": 0.065,
|
81
|
+
"2020": 0.070
|
82
|
+
},
|
83
|
+
// raise Medicare payroll tax rate in 2019 and 2021
|
84
|
+
"FICA_mc_trt_employer": {
|
85
|
+
"2019": 0.015,
|
86
|
+
"2021": 0.016
|
87
|
+
},
|
88
|
+
"FICA_mc_trt_employee": {
|
89
|
+
"2019": 0.015,
|
90
|
+
"2021": 0.016
|
91
|
+
}
|
92
|
+
}
|
93
|
+
"""
|
94
|
+
reform_url = ('https://raw.githubusercontent.com/PSLmodels/'
|
95
|
+
'Tax-Calculator/master/taxcalc/reforms/ptaxes0.json')
|
96
|
+
params_str = Policy.read_json_reform(reform_str)
|
97
|
+
params_url = Policy.read_json_reform(reform_url)
|
98
|
+
assert params_str == params_url
|
99
|
+
|
100
|
+
reform_gh_url = (
|
101
|
+
"github://PSLmodels:Tax-Calculator@master/taxcalc/reforms/ptaxes0.json"
|
102
|
+
)
|
103
|
+
params_gh_url = Policy.read_json_reform(reform_gh_url)
|
104
|
+
assert params_gh_url
|
105
|
+
assert params_gh_url == params_str
|
106
|
+
|
107
|
+
|
108
|
+
REFORM_JSON = """
|
109
|
+
// Example of a reform file suitable for Policy.read_json_reform().
|
110
|
+
// This JSON file can contain any number of trailing //-style comments, which
|
111
|
+
// will be removed before the contents are converted from JSON to a dictionary.
|
112
|
+
// The primary keys are parameters and the secondary keys are years.
|
113
|
+
// Both the primary and secondary key values must be enclosed in quotes (").
|
114
|
+
// Boolean variables are specified as true or false (no quotes; all lowercase).
|
115
|
+
{
|
116
|
+
"AMT_brk1": // top of first AMT tax bracket
|
117
|
+
{"2015": 200000,
|
118
|
+
"2017": 300000
|
119
|
+
},
|
120
|
+
"EITC_c": // maximum EITC amount by number of qualifying kids (0,1,2,3+)
|
121
|
+
{"2016": [ 900, 5000, 8000, 9000],
|
122
|
+
"2019": [1200, 7000, 10000, 12000]
|
123
|
+
},
|
124
|
+
"II_em": // personal exemption amount (see indexing changes below)
|
125
|
+
{"2016": 6000,
|
126
|
+
"2018": 7500,
|
127
|
+
"2020": 9000
|
128
|
+
},
|
129
|
+
"II_em-indexed": // personal exemption amount indexing status
|
130
|
+
{"2016": false, // values in future years are same as this year value
|
131
|
+
"2018": true // values in future years indexed with this year as base
|
132
|
+
},
|
133
|
+
"SS_Earnings_c": // social security (OASDI) maximum taxable earnings
|
134
|
+
{"2016": 300000,
|
135
|
+
"2018": 500000,
|
136
|
+
"2020": 700000
|
137
|
+
},
|
138
|
+
"AMT_em-indexed": // AMT exemption amount indexing status
|
139
|
+
{"2017": false, // values in future years are same as this year value
|
140
|
+
"2020": true // values in future years indexed with this year as base
|
141
|
+
}
|
142
|
+
}
|
143
|
+
"""
|
144
|
+
|
145
|
+
|
146
|
+
# pylint: disable=protected-access,no-member
|
147
|
+
|
148
|
+
|
149
|
+
@pytest.mark.parametrize("set_year", [False, True])
|
150
|
+
def test_read_json_reform_file_and_implement_reform(set_year):
|
151
|
+
"""
|
152
|
+
Test reading and translation of reform JSON into a reform dictionary
|
153
|
+
and then using that reform dictionary to implement reform.
|
154
|
+
"""
|
155
|
+
pol = Policy()
|
156
|
+
if set_year:
|
157
|
+
pol.set_year(2015)
|
158
|
+
pol.implement_reform(Policy.read_json_reform(REFORM_JSON))
|
159
|
+
syr = pol.start_year
|
160
|
+
# pylint: disable=protected-access
|
161
|
+
amt_brk1 = pol._AMT_brk1
|
162
|
+
assert amt_brk1[2015 - syr] == 200000
|
163
|
+
assert amt_brk1[2016 - syr] > 200000
|
164
|
+
assert amt_brk1[2017 - syr] == 300000
|
165
|
+
assert amt_brk1[2018 - syr] > 300000
|
166
|
+
ii_em = pol._II_em
|
167
|
+
assert ii_em[2016 - syr] == 6000
|
168
|
+
assert ii_em[2017 - syr] == 6000
|
169
|
+
assert ii_em[2018 - syr] == 7500
|
170
|
+
assert ii_em[2019 - syr] > 7500
|
171
|
+
assert ii_em[2020 - syr] == 9000
|
172
|
+
assert ii_em[2021 - syr] > 9000
|
173
|
+
amt_em = pol._AMT_em
|
174
|
+
assert amt_em[2016 - syr, 0] > amt_em[2015 - syr, 0]
|
175
|
+
assert amt_em[2017 - syr, 0] > amt_em[2016 - syr, 0]
|
176
|
+
assert amt_em[2018 - syr, 0] == amt_em[2017 - syr, 0]
|
177
|
+
assert amt_em[2019 - syr, 0] == amt_em[2017 - syr, 0]
|
178
|
+
assert amt_em[2020 - syr, 0] == amt_em[2017 - syr, 0]
|
179
|
+
assert amt_em[2021 - syr, 0] > amt_em[2020 - syr, 0]
|
180
|
+
assert amt_em[2022 - syr, 0] > amt_em[2021 - syr, 0]
|
181
|
+
add4aged = pol._ID_Medical_frt_add4aged
|
182
|
+
assert add4aged[2015 - syr] == -0.025
|
183
|
+
assert add4aged[2016 - syr] == -0.025
|
184
|
+
assert add4aged[2017 - syr] == 0.0
|
185
|
+
assert add4aged[2022 - syr] == 0.0
|
186
|
+
|
187
|
+
|
188
|
+
def test_constant_inflation_rate_with_reform():
|
189
|
+
"""
|
190
|
+
Test indexing of policy parameters involved in a reform.
|
191
|
+
"""
|
192
|
+
pol = Policy()
|
193
|
+
# implement reform in year before final year
|
194
|
+
fyr = 2034
|
195
|
+
ryr = fyr - 1
|
196
|
+
reform = {
|
197
|
+
'II_em': {(ryr - 3): 1000, # to avoid divide-by-zero under TCJA
|
198
|
+
ryr: 20000}
|
199
|
+
}
|
200
|
+
pol.implement_reform(reform)
|
201
|
+
# extract price inflation rates
|
202
|
+
pirates = pol.inflation_rates()
|
203
|
+
syr = Policy.JSON_START_YEAR
|
204
|
+
irate_b = pirates[ryr - 2 - syr]
|
205
|
+
irate_a = pirates[ryr - syr]
|
206
|
+
# check implied inflation rate just before reform
|
207
|
+
grate = float(pol._II_em[ryr - 1 - syr]) / float(pol._II_em[ryr - 2 - syr])
|
208
|
+
assert round(grate - 1.0, 4) == round(irate_b, 4)
|
209
|
+
# check implied inflation rate just after reform
|
210
|
+
grate = float(pol._II_em[ryr + 1 - syr]) / float(pol._II_em[ryr - syr])
|
211
|
+
assert round(grate - 1.0, 6) == round(irate_a, 6)
|
212
|
+
|
213
|
+
|
214
|
+
def test_variable_inflation_rate_with_reform():
|
215
|
+
"""
|
216
|
+
Test indexing of policy parameters involved in a reform.
|
217
|
+
"""
|
218
|
+
pol = Policy()
|
219
|
+
syr = Policy.JSON_START_YEAR
|
220
|
+
assert pol._II_em[2013 - syr] == 3900
|
221
|
+
# implement reform in 2020 which is two years before the last year, 2022
|
222
|
+
reform = {
|
223
|
+
'II_em': {2018: 1000, # to avoid divide-by-zero under TCJA
|
224
|
+
2020: 20000}
|
225
|
+
}
|
226
|
+
pol.implement_reform(reform)
|
227
|
+
pol.set_year(2020)
|
228
|
+
assert pol.current_year == 2020
|
229
|
+
# extract price inflation rates
|
230
|
+
pirates = pol.inflation_rates()
|
231
|
+
irate2018 = pirates[2018 - syr]
|
232
|
+
irate2020 = pirates[2020 - syr]
|
233
|
+
irate2021 = pirates[2021 - syr]
|
234
|
+
# check implied inflation rate between 2018 and 2019 (before the reform)
|
235
|
+
grate = float(pol._II_em[2019 - syr]) / float(pol._II_em[2018 - syr])
|
236
|
+
assert round(grate - 1.0, 5) == round(irate2018, 5)
|
237
|
+
# check implied inflation rate between 2020 and 2021 (after the reform)
|
238
|
+
grate = float(pol._II_em[2021 - syr]) / float(pol._II_em[2020 - syr])
|
239
|
+
assert round(grate - 1.0, 5) == round(irate2020, 5)
|
240
|
+
# check implied inflation rate between 2021 and 2022 (after the reform)
|
241
|
+
grate = float(pol._II_em[2022 - syr]) / float(pol._II_em[2021 - syr])
|
242
|
+
assert round(grate - 1.0, 5) == round(irate2021, 5)
|
243
|
+
|
244
|
+
|
245
|
+
def test_multi_year_reform():
|
246
|
+
"""
|
247
|
+
Test multi-year reform involving 1D and 2D parameters.
|
248
|
+
"""
|
249
|
+
# specify dimensions of policy Policy object
|
250
|
+
syr = Policy.JSON_START_YEAR
|
251
|
+
nyrs = Policy.DEFAULT_NUM_YEARS
|
252
|
+
pol = Policy()
|
253
|
+
iratelist = pol.inflation_rates()
|
254
|
+
ifactor = {}
|
255
|
+
for i in range(0, nyrs):
|
256
|
+
ifactor[syr + i] = 1.0 + iratelist[i]
|
257
|
+
wratelist = pol.wage_growth_rates()
|
258
|
+
wfactor = {}
|
259
|
+
for i in range(0, nyrs):
|
260
|
+
wfactor[syr + i] = 1.0 + wratelist[i]
|
261
|
+
# specify multi-year reform using a param:year:value-fomatted dictionary
|
262
|
+
reform = {
|
263
|
+
'SS_Earnings_c': {2016: 300000,
|
264
|
+
2017: 500000,
|
265
|
+
2019: 700000},
|
266
|
+
'SS_Earnings_c-indexed': {2017: False,
|
267
|
+
2019: True},
|
268
|
+
'CTC_c': {2015: 2000},
|
269
|
+
'EITC_c': {2016: [900, 5000, 8000, 9000],
|
270
|
+
2019: [1200, 7000, 10000, 12000]},
|
271
|
+
'II_em': {2016: 7000,
|
272
|
+
2019: 9000}
|
273
|
+
}
|
274
|
+
# implement multi-year reform
|
275
|
+
pol.implement_reform(reform)
|
276
|
+
assert pol.current_year == syr
|
277
|
+
# move policy Policy object forward in time so current_year is syr+2
|
278
|
+
# Note: this would be typical usage because the first budget year
|
279
|
+
# is typically greater than Policy start_year.
|
280
|
+
pol.set_year(pol.start_year + 2)
|
281
|
+
assert pol.current_year == syr + 2
|
282
|
+
# confirm that actual parameters have expected post-reform values
|
283
|
+
check_eitc_c(pol, reform, ifactor)
|
284
|
+
check_ii_em(pol, reform, ifactor)
|
285
|
+
check_ss_earnings_c(pol, reform, wfactor)
|
286
|
+
check_ctc_c(pol, reform)
|
287
|
+
# end of test_multi_year_reform with the check_* functions below:
|
288
|
+
|
289
|
+
|
290
|
+
def check_ctc_c(ppo, reform):
|
291
|
+
"""
|
292
|
+
Compare actual and expected _CTC_c parameter values
|
293
|
+
generated by the test_multi_year_reform() function above.
|
294
|
+
Ensure that future-year values in policy_current_law.json
|
295
|
+
are overwritten by reform.
|
296
|
+
"""
|
297
|
+
actual = {}
|
298
|
+
arr = getattr(ppo, '_CTC_c')
|
299
|
+
for i in range(0, ppo.num_years):
|
300
|
+
actual[ppo.start_year + i] = arr[i]
|
301
|
+
assert actual[2013] == 1000
|
302
|
+
assert actual[2014] == 1000
|
303
|
+
e2015 = reform['CTC_c'][2015]
|
304
|
+
assert actual[2015] == e2015
|
305
|
+
e2016 = actual[2015]
|
306
|
+
assert actual[2016] == e2016
|
307
|
+
e2017 = actual[2016]
|
308
|
+
assert actual[2017] == e2017
|
309
|
+
e2018 = actual[2017]
|
310
|
+
assert actual[2018] == e2018
|
311
|
+
e2019 = actual[2018]
|
312
|
+
assert actual[2019] == e2019
|
313
|
+
|
314
|
+
|
315
|
+
def check_eitc_c(ppo, reform, ifactor):
|
316
|
+
"""
|
317
|
+
Compare actual and expected _EITC_c parameter values
|
318
|
+
generated by the test_multi_year_reform() function above.
|
319
|
+
"""
|
320
|
+
actual = {}
|
321
|
+
arr = getattr(ppo, '_EITC_c')
|
322
|
+
alen = len(arr[0])
|
323
|
+
for i in range(0, ppo.num_years):
|
324
|
+
actual[ppo.start_year + i] = arr[i]
|
325
|
+
assert np.allclose(actual[2013], [487, 3250, 5372, 6044],
|
326
|
+
atol=0.01, rtol=0.0)
|
327
|
+
assert np.allclose(actual[2014], [496, 3305, 5460, 6143],
|
328
|
+
atol=0.01, rtol=0.0)
|
329
|
+
assert np.allclose(actual[2015], [503, 3359, 5548, 6242],
|
330
|
+
atol=0.01, rtol=0.0)
|
331
|
+
e2016 = reform['EITC_c'][2016]
|
332
|
+
assert np.allclose(actual[2016], e2016, atol=0.01, rtol=0.0)
|
333
|
+
e2017 = [ifactor[2016] * actual[2016][j] for j in range(0, alen)]
|
334
|
+
assert np.allclose(actual[2017], e2017, atol=0.01, rtol=0.0)
|
335
|
+
e2018 = [ifactor[2017] * actual[2017][j] for j in range(0, alen)]
|
336
|
+
assert np.allclose(actual[2018], e2018, atol=0.01, rtol=0.0)
|
337
|
+
e2019 = reform['EITC_c'][2019]
|
338
|
+
assert np.allclose(actual[2019], e2019, atol=0.01, rtol=0.0)
|
339
|
+
e2020 = [ifactor[2019] * actual[2019][j] for j in range(0, alen)]
|
340
|
+
assert np.allclose(actual[2020], e2020, atol=0.01, rtol=0.0)
|
341
|
+
e2021 = [ifactor[2020] * actual[2020][j] for j in range(0, alen)]
|
342
|
+
assert np.allclose(actual[2021], e2021, atol=0.01, rtol=0.0)
|
343
|
+
e2022 = [ifactor[2021] * actual[2021][j] for j in range(0, alen)]
|
344
|
+
assert np.allclose(actual[2022], e2022, atol=0.01, rtol=0.0)
|
345
|
+
|
346
|
+
|
347
|
+
def check_ii_em(ppo, reform, ifactor):
|
348
|
+
"""
|
349
|
+
Compare actual and expected _II_em parameter values
|
350
|
+
generated by the test_multi_year_reform() function above.
|
351
|
+
"""
|
352
|
+
actual = {}
|
353
|
+
arr = getattr(ppo, '_II_em')
|
354
|
+
for i in range(0, ppo.num_years):
|
355
|
+
actual[ppo.start_year + i] = arr[i]
|
356
|
+
assert actual[2013] == 3900
|
357
|
+
assert actual[2014] == 3950
|
358
|
+
assert actual[2015] == 4000
|
359
|
+
e2016 = reform['II_em'][2016]
|
360
|
+
assert actual[2016] == e2016
|
361
|
+
e2017 = ifactor[2016] * actual[2016]
|
362
|
+
assert np.allclose([actual[2017]], [e2017], atol=0.01, rtol=0.0)
|
363
|
+
e2018 = ifactor[2017] * actual[2017]
|
364
|
+
assert np.allclose([actual[2018]], [e2018], atol=0.01, rtol=0.0)
|
365
|
+
e2019 = reform['II_em'][2019]
|
366
|
+
assert actual[2019] == e2019
|
367
|
+
e2020 = ifactor[2019] * actual[2019]
|
368
|
+
assert np.allclose([actual[2020]], [e2020], atol=0.01, rtol=0.0)
|
369
|
+
e2021 = ifactor[2020] * actual[2020]
|
370
|
+
assert np.allclose([actual[2021]], [e2021], atol=0.01, rtol=0.0)
|
371
|
+
e2022 = ifactor[2021] * actual[2021]
|
372
|
+
assert np.allclose([actual[2022]], [e2022], atol=0.01, rtol=0.0)
|
373
|
+
|
374
|
+
|
375
|
+
def check_ss_earnings_c(ppo, reform, wfactor):
|
376
|
+
"""
|
377
|
+
Compare actual and expected _SS_Earnings_c parameter values
|
378
|
+
generated by the test_multi_year_reform() function above.
|
379
|
+
"""
|
380
|
+
actual = {}
|
381
|
+
arr = getattr(ppo, '_SS_Earnings_c')
|
382
|
+
for i in range(0, ppo.num_years):
|
383
|
+
actual[ppo.start_year + i] = arr[i]
|
384
|
+
assert actual[2013] == 113700
|
385
|
+
assert actual[2014] == 117000
|
386
|
+
assert actual[2015] == 118500
|
387
|
+
e2016 = reform['SS_Earnings_c'][2016]
|
388
|
+
assert actual[2016] == e2016
|
389
|
+
e2017 = reform['SS_Earnings_c'][2017]
|
390
|
+
assert actual[2017] == e2017
|
391
|
+
e2018 = actual[2017] # no indexing after 2017
|
392
|
+
assert actual[2018] == e2018
|
393
|
+
e2019 = reform['SS_Earnings_c'][2019]
|
394
|
+
assert actual[2019] == e2019
|
395
|
+
e2020 = wfactor[2019] * actual[2019] # indexing after 2019
|
396
|
+
assert np.allclose([actual[2020]], [e2020], atol=0.01, rtol=0.0)
|
397
|
+
e2021 = wfactor[2020] * actual[2020]
|
398
|
+
assert np.allclose([actual[2021]], [e2021], atol=0.01, rtol=0.0)
|
399
|
+
e2022 = wfactor[2021] * actual[2021]
|
400
|
+
assert np.allclose([actual[2022]], [e2022], atol=0.01, rtol=0.0)
|
401
|
+
|
402
|
+
|
403
|
+
def test_policy_metadata():
|
404
|
+
"""
|
405
|
+
Test that metadata() method returns expected dictionary.
|
406
|
+
"""
|
407
|
+
clp = Policy()
|
408
|
+
mdata = clp.metadata()
|
409
|
+
assert mdata
|
410
|
+
|
411
|
+
|
412
|
+
def test_implement_reform_raises_on_no_year():
|
413
|
+
"""
|
414
|
+
Test that implement_reform raises error for missing year.
|
415
|
+
"""
|
416
|
+
reform = {'STD_Aged': [1400, 1200, 1400, 1400, 1400]}
|
417
|
+
ppo = Policy()
|
418
|
+
with pytest.raises(pt.ValidationError):
|
419
|
+
ppo.implement_reform(reform)
|
420
|
+
|
421
|
+
|
422
|
+
def test_implement_reform_raises_on_early_year():
|
423
|
+
"""
|
424
|
+
Test that implement_reform raises error for early year.
|
425
|
+
"""
|
426
|
+
ppo = Policy()
|
427
|
+
reform = {'STD_Aged': {2010: [1400, 1100, 1100, 1400, 1400]}}
|
428
|
+
with pytest.raises(pt.ValidationError):
|
429
|
+
ppo.implement_reform(reform)
|
430
|
+
|
431
|
+
|
432
|
+
def test_reform_with_default_indexed():
|
433
|
+
"""
|
434
|
+
Test that implement_reform indexes after first reform year.
|
435
|
+
"""
|
436
|
+
ppo = Policy()
|
437
|
+
reform = {'II_em': {2015: 4300}}
|
438
|
+
ppo.implement_reform(reform)
|
439
|
+
# II_em has a default indexing status of true, so
|
440
|
+
# in 2016 its value should be greater than 4300
|
441
|
+
ppo.set_year(2016)
|
442
|
+
assert ppo.II_em > 4300
|
443
|
+
|
444
|
+
|
445
|
+
def test_reform_makes_no_changes_before_year():
|
446
|
+
"""
|
447
|
+
Test that implement_reform makes no changes before first reform year.
|
448
|
+
"""
|
449
|
+
ppo = Policy()
|
450
|
+
reform = {'II_em': {2015: 4400}, 'II_em-indexed': {2015: True}}
|
451
|
+
ppo.implement_reform(reform)
|
452
|
+
ppo.set_year(2015)
|
453
|
+
assert np.allclose(ppo._II_em[:3], np.array([3900, 3950, 4400]),
|
454
|
+
atol=0.01, rtol=0.0)
|
455
|
+
assert ppo.II_em == 4400
|
456
|
+
|
457
|
+
|
458
|
+
@pytest.mark.parametrize("set_year", [False, True])
|
459
|
+
def test_read_json_reform_and_implement_reform(set_year):
|
460
|
+
"""
|
461
|
+
Test reading and translation of reform file into a reform dictionary
|
462
|
+
that is then used to call implement_reform method.
|
463
|
+
NOTE: implement_reform called when policy.current_year == policy.start_year
|
464
|
+
"""
|
465
|
+
reform_json = """
|
466
|
+
// Example of JSON reform text suitable for the
|
467
|
+
// Policy.read_json_reform() method.
|
468
|
+
// This JSON text can contain any number of trailing //-style comments,
|
469
|
+
// which will be removed before the contents are converted from JSON to
|
470
|
+
// a dictionary.
|
471
|
+
// The primary keys are policy parameters and secondary keys are years.
|
472
|
+
// Both the primary & secondary key values must be enclosed in quotes (").
|
473
|
+
// Boolean variables are specified as true or false with no quotes and all
|
474
|
+
// lowercase characters.
|
475
|
+
{
|
476
|
+
"AMT_brk1": // top of first AMT tax bracket
|
477
|
+
{"2015": 200000,
|
478
|
+
"2017": 300000
|
479
|
+
},
|
480
|
+
"EITC_c": // max EITC amount by number of qualifying kids (0,1,2,3+)
|
481
|
+
{"2016": [ 900, 5000, 8000, 9000],
|
482
|
+
"2019": [1200, 7000, 10000, 12000]
|
483
|
+
},
|
484
|
+
"II_em": // personal exemption amount (see indexing changes below)
|
485
|
+
{"2016": 6000,
|
486
|
+
"2018": 7500,
|
487
|
+
"2020": 9000
|
488
|
+
},
|
489
|
+
"II_em-indexed": // personal exemption amount indexing status
|
490
|
+
{"2016": false, // values in future years are same as this year value
|
491
|
+
"2018": true // vals in future years indexed with this year as base
|
492
|
+
},
|
493
|
+
"SS_Earnings_c": // Social Security (OASDI) maximum taxable earnings
|
494
|
+
{"2016": 300000,
|
495
|
+
"2018": 500000,
|
496
|
+
"2020": 700000
|
497
|
+
},
|
498
|
+
"AMT_em-indexed": // AMT exemption amount indexing status
|
499
|
+
{"2017": false, // values in future years are same as this year value
|
500
|
+
"2020": true // vals in future years indexed with this year as base
|
501
|
+
}
|
502
|
+
}
|
503
|
+
"""
|
504
|
+
policy = Policy()
|
505
|
+
if set_year:
|
506
|
+
policy.set_year(2015)
|
507
|
+
reform_dict = Policy.read_json_reform(reform_json)
|
508
|
+
policy.implement_reform(reform_dict)
|
509
|
+
syr = policy.start_year
|
510
|
+
amt_brk1 = policy._AMT_brk1
|
511
|
+
assert amt_brk1[2015 - syr] == 200000
|
512
|
+
assert amt_brk1[2016 - syr] > 200000
|
513
|
+
assert amt_brk1[2017 - syr] == 300000
|
514
|
+
assert amt_brk1[2018 - syr] > 300000
|
515
|
+
ii_em = policy._II_em
|
516
|
+
assert ii_em[2016 - syr] == 6000
|
517
|
+
assert ii_em[2017 - syr] == 6000
|
518
|
+
assert ii_em[2018 - syr] == 7500
|
519
|
+
assert ii_em[2019 - syr] > 7500
|
520
|
+
assert ii_em[2020 - syr] == 9000
|
521
|
+
assert ii_em[2021 - syr] > 9000
|
522
|
+
amt_em = policy._AMT_em
|
523
|
+
assert amt_em[2016 - syr, 0] > amt_em[2015 - syr, 0]
|
524
|
+
assert amt_em[2017 - syr, 0] > amt_em[2016 - syr, 0]
|
525
|
+
assert amt_em[2018 - syr, 0] == amt_em[2017 - syr, 0]
|
526
|
+
assert amt_em[2019 - syr, 0] == amt_em[2017 - syr, 0]
|
527
|
+
assert amt_em[2020 - syr, 0] == amt_em[2017 - syr, 0]
|
528
|
+
assert amt_em[2021 - syr, 0] > amt_em[2020 - syr, 0]
|
529
|
+
assert amt_em[2022 - syr, 0] > amt_em[2021 - syr, 0]
|
530
|
+
add4aged = policy._ID_Medical_frt_add4aged
|
531
|
+
assert add4aged[2015 - syr] == -0.025
|
532
|
+
assert add4aged[2016 - syr] == -0.025
|
533
|
+
assert add4aged[2017 - syr] == 0.0
|
534
|
+
assert add4aged[2022 - syr] == 0.0
|
535
|
+
|
536
|
+
|
537
|
+
def test_pop_the_cap_reform():
|
538
|
+
"""
|
539
|
+
Test eliminating the maximum taxable earnings (MTE)
|
540
|
+
used in the calculation of the OASDI payroll tax.
|
541
|
+
"""
|
542
|
+
# create Policy parameters object
|
543
|
+
ppo = Policy()
|
544
|
+
assert ppo.current_year == Policy.JSON_START_YEAR
|
545
|
+
# confirm that MTE has current-law values in 2015 and 2016
|
546
|
+
mte = ppo._SS_Earnings_c
|
547
|
+
syr = Policy.JSON_START_YEAR
|
548
|
+
assert mte[2015 - syr] == 118500
|
549
|
+
assert mte[2016 - syr] == 118500
|
550
|
+
# specify a "pop the cap" reform that eliminates MTE cap in 2016
|
551
|
+
reform = {'SS_Earnings_c': {2016: 9e99}}
|
552
|
+
ppo.implement_reform(reform)
|
553
|
+
mte = ppo._SS_Earnings_c
|
554
|
+
assert mte[2015 - syr] == 118500
|
555
|
+
assert mte[2016 - syr] == 9e99
|
556
|
+
assert mte[ppo.end_year - syr] == 9e99
|
557
|
+
|
558
|
+
|
559
|
+
def test_order_of_indexing_and_level_reforms():
|
560
|
+
"""
|
561
|
+
Test that the order of the two reform provisions for the same parameter
|
562
|
+
make no difference to the post-reform policy parameter values.
|
563
|
+
"""
|
564
|
+
# specify two reforms that raises the MTE and stops its indexing in 2015
|
565
|
+
reform = [
|
566
|
+
{
|
567
|
+
'SS_Earnings_c': {2015: 500000},
|
568
|
+
'SS_Earnings_c-indexed': {2015: False}
|
569
|
+
},
|
570
|
+
# now reverse the order of the two reform provisions
|
571
|
+
{
|
572
|
+
'SS_Earnings_c-indexed': {2015: False},
|
573
|
+
'SS_Earnings_c': {2015: 500000}
|
574
|
+
}
|
575
|
+
]
|
576
|
+
# specify two Policy objects
|
577
|
+
ppo = [Policy(), Policy()]
|
578
|
+
# apply reforms to corresponding Policy object & check post-reform values
|
579
|
+
syr = Policy.JSON_START_YEAR
|
580
|
+
for ref in range(len(reform)): # pylint: disable=consider-using-enumerate
|
581
|
+
# confirm pre-reform MTE values in 2014-2017
|
582
|
+
mte = ppo[ref]._SS_Earnings_c
|
583
|
+
assert mte[2014 - syr] == 117000
|
584
|
+
assert mte[2015 - syr] == 118500
|
585
|
+
assert mte[2016 - syr] == 118500
|
586
|
+
assert mte[2017 - syr] < 500000
|
587
|
+
# implement reform in 2015
|
588
|
+
ppo[ref].implement_reform(reform[ref])
|
589
|
+
# confirm post-reform MTE values in 2014-2017
|
590
|
+
mte = ppo[ref]._SS_Earnings_c
|
591
|
+
assert mte[2014 - syr] == 117000
|
592
|
+
assert mte[2015 - syr] == 500000
|
593
|
+
assert mte[2016 - syr] == 500000
|
594
|
+
assert mte[2017 - syr] == 500000
|
595
|
+
|
596
|
+
|
597
|
+
def test_misspecified_reform_dictionary():
|
598
|
+
"""
|
599
|
+
Demonstrate pitfalls of careless specification of policy reform
|
600
|
+
dictionaries involving non-unique dictionary keys.
|
601
|
+
"""
|
602
|
+
# specify apparently the same reform in two different ways, forgetting
|
603
|
+
# that Python dictionaries have unique keys
|
604
|
+
reform1 = {'II_em': {2019: 1000, 2020: 2000}}
|
605
|
+
# pylint: disable=duplicate-key
|
606
|
+
reform2 = {'II_em': {2019: 1000}, 'II_em': {2020: 2000}}
|
607
|
+
# these two reform dictionaries are not the same: the second
|
608
|
+
# 'II_em' key value for 2020 in reform2 OVERWRITES and REPLACES
|
609
|
+
# the first 'II_em' key value for 2019 in reform2
|
610
|
+
assert reform1 != reform2
|
611
|
+
|
612
|
+
|
613
|
+
def test_section_titles(tests_path):
|
614
|
+
"""
|
615
|
+
Check section titles in policy_current_law.json and uguide.htmx files.
|
616
|
+
"""
|
617
|
+
# pylint: disable=too-many-locals
|
618
|
+
def generate_section_dictionary(md_text):
|
619
|
+
"""
|
620
|
+
Returns dictionary of section titles that is
|
621
|
+
structured like the VALID_SECTION dictionary (see below) and
|
622
|
+
extracted from the specified html_text.
|
623
|
+
"""
|
624
|
+
sdict = dict()
|
625
|
+
for line in md_text.splitlines():
|
626
|
+
# This is shown as an empty case in current law policy and
|
627
|
+
# validation.
|
628
|
+
if line.startswith('## Other Parameters (not in Tax-Brain webapp'):
|
629
|
+
sdict[''] = {}
|
630
|
+
sdict[''][''] = 0
|
631
|
+
continue
|
632
|
+
sec2line = line.startswith('### ')
|
633
|
+
sec1line = line.startswith('## ')
|
634
|
+
# Create outer-layer dictionary entry for sec1.
|
635
|
+
if sec1line:
|
636
|
+
sec1 = line.replace('##', '', 1).strip()
|
637
|
+
sdict[sec1] = {}
|
638
|
+
# Create inner dictionary entry for sec1-sec2.
|
639
|
+
# Note that sec1 will have been defined from a previous loop.
|
640
|
+
if sec2line:
|
641
|
+
sec2 = line.replace('###', '', 1).strip()
|
642
|
+
sdict[sec1][sec2] = 0
|
643
|
+
return sdict
|
644
|
+
# begin main logic of test_section_titles
|
645
|
+
# specify expected section titles ordered as on the Tax-Brain webapp
|
646
|
+
ided_ceiling_pct = ('Ceiling On The Benefit Of Itemized Deductions '
|
647
|
+
'As A Percent Of Deductible Expenses')
|
648
|
+
cgqd_tax_same = ('Tax All Capital Gains And Dividends The Same '
|
649
|
+
'As Regular Taxable Income')
|
650
|
+
# pylint: disable=bad-continuation
|
651
|
+
valid_dict = {
|
652
|
+
'': { # empty section_1 implies parameter not displayed in Tax-Brain
|
653
|
+
'': 0
|
654
|
+
},
|
655
|
+
'Parameter Indexing': {
|
656
|
+
'Offsets': 0
|
657
|
+
},
|
658
|
+
'Payroll Taxes': {
|
659
|
+
'Social Security FICA': 0,
|
660
|
+
'Medicare FICA': 0,
|
661
|
+
'Additional Medicare FICA': 0
|
662
|
+
},
|
663
|
+
'Social Security Taxability': {
|
664
|
+
'Social Security Benefit Taxability': 0,
|
665
|
+
},
|
666
|
+
'Above The Line Deductions': {
|
667
|
+
'Misc. Adjustment Haircuts': 0,
|
668
|
+
'Misc. Exclusions': 0,
|
669
|
+
'Child And Elderly Care': 0
|
670
|
+
},
|
671
|
+
'Personal Exemptions': {
|
672
|
+
'Personal And Dependent Exemption Amount': 0,
|
673
|
+
# 'Personal Exemption Phaseout Starting Income': 0,
|
674
|
+
'Personal Exemption Phaseout Rate': 0,
|
675
|
+
'Repeal for Dependents Under Age 18': 0
|
676
|
+
},
|
677
|
+
'Standard Deduction': {
|
678
|
+
'Standard Deduction Amount': 0,
|
679
|
+
'Additional Standard Deduction For Blind And Aged': 0
|
680
|
+
# 'Standard Deduction For Dependents': 0
|
681
|
+
},
|
682
|
+
'Nonrefundable Credits': {
|
683
|
+
'Misc. Credit Limits': 0,
|
684
|
+
'Child And Dependent Care': 0,
|
685
|
+
'Personal Nonrefundable Credit': 0
|
686
|
+
},
|
687
|
+
'Child/Dependent Credits': {
|
688
|
+
'Child Tax Credit': 0,
|
689
|
+
'Additional Child Tax Credit': 0,
|
690
|
+
'Other Dependent Tax Credit': 0
|
691
|
+
},
|
692
|
+
'Itemized Deductions': {
|
693
|
+
'Medical Expenses': 0,
|
694
|
+
'State And Local Income And Sales Taxes': 0,
|
695
|
+
'State, Local, And Foreign Real Estate Taxes': 0,
|
696
|
+
'State And Local Taxes And Real Estate Taxes': 0,
|
697
|
+
'Interest Paid': 0,
|
698
|
+
'Charity': 0,
|
699
|
+
'Casualty': 0,
|
700
|
+
'Miscellaneous': 0,
|
701
|
+
'Itemized Deduction Limitation': 0,
|
702
|
+
'Surtax On Itemized Deduction Benefits Above An AGI Threshold': 0,
|
703
|
+
ided_ceiling_pct: 0,
|
704
|
+
'Ceiling On The Amount Of Itemized Deductions Allowed': 0
|
705
|
+
},
|
706
|
+
'Capital Gains And Dividends': {
|
707
|
+
'Regular - Long Term Capital Gains And Qualified Dividends': 0,
|
708
|
+
'AMT - Long Term Capital Gains And Qualified Dividends': 0,
|
709
|
+
cgqd_tax_same: 0
|
710
|
+
},
|
711
|
+
'Personal Income': {
|
712
|
+
'Regular: Non-AMT, Non-Pass-Through': 0,
|
713
|
+
'Pass-Through': 0,
|
714
|
+
'Alternative Minimum Tax': 0
|
715
|
+
},
|
716
|
+
'Other Taxes': {
|
717
|
+
'Net Investment Income Tax': 0
|
718
|
+
},
|
719
|
+
'Refundable Credits': {
|
720
|
+
'Earned Income Tax Credit': 0,
|
721
|
+
'New Refundable Child Tax Credit': 0,
|
722
|
+
'Personal Refundable Credit': 0,
|
723
|
+
'Refundable Payroll Tax Credit': 0
|
724
|
+
},
|
725
|
+
'Surtaxes': {
|
726
|
+
'New Minimum Tax': 0,
|
727
|
+
'New AGI Surtax': 0,
|
728
|
+
'Lump-Sum Tax': 0
|
729
|
+
},
|
730
|
+
'Universal Basic Income': {
|
731
|
+
'UBI Benefits': 0,
|
732
|
+
'UBI Taxability': 0
|
733
|
+
},
|
734
|
+
'Benefits': {
|
735
|
+
'Benefit Repeal': 0,
|
736
|
+
}
|
737
|
+
}
|
738
|
+
# check validity of parameter section titles in policy_current_law.json
|
739
|
+
path = os.path.join(tests_path, '..', 'policy_current_law.json')
|
740
|
+
with open(path, 'r') as clpfile:
|
741
|
+
clpdict = json.load(clpfile)
|
742
|
+
clpdict.pop("schema", None)
|
743
|
+
# ... make sure ever clpdict section title is in valid_dict
|
744
|
+
clp_dict = dict() # dictionary of clp section titles structured like valid
|
745
|
+
for pname in clpdict:
|
746
|
+
param = clpdict[pname]
|
747
|
+
assert isinstance(param, dict)
|
748
|
+
sec1title = param['section_1']
|
749
|
+
assert sec1title in valid_dict
|
750
|
+
sec2title = param['section_2']
|
751
|
+
assert sec2title in valid_dict[sec1title]
|
752
|
+
if sec1title not in clp_dict:
|
753
|
+
clp_dict[sec1title] = {}
|
754
|
+
if sec2title not in clp_dict[sec1title]:
|
755
|
+
clp_dict[sec1title][sec2title] = 0
|
756
|
+
# ... make sure every valid_dict section title is in clpdict
|
757
|
+
for sec1title in valid_dict:
|
758
|
+
assert isinstance(valid_dict[sec1title], dict)
|
759
|
+
assert sec1title in clp_dict
|
760
|
+
for sec2title in valid_dict[sec1title]:
|
761
|
+
assert sec2title in clp_dict[sec1title]
|
762
|
+
# check validity of parameter section titles in docs/uguide.htmx skeleton
|
763
|
+
path = os.path.join(tests_path, '..', '..', 'docs', 'guide',
|
764
|
+
'policy_params.md')
|
765
|
+
with open(path, 'r') as md_file:
|
766
|
+
md_text = md_file.read()
|
767
|
+
md_dict = generate_section_dictionary(md_text)
|
768
|
+
# ... make sure every md_dict section title is in valid_dict
|
769
|
+
for sec1title in md_dict:
|
770
|
+
assert isinstance(md_dict[sec1title], dict)
|
771
|
+
assert sec1title in valid_dict
|
772
|
+
for sec2title in md_dict[sec1title]:
|
773
|
+
assert sec2title in valid_dict[sec1title]
|
774
|
+
# ... make sure every valid_dict section title is in md_dict
|
775
|
+
for sec1title in valid_dict:
|
776
|
+
assert isinstance(valid_dict[sec1title], dict)
|
777
|
+
assert sec1title in md_dict
|
778
|
+
for sec2title in valid_dict[sec1title]:
|
779
|
+
assert sec2title in md_dict[sec1title]
|
780
|
+
|
781
|
+
|
782
|
+
def test_description_punctuation(tests_path):
|
783
|
+
"""
|
784
|
+
Check that each description ends in a period.
|
785
|
+
"""
|
786
|
+
# read JSON file into a dictionary
|
787
|
+
path = os.path.join(tests_path, '..', 'policy_current_law.json')
|
788
|
+
with open(path, 'r') as jsonfile:
|
789
|
+
dct = json.load(jsonfile)
|
790
|
+
dct.pop("schema", None)
|
791
|
+
all_desc_ok = True
|
792
|
+
for param in dct.keys():
|
793
|
+
if not dct[param]['description'].endswith('.'):
|
794
|
+
all_desc_ok = False
|
795
|
+
print('param,description=',
|
796
|
+
str(param),
|
797
|
+
dct[param]['description'])
|
798
|
+
assert all_desc_ok
|
799
|
+
|
800
|
+
|
801
|
+
def test_get_index_rate():
|
802
|
+
"""
|
803
|
+
Test Parameters.get_index_rate.
|
804
|
+
"""
|
805
|
+
pol = Policy()
|
806
|
+
wgrates = pol.get_index_rate('SS_Earnings_c', 2017)
|
807
|
+
pirates = pol.get_index_rate('II_em', 2017)
|
808
|
+
assert isinstance(wgrates, np.float64)
|
809
|
+
assert wgrates == pol.wage_growth_rates(2017)
|
810
|
+
assert pirates == pol.inflation_rates(2017)
|
811
|
+
assert isinstance(pirates, np.float64)
|
812
|
+
assert pol.inflation_rates() == pol._inflation_rates
|
813
|
+
assert pol.wage_growth_rates() == pol._wage_growth_rates
|
814
|
+
|
815
|
+
|
816
|
+
def test_reform_with_bad_ctc_levels():
|
817
|
+
"""
|
818
|
+
Implement a reform with _ACTC > _CTC_c values.
|
819
|
+
"""
|
820
|
+
pol = Policy()
|
821
|
+
child_credit_reform = {
|
822
|
+
'CTC_c': {2020: 2200},
|
823
|
+
'ACTC_c': {2020: 2500}
|
824
|
+
}
|
825
|
+
with pytest.raises(pt.ValidationError):
|
826
|
+
pol.implement_reform(child_credit_reform)
|
827
|
+
|
828
|
+
|
829
|
+
def test_reform_with_removed_parameter(monkeypatch):
|
830
|
+
"""
|
831
|
+
Try to use removed parameter in a reform.
|
832
|
+
"""
|
833
|
+
policy1 = Policy()
|
834
|
+
reform1 = {'FilerCredit_c': {2020: 1000}}
|
835
|
+
with pytest.raises(pt.ValidationError):
|
836
|
+
policy1.implement_reform(reform1)
|
837
|
+
policy2 = Policy()
|
838
|
+
reform2 = {'FilerCredit_c-indexed': {2020: True}}
|
839
|
+
with pytest.raises(pt.ValidationError):
|
840
|
+
policy2.implement_reform(reform2)
|
841
|
+
|
842
|
+
redefined_msg = {"some_redefined": "some_redefined was redefined."}
|
843
|
+
monkeypatch.setattr(Policy, "REDEFINED_PARAMS", redefined_msg)
|
844
|
+
|
845
|
+
pol = Policy()
|
846
|
+
with pytest.raises(pt.ValidationError):
|
847
|
+
pol.implement_reform({"some_redefined": "hello world"})
|
848
|
+
|
849
|
+
|
850
|
+
def test_reform_with_out_of_range_error():
|
851
|
+
"""
|
852
|
+
Try to use out-of-range values versus other parameter values in a reform.
|
853
|
+
"""
|
854
|
+
pol = Policy()
|
855
|
+
reform = {'SS_thd85': {2020: [20000, 20000, 20000, 20000, 20000]}}
|
856
|
+
pol.implement_reform(reform, raise_errors=False)
|
857
|
+
assert pol.parameter_errors
|
858
|
+
|
859
|
+
|
860
|
+
def test_reform_with_warning():
|
861
|
+
"""
|
862
|
+
Try to use warned out-of-range parameter value in reform.
|
863
|
+
"""
|
864
|
+
exp_warnings = {
|
865
|
+
'ID_Medical_frt': [
|
866
|
+
'ID_Medical_frt[year=2020] 0.05 < min 0.075 '
|
867
|
+
]
|
868
|
+
}
|
869
|
+
pol = Policy()
|
870
|
+
reform = {'ID_Medical_frt': {2020: 0.05}}
|
871
|
+
|
872
|
+
pol.implement_reform(reform, print_warnings=True)
|
873
|
+
assert pol.warnings == exp_warnings
|
874
|
+
pol.set_state(year=2020)
|
875
|
+
assert pol.ID_Medical_frt == np.array([0.05])
|
876
|
+
|
877
|
+
pol.implement_reform(reform, print_warnings=False)
|
878
|
+
assert pol.warnings == {}
|
879
|
+
pol.set_state(year=2020)
|
880
|
+
assert pol.ID_Medical_frt == np.array([0.05])
|
881
|
+
|
882
|
+
|
883
|
+
def test_reform_with_scalar_vector_errors():
|
884
|
+
"""
|
885
|
+
Test catching scalar-vector confusion.
|
886
|
+
"""
|
887
|
+
policy1 = Policy()
|
888
|
+
reform1 = {'SS_thd85': {2020: 30000}}
|
889
|
+
with pytest.raises(pt.ValidationError):
|
890
|
+
policy1.implement_reform(reform1)
|
891
|
+
|
892
|
+
policy2 = Policy()
|
893
|
+
reform2 = {'ID_Medical_frt': {2020: [0.08]}}
|
894
|
+
with pytest.raises(pt.ValidationError):
|
895
|
+
policy2.implement_reform(reform2)
|
896
|
+
|
897
|
+
policy3 = Policy()
|
898
|
+
reform3 = {'ID_Medical_frt': [{"year": 2020, "value": [0.08]}]}
|
899
|
+
with pytest.raises(pt.ValidationError):
|
900
|
+
policy3.adjust(reform3)
|
901
|
+
|
902
|
+
# Check that error is thrown if there are extra elements in array.
|
903
|
+
policy4 = Policy()
|
904
|
+
ref4 = {"II_brk1": {2020: [9700, 19400, 9700, 13850, 19400, 19400]}}
|
905
|
+
with pytest.raises(pt.ValidationError):
|
906
|
+
policy4.implement_reform(ref4)
|
907
|
+
|
908
|
+
policy5 = Policy()
|
909
|
+
ref5 = {"II_rt1": {2029: [.2, .3]}}
|
910
|
+
with pytest.raises(pt.ValidationError):
|
911
|
+
policy5.implement_reform(ref5)
|
912
|
+
|
913
|
+
|
914
|
+
def test_index_offset_reform():
|
915
|
+
"""
|
916
|
+
Test a reform that includes both a change in parameter_indexing_CPI_offset
|
917
|
+
and a change in a variable's indexed status in the same year.
|
918
|
+
"""
|
919
|
+
# create policy0 to extract inflation rates before any
|
920
|
+
# parameter_indexing_CPI_offset
|
921
|
+
policy0 = Policy()
|
922
|
+
policy0.implement_reform({'parameter_indexing_CPI_offset': {2017: 0}})
|
923
|
+
cpiu_rates = policy0.inflation_rates()
|
924
|
+
|
925
|
+
reform1 = {'CTC_c-indexed': {2020: True}}
|
926
|
+
policy1 = Policy()
|
927
|
+
policy1.implement_reform(reform1)
|
928
|
+
offset = -0.005
|
929
|
+
reform2 = {'CTC_c-indexed': {2020: True},
|
930
|
+
'parameter_indexing_CPI_offset': {2020: offset}}
|
931
|
+
policy2 = Policy()
|
932
|
+
policy2.implement_reform(reform2) # caused T-C crash before PR#2364
|
933
|
+
# extract from policy1 and policy2 the parameter values of CTC_c
|
934
|
+
pvalue1 = dict()
|
935
|
+
pvalue2 = dict()
|
936
|
+
for cyr in [2019, 2020, 2021]:
|
937
|
+
policy1.set_year(cyr)
|
938
|
+
pvalue1[cyr] = policy1.CTC_c[0]
|
939
|
+
policy2.set_year(cyr)
|
940
|
+
pvalue2[cyr] = policy2.CTC_c[0]
|
941
|
+
# check that pvalue1 and pvalue2 dictionaries contain the expected values
|
942
|
+
assert pvalue2[2019] == pvalue1[2019]
|
943
|
+
assert pvalue2[2020] == pvalue1[2020]
|
944
|
+
assert pvalue2[2020] == pvalue2[2019]
|
945
|
+
# ... indexing of CTC_c begins shows up first in 2021 parameter values
|
946
|
+
assert pvalue1[2021] > pvalue1[2020]
|
947
|
+
assert pvalue2[2021] > pvalue2[2020]
|
948
|
+
# ... calculate expected pvalue2[2021] from offset and pvalue1 values
|
949
|
+
indexrate1 = pvalue1[2021] / pvalue1[2020] - 1.
|
950
|
+
syear = Policy.JSON_START_YEAR
|
951
|
+
expindexrate = cpiu_rates[2020 - syear] + offset
|
952
|
+
expvalue = round(pvalue2[2020] * (1. + expindexrate), 2)
|
953
|
+
# ... compare expected value with actual value of pvalue2 for 2021
|
954
|
+
assert np.allclose([expvalue], [pvalue2[2021]])
|
955
|
+
|
956
|
+
|
957
|
+
def test_cpi_offset_affect_on_prior_years():
|
958
|
+
"""
|
959
|
+
Test that parameter_indexing_CPI_offset does not have affect
|
960
|
+
on inflation rates in earlier years.
|
961
|
+
"""
|
962
|
+
reform1 = {'parameter_indexing_CPI_offset': {2022: 0}}
|
963
|
+
reform2 = {'parameter_indexing_CPI_offset': {2022: -0.005}}
|
964
|
+
p1 = Policy()
|
965
|
+
p2 = Policy()
|
966
|
+
p1.implement_reform(reform1)
|
967
|
+
p2.implement_reform(reform2)
|
968
|
+
|
969
|
+
start_year = p1.start_year
|
970
|
+
p1_rates = np.array(p1.inflation_rates())
|
971
|
+
p2_rates = np.array(p2.inflation_rates())
|
972
|
+
|
973
|
+
# Inflation rates prior to 2022 are the same.
|
974
|
+
np.testing.assert_allclose(
|
975
|
+
p1_rates[:2022 - start_year],
|
976
|
+
p2_rates[:2022 - start_year]
|
977
|
+
)
|
978
|
+
|
979
|
+
# Inflation rate in 2022 was updated.
|
980
|
+
np.testing.assert_allclose(
|
981
|
+
p1_rates[2022 - start_year],
|
982
|
+
p2_rates[2022 - start_year] - (-0.005)
|
983
|
+
)
|
984
|
+
|
985
|
+
|
986
|
+
def test_cpi_offset_on_reverting_params():
|
987
|
+
"""
|
988
|
+
Test that params that revert to their pre-TCJA values
|
989
|
+
in 2026 revert if a parameter_indexing_CPI_offset is specified.
|
990
|
+
"""
|
991
|
+
reform0 = {'parameter_indexing_CPI_offset': {2020: -0.001}}
|
992
|
+
reform1 = {'STD': {2017: [6350, 12700, 6350, 9350, 12700]},
|
993
|
+
'parameter_indexing_CPI_offset': {2020: -0.001}}
|
994
|
+
reform2 = {'STD': {2020: [10000, 20000, 10000, 10000, 20000]},
|
995
|
+
'parameter_indexing_CPI_offset': {2020: -0.001}}
|
996
|
+
|
997
|
+
p0 = Policy()
|
998
|
+
p1 = Policy()
|
999
|
+
p2 = Policy()
|
1000
|
+
p0.implement_reform(reform0)
|
1001
|
+
p1.implement_reform(reform1)
|
1002
|
+
p2.implement_reform(reform2)
|
1003
|
+
|
1004
|
+
ryear = 2026
|
1005
|
+
syear = Policy.JSON_START_YEAR
|
1006
|
+
|
1007
|
+
# STD was reverted in 2026
|
1008
|
+
# atol=0.5 because ppp.py rounds params to nearest int
|
1009
|
+
assert np.allclose(
|
1010
|
+
p0._STD[ryear - syear],
|
1011
|
+
p1._STD[ryear - syear], atol=0.5)
|
1012
|
+
|
1013
|
+
# STD was not reverted in 2026 if included in revision
|
1014
|
+
assert not np.allclose(
|
1015
|
+
p1._STD[ryear - syear],
|
1016
|
+
p2._STD[ryear - syear], atol=0.5)
|
1017
|
+
|
1018
|
+
|
1019
|
+
def test_raise_errors_regression():
|
1020
|
+
"""
|
1021
|
+
This tests that raise_errors prevents the error from being thrown. The
|
1022
|
+
correct behavior is to exit the `adjust` function and store the errors.
|
1023
|
+
"""
|
1024
|
+
ref = {
|
1025
|
+
"II_brk7-indexed": [{"value": True}],
|
1026
|
+
"II_brk6": [{"value": 316700, "MARS": "single", "year": 2020}],
|
1027
|
+
"II_brk7": [{"value": 445400, "MARS": "single", "year": 2020}],
|
1028
|
+
|
1029
|
+
}
|
1030
|
+
pol = Policy()
|
1031
|
+
pol.adjust(ref, raise_errors=False)
|
1032
|
+
assert pol.errors
|
1033
|
+
|
1034
|
+
|
1035
|
+
class TestAdjust:
|
1036
|
+
"""
|
1037
|
+
Test update and indexing rules as defined in the Parameters docstring.
|
1038
|
+
|
1039
|
+
Each test implements a Tax-Calculator style reform and a pt styled
|
1040
|
+
reform, checks that the updated values are equal, and then, tests that
|
1041
|
+
values were extended and indexed (or not indexed) correctly.
|
1042
|
+
"""
|
1043
|
+
|
1044
|
+
def test_simple_adj(self):
|
1045
|
+
"""
|
1046
|
+
Test updating a 2D parameter that is indexed to inflation.
|
1047
|
+
"""
|
1048
|
+
pol1 = Policy()
|
1049
|
+
pol1.implement_reform(
|
1050
|
+
{
|
1051
|
+
"EITC_c": {
|
1052
|
+
2020: [10000, 10001, 10002, 10003],
|
1053
|
+
2023: [20000, 20001, 20002, 20003],
|
1054
|
+
}
|
1055
|
+
}
|
1056
|
+
)
|
1057
|
+
|
1058
|
+
pol2 = Policy()
|
1059
|
+
pol2.adjust(
|
1060
|
+
{
|
1061
|
+
"EITC_c": [
|
1062
|
+
{"year": 2020, "EIC": "0kids", "value": 10000},
|
1063
|
+
{"year": 2020, "EIC": "1kid", "value": 10001},
|
1064
|
+
{"year": 2020, "EIC": "2kids", "value": 10002},
|
1065
|
+
{"year": 2020, "EIC": "3+kids", "value": 10003},
|
1066
|
+
{"year": 2023, "EIC": "0kids", "value": 20000},
|
1067
|
+
{"year": 2023, "EIC": "1kid", "value": 20001},
|
1068
|
+
{"year": 2023, "EIC": "2kids", "value": 20002},
|
1069
|
+
{"year": 2023, "EIC": "3+kids", "value": 20003},
|
1070
|
+
]
|
1071
|
+
}
|
1072
|
+
)
|
1073
|
+
cmp_policy_objs(pol1, pol2)
|
1074
|
+
|
1075
|
+
pol0 = Policy()
|
1076
|
+
pol0.set_year(2019)
|
1077
|
+
pol2.set_year(2019)
|
1078
|
+
|
1079
|
+
assert np.allclose(pol0.EITC_c, pol2.EITC_c)
|
1080
|
+
|
1081
|
+
pol2.set_state(year=[2020, 2021, 2022, 2023, 2024])
|
1082
|
+
val2020 = np.array([[10000, 10001, 10002, 10003]])
|
1083
|
+
val2023 = np.array([[20000, 20001, 20002, 20003]])
|
1084
|
+
|
1085
|
+
exp = np.vstack([
|
1086
|
+
val2020,
|
1087
|
+
val2020 * (1 + pol2.inflation_rates(year=2020)),
|
1088
|
+
(
|
1089
|
+
val2020 * (1 + pol2.inflation_rates(year=2020))
|
1090
|
+
).round(2) * (1 + pol2.inflation_rates(year=2021)),
|
1091
|
+
val2023,
|
1092
|
+
val2023 * (1 + pol2.inflation_rates(year=2023)),
|
1093
|
+
]).round(2)
|
1094
|
+
np.testing.assert_allclose(pol2.EITC_c, exp)
|
1095
|
+
|
1096
|
+
def test_adj_without_index_1(self):
|
1097
|
+
"""
|
1098
|
+
Test update indexed parameter after turning off its
|
1099
|
+
indexed status.
|
1100
|
+
"""
|
1101
|
+
pol1 = Policy()
|
1102
|
+
pol1.implement_reform(
|
1103
|
+
{
|
1104
|
+
"EITC_c": {
|
1105
|
+
2020: [10000, 10001, 10002, 10003],
|
1106
|
+
2023: [20000, 20001, 20002, 20003],
|
1107
|
+
},
|
1108
|
+
"EITC_c-indexed": {2019: False},
|
1109
|
+
}
|
1110
|
+
)
|
1111
|
+
|
1112
|
+
pol2 = Policy()
|
1113
|
+
pol2.adjust(
|
1114
|
+
{
|
1115
|
+
"EITC_c": [
|
1116
|
+
{"year": 2020, "EIC": "0kids", "value": 10000},
|
1117
|
+
{"year": 2020, "EIC": "1kid", "value": 10001},
|
1118
|
+
{"year": 2020, "EIC": "2kids", "value": 10002},
|
1119
|
+
{"year": 2020, "EIC": "3+kids", "value": 10003},
|
1120
|
+
{"year": 2023, "EIC": "0kids", "value": 20000},
|
1121
|
+
{"year": 2023, "EIC": "1kid", "value": 20001},
|
1122
|
+
{"year": 2023, "EIC": "2kids", "value": 20002},
|
1123
|
+
{"year": 2023, "EIC": "3+kids", "value": 20003},
|
1124
|
+
],
|
1125
|
+
"EITC_c-indexed": [{"year": 2019, "value": False}],
|
1126
|
+
}
|
1127
|
+
)
|
1128
|
+
cmp_policy_objs(pol1, pol2)
|
1129
|
+
|
1130
|
+
pol0 = Policy()
|
1131
|
+
pol0.set_year(2019)
|
1132
|
+
pol2.set_year(2019)
|
1133
|
+
|
1134
|
+
assert np.allclose(pol0.EITC_c, pol2.EITC_c)
|
1135
|
+
|
1136
|
+
pol2.set_state(year=[2020, 2021, 2022, 2023, 2024])
|
1137
|
+
|
1138
|
+
val2020 = np.array([[10000, 10001, 10002, 10003]])
|
1139
|
+
val2023 = np.array([[20000, 20001, 20002, 20003]])
|
1140
|
+
|
1141
|
+
exp = np.vstack([
|
1142
|
+
val2020,
|
1143
|
+
val2020,
|
1144
|
+
val2020,
|
1145
|
+
val2023,
|
1146
|
+
val2023,
|
1147
|
+
]).round(2)
|
1148
|
+
np.testing.assert_allclose(pol2.EITC_c, exp)
|
1149
|
+
|
1150
|
+
def test_adj_without_index_2(self):
|
1151
|
+
"""
|
1152
|
+
Test updating an indexed parameter, making it unindexed,
|
1153
|
+
and then adjusting it again.
|
1154
|
+
"""
|
1155
|
+
pol1 = Policy()
|
1156
|
+
pol1.implement_reform(
|
1157
|
+
{
|
1158
|
+
"EITC_c": {
|
1159
|
+
2020: [10000, 10001, 10002, 10003],
|
1160
|
+
2023: [20000, 20001, 20002, 20003],
|
1161
|
+
},
|
1162
|
+
"EITC_c-indexed": {2022: False},
|
1163
|
+
}
|
1164
|
+
)
|
1165
|
+
|
1166
|
+
pol2 = Policy()
|
1167
|
+
pol2.adjust(
|
1168
|
+
{
|
1169
|
+
"EITC_c": [
|
1170
|
+
{"year": 2020, "EIC": "0kids", "value": 10000},
|
1171
|
+
{"year": 2020, "EIC": "1kid", "value": 10001},
|
1172
|
+
{"year": 2020, "EIC": "2kids", "value": 10002},
|
1173
|
+
{"year": 2020, "EIC": "3+kids", "value": 10003},
|
1174
|
+
{"year": 2023, "EIC": "0kids", "value": 20000},
|
1175
|
+
{"year": 2023, "EIC": "1kid", "value": 20001},
|
1176
|
+
{"year": 2023, "EIC": "2kids", "value": 20002},
|
1177
|
+
{"year": 2023, "EIC": "3+kids", "value": 20003},
|
1178
|
+
],
|
1179
|
+
"EITC_c-indexed": [{"year": 2022, "value": False}],
|
1180
|
+
}
|
1181
|
+
)
|
1182
|
+
cmp_policy_objs(pol1, pol2)
|
1183
|
+
|
1184
|
+
pol0 = Policy()
|
1185
|
+
pol0.set_year(2019)
|
1186
|
+
pol2.set_year(2019)
|
1187
|
+
|
1188
|
+
assert np.allclose(pol0.EITC_c, pol2.EITC_c)
|
1189
|
+
|
1190
|
+
pol2.set_state(year=[2020, 2021, 2022, 2023, 2024])
|
1191
|
+
|
1192
|
+
val2020 = np.array([[10000, 10001, 10002, 10003]])
|
1193
|
+
val2023 = np.array([[20000, 20001, 20002, 20003]])
|
1194
|
+
|
1195
|
+
exp = np.vstack([
|
1196
|
+
val2020,
|
1197
|
+
val2020 * (1 + pol2.inflation_rates(year=2020)),
|
1198
|
+
(
|
1199
|
+
val2020 * (1 + pol2.inflation_rates(year=2020))
|
1200
|
+
).round(2) * (1 + pol2.inflation_rates(year=2021)),
|
1201
|
+
val2023,
|
1202
|
+
val2023,
|
1203
|
+
]).round(2)
|
1204
|
+
np.testing.assert_allclose(pol2.EITC_c, exp)
|
1205
|
+
|
1206
|
+
def test_activate_index(self):
|
1207
|
+
"""
|
1208
|
+
Test changing a non-indexed parameter to an indexed parameter.
|
1209
|
+
"""
|
1210
|
+
pol1 = Policy()
|
1211
|
+
pol1.implement_reform({
|
1212
|
+
"CTC_c": {2022: 2000},
|
1213
|
+
"CTC_c-indexed": {2022: True}
|
1214
|
+
})
|
1215
|
+
|
1216
|
+
pol2 = Policy()
|
1217
|
+
pol2.adjust(
|
1218
|
+
{
|
1219
|
+
"CTC_c": [{"year": 2022, "value": 2000}],
|
1220
|
+
"CTC_c-indexed": [{"year": 2022, "value": True}],
|
1221
|
+
}
|
1222
|
+
)
|
1223
|
+
cmp_policy_objs(pol1, pol2)
|
1224
|
+
|
1225
|
+
pol0 = Policy()
|
1226
|
+
pol0.set_year(year=2021)
|
1227
|
+
pol2.set_state(year=[2021, 2022, 2023])
|
1228
|
+
exp = np.array([
|
1229
|
+
pol0.CTC_c[0],
|
1230
|
+
2000,
|
1231
|
+
2000 * (1 + pol2.inflation_rates(year=2022))
|
1232
|
+
]).round(2)
|
1233
|
+
|
1234
|
+
np.testing.assert_allclose(pol2.CTC_c, exp)
|
1235
|
+
|
1236
|
+
def test_apply_cpi_offset(self):
|
1237
|
+
"""
|
1238
|
+
Test applying the parameter_indexing_CPI_offset parameter
|
1239
|
+
without any other parameters.
|
1240
|
+
"""
|
1241
|
+
pol1 = Policy()
|
1242
|
+
pol1.implement_reform(
|
1243
|
+
{"parameter_indexing_CPI_offset": {2021: -0.001}}
|
1244
|
+
)
|
1245
|
+
|
1246
|
+
pol2 = Policy()
|
1247
|
+
pol2.adjust(
|
1248
|
+
{"parameter_indexing_CPI_offset": [
|
1249
|
+
{"year": 2021, "value": -0.001}
|
1250
|
+
]}
|
1251
|
+
)
|
1252
|
+
|
1253
|
+
cmp_policy_objs(pol1, pol2)
|
1254
|
+
|
1255
|
+
pol0 = Policy()
|
1256
|
+
pol0.implement_reform({"parameter_indexing_CPI_offset": {2021: 0}})
|
1257
|
+
|
1258
|
+
init_rates = pol0.inflation_rates()
|
1259
|
+
new_rates = pol2.inflation_rates()
|
1260
|
+
|
1261
|
+
start_ix = 2021 - pol2.start_year
|
1262
|
+
|
1263
|
+
exp_rates = copy.deepcopy(new_rates)
|
1264
|
+
exp_rates[start_ix:] -= pol2._parameter_indexing_CPI_offset[start_ix:]
|
1265
|
+
np.testing.assert_allclose(init_rates, exp_rates)
|
1266
|
+
|
1267
|
+
# make sure values prior to 2021 were not affected.
|
1268
|
+
cmp_policy_objs(pol0, pol2, year_range=range(pol2.start_year, 2021))
|
1269
|
+
|
1270
|
+
pol2.set_state(year=[2022, 2023])
|
1271
|
+
np.testing.assert_equal(
|
1272
|
+
(pol2.EITC_c[1] / pol2.EITC_c[0] - 1).round(4),
|
1273
|
+
(pol0.inflation_rates(year=2022) + (-0.001)).round(4),
|
1274
|
+
)
|
1275
|
+
|
1276
|
+
def test_multiple_cpi_swaps(self):
|
1277
|
+
"""
|
1278
|
+
Test changing a parameter's indexed status multiple times.
|
1279
|
+
"""
|
1280
|
+
pol1 = Policy()
|
1281
|
+
pol1.implement_reform(
|
1282
|
+
{
|
1283
|
+
"II_em": {2016: 6000, 2018: 7500, 2020: 9000},
|
1284
|
+
"II_em-indexed": {2016: False, 2018: True},
|
1285
|
+
}
|
1286
|
+
)
|
1287
|
+
|
1288
|
+
pol2 = Policy()
|
1289
|
+
pol2.adjust(
|
1290
|
+
{
|
1291
|
+
"II_em": [
|
1292
|
+
{"year": 2016, "value": 6000},
|
1293
|
+
{"year": 2018, "value": 7500},
|
1294
|
+
{"year": 2020, "value": 9000},
|
1295
|
+
],
|
1296
|
+
"II_em-indexed": [
|
1297
|
+
{"year": 2016, "value": False},
|
1298
|
+
{"year": 2018, "value": True},
|
1299
|
+
],
|
1300
|
+
}
|
1301
|
+
)
|
1302
|
+
|
1303
|
+
cmp_policy_objs(pol1, pol2)
|
1304
|
+
|
1305
|
+
# check inflation is not applied.
|
1306
|
+
pol2.set_state(year=[2016, 2017])
|
1307
|
+
np.testing.assert_equal(
|
1308
|
+
pol2.II_em[0], pol2.II_em[1]
|
1309
|
+
)
|
1310
|
+
|
1311
|
+
# check inflation rate is applied.
|
1312
|
+
pol2.set_state(year=[2018, 2019])
|
1313
|
+
np.testing.assert_equal(
|
1314
|
+
(pol2.II_em[1] / pol2.II_em[0] - 1).round(4),
|
1315
|
+
pol2.inflation_rates(year=2018),
|
1316
|
+
)
|
1317
|
+
|
1318
|
+
# check inflation rate applied for rest of window.
|
1319
|
+
window = list(range(2020, pol2.end_year + 1))
|
1320
|
+
pol2.set_state(year=window)
|
1321
|
+
np.testing.assert_equal(
|
1322
|
+
(pol2.II_em[1:] / pol2.II_em[:-1] - 1).round(4),
|
1323
|
+
[pol2.inflation_rates(year=year) for year in window[:-1]],
|
1324
|
+
)
|
1325
|
+
|
1326
|
+
def test_multiple_cpi_swaps2(self):
|
1327
|
+
"""
|
1328
|
+
Test changing the indexed status of multiple parameters multiple
|
1329
|
+
times.
|
1330
|
+
"""
|
1331
|
+
pol1 = Policy()
|
1332
|
+
pol1.implement_reform(
|
1333
|
+
{
|
1334
|
+
"II_em": {2016: 6000, 2018: 7500, 2020: 9000},
|
1335
|
+
"II_em-indexed": {2016: False, 2018: True},
|
1336
|
+
"SS_Earnings_c": {2016: 300000, 2018: 500000},
|
1337
|
+
"SS_Earnings_c-indexed": {2017: False, 2019: True},
|
1338
|
+
"AMT_em-indexed": {2017: False, 2020: True},
|
1339
|
+
}
|
1340
|
+
)
|
1341
|
+
|
1342
|
+
pol2 = Policy()
|
1343
|
+
pol2.adjust(
|
1344
|
+
{
|
1345
|
+
"SS_Earnings_c": [
|
1346
|
+
{"year": 2016, "value": 300000},
|
1347
|
+
{"year": 2018, "value": 500000},
|
1348
|
+
],
|
1349
|
+
"SS_Earnings_c-indexed": [
|
1350
|
+
{"year": 2017, "value": False},
|
1351
|
+
{"year": 2019, "value": True},
|
1352
|
+
],
|
1353
|
+
"AMT_em-indexed": [
|
1354
|
+
{"year": 2017, "value": False},
|
1355
|
+
{"year": 2020, "value": True},
|
1356
|
+
],
|
1357
|
+
"II_em": [
|
1358
|
+
{"year": 2016, "value": 6000},
|
1359
|
+
{"year": 2018, "value": 7500},
|
1360
|
+
{"year": 2020, "value": 9000},
|
1361
|
+
],
|
1362
|
+
"II_em-indexed": [
|
1363
|
+
{"year": 2016, "value": False},
|
1364
|
+
{"year": 2018, "value": True},
|
1365
|
+
],
|
1366
|
+
}
|
1367
|
+
)
|
1368
|
+
|
1369
|
+
cmp_policy_objs(pol1, pol2)
|
1370
|
+
|
1371
|
+
# Test SS_Earnings_c
|
1372
|
+
# check inflation is still applied from 2016 to 2017.
|
1373
|
+
pol2.set_state(year=[2016, 2017])
|
1374
|
+
np.testing.assert_equal(
|
1375
|
+
(pol2.SS_Earnings_c[1] / pol2.SS_Earnings_c[0] - 1).round(4),
|
1376
|
+
pol2.wage_growth_rates(year=2016),
|
1377
|
+
)
|
1378
|
+
|
1379
|
+
# check inflation rate is not applied after adjustment in 2018.
|
1380
|
+
pol2.set_state(year=[2018, 2019])
|
1381
|
+
np.testing.assert_equal(
|
1382
|
+
pol2.SS_Earnings_c[0], pol2.SS_Earnings_c[1]
|
1383
|
+
)
|
1384
|
+
|
1385
|
+
# check inflation rate applied for rest of window.
|
1386
|
+
window = list(range(2019, pol2.end_year + 1))
|
1387
|
+
pol2.set_state(year=window)
|
1388
|
+
np.testing.assert_equal(
|
1389
|
+
(pol2.SS_Earnings_c[1:] / pol2.SS_Earnings_c[:-1] - 1).round(4),
|
1390
|
+
[pol2.wage_growth_rates(year=year) for year in window[:-1]],
|
1391
|
+
)
|
1392
|
+
|
1393
|
+
# Test AMT
|
1394
|
+
# Check values for 2017 through 2020 are equal.
|
1395
|
+
pol2.set_state(year=[2017, 2018, 2019, 2020])
|
1396
|
+
for i in (1, 2, 3):
|
1397
|
+
np.testing.assert_equal(
|
1398
|
+
pol2.AMT_em[0], pol2.AMT_em[i]
|
1399
|
+
)
|
1400
|
+
|
1401
|
+
# check inflation rate applied for rest of window.
|
1402
|
+
window = list(range(2020, pol2.end_year + 1))
|
1403
|
+
pol2.set_state(year=window)
|
1404
|
+
# repeat inflation rates accross matrix so they can be compared to the
|
1405
|
+
# rates derived from AMT_em, a 5 * N matrix.
|
1406
|
+
exp_rates = [pol2.inflation_rates(year=year) for year in window[:-1]]
|
1407
|
+
exp_rates = np.tile([exp_rates], (5, 1)).transpose()
|
1408
|
+
np.testing.assert_equal(
|
1409
|
+
(pol2.AMT_em[1:] / pol2.AMT_em[:-1] - 1).round(4),
|
1410
|
+
exp_rates,
|
1411
|
+
)
|
1412
|
+
|
1413
|
+
# Test II_em
|
1414
|
+
# check inflation is not applied.
|
1415
|
+
pol2.set_state(year=[2016, 2017])
|
1416
|
+
np.testing.assert_equal(
|
1417
|
+
pol2.II_em[0], pol2.II_em[1]
|
1418
|
+
)
|
1419
|
+
|
1420
|
+
# check inflation rate is applied.
|
1421
|
+
pol2.set_state(year=[2018, 2019])
|
1422
|
+
np.testing.assert_equal(
|
1423
|
+
(pol2.II_em[1] / pol2.II_em[0] - 1).round(4),
|
1424
|
+
pol2.inflation_rates(year=2018),
|
1425
|
+
)
|
1426
|
+
|
1427
|
+
# check inflation rate applied for rest of window.
|
1428
|
+
window = list(range(2020, pol2.end_year + 1))
|
1429
|
+
pol2.set_state(year=window)
|
1430
|
+
np.testing.assert_equal(
|
1431
|
+
(pol2.II_em[1:] / pol2.II_em[:-1] - 1).round(4),
|
1432
|
+
[pol2.inflation_rates(year=year) for year in window[:-1]],
|
1433
|
+
)
|
1434
|
+
|
1435
|
+
def test_adj_CPI_offset_and_index_status(self):
|
1436
|
+
"""
|
1437
|
+
Test changing parameter_indexing_CPI_offset and another
|
1438
|
+
parameter simultaneously.
|
1439
|
+
"""
|
1440
|
+
pol1 = Policy()
|
1441
|
+
pol1.implement_reform({
|
1442
|
+
"CTC_c-indexed": {2020: True},
|
1443
|
+
"parameter_indexing_CPI_offset": {2020: -0.005}},
|
1444
|
+
)
|
1445
|
+
|
1446
|
+
pol2 = Policy()
|
1447
|
+
pol2.adjust(
|
1448
|
+
{
|
1449
|
+
"parameter_indexing_CPI_offset":
|
1450
|
+
[{"year": 2020, "value": -0.005}],
|
1451
|
+
"CTC_c-indexed": [{"year": 2020, "value": True}],
|
1452
|
+
}
|
1453
|
+
)
|
1454
|
+
|
1455
|
+
cmp_policy_objs(pol1, pol2)
|
1456
|
+
|
1457
|
+
# Check no difference prior to 2020
|
1458
|
+
pol0 = Policy()
|
1459
|
+
pol0.implement_reform({"parameter_indexing_CPI_offset": {2020: 0}})
|
1460
|
+
cmp_policy_objs(
|
1461
|
+
pol0,
|
1462
|
+
pol2,
|
1463
|
+
year_range=range(pol2.start_year, 2020 + 1),
|
1464
|
+
exclude=["parameter_indexing_CPI_offset"]
|
1465
|
+
)
|
1466
|
+
|
1467
|
+
pol2.set_state(year=[2021, 2022])
|
1468
|
+
np.testing.assert_equal(
|
1469
|
+
(pol2.CTC_c[1] / pol2.CTC_c[0] - 1).round(4),
|
1470
|
+
round(pol0.inflation_rates(year=2021) + (-0.005), 4),
|
1471
|
+
)
|
1472
|
+
|
1473
|
+
def test_adj_related_parameters_and_index_status(self):
|
1474
|
+
"""
|
1475
|
+
Test changing two related parameters simulataneously and
|
1476
|
+
one of their indexed statuses.
|
1477
|
+
"""
|
1478
|
+
|
1479
|
+
pol = Policy()
|
1480
|
+
pol.adjust(
|
1481
|
+
{
|
1482
|
+
"II_brk7-indexed": [{"year": 2020, "value": True}],
|
1483
|
+
# Update II_brk5 in 2026 to make reform valid after reset.
|
1484
|
+
"II_brk5": [{"value": 330000, "MARS": "single", "year": 2026}],
|
1485
|
+
"II_brk6": [{"value": 316700, "MARS": "single", "year": 2020}],
|
1486
|
+
"II_brk7": [{"value": 445400, "MARS": "single", "year": 2020}],
|
1487
|
+
}
|
1488
|
+
)
|
1489
|
+
|
1490
|
+
# Check no difference prior to 2020
|
1491
|
+
pol0 = Policy()
|
1492
|
+
cmp_policy_objs(
|
1493
|
+
pol0,
|
1494
|
+
pol,
|
1495
|
+
year_range=range(pol.start_year, 2019 + 1),
|
1496
|
+
)
|
1497
|
+
|
1498
|
+
res = (
|
1499
|
+
(pol.sel["II_brk6"]["MARS"] == "single")
|
1500
|
+
& (pol.sel["II_brk6"]["year"] == 2020)
|
1501
|
+
)
|
1502
|
+
assert res.isel[0]["value"] == [316700]
|
1503
|
+
res = (
|
1504
|
+
(pol.sel["II_brk7"]["MARS"] == "single")
|
1505
|
+
& (pol.sel["II_brk7"]["year"] == 2020)
|
1506
|
+
)
|
1507
|
+
assert res.isel[0]["value"] == [445400]
|
1508
|
+
|
1509
|
+
II_brk7 = pol.to_array("II_brk7", year=[2021, 2022])
|
1510
|
+
II_brk7_single = II_brk7[:, 0]
|
1511
|
+
np.testing.assert_equal(
|
1512
|
+
(II_brk7_single[1] / II_brk7_single[0] - 1).round(4),
|
1513
|
+
pol.inflation_rates(year=2021),
|
1514
|
+
)
|
1515
|
+
|
1516
|
+
def test_indexed_status_parsing(self):
|
1517
|
+
pol1 = Policy()
|
1518
|
+
|
1519
|
+
pol1.implement_reform({"EITC_c-indexed": {pol1.start_year: False}})
|
1520
|
+
|
1521
|
+
pol2 = Policy()
|
1522
|
+
pol2.adjust({"EITC_c-indexed": False})
|
1523
|
+
|
1524
|
+
cmp_policy_objs(pol1, pol2)
|
1525
|
+
|
1526
|
+
with pytest.raises(pt.ValidationError):
|
1527
|
+
pol2.adjust({"EITC_c-indexed": 123})
|
1528
|
+
|
1529
|
+
def test_cpi_offset_does_not_affect_wage_indexed_params(self):
|
1530
|
+
"""
|
1531
|
+
Test adjusting parameter_indexing_CPI_offset does not affect unknown
|
1532
|
+
values of wage indexed parameters like SS_Earnings_c.
|
1533
|
+
"""
|
1534
|
+
base_reform = {
|
1535
|
+
"parameter_indexing_CPI_offset": {2021: -0.001},
|
1536
|
+
"SS_Earnings_c": {2024: 300000},
|
1537
|
+
}
|
1538
|
+
|
1539
|
+
pol0 = Policy()
|
1540
|
+
pol0.implement_reform(base_reform)
|
1541
|
+
|
1542
|
+
pol1 = Policy()
|
1543
|
+
pol1.implement_reform(base_reform)
|
1544
|
+
pol1.implement_reform(dict(base_reform, SS_Earnings_c={2025: 500000}))
|
1545
|
+
|
1546
|
+
exp_before_2025 = pol0.to_array(
|
1547
|
+
"SS_Earnings_c", year=list(range(2021, 2024 + 1))
|
1548
|
+
)
|
1549
|
+
act_before_2025 = pol1.to_array(
|
1550
|
+
"SS_Earnings_c", year=list(range(2021, 2024 + 1))
|
1551
|
+
)
|
1552
|
+
|
1553
|
+
np.testing.assert_equal(act_before_2025, exp_before_2025)
|
1554
|
+
|
1555
|
+
|
1556
|
+
def test_two_sets_of_tax_brackets():
|
1557
|
+
"""
|
1558
|
+
Test that II_brk? and PT_brk? values are the same under current law.
|
1559
|
+
"""
|
1560
|
+
pol = Policy()
|
1561
|
+
brackets = range(1, 7+1)
|
1562
|
+
years = range(Policy.JSON_START_YEAR, Policy.LAST_KNOWN_YEAR+1)
|
1563
|
+
emsg = ''
|
1564
|
+
for year in years:
|
1565
|
+
pol.set_year(year)
|
1566
|
+
pdata = dict(pol.items())
|
1567
|
+
for bnum in brackets:
|
1568
|
+
ii_val = pdata[f'II_brk{bnum}']
|
1569
|
+
pt_val = pdata[f'PT_brk{bnum}']
|
1570
|
+
if not np.allclose(ii_val, pt_val):
|
1571
|
+
emsg += f'II_brk{bnum} != PT_brk{bnum} for year {year}\n'
|
1572
|
+
emsg += f' II_brk{bnum} is {ii_val}\n'
|
1573
|
+
emsg += f' PT_brk{bnum} is {pt_val}\n'
|
1574
|
+
if emsg:
|
1575
|
+
raise ValueError(emsg)
|