owlplanner 2025.2.8__tar.gz → 2025.2.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/PKG-INFO +1 -1
  2. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/owl.tex +1 -1
  3. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/examples/case_jack+jill.toml +2 -2
  4. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/examples/case_joe.toml +2 -2
  5. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/examples/case_john+sally.toml +2 -2
  6. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/examples/case_kim+sam-bequest.toml +2 -2
  7. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/examples/case_kim+sam-spending.toml +2 -2
  8. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/pyproject.toml +1 -1
  9. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/src/owlplanner/abcapi.py +15 -15
  10. owlplanner-2025.2.9/src/owlplanner/config.py +299 -0
  11. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/src/owlplanner/logging.py +13 -13
  12. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/src/owlplanner/plan.py +622 -619
  13. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/src/owlplanner/progress.py +2 -2
  14. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/src/owlplanner/rates.py +72 -72
  15. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/src/owlplanner/tax2025.py +3 -3
  16. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/src/owlplanner/timelists.py +31 -29
  17. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/src/owlplanner/utils.py +9 -9
  18. owlplanner-2025.2.9/src/owlplanner/version.py +1 -0
  19. owlplanner-2025.2.9/test.py +18 -0
  20. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/ui/About_Owl.py +5 -3
  21. owlplanner-2025.2.9/ui/Asset_Allocation.py +142 -0
  22. owlplanner-2025.2.9/ui/Assets.py +63 -0
  23. owlplanner-2025.2.9/ui/Create_Case.py +119 -0
  24. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/ui/Documentation.py +21 -13
  25. owlplanner-2025.2.9/ui/Fixed_Income.py +68 -0
  26. owlplanner-2025.2.9/ui/Graphs.py +35 -0
  27. owlplanner-2025.2.9/ui/Historical_Range.py +55 -0
  28. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/ui/Logs.py +3 -3
  29. owlplanner-2025.2.9/ui/Monte_Carlo.py +31 -0
  30. owlplanner-2025.2.9/ui/Optimization_Parameters.py +120 -0
  31. owlplanner-2025.2.9/ui/Output_Files.py +58 -0
  32. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/ui/Quick_Start.py +8 -6
  33. owlplanner-2025.2.9/ui/Rates_Selection.py +228 -0
  34. owlplanner-2025.2.9/ui/Settings.py +23 -0
  35. owlplanner-2025.2.9/ui/Wages_And_Contributions.py +67 -0
  36. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/ui/Worksheets.py +3 -3
  37. owlplanner-2025.2.9/ui/main.py +52 -0
  38. owlplanner-2025.2.9/ui/owlbridge.py +682 -0
  39. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/ui/plots.py +3 -3
  40. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/ui/progress.py +2 -2
  41. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/ui/requirements.txt +1 -1
  42. owlplanner-2025.2.9/ui/sskeys.py +480 -0
  43. owlplanner-2025.2.8/src/owlplanner/config.py +0 -294
  44. owlplanner-2025.2.8/src/owlplanner/version.py +0 -1
  45. owlplanner-2025.2.8/ui/Asset_Allocation.py +0 -135
  46. owlplanner-2025.2.8/ui/Assets.py +0 -58
  47. owlplanner-2025.2.8/ui/Create_Case.py +0 -94
  48. owlplanner-2025.2.8/ui/Fixed_Income.py +0 -60
  49. owlplanner-2025.2.8/ui/Graphs.py +0 -32
  50. owlplanner-2025.2.8/ui/Historical_Range.py +0 -43
  51. owlplanner-2025.2.8/ui/Monte_Carlo.py +0 -30
  52. owlplanner-2025.2.8/ui/Optimization_Parameters.py +0 -114
  53. owlplanner-2025.2.8/ui/Output_Files.py +0 -57
  54. owlplanner-2025.2.8/ui/Rates_Selection.py +0 -214
  55. owlplanner-2025.2.8/ui/Settings.py +0 -17
  56. owlplanner-2025.2.8/ui/Wages_And_Contributions.py +0 -57
  57. owlplanner-2025.2.8/ui/main.py +0 -44
  58. owlplanner-2025.2.8/ui/owlbridge.py +0 -677
  59. owlplanner-2025.2.8/ui/sskeys.py +0 -446
  60. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/.devcontainer/devcontainer.json +0 -0
  61. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/.flake8 +0 -0
  62. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/.github/workflows/github-actions-runtests.yml +0 -0
  63. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/.gitignore +0 -0
  64. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/INSTALL.md +0 -0
  65. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/LICENSE +0 -0
  66. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/README.md +0 -0
  67. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/AD-taxDef.png +0 -0
  68. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/AD-taxFree.png +0 -0
  69. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/AD-taxable.png +0 -0
  70. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/Hist_Bequest.png +0 -0
  71. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/Hist_Spending.png +0 -0
  72. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/MC-tutorial2a.png +0 -0
  73. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/MC-tutorial2b.png +0 -0
  74. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/OwlUI.png +0 -0
  75. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/allocations.png +0 -0
  76. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/owl.png +0 -0
  77. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/profile.png +0 -0
  78. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/ratesCorrelations.png +0 -0
  79. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/ratesPlot.png +0 -0
  80. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/savingsPlot.png +0 -0
  81. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/sourcesPlot.png +0 -0
  82. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/spendingPlot.png +0 -0
  83. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/taxIncomePlot.png +0 -0
  84. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/images/taxesPlot.png +0 -0
  85. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/docs/owl.pdf +0 -0
  86. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/examples/jack+jill.xlsx +0 -0
  87. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/examples/joe.xlsx +0 -0
  88. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/examples/john+sally.xlsx +0 -0
  89. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/examples/template.xlsx +0 -0
  90. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/notebooks/john+sally.ipynb +0 -0
  91. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/notebooks/kim+sam.ipynb +0 -0
  92. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/notebooks/template.ipynb +0 -0
  93. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/notebooks/tutorial_1.ipynb +0 -0
  94. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/notebooks/tutorial_2.ipynb +0 -0
  95. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/notebooks/tutorial_3.ipynb +0 -0
  96. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/owlplanner.cmd +0 -0
  97. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/requirements.txt +0 -0
  98. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/src/owlplanner/__init__.py +0 -0
  99. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/src/owlplanner/data/__init__.py +0 -0
  100. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/src/owlplanner/data/rates.csv +0 -0
  101. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/tests/test_logger.py +0 -0
  102. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/tests/test_regressions.py +0 -0
  103. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/tests/test_repro.py +0 -0
  104. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/tests/test_toml_cases.py +0 -0
  105. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/tests/test_units.py +0 -0
  106. {owlplanner-2025.2.8 → owlplanner-2025.2.9}/ui/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.2.8
