owlplanner 2025.3.12__tar.gz → 2025.3.14__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 (108) hide show
  1. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/.devcontainer/devcontainer.json +32 -32
  2. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/PKG-INFO +1 -1
  3. owlplanner-2025.3.14/Papers/FE00006821-Class-VI-Injection-Permit--Salient-Features-and-Regulatory-Challenges_Final.pdf +0 -0
  4. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/owl.tex +12 -2
  5. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/pyproject.toml +1 -1
  6. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/plan.py +40 -20
  7. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/utils.py +9 -2
  8. owlplanner-2025.3.14/src/owlplanner/version.py +1 -0
  9. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/tests/test_regressions.py +27 -27
  10. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Documentation.py +7 -0
  11. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Output_Files.py +1 -1
  12. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Rates_Selection.py +3 -2
  13. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/requirements.txt +1 -1
  14. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/sskeys.py +0 -1
  15. owlplanner-2025.3.12/src/owlplanner/version.py +0 -1
  16. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/.flake8 +0 -0
  17. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/.gitattributes +0 -0
  18. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/.github/workflows/github-actions-runtests.yml +0 -0
  19. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/.gitignore +0 -0
  20. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/INSTALL.md +0 -0
  21. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/LICENSE +0 -0
  22. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/Papers/Kou-OptionPricingDouble-2004.pdf +0 -0
  23. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/Papers/Multi-Period Mean Expected-Shortfall Strategies Cut Your Losses and Ride Your Gains .pdf +0 -0
  24. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/Papers/Optimal Asset Allocation for Retirement Saving Deterministic Vs. Time Consistent Adaptive Strategies.pdf +0 -0
  25. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/Papers/Rule-based_strategies_for_dynamic_life_cycle_inves.pdf +0 -0
  26. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/Papers/s10436-006-0062-y.pdf +0 -0
  27. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/README.md +0 -0
  28. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/USER_GUIDE.md +0 -0
  29. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docker/Dockerfile +0 -0
  30. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docker/README.md +0 -0
  31. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docker/docker-compose.yml +0 -0
  32. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docker/fastentrypoint.sh +0 -0
  33. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/AD-taxDef.png +0 -0
  34. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/AD-taxFree.png +0 -0
  35. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/AD-taxable.png +0 -0
  36. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/Hist_Bequest.png +0 -0
  37. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/Hist_Spending.png +0 -0
  38. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/MC-tutorial2a.png +0 -0
  39. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/MC-tutorial2b.png +0 -0
  40. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/OwlUI.png +0 -0
  41. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/allocations.png +0 -0
  42. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/owl.png +0 -0
  43. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/profile.png +0 -0
  44. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/ratesCorrelations.png +0 -0
  45. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/ratesPlot.png +0 -0
  46. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/savingsPlot.png +0 -0
  47. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/sourcesPlot.png +0 -0
  48. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/spendingPlot.png +0 -0
  49. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/taxIncomePlot.png +0 -0
  50. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/images/taxesPlot.png +0 -0
  51. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/docs/owl.pdf +0 -0
  52. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/examples/case_jack+jill.toml +0 -0
  53. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/examples/case_joe.toml +0 -0
  54. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/examples/case_john+sally.toml +0 -0
  55. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/examples/case_jon+jane.toml +0 -0
  56. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/examples/case_kim+sam-bequest.toml +0 -0
  57. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/examples/case_kim+sam-spending.toml +0 -0
  58. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/examples/jack+jill.xlsx +0 -0
  59. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/examples/joe.xlsx +0 -0
  60. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/examples/john+sally.xlsx +0 -0
  61. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/examples/jon+jane.xlsx +0 -0
  62. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/examples/template.xlsx +0 -0
  63. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/notebooks/john+sally.ipynb +0 -0
  64. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/notebooks/kim+sam.ipynb +0 -0
  65. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/notebooks/template.ipynb +0 -0
  66. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/notebooks/tutorial_1.ipynb +0 -0
  67. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/notebooks/tutorial_2.ipynb +0 -0
  68. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/notebooks/tutorial_3.ipynb +0 -0
  69. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/owlplanner.cmd +0 -0
  70. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/owlplanner.sh +0 -0
  71. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/requirements.txt +0 -0
  72. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/__init__.py +0 -0
  73. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/abcapi.py +0 -0
  74. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/config.py +0 -0
  75. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/data/__init__.py +0 -0
  76. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/data/rates.csv +0 -0
  77. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/logging.py +0 -0
  78. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/progress.py +0 -0
  79. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/rates.py +0 -0
  80. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/tax2025.py +0 -0
  81. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/src/owlplanner/timelists.py +0 -0
  82. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/tests/test_logger.py +0 -0
  83. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/tests/test_repro.py +0 -0
  84. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/tests/test_toml_cases.py +0 -0
  85. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/tests/test_units.py +0 -0
  86. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ttt.py +0 -0
  87. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/About_Owl.py +0 -0
  88. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Asset_Allocation.py +0 -0
  89. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Create_Case.py +0 -0
  90. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Current_Assets.py +0 -0
  91. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Fixed_Income.py +0 -0
  92. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Graphs.py +0 -0
  93. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Historical_Range.py +0 -0
  94. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Logs.py +0 -0
  95. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Monte_Carlo.py +0 -0
  96. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Optimization_Parameters.py +0 -0
  97. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Quick_Start.py +0 -0
  98. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/README.md +0 -0
  99. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Settings.py +0 -0
  100. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Wages_And_Contributions.py +0 -0
  101. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/Worksheets.py +0 -0
  102. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/main+fonts.py +0 -0
  103. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/main.py +0 -0
  104. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/owlbridge.py +0 -0
  105. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/plots.py +0 -0
  106. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/progress.py +0 -0
  107. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/style.css +0 -0
  108. {owlplanner-2025.3.12 → owlplanner-2025.3.14}/ui/tomlexamples.py +0 -0