3
+ Version: 2025.2.9
4
4
  Summary: Owl: Retirement planner with great wisdom
5
5
  Project-URL: HomePage, https://github.com/mdlacasse/owl
6
6
  Project-URL: Repository, https://github.com/mdlacasse/owl
@@ -205,7 +205,7 @@ Parameter values are either set by the user, historical data, or by the tax code
205
205
  \item [$\Gamma_{tn}$]
206
206
  Bound for Federal income tax bracket. We define $\Gamma_{(-1)n} := 0$, so that
207
207
  $\Gamma_{0n}$ is the upper bound for the 10\% tax bracket in year $n$. As the filing status
208
- can change for couples, and so can the tax code, $\Gamma_{tn}$ will be changing over $n$.
208
+ can change for couples, and so can the tax code, $\Gamma_{tn}$ will be changing over $n$.
209
209
  \item [$\Delta_{tn}$]
210
210
  Difference between upper bound $\Gamma_t$ and lower bound $\Gamma_{t-1}$
211
211
  of a federal income tax bracket,
@@ -24,7 +24,7 @@ Names = [ "Jack", "Jill",]
24
24
  "Social security amounts" = [ 28.4, 19.7,]
25
25
  "Social security ages" = [ 70, 62,]
26
26
 
27
- ["Rate Selection"]
27
+ ["Rates Selection"]
28
28
  "Heirs rate on tax-deferred estate" = 30.0
29
29
  "Long-term capital gain tax rate" = 15.0