@@ -1,33 +1,33 @@
1
- {
2
- "name": "Python 3",
3
- // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
4
- "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
5
- "customizations": {
6
- "codespaces": {
7
- "openFiles": [
8
- "README.md",
9
- "ui/main.py"
10
- ]
11
- },
12
- "vscode": {
13
- "settings": {},
14
- "extensions": [
15
- "ms-python.python",
16
- "ms-python.vscode-pylance"
17
- ]
18
- }
19
- },
20
- "updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y <packages.txt; [ -f requirements.txt ] && pip3 install --user -r requirements.txt; pip3 install --user streamlit; echo '✅ Packages installed and Requirements met'",
21
- "postAttachCommand": {
22
- "server": "streamlit run ui/main.py --server.enableCORS false --server.enableXsrfProtection false"
23
- },
24
- "portsAttributes": {
25
- "8501": {
26
- "label": "Application",
27
- "onAutoForward": "openPreview"
28
- }
29
- },
30
- "forwardPorts": [
31
- 8501
32
- ]
1
+ {
2
+ "name": "Python 3",
3
+ // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
4
+ "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
5
+ "customizations": {
6
+ "codespaces": {
7
+ "openFiles": [
8
+ "README.md",
9
+ "ui/main.py"
10
+ ]
11
+ },
12
+ "vscode": {
13
+ "settings": {},
14
+ "extensions": [
15
+ "ms-python.python",
16
+ "ms-python.vscode-pylance"
17
+ ]
18
+ }
19
+ },
20
+ "updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y <packages.txt; [ -f requirements.txt ] && pip3 install --user -r requirements.txt; pip3 install --user streamlit; echo '✅ Packages installed and Requirements met'",
21
+ "postAttachCommand": {
22
+ "server": "streamlit run ui/main.py --server.enableCORS false --server.enableXsrfProtection false"
23
+ },
24
+ "portsAttributes": {
25
+ "8501": {
26
+ "label": "Application",
27
+ "onAutoForward": "openPreview"
28
+ }
29
+ },
30
+ "forwardPorts": [
31
+ 8501
32
+ ]
33
33
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.3.12
3
+ Version: 2025.3.14
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
@@ -91,7 +91,7 @@ index name as a subscript, e.g., $N_i$ for index $i$.
91
91
  the end of year $N_n-1$, and therefore $N_n + 1$ years are considered.
92
92
  Year $N_n$ is the first year following the passing of all
93
93
  individuals in the plan. The time period for all decision variables is annual.
94
- For spouses, the end of year $n_d-1$ is the year in which the first individual passes while
94
+ For spouses, the end of year $n_d-1$ is when the first individual is assumed to pass while
95
95
  the survivor will decease at the end of year $N_n-1$ of the plan.
96
96
  \item [$t$]
97
97
  Federal income tax bracket. $t$ goes from 0 to $N_t - 1$, from low to high.
@@ -441,8 +441,18 @@ All intermediate variables are in uppercase letters.
441
441
  paid in each tax bracket as
442
442
  \begin{equation}
443
443
  \label{Eq:IncTax0}
444
- T_n = \sum_t f_{tn}\bar{\Delta}_{tn}\theta_{tn}.
444
+ T_n = \sum_t f_{tn}\bar{\Delta}_{tn}\theta_{tn} + 0.10 \sum_i (1 - \mathcal{H}(n - n_{i,60})) w_{i1n},
445
445
  \end{equation}
446
+ where $H(n - n_{i, 60})$ is a Heavyside step function which is 0 or 1, depending on the sign of
447
+ its argument:
448
+ \[
449
+ \mathcal{H}(x) \def
450
+ \begin{cases}
451
+ 0 & x < 0 \\
452
+ 1 & x \geq 0. \\
453
+ \end{cases}
454
+ \]
455
+ Here, $n_{i, 60}$ is the year when individual $i$ will turn 60.
446
456
  Notice that $G_n$ is also defined by Eq.~(\ref{Eq:Tx1}), and that optimal
447
457
  values of $f_{tn}$ have to
448
458
  minimize $T_n$ when either the bequest or the desired net spending are maximized.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "owlplanner"
7
- version = "2025.03.12"
7
+ version = "2025.03.14"
8
8
  authors = [
9
9
  { name="Martin-D. Lacasse", email="martin.d.lacasse@gmail.com" },
10
10
  ]
@@ -34,7 +34,7 @@ from owlplanner import logging
34
34
  from owlplanner import progress
35
35
 
36
36
 
37
- # This makes all graphs appear have the same height.
37
+ # This makes all graphs to have the same height.
38
38
  plt.rcParams.update({'figure.autolayout': True})
39
39
 
40
40
 
@@ -266,6 +266,9 @@ class Plan(object):
266
266
  # self.horizons = [yobs[i] + expectancy[i] - thisyear + 1 for i in range(self.N_i)]
267
267
  self.N_n = np.max(self.horizons)
268
268
  self.year_n = np.linspace(thisyear, thisyear + self.N_n - 1, self.N_n, dtype=np.int32)
269
+ # Year in the plan (if any) where individuals turn 59. For 10% withdrawal penalty.
270
+ self.n59 = 59 - thisyear + self.yobs
271
+ self.n59[self.n59 < 0] = 0
269
272
  # Handle passing of one spouse before the other.
270
273
  if self.N_i == 2 and np.min(self.horizons) != np.max(self.horizons):
271
274
  self.n_d = np.min(self.horizons)
@@ -1289,6 +1292,11 @@ class Plan(object):
1289
1292
  for t in range(Nt):
1290
1293
  row.addElem(_q2(CF, t, n, Nt, Nn), self.theta_tn[t, n])
1291
1294
 
1295
+ # Minus 10% penalty on early withdrawals.
1296
+ if n < self.n59[i]:
1297
+ row.addElem(_q3(Cw, i, 1, n, Ni, Nj, Nn), 0.1)
1298
+ row.addElem(_q3(Cw, i, 2, n, Ni, Nj, Nn), 0.1)
1299
+
1292
1300
  A.addRow(row, rhs, rhs)
1293
1301
 
1294
1302
  # Impose income profile.
@@ -1970,6 +1978,12 @@ class Plan(object):
1970
1978
  self.G_n = np.sum(self.F_tn, axis=0)
1971
1979
  self.T_tn = self.F_tn * self.theta_tn
1972
1980
  self.T_n = np.sum(self.T_tn, axis=0)
1981
+ self.penalty_n = np.zeros(Nn)
1982
+ # Add early withdrawal penalties if any.
1983
+ for i in range(Ni):
1984
+ self.penalty_n[0:self.n59[i]] += 0.1*(self.w_ijn[i, 1, 0:self.n59[i]] + self.w_ijn[i, 2, 0:self.n59[i]])
1985
+
1986
+ self.T_n += self.penalty_n
1973
1987
 
1974
1988
  tau_0 = np.array(self.tau_kn[0, :])
1975
1989
  tau_0[tau_0 < 0] = 0
@@ -2094,33 +2108,38 @@ class Plan(object):
2094
2108
  totIncome = np.sum(self.g_n, axis=0)
2095
2109
  totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
2096
2110
  dic["Total net spending"] = f"{u.d(totIncomeNow)}"
2097
- dic["- Total net spending (nominal)"] = f"{u.d(totIncome)}"
2111
+ dic["[Total net spending]"] = f"{u.d(totIncome)}"
2098
2112
 
2099
2113
  totRoth = np.sum(self.x_in, axis=(0, 1))
2100
2114
  totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
2101
2115
  dic["Total Roth conversions"] = f"{u.d(totRothNow)}"
2102
- dic["- Total Roth conversions (nominal)"] = f"{u.d(totRoth)}"
2116
+ dic["[Total Roth conversions]"] = f"{u.d(totRoth)}"
2103
2117
 
2104
2118
  taxPaid = np.sum(self.T_n, axis=0)
2105
2119
  taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
2106
2120
  dic["Total income tax paid on ordinary income"] = f"{u.d(taxPaidNow)}"
2107
- dic["- Total income tax paid on ordinary income (nominal)"] = f"{u.d(taxPaid)}"
2121
+ dic["[Total income tax paid on ordinary income]"] = f"{u.d(taxPaid)}"
2108
2122
  for t in range(self.N_t):
2109
2123
  taxPaid = np.sum(self.T_tn[t], axis=0)
2110
2124
  taxPaidNow = np.sum(self.T_tn[t] / self.gamma_n[:-1], axis=0)
2111
2125
  tname = tx.taxBracketNames[t]
2112
2126
  dic[f"-- Subtotal in tax bracket {tname}"] = f"{u.d(taxPaidNow)}"
2113
- dic[f"--- Subtotal in tax bracket {tname} (nominal)"] = f"{u.d(taxPaid)}"
2127
+ dic[f"-- [Subtotal in tax bracket {tname}]"] = f"{u.d(taxPaid)}"
2128
+
2129
+ penaltyPaid = np.sum(self.penalty_n, axis=0)
2130
+ penaltyPaidNow = np.sum(self.penalty_n / self.gamma_n[:-1], axis=0)
2131
+ dic["-- Subtotal in early withdrawal penalty"] = f"{u.d(penaltyPaidNow)}"
2132
+ dic["-- [Subtotal in early withdrawal penalty]"] = f"{u.d(penaltyPaid)}"
2114
2133
 
2115
2134
  taxPaid = np.sum(self.U_n, axis=0)
2116
2135
  taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
2117
2136
  dic["Total tax paid on gains and dividends"] = f"{u.d(taxPaidNow)}"
2118
- dic["- Total tax paid on gains and dividends (nominal)"] = f"{u.d(taxPaid)}"
2137
+ dic["[Total tax paid on gains and dividends]"] = f"{u.d(taxPaid)}"
2119
2138
 
2120
2139
  taxPaid = np.sum(self.M_n, axis=0)
2121
2140
  taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
2122
2141
  dic["Total Medicare premiums paid"] = f"{u.d(taxPaidNow)}"
2123
- dic["- Total Medicare premiums paid (nominal)"] = f"{u.d(taxPaid)}"
2142
+ dic["[Total Medicare premiums paid]"] = f"{u.d(taxPaid)}"
2124
2143
 
2125
2144
  if self.N_i == 2 and self.n_d < self.N_n:
2126
2145
  p_j = self.partialEstate_j * (1 - self.phi_j)
@@ -2135,24 +2154,24 @@ class Plan(object):
2135
2154
  iname_s = self.inames[self.i_s]
2136
2155
  iname_d = self.inames[self.i_d]
2137
2156
  dic[f"Sum of spousal transfer to {iname_s} in year {ynx}"] = (f"{u.d(totSpousalNow)}")
2138
- dic[f"- Sum of spousal transfer to {iname_s} in year {ynx} (nominal)"] = (
2157
+ dic[f"[Sum of spousal transfer to {iname_s} in year {ynx}]"] = (
2139
2158
  f"{u.d(totSpousal)}")
2140
- dic[f"-- Spousal transfer to {iname_s} in year {ynx} - taxable (nominal)"] = (
2159
+ dic[f"-- [Spousal transfer to {iname_s} in year {ynx} - taxable]"] = (
2141
2160
  f"{u.d(q_j[0])}")
2142
- dic[f"-- Spousal transfer to {iname_s} in year {ynx} - tax-def (nominal)"] = (
2161
+ dic[f"-- [Spousal transfer to {iname_s} in year {ynx} - tax-def]"] = (
2143
2162
  f"{u.d(q_j[1])}")
2144
- dic[f"-- Spousal transfer to {iname_s} in year {ynx} - tax-free (nominal)"] = (
2163
+ dic[f"-- [Spousal transfer to {iname_s} in year {ynx} - tax-free]"] = (
2145
2164
  f"{u.d(q_j[2])}")
2146
2165
 
2147
2166
  dic[f"Sum of post-tax non-spousal bequests from {iname_d} in year {ynx}"] = (
2148
2167
  f"{u.d(totOthersNow)}")
2149
- dic[f"- Sum of post-tax non-spousal bequests from {iname_d} in year {ynx} (nominal)"] = (
2168
+ dic[f"[Sum of post-tax non-spousal bequests from {iname_d} in year {ynx}]"] = (
2150
2169
  f"{u.d(totOthers)}")
2151
- dic[f"-- Post-tax non-spousal bequests from {iname_d} in year {ynx} - taxable (nominal)"] = (
2170
+ dic[f"-- [Post-tax non-spousal bequests from {iname_d} in year {ynx} - taxable]"] = (
2152
2171
  f"{u.d(p_j[0])}")
2153
- dic[f"-- Post-tax non-spousal bequests from {iname_d} in year {ynx} - tax-def (nominal)"] = (
2172
+ dic[f"-- [Post-tax non-spousal bequests from {iname_d} in year {ynx} - tax-def]"] = (
2154
2173
  f"{u.d(p_j[1])}")
2155
- dic[f"-- Post-tax non-spousal bequests from {iname_d} in year {ynx} - tax-free (nominal)"] = (
2174
+ dic[f"-- [Post-tax non-spousal bequests from {iname_d} in year {ynx} - tax-free]"] = (
2156
2175
  f"{u.d(p_j[2])}")
2157
2176
 
2158
2177
  estate = np.sum(self.b_ijn[:, :, self.N_n], axis=0)
@@ -2161,10 +2180,10 @@ class Plan(object):
2161
2180
  totEstate = np.sum(estate)
2162
2181
  totEstateNow = totEstate / self.gamma_n[-1]
2163
2182
  dic[f"Total estate value at the end of {lastyear}"] = (f"{u.d(totEstateNow)}")
2164
- dic[f"- Total estate value at the end of {lastyear} (nominal)"] = (f"{u.d(totEstate)}")
2165
- dic[f"-- Post-tax account value at the end of {lastyear} - taxable (nominal)"] = (f"{u.d(estate[0])}")
2166
- dic[f"-- Post-tax account value at the end of {lastyear} - tax-def (nominal)"] = (f"{u.d(estate[1])}")
2167
- dic[f"-- Post-tax account value at the end of {lastyear} - tax-free (nominal)"] = (f"{u.d(estate[2])}")
2183
+ dic[f"[Total estate value at the end of {lastyear}]"] = (f"{u.d(totEstate)}")
2184
+ dic[f"-- [Post-tax account value at the end of {lastyear} - taxable]"] = (f"{u.d(estate[0])}")
2185
+ dic[f"-- [Post-tax account value at the end of {lastyear} - tax-def]"] = (f"{u.d(estate[1])}")
2186
+ dic[f"-- [Post-tax account value at the end of {lastyear} - tax-free]"] = (f"{u.d(estate[2])}")
2168
2187
 
2169
2188
  dic["Plan starting date"] = str(self.startDate)
2170
2189
  dic[f"Cumulative inflation factor from start date to end of {lastyear}"] = (f"{self.gamma_n[-1]:.2f}")
@@ -2507,6 +2526,7 @@ class Plan(object):
2507
2526
 
2508
2527
  title = self._name + "\nRaw Income Sources"
2509
2528
  stypes = self.sources_in.keys()
2529
+ # stypes = [item for item in stypes if "RothX" not in item]
2510
2530
 
2511
2531
  if tag != "":
2512
2532
  title += " - " + tag
@@ -2517,7 +2537,7 @@ class Plan(object):
2517
2537
  else:
2518
2538
  yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2519
2539
  sources_in = {}
2520
- for key in self.sources_in:
2540
+ for key in stypes:
2521
2541
  sources_in[key] = self.sources_in[key] / self.gamma_n[:-1]
2522
2542
 
2523
2543
  fig, ax = _stackPlot(
@@ -70,8 +70,8 @@ def getUnits(units) -> int:
70
70
  return fac
71
71
 
72
72
 
73
- # Could be a one-line lambda function:
74
- # krond = lambda a, b: 1 if a == b else 0
73
+ # Next two functins could be a one-line lambda functions.
74
+ # e.g., krond = lambda a, b: 1 if a == b else 0
75
75
  def krond(a, b) -> int:
76
76
  """
77
77
  Kronecker integer delta function.
@@ -79,6 +79,13 @@ def krond(a, b) -> int:
79
79
  return 1 if a == b else 0
80
80
 
81
81
 
82
+ def heavyside(x) -> int:
83
+ """
84
+ Heavyside step function.
85
+ """
86
+ return 1 if x >= 0 else 0
87
+
88
+
82
89
  def roundCents(values, decimals=2):
83
90
  """
84
91
  Round values in NumPy array down to second decimal.
@@ -0,0 +1 @@
1
+ __version__ = "2025.03.14"
@@ -89,7 +89,7 @@ def createPlan(ni, name, ny, topAge):
89
89
 
90
90
  def test_withdrawal1():
91
91
  n = 10
92
- p = createPlan(1, 'withdrawal1', n, 64)
92
+ p = createPlan(1, 'withdrawal1', n, 70)
93
93
  amount = 3.0
94
94
  p.setAccountBalances(taxable=[0], taxDeferred=[amount], taxFree=[0])
95
95
  p.setAllocationRatios('individual', generic=[[[0, 0, 0, 100], [0, 0, 0, 100]]])
@@ -102,7 +102,7 @@ def test_withdrawal1():
102
102
 
103
103
  def test_withdrawal2():
104
104
  n = 10
105
- p = createPlan(1, 'withdrawal2', n, 64)
105
+ p = createPlan(1, 'withdrawal2', n, 70)
106
106
  # Small taxable income creates an income smaller than standard deduction. Testing e_n.
107
107
  amount = 40.0
108
108
  p.setAccountBalances(taxable=[0], taxDeferred=[amount], taxFree=[0])
@@ -116,7 +116,7 @@ def test_withdrawal2():
116
116
 
117
117
  def test_withdrawal2_2():
118
118
  n = 10
119
- p = createPlan(2, 'withdrawal2_2', n, 64)
119
+ p = createPlan(2, 'withdrawal2_2', n, 70)
120
120
  # Small taxable income creates an income smaller than standard deduction. Testing e_n.
121
121
  amount = 50
122
122
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[amount/2, amount/2], taxFree=[0, 0])
@@ -130,7 +130,7 @@ def test_withdrawal2_2():
130
130
 
131
131
  def test_withdrawal3():
132
132
  n = 6
133
- p = createPlan(1, 'withdrawal3', n, 64)
133
+ p = createPlan(1, 'withdrawal3', n, 70)
134
134
  amount = 60
135
135
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
136
136
  p.setAllocationRatios('individual', generic=[[[0, 0, 0, 100], [0, 0, 0, 100]]])
@@ -143,7 +143,7 @@ def test_withdrawal3():
143
143
 
144
144
  def test_withdrawal3_2():
145
145
  n = 6
146
- p = createPlan(2, 'withdrawal3', n, 64)
146
+ p = createPlan(2, 'withdrawal3', n, 70)
147
147
  amount = 60
148
148
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
149
149
  p.setAllocationRatios('spouses', generic=[[0, 0, 0, 100], [0, 0, 0, 100]])
@@ -156,7 +156,7 @@ def test_withdrawal3_2():
156
156
 
157
157
  def test_taxfreegrowth1():
158
158
  n = 12
159
- p = createPlan(1, 'taxfreegrowth1', n, 64)
159
+ p = createPlan(1, 'taxfreegrowth1', n, 72)
160
160
  amount = 120
161
161
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
162
162
  p.setAllocationRatios('individual', generic=[[[0, 0, 100, 0], [0, 0, 100, 0]]])
@@ -170,7 +170,7 @@ def test_taxfreegrowth1():
170
170
 
171
171
  def test_taxfreegrowth1_2():
172
172
  n = 12
173
- p = createPlan(2, 'taxfreegrowth1', n, 64)
173
+ p = createPlan(2, 'taxfreegrowth1', n, 72)
174
174
  amount = 120
175
175
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
176
176
  p.setAllocationRatios('spouses', generic=[[0, 0, 100, 0], [0, 0, 100, 0]])
@@ -184,7 +184,7 @@ def test_taxfreegrowth1_2():
184
184
 
185
185
  def test_taxfreegrowth2():
186
186
  n = 15
187
- p = createPlan(1, 'taxfreegrowth2', n, 64)
187
+ p = createPlan(1, 'taxfreegrowth2', n, 75)
188
188
  amount = 120
189
189
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
190
190
  p.setAllocationRatios('individual', generic=[[[0, 50, 50, 0], [0, 50, 50, 0]]])
@@ -198,7 +198,7 @@ def test_taxfreegrowth2():
198
198
 
199
199
  def test_taxfreegrowth2_2():
200
200
  n = 15
201
- p = createPlan(2, 'taxfreegrowth2', n, 64)
201
+ p = createPlan(2, 'taxfreegrowth2', n, 75)
202
202
  amount = 120
203
203
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
204
204
  p.setAllocationRatios('spouses', generic=[[0, 50, 50, 0], [0, 50, 50, 0]])
@@ -212,7 +212,7 @@ def test_taxfreegrowth2_2():
212
212
 
213
213
  def test_taxfreegrowth3():
214
214
  n = 15
215
- p = createPlan(1, 'taxfreegrowth3', n, 64)
215
+ p = createPlan(1, 'taxfreegrowth3', n, 75)
216
216
  amount = 120
217
217
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
218
218
  p.setAllocationRatios('individual', generic=[[[50, 50, 0, 0], [50, 50, 0, 0]]])
@@ -226,7 +226,7 @@ def test_taxfreegrowth3():
226
226
 
227
227
  def test_taxfreegrowth3_2():
228
228
  n = 15
229
- p = createPlan(2, 'taxfreegrowth3', n, 64)
229
+ p = createPlan(2, 'taxfreegrowth3', n, 75)
230
230
  amount = 120
231
231
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
232
232
  p.setAllocationRatios('spouses', generic=[[50, 50, 0, 0], [50, 50, 0, 0]])
@@ -240,7 +240,7 @@ def test_taxfreegrowth3_2():
240
240
 
241
241
  def test_taxfreegrowth4():
242
242
  n = 16
243
- p = createPlan(1, 'taxfreegrowth4', n, 64)
243
+ p = createPlan(1, 'taxfreegrowth4', n, 76)
244
244
  amount = 120
245
245
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
246
246
  p.setAllocationRatios('individual', generic=[[[0, 50, 50, 0], [0, 50, 50, 0]]])
@@ -255,7 +255,7 @@ def test_taxfreegrowth4():
255
255
 
256
256
  def test_taxfreegrowth4_2():
257
257
  n = 16
258
- p = createPlan(2, 'taxfreegrowth4', n, 64)
258
+ p = createPlan(2, 'taxfreegrowth4', n, 76)
259
259
  amount = 120
260
260
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
261
261
  p.setAllocationRatios('spouses', generic=[[0, 50, 50, 0], [0, 50, 50, 0]])
@@ -270,7 +270,7 @@ def test_taxfreegrowth4_2():
270
270
 
271
271
  def test_taxfreegrowth5():
272
272
  n = 15
273
- p = createPlan(1, 'taxfreegrowth5', n, 64)
273
+ p = createPlan(1, 'taxfreegrowth5', n, 76)
274
274
  amount = 120
275
275
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
276
276
  p.setAllocationRatios('individual', generic=[[[0, 0, 100, 0], [0, 0, 100, 0]]])
@@ -284,7 +284,7 @@ def test_taxfreegrowth5():
284
284
 
285
285
  def test_taxfreegrowth5_2():
286
286
  n = 15
287
- p = createPlan(2, 'taxfreegrowth5', n, 64)
287
+ p = createPlan(2, 'taxfreegrowth5', n, 76)
288
288
  amount = 120
289
289
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
290
290
  p.setAllocationRatios('spouses', generic=[[0, 0, 100, 0], [0, 0, 100, 0]])
@@ -298,7 +298,7 @@ def test_taxfreegrowth5_2():
298
298
 
299
299
  def test_taxfreegrowth6():
300
300
  n = 15
301
- p = createPlan(1, 'taxfreegrowth6', n, 64)
301
+ p = createPlan(1, 'taxfreegrowth6', n, 76)
302
302
  amount = 120
303
303
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
304
304
  p.setAllocationRatios('individual', generic=[[[0, 0, 0, 100], [0, 0, 0, 100]]])
@@ -312,7 +312,7 @@ def test_taxfreegrowth6():
312
312
 
313
313
  def test_taxfreegrowth6_2():
314
314
  n = 15
315
- p = createPlan(2, 'taxfreegrowth6', n, 64)
315
+ p = createPlan(2, 'taxfreegrowth6', n, 76)
316
316
  amount = 120
317
317
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
318
318
  p.setAllocationRatios('spouses', generic=[[0, 0, 0, 100], [0, 0, 0, 100]])
@@ -326,7 +326,7 @@ def test_taxfreegrowth6_2():
326
326
 
327
327
  def test_taxfreegrowth7():
328
328
  n = 15
329
- p = createPlan(1, 'taxfreegrowth7', n, 64)
329
+ p = createPlan(1, 'taxfreegrowth7', n, 76)
330
330
  amount = 120
331
331
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
332
332
  p.setAllocationRatios('individual', generic=[[[0, 100, 0, 0], [0, 100, 0, 0]]])
@@ -340,7 +340,7 @@ def test_taxfreegrowth7():
340
340
 
341
341
  def test_taxfreegrowth7_2():
342
342
  n = 15
343
- p = createPlan(2, 'taxfreegrowth7', n, 64)
343
+ p = createPlan(2, 'taxfreegrowth7', n, 76)
344
344
  amount = 120
345
345
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
346
346
  p.setAllocationRatios('spouses', generic=[[0, 100, 0, 0], [0, 100, 0, 0]])
@@ -354,7 +354,7 @@ def test_taxfreegrowth7_2():
354
354
 
355
355
  def test_taxfreegrowth8():
356
356
  n = 15
357
- p = createPlan(1, 'taxfreegrowth8', n, 64)
357
+ p = createPlan(1, 'taxfreegrowth8', n, 76)
358
358
  amount = 120
359
359
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
360
360
  p.setAllocationRatios('individual', generic=[[[100, 0, 0, 0], [100, 0, 0, 0]]])
@@ -368,7 +368,7 @@ def test_taxfreegrowth8():
368
368
 
369
369
  def test_taxfreegrowth8_2():
370
370
  n = 15
371
- p = createPlan(2, 'taxfreegrowth8', n, 64)
371
+ p = createPlan(2, 'taxfreegrowth8', n, 76)
372
372
  amount = 120
373
373
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
374
374
  p.setAllocationRatios('spouses', generic=[[100, 0, 0, 0], [100, 0, 0, 0]])
@@ -382,7 +382,7 @@ def test_taxfreegrowth8_2():
382
382
 
383
383
  def test_annuity1():
384
384
  n = 12
385
- p = createPlan(1, 'annuity1', n, 64)
385
+ p = createPlan(1, 'annuity1', n, 76)
386
386
  amount = 120
387
387
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
388
388
  p.setAllocationRatios('individual', generic=[[[0, 0, 100, 0], [0, 0, 100, 0]]])
@@ -400,7 +400,7 @@ def test_annuity1():
400
400
 
401
401
  def test_annuity1_2():
402
402
  n = 12
403
- p = createPlan(2, 'annuity1', n, 64)
403
+ p = createPlan(2, 'annuity1', n, 76)
404
404
  amount = 120
405
405
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
406
406
  p.setAllocationRatios('spouses', generic=[[0, 0, 100, 0], [0, 0, 100, 0]])
@@ -418,7 +418,7 @@ def test_annuity1_2():
418
418
 
419
419
  def test_annuity2():
420
420
  n = 18
421
- p = createPlan(1, 'annuity2', n, 64)
421
+ p = createPlan(1, 'annuity2', n, 76)
422
422
  amount = 120
423
423
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
424
424
  p.setAllocationRatios('individual', generic=[[[0, 0, 100, 0], [0, 0, 100, 0]]])
@@ -436,7 +436,7 @@ def test_annuity2():
436
436
 
437
437
  def test_annuity2_2():
438
438
  n = 18
439
- p = createPlan(2, 'annuity2', n, 64)
439
+ p = createPlan(2, 'annuity2', n, 78)
440
440
  amount = 120
441
441
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
442
442
  p.setAllocationRatios('spouses', generic=[[0, 0, 100, 0], [0, 0, 100, 0]])
@@ -454,7 +454,7 @@ def test_annuity2_2():
454
454
 
455
455
  def test_annuity3():
456
456
  n = 30
457
- p = createPlan(1, 'annuity2', n, 64)
457
+ p = createPlan(1, 'annuity2', n, 90)
458
458
  amount = 100
459
459
  p.setAccountBalances(taxable=[0], taxDeferred=[0], taxFree=[amount])
460
460
  p.setAllocationRatios('individual', generic=[[[0, 0, 100, 0], [0, 0, 100, 0]]])
@@ -472,7 +472,7 @@ def test_annuity3():
472
472
 
473
473
  def test_annuity3_2():
474
474
  n = 30
475
- p = createPlan(2, 'annuity2', n, 64)
475
+ p = createPlan(2, 'annuity2', n, 90)
476
476
  amount = 100
477
477
  p.setAccountBalances(taxable=[0, 0], taxDeferred=[0, 0], taxFree=[amount/2, amount/2])
478
478
  p.setAllocationRatios('spouses', generic=[[0, 0, 100, 0], [0, 0, 100, 0]])
@@ -53,6 +53,13 @@ reported as yearly values. These include wages, income, rates, social security,
53
53
  Dollar values are typically entered in thousands, unless in tables, where they
54
54
  are entered and reported in unit dollars.
55
55
 
56
+ If you are accessing Owl through the Chrome browser,
57
+ the performance manager might be configured to disable hidden or inactive tabs.
58
+ This will cause your Owl session to inadvertently reset, and losing the state of the calculator.
59
+ To avoid this, configure Chrome to keep the page active using
60
+ `More Tools` -> `Performance` -> `Always keep these sites active` and
61
+ add the site *owlplanner.streamlit.app*.
62
+
56
63
  There are four sections in the user interface:
57
64
  **Case Setup**, **Single Scenario**, **Multiple Scenarios**, and **Resources**.
58
65
  The sections below follow the same logical order.
@@ -20,7 +20,7 @@ else:
20
20
  st.write("#### Synopsis")
21
21
  styledDf = df[1:].style.map(kz.colorBySign)
22
22
  st.dataframe(styledDf, use_container_width=True)
23
- st.caption("Values are in today's \\$ unless marked otherwise.")
23
+ st.caption("Values with [legend] are nominal, otherwise in today's \\$.")
24
24
  st.download_button(
25
25
  "Download synopsis", data=df[1:].to_string(), file_name=f"Synopsis_{caseName}.txt",
26
26
  mime="text/plain;charset=UTF-8"
@@ -93,10 +93,11 @@ else:
93
93
 
94
94
  col1, col2, col3, col4 = st.columns(4, gap="large", vertical_alignment="top")
95
95
  with col1:
96
+ maxValue = owb.TO if kz.getKey("varyingType") == "historical" else kz.getKey("yto") - 1
96
97
  st.number_input(
97
98
  "Starting year",
98
99
  min_value=owb.FROM,
99
- max_value=kz.getKey("yto") - 1,
100
+ max_value=maxValue,
100
101
  value=kz.getKey("yfrm"),
101
102
  on_change=updateRates,
102
103
  args=["yfrm"],
@@ -228,5 +229,5 @@ else:
228
229
 
229
230
  with col3:
230
231
  kz.initKey("yTCJA", 2026)
231
- helpmsg = "Year at which the Tax Cut And Job Act tax rates are speculated to expire."
232
+ helpmsg = "Year at which the Tax Cut And Job Act tax rates are speculated to be expired."
232
233
  ret = kz.getIntNum("TCJA expiration year", "yTCJA", help=helpmsg)
@@ -7,4 +7,4 @@ scipy
7
7
  streamlit
8
8
  toml
9
9
  # --extra-index-url https://test.pypi.org/simple
10
- owlplanner >= 2025.03.12
10
+ owlplanner >= 2025.03.14
@@ -510,7 +510,6 @@ def orangeDivider():
510
510
 
511
511
 
512
512
  def titleBar(txt, choices=None):
513
- # st.html(f"<div style='text-align: left;color: orange;font-style: italic;'>{currentCaseName()}</div>")
514
513
  if choices is None:
515
514
  choices = onlyCaseNames()
516
515
  helpmsg = "Select an existing case."
@@ -1 +0,0 @@
1
- __version__ = "2025.03.12"
File without changes
File without changes
File without changes
File without changes