30
30
  "Dividend tax rate" = 2.0
@@ -32,7 +32,7 @@ Method = "historical"
32
32
  From = 1969
33
33
  To = 2002
34
34
 
35
- ["Asset Allocations"]
35
+ ["Asset Allocation"]
36
36
  "Interpolation method" = "s-curve"
37
37
  "Interpolation center" = 15
38
38
  "Interpolation width" = 5
@@ -22,7 +22,7 @@ Names = [ "Joe",]
22
22
  "Social security amounts" = [ 28.4,]
23
23
  "Social security ages" = [ 67,]
24
24
 
25
- ["Rate Selection"]
25
+ ["Rates Selection"]
26
26
  "Heirs rate on tax-deferred estate" = 30.0
27
27
  "Long-term capital gain tax rate" = 15.0
28
28
  "Dividend tax rate" = 2.0
@@ -30,7 +30,7 @@ Method = "historical average"
30
30
  From = 1969
31
31
  To = 2002
32
32
 
33
- ["Asset Allocations"]
33
+ ["Asset Allocation"]
34
34
  "Interpolation method" = "s-curve"
35
35
  "Interpolation center" = 15
36
36
  "Interpolation width" = 5
@@ -24,7 +24,7 @@ Names = [ "John", "Sally",]
24
24
  "Social security amounts" = [ 36.0, 21.6,]
25
25
  "Social security ages" = [ 67, 67,]
26
26
 
27
- ["Rate Selection"]
27
+ ["Rates Selection"]
28
28
  "Heirs rate on tax-deferred estate" = 30.0
29
29
  "Long-term capital gain tax rate" = 15.0
30
30
  "Dividend tax rate" = 2.0
@@ -32,7 +32,7 @@ Method = "historical average"
32
32
  From = 1990
33
33
  To = 2023
34
34
 
35
- ["Asset Allocations"]
35
+ ["Asset Allocation"]
36
36
  "Interpolation method" = "linear"
37
37
  "Interpolation center" = 15
38
38
  "Interpolation width" = 5
@@ -24,7 +24,7 @@ Names = [ "Kim", "Sam",]
24
24
  "Social security amounts" = [ 45.0, 25.0,]
25
25
  "Social security ages" = [ 70, 68,]
26
26
 
27
- ["Rate Selection"]
27
+ ["Rates Selection"]
28
28
  "Heirs rate on tax-deferred estate" = 33.0
29
29
  "Long-term capital gain tax rate" = 15.0
30
30
  "Dividend tax rate" = 2.0
@@ -32,7 +32,7 @@ Method = "conservative"
32
32
  From = 1922
33
33
  To = 2023
34
34
 
35
- ["Asset Allocations"]
35
+ ["Asset Allocation"]
36
36
  "Interpolation method" = "s-curve"
37
37
  "Interpolation center" = 15
38
38
  "Interpolation width" = 5
@@ -24,7 +24,7 @@ Names = [ "Kim", "Sam",]
24
24
  "Social security amounts" = [ 45.0, 25.0,]
25
25
  "Social security ages" = [ 70, 68,]
26
26
 
27
- ["Rate Selection"]
27
+ ["Rates Selection"]
28
28
  "Heirs rate on tax-deferred estate" = 33.0
29
29
  "Long-term capital gain tax rate" = 15.0
30
30
  "Dividend tax rate" = 2.0
@@ -32,7 +32,7 @@ Method = "conservative"
32
32
  From = 1922
33
33
  To = 2023
34
34
 
35
- ["Asset Allocations"]
35
+ ["Asset Allocation"]
36
36
  "Interpolation method" = "s-curve"
37
37
  "Interpolation center" = 15
38
38
  "Interpolation width" = 5
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "owlplanner"
7
- version = "2025.02.08"
7
+ version = "2025.02.09"
8
8
  authors = [
9
9
  { name="Martin-D. Lacasse", email="martin.d.lacasse@gmail.com" },
10
10
  ]
@@ -43,7 +43,7 @@ class Row(object):
43
43
  """
44
44
  Add an element at index ``ind`` of value ``val`` to the row.
45
45
  """
46
- assert 0 <= ind and ind < self.nvars, 'Index %d out of range.' % ind
46
+ assert 0 <= ind and ind < self.nvars, "Index %d out of range." % ind
47
47
  self.ind.append(ind)
48
48
  self.val.append(val)
49
49
 
@@ -96,11 +96,11 @@ class ConstraintMatrix(object):
96
96
  self.lb.append(lb)
97
97
  self.ub.append(ub)
98
98
  if lb == ub:
99
- self.key.append('fx')
99
+ self.key.append("fx")
100
100
  elif ub == np.inf:
101
- self.key.append('lo')
101
+ self.key.append("lo")
102
102
  else:
103
- self.key.append('ra')
103
+ self.key.append("ra")
104
104
  self.ncons += 1
105
105
 
106
106
  def addNewRow(self, rowDic, lb, ub):
@@ -154,39 +154,39 @@ class Bounds(object):
154
154
  self.integrality = []
155
155
 
156
156
  def setBinary(self, ii):
157
- assert 0 <= ii and ii < self.nvars, 'Index %d out of range.' % ii
157
+ assert 0 <= ii and ii < self.nvars, "Index %d out of range." % ii
158
158
  self.ind.append(ii)
159
159
  self.lb.append(0)
160
160
  self.ub.append(1)
161
- self.key.append('ra')
161
+ self.key.append("ra")
162
162
  self.integrality.append(ii)
163
163
 
164
164
  def set0_Ub(self, ii, ub):
165
- assert 0 <= ii and ii < self.nvars, 'Index %d out of range.' % ii
165
+ assert 0 <= ii and ii < self.nvars, "Index %d out of range." % ii
166
166
  self.ind.append(ii)
167
167
  self.lb.append(0)
168
168
  self.ub.append(ub)
169
- self.key.append('ra')
169
+ self.key.append("ra")
170
170
 
171
171
  def setLb_Inf(self, ii, lb):
172
- assert 0 <= ii and ii < self.nvars, 'Index %d out of range.' % ii
172
+ assert 0 <= ii and ii < self.nvars, "Index %d out of range." % ii
173
173
  self.ind.append(ii)
174
174
  self.lb.append(lb)
175
175
  self.ub.append(np.inf)
176
- self.key.append('lo')
176
+ self.key.append("lo")
177
177
 
178
178
  def setRange(self, ii, lb, ub):
179
- assert 0 <= ii and ii < self.nvars, 'Index %d out of range.' % ii
179
+ assert 0 <= ii and ii < self.nvars, "Index %d out of range." % ii
180
180
  self.ind.append(ii)
181
181
  self.lb.append(lb)
182
182
  self.ub.append(ub)
183
183
  if lb == ub:
184
- self.key.append('fx')
184
+ self.key.append("fx")
185
185
  else:
186
- self.key.append('ra')
186
+ self.key.append("ra")
187
187
 
188
188
  def keys(self):
189
- keys = ['lo'] * self.nvars
189
+ keys = ["lo"] * self.nvars
190
190
  for ii in range(len(self.ind)):
191
191
  keys[self.ind[ii]] = self.key[ii]
192
192
 
@@ -223,7 +223,7 @@ class Objective(object):
223
223
  self.val = []
224
224
 
225
225
  def setElem(self, ind, val):
226
- assert 0 <= ind and ind < self.nvars, 'Index %d out of range.' % ind
226
+ assert 0 <= ind and ind < self.nvars, "Index %d out of range." % ind
227
227
  self.ind.append(ind)
228
228
  self.val.append(val)
229
229
 
@@ -0,0 +1,299 @@
1
+ """
2
+
3
+ Owl/conftoml
4
+
5
+ This file contains utility functions to save case parameters.
6
+
7
+ Copyright (C) 2024 -- Martin-D. Lacasse
8
+
9
+ Disclaimer: This program comes with no guarantee. Use at your own risk.
10
+ """
11
+
12
+ import toml as toml
13
+ from io import StringIO, BytesIO
14
+ import numpy as np
15
+ import os
16
+
17
+ from owlplanner import plan
18
+ from owlplanner import logging
19
+ from owlplanner.rates import FROM, TO
20
+
21
+
22
+ def saveConfig(plan, file, mylog):
23
+ """
24
+ Save case parameters and return a dictionary containing all parameters.
25
+ """
26
+ # np.set_printoptions(legacy='1.21')
27
+ accountTypes = ["taxable", "tax-deferred", "tax-free"]
28
+
29
+ diconf = {}
30
+ diconf["Plan Name"] = plan._name
31
+
32
+ # Basic Info.
33
+ diconf["Basic Info"] = {
34
+ "Status": ["unknown", "single", "married"][plan.N_i],
35
+ "Names": plan.inames,
36
+ "Birth year": plan.yobs.tolist(),
37
+ "Life expectancy": plan.expectancy.tolist(),
38
+ "Start date": plan.startDate,
39
+ }
40
+
41
+ # Assets.
42
+ diconf["Assets"] = {}
43
+ for j in range(plan.N_j):
44
+ amounts = plan.beta_ij[:, j] / 1000
45
+ diconf["Assets"]["%s savings balances" % accountTypes[j]] = amounts.tolist()
46
+ if plan.N_i == 2:
47
+ diconf["Assets"]["Beneficiary fractions"] = plan.phi_j.tolist()
48
+ diconf["Assets"]["Spousal surplus deposit fraction"] = plan.eta
49
+
50
+ # Wages and Contributions.
51
+ diconf["Wages and Contributions"] = {"Contributions file name": plan.timeListsFileName}
52
+
53
+ # Fixed Income.
54
+ diconf["Fixed Income"] = {
55
+ "Pension amounts": (plan.pensionAmounts / 1000).tolist(),
56
+ "Pension ages": plan.pensionAges.tolist(),
57
+ "Pension indexed": plan.pensionIndexed,
58
+ "Social security amounts": (plan.ssecAmounts / 1000).tolist(),
59
+ "Social security ages": plan.ssecAges.tolist(),
60
+ }
61
+
62
+ # Rates Selection.
63
+ diconf["Rates Selection"] = {
64
+ "Heirs rate on tax-deferred estate": float(100 * plan.nu),
65
+ "Long-term capital gain tax rate": float(100 * plan.psi),
66
+ "Dividend tax rate": float(100 * plan.mu),
67
+ "Method": plan.rateMethod,
68
+ }
69
+ if plan.rateMethod in ["user", "stochastic"]:
70
+ diconf["Rates Selection"]["Values"] = (100 * plan.rateValues).tolist()
71
+ if plan.rateMethod in ["stochastic"]:
72
+ diconf["Rates Selection"]["Standard deviations"] = (100 * plan.rateStdev).tolist()
73
+ diconf["Rates Selection"]["Correlations"] = plan.rateCorr.tolist()
74
+ if plan.rateMethod in ["historical average", "historical", "histochastic"]:
75
+ diconf["Rates Selection"]["From"] = int(plan.rateFrm)
76
+ diconf["Rates Selection"]["To"] = int(plan.rateTo)
77
+ else:
78
+ diconf["Rates Selection"]["From"] = int(FROM)
79
+ diconf["Rates Selection"]["To"] = int(TO)
80
+
81
+ # Asset Allocation.
82
+ diconf["Asset Allocation"] = {
83
+ "Interpolation method": plan.interpMethod,
84
+ "Interpolation center": float(plan.interpCenter),
85
+ "Interpolation width": float(plan.interpWidth),
86
+ "Type": plan.ARCoord,
87
+ }
88
+ if plan.ARCoord == "account":
89
+ for accType in accountTypes:
90
+ diconf["Asset Allocation"][accType] = plan.boundsAR[accType]
91
+ else:
92
+ diconf["Asset Allocation"]["generic"] = plan.boundsAR["generic"]
93
+
94
+ # Optimization Parameters.
95
+ diconf["Optimization Parameters"] = {
96
+ "Spending profile": plan.spendingProfile,
97
+ "Surviving spouse spending percent": int(100 * plan.chi),
98
+ }
99
+ if plan.spendingProfile == "smile":
100
+ diconf["Optimization Parameters"]["Smile dip"] = int(plan.smileDip)
101
+ diconf["Optimization Parameters"]["Smile increase"] = int(plan.smileIncrease)
102
+ diconf["Optimization Parameters"]["Smile delay"] = int(plan.smileDelay)
103
+
104
+ diconf["Optimization Parameters"]["Objective"] = plan.objective
105
+ diconf["Solver Options"] = plan.solverOptions
106
+
107
+ # Results.
108
+ diconf["Results"] = {"Default plots": plan.defaultPlots}
109
+
110
+ if isinstance(file, str):
111
+ filename = file
112
+ if not file.endswith(".toml"):
113
+ filename = filename + ".toml"
114
+ if not filename.startswith("case_"):
115
+ filename = "case_" + filename
116
+ mylog.vprint("Saving plan case file as '%s'." % filename)
117
+
118
+ try:
119
+ with open(filename, "w") as casefile:
120
+ toml.dump(diconf, casefile, encoder=toml.TomlNumpyEncoder())
121
+ except Exception as e:
122
+ raise RuntimeError("Failed to save case file %s: %s" % (filename, e))
123
+ elif isinstance(file, StringIO):
124
+ try:
125
+ string = toml.dumps(diconf, encoder=toml.TomlNumpyEncoder())
126
+ file.write(string)
127
+ except Exception as e:
128
+ raise RuntimeError("Failed to save case to StringIO: %s", e)
129
+ elif file is None:
130
+ pass
131
+ else:
132
+ raise ValueError("Argument %s has unknown type" % type(file))
133
+
134
+ return diconf
135
+
136
+
137
+ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
138
+ """
139
+ Read plan parameters from case file *basename*.toml.
140
+ A new plan is created and returned.
141
+ Argument file can be a filename, a file, or a stringIO.
142
+ """
143
+ mylog = logging.Logger(verbose, logstreams)
144
+
145
+ accountTypes = ["taxable", "tax-deferred", "tax-free"]
146
+
147
+ dirname = ""
148
+ if isinstance(file, str):
149
+ filename = file
150
+ dirname = os.path.dirname(filename)
151
+ if not filename.endswith(".toml"):
152
+ filename = filename + ".toml"
153
+
154
+ mylog.vprint("Reading plan from case file '%s'." % filename)
155
+
156
+ try:
157
+ with open(filename, "r") as f:
158
+ diconf = toml.load(f)
159
+ except Exception as e:
160
+ raise FileNotFoundError("File %s not found: %s" % (filename, e))
161
+ elif isinstance(file, BytesIO):
162
+ try:
163
+ string = file.getvalue().decode("utf-8")
164
+ diconf = toml.loads(string)
165
+ except Exception as e:
166
+ raise RuntimeError("Cannot read from BytesIO: %s" % e)
167
+ elif isinstance(file, StringIO):
168
+ try:
169
+ string = file.getvalue()
170
+ diconf = toml.loads(string)
171
+ except Exception as e:
172
+ raise RuntimeError("Cannot read from StringIO: %s" % e)
173
+ else:
174
+ raise ValueError("%s not a valid type" % type(file))
175
+
176
+ # Basic Info.
177
+ name = diconf["Plan Name"]
178
+ inames = diconf["Basic Info"]["Names"]
179
+ # status = diconf['Basic Info']['Status']
180
+ yobs = diconf["Basic Info"]["Birth year"]
181
+ expectancy = diconf["Basic Info"]["Life expectancy"]
182
+ startDate = diconf["Basic Info"]["Start date"]
183
+ icount = len(yobs)
184
+
185
+ mylog.vprint("Plan for %d individual%s: %s." % (icount, ["", "s"][icount - 1], inames))
186
+ p = plan.Plan(inames, yobs, expectancy, name, startDate=startDate, verbose=True, logstreams=logstreams)
187
+
188
+ # Assets.
189
+ balances = {}
190
+ for acc in accountTypes:
191
+ balances[acc] = diconf["Assets"]["%s savings balances" % acc]
192
+ p.setAccountBalances(
193
+ taxable=balances["taxable"], taxDeferred=balances["tax-deferred"], taxFree=balances["tax-free"]
194
+ )
195
+ if icount == 2:
196
+ phi_j = diconf["Assets"]["Beneficiary fractions"]
197
+ p.setBeneficiaryFractions(phi_j)
198
+ eta = diconf["Assets"]["Spousal surplus deposit fraction"]
199
+ p.setSpousalDepositFraction(eta)
200
+
201
+ # Wages and Contributions.
202
+ timeListsFileName = diconf["Wages and Contributions"]["Contributions file name"]
203
+ if timeListsFileName != "None":
204
+ if readContributions:
205
+ if os.path.exists(timeListsFileName):
206
+ myfile = timeListsFileName
207
+ elif dirname != "" and os.path.exists(dirname + "/" + timeListsFileName):
208
+ myfile = dirname + "/" + timeListsFileName
209
+ else:
210
+ raise FileNotFoundError("File '%s' not found." % timeListsFileName)
211
+ p.readContributions(myfile)
212
+ else:
213
+ p.timeListsFileName = timeListsFileName
214
+ mylog.vprint("Ignoring to read contributions file %s." % timeListsFileName)
215
+
216
+ # Fixed Income.
217
+ ssecAmounts = np.array(diconf["Fixed Income"]["Social security amounts"], dtype=np.float32)
218
+ ssecAges = np.array(diconf["Fixed Income"]["Social security ages"], dtype=np.int32)
219
+ p.setSocialSecurity(ssecAmounts, ssecAges)
220
+ pensionAmounts = np.array(diconf["Fixed Income"]["Pension amounts"], dtype=np.float32)
221
+ pensionAges = np.array(diconf["Fixed Income"]["Pension ages"], dtype=np.int32)
222
+ pensionIndexed = diconf["Fixed Income"]["Pension indexed"]
223
+ p.setPension(pensionAmounts, pensionAges, pensionIndexed)
224
+
225
+ # Rates Selection.
226
+ p.setDividendRate(float(diconf["Rates Selection"]["Dividend tax rate"]))
227
+ p.setLongTermCapitalTaxRate(float(diconf["Rates Selection"]["Long-term capital gain tax rate"]))
228
+ p.setHeirsTaxRate(float(diconf["Rates Selection"]["Heirs rate on tax-deferred estate"]))
229
+
230
+ frm = None
231
+ to = None
232
+ rateValues = None
233
+ stdev = None
234
+ rateCorr = None
235
+ rateMethod = diconf["Rates Selection"]["Method"]
236
+ if rateMethod in ["historical average", "historical", "histochastic"]:
237
+ frm = diconf["Rates Selection"]["From"]
238
+ if not isinstance(frm, int):
239
+ frm = int(frm)
240
+ to = int(diconf["Rates Selection"]["To"])
241
+ if not isinstance(to, int):
242
+ to = int(to)
243
+ if rateMethod in ["user", "stochastic"]:
244
+ rateValues = np.array(diconf["Rates Selection"]["Values"], dtype=np.float32)
245
+ if rateMethod in ["stochastic"]:
246
+ stdev = np.array(diconf["Rates Selection"]["Standard deviations"], dtype=np.float32)
247
+ rateCorr = np.array(diconf["Rates Selection"]["Correlations"], dtype=np.float32)
248
+ p.setRates(rateMethod, frm, to, rateValues, stdev, rateCorr)
249
+
250
+ # Asset Allocation.
251
+ boundsAR = {}
252
+ p.setInterpolationMethod(
253
+ diconf["Asset Allocation"]["Interpolation method"],
254
+ float(diconf["Asset Allocation"]["Interpolation center"]),
255
+ float(diconf["Asset Allocation"]["Interpolation width"]),
256
+ )
257
+ allocType = diconf["Asset Allocation"]["Type"]
258
+ if allocType == "account":
259
+ for aType in accountTypes:
260
+ boundsAR[aType] = np.array(diconf["Asset Allocation"][aType], dtype=np.float32)
261
+
262
+ p.setAllocationRatios(
263
+ allocType,
264
+ taxable=boundsAR["taxable"],
265
+ taxDeferred=boundsAR["tax-deferred"],
266
+ taxFree=boundsAR["tax-free"],
267
+ )
268
+ elif allocType == "individual" or allocType == "spouses":
269
+ boundsAR["generic"] = np.array(diconf["Asset Allocation"]["generic"], dtype=np.float32)
270
+ p.setAllocationRatios(
271
+ allocType,
272
+ generic=boundsAR["generic"],
273
+ )
274
+ else:
275
+ raise ValueError("Unknown asset allocation type %s." % allocType)
276
+
277
+ # Optimization Parameters.
278
+ p.objective = diconf["Optimization Parameters"]["Objective"]
279
+
280
+ profile = diconf["Optimization Parameters"]["Spending profile"]
281
+ survivor = int(diconf["Optimization Parameters"]["Surviving spouse spending percent"])
282
+ if profile == "smile":
283
+ dip = int(diconf["Optimization Parameters"]["Smile dip"])
284
+ increase = int(diconf["Optimization Parameters"]["Smile increase"])
285
+ delay = int(diconf["Optimization Parameters"]["Smile delay"])
286
+ else:
287
+ dip = 15
288
+ increase = 12
289
+ delay = 0
290
+
291
+ p.setSpendingProfile(profile, survivor, dip, increase, delay)
292
+
293
+ # Solver Options.
294
+ p.solverOptions = diconf["Solver Options"]
295
+
296
+ # Results.
297
+ p.setDefaultPlots(diconf["Results"]["Default plots"])
298
+
299
+ return p
@@ -19,15 +19,15 @@ class Logger(object):
19
19
  self._prevState = self._verbose
20
20
  if logstreams is None or logstreams == [] or len(logstreams) > 2:
21
21
  self._logstreams = [sys.stdout, sys.stderr]
22
- self.vprint('Using stdout and stderr as stream loggers.')
22
+ self.vprint("Using stdout and stderr as stream loggers.")
23
23
  elif len(logstreams) == 2:
24
24
  self._logstreams = logstreams
25
- self.vprint('Using logstreams as stream loggers.')
25
+ self.vprint("Using logstreams as stream loggers.")
26
26
  elif len(logstreams) == 1:
27
- self._logstreams = 2*logstreams
28
- self.vprint('Using logstream as stream logger.')
27
+ self._logstreams = 2 * logstreams
28
+ self.vprint("Using logstream as stream logger.")
29
29
  else:
30
- raise ValueError('Log streams %r must be a list.' % logstreams)
30
+ raise ValueError("Log streams %r must be a list." % logstreams)
31
31
 
32
32
  def setVerbose(self, verbose=True):
33
33
  """
@@ -36,7 +36,7 @@ class Logger(object):
36
36
  """
37
37
  self._prevState = self._verbose
38
38
  self._verbose = verbose
39
- self.vprint('Setting verbose to', verbose)
39
+ self.vprint("Setting verbose to", verbose)
40
40
 
41
41
  return self._prevState
42
42
 
@@ -51,9 +51,9 @@ class Logger(object):
51
51
  Unconditional printing regardless of the value of the verbose variable
52
52
  previously set.
53
53
  """
54
- if 'file' not in kwargs:
54
+ if "file" not in kwargs:
55
55
  file = self._logstreams[0]
56
- kwargs['file'] = file
56
+ kwargs["file"] = file
57
57
 
58
58
  print(*args, **kwargs)
59
59
  file.flush()
@@ -71,14 +71,14 @@ class Logger(object):
71
71
  Print message and exit. Use to print error messages on stderr.
72
72
  The exit() used throws an exception in an interactive environment.
73
73
  """
74
- if 'file' not in kwargs:
74
+ if "file" not in kwargs:
75
75
  file = self._logstreams[1]
76
- kwargs['file'] = file
76
+ kwargs["file"] = file
77
77
 
78
78
  if self._verbose:
79
- print('ERROR:', *args, **kwargs)
80
- print('Exiting...')
79
+ print("ERROR:", *args, **kwargs)
80
+ print("Exiting...")
81
81
  file.flush()
82
82
 
83
- raise Exception('Fatal error.')
83
+ raise Exception("Fatal error.")
84
84
  # sys.exit(-1)