owlplanner 2025.3.30__py3-none-any.whl → 2025.4.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.
- owlplanner/config.py +6 -0
- owlplanner/plan.py +39 -25
- owlplanner/version.py +1 -1
- {owlplanner-2025.3.30.dist-info → owlplanner-2025.4.2.dist-info}/METADATA +1 -1
- {owlplanner-2025.3.30.dist-info → owlplanner-2025.4.2.dist-info}/RECORD +7 -7
- {owlplanner-2025.3.30.dist-info → owlplanner-2025.4.2.dist-info}/WHEEL +0 -0
- {owlplanner-2025.3.30.dist-info → owlplanner-2025.4.2.dist-info}/licenses/LICENSE +0 -0
owlplanner/config.py
CHANGED
|
@@ -12,6 +12,7 @@ Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
|
12
12
|
import toml as toml
|
|
13
13
|
from io import StringIO, BytesIO
|
|
14
14
|
import numpy as np
|
|
15
|
+
from datetime import date
|
|
15
16
|
import os
|
|
16
17
|
|
|
17
18
|
from owlplanner import plan
|
|
@@ -302,6 +303,11 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
|
302
303
|
if name != "None" and name not in p.inames:
|
|
303
304
|
raise ValueError(f"Unknown name {name} for noRothConversions.")
|
|
304
305
|
|
|
306
|
+
# Rebase startRothConversions on year change.
|
|
307
|
+
thisyear = date.today().year
|
|
308
|
+
year = p.solverOptions.get("startRothConversions", thisyear)
|
|
309
|
+
diconf["Solver Options"]["startRothConversions"] = max(year, thisyear)
|
|
310
|
+
|
|
305
311
|
# Results.
|
|
306
312
|
p.setDefaultPlots(diconf["Results"]["Default plots"])
|
|
307
313
|
|
owlplanner/plan.py
CHANGED
|
@@ -1122,16 +1122,17 @@ class Plan(object):
|
|
|
1122
1122
|
B.set0_Ub(_q1(Ce, n, Nn), self.sigmaBar_n[n])
|
|
1123
1123
|
|
|
1124
1124
|
# Roth conversions equalities/inequalities.
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1125
|
+
# This condition supercedes everything else.
|
|
1126
|
+
if "maxRothConversion" in options and options["maxRothConversion"] == "file":
|
|
1127
|
+
# self.mylog.vprint(f"Fixing Roth conversions to those from file {self.timeListsFileName}.")
|
|
1128
|
+
for i in range(Ni):
|
|
1129
|
+
for n in range(self.horizons[i]):
|
|
1130
|
+
rhs = self.myRothX_in[i][n]
|
|
1131
|
+
B.setRange(_q2(Cx, i, n, Ni, Nn), rhs, rhs)
|
|
1132
|
+
else:
|
|
1133
|
+
if "maxRothConversion" in options:
|
|
1133
1134
|
rhsopt = options["maxRothConversion"]
|
|
1134
|
-
assert isinstance(rhsopt, (int, float)), "Specified
|
|
1135
|
+
assert isinstance(rhsopt, (int, float)), "Specified maxRothConversion is not a number."
|
|
1135
1136
|
rhsopt *= units
|
|
1136
1137
|
if rhsopt < 0:
|
|
1137
1138
|
# self.mylog.vprint('Unlimited Roth conversions (<0)')
|
|
@@ -1143,16 +1144,28 @@ class Plan(object):
|
|
|
1143
1144
|
# Should we adjust Roth conversion cap with inflation?
|
|
1144
1145
|
B.set0_Ub(_q2(Cx, i, n, Ni, Nn), rhsopt)
|
|
1145
1146
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
raise ValueError(f"Unknown individual {rhsopt} for noRothConversions:")
|
|
1147
|
+
# Process startRothConversions option.
|
|
1148
|
+
if "startRothConversions" in options:
|
|
1149
|
+
rhsopt = options["startRothConversions"]
|
|
1150
|
+
assert isinstance(rhsopt, (int, float)), "Specified startRothConversions is not a number."
|
|
1151
|
+
thisyear = date.today().year
|
|
1152
|
+
yearn = max(rhsopt - thisyear, 0)
|
|
1153
1153
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1154
|
+
for i in range(Ni):
|
|
1155
|
+
nstart = min(yearn, self.horizons[i])
|
|
1156
|
+
for n in range(0, nstart):
|
|
1157
|
+
B.set0_Ub(_q2(Cx, i, n, Ni, Nn), zero)
|
|
1158
|
+
|
|
1159
|
+
# Process noRothConversions option. Also valid when N_i == 1, why not?
|
|
1160
|
+
if "noRothConversions" in options and options["noRothConversions"] != "None":
|
|
1161
|
+
rhsopt = options["noRothConversions"]
|
|
1162
|
+
try:
|
|
1163
|
+
i_x = self.inames.index(rhsopt)
|
|
1164
|
+
except ValueError:
|
|
1165
|
+
raise ValueError(f"Unknown individual {rhsopt} for noRothConversions:")
|
|
1166
|
+
|
|
1167
|
+
for n in range(Nn):
|
|
1168
|
+
B.set0_Ub(_q2(Cx, i_x, n, Ni, Nn), zero)
|
|
1156
1169
|
|
|
1157
1170
|
# Impose withdrawal limits on taxable and tax-exempt accounts.
|
|
1158
1171
|
for i in range(Ni):
|
|
@@ -1197,7 +1210,7 @@ class Plan(object):
|
|
|
1197
1210
|
# Account for time elapsed in the current year.
|
|
1198
1211
|
spending *= units * self.yearFracLeft
|
|
1199
1212
|
# self.mylog.vprint('Maximizing bequest with desired net spending of:', u.d(spending))
|
|
1200
|
-
# To allow slack in first year, Cg can be made Nn+1 and store basis in g[Nn].
|
|
1213
|
+
# To allow slack in first year, Cg can be made Nn+1 and store basis in g[Nn].
|
|
1201
1214
|
A.addNewRow({_q1(Cg, 0, Nn): 1}, spending, spending)
|
|
1202
1215
|
|
|
1203
1216
|
# Set initial balances through constraints.
|
|
@@ -1616,16 +1629,17 @@ class Plan(object):
|
|
|
1616
1629
|
knownObjectives = ["maxBequest", "maxSpending"]
|
|
1617
1630
|
knownSolvers = ["HiGHS", "MOSEK"]
|
|
1618
1631
|
knownOptions = [
|
|
1619
|
-
"units",
|
|
1620
|
-
"maxRothConversion",
|
|
1621
|
-
"netSpending",
|
|
1622
|
-
"spendingSlack",
|
|
1623
1632
|
"bequest",
|
|
1624
1633
|
"bigM",
|
|
1634
|
+
"maxRothConversion",
|
|
1635
|
+
"netSpending",
|
|
1625
1636
|
"noRothConversions",
|
|
1626
|
-
"withMedicare",
|
|
1627
|
-
"solver",
|
|
1628
1637
|
"previousMAGIs",
|
|
1638
|
+
"solver",
|
|
1639
|
+
"spendingSlack",
|
|
1640
|
+
"startRothConversions",
|
|
1641
|
+
"units",
|
|
1642
|
+
"withMedicare",
|
|
1629
1643
|
]
|
|
1630
1644
|
# We will modify options if required.
|
|
1631
1645
|
if options is None:
|
owlplanner/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2025.
|
|
1
|
+
__version__ = "2025.04.02"
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
|
|
2
2
|
owlplanner/abcapi.py,sha256=LbzW_KcNy0IeHp42MUHwGu_H67B2h_e1_vu-c2ACTkQ,6646
|
|
3
|
-
owlplanner/config.py,sha256=
|
|
3
|
+
owlplanner/config.py,sha256=F6GS3n02VeFX0GCVeM4J7Ra0in4N632W6TZIXk7Yj2w,12519
|
|
4
4
|
owlplanner/logging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
|
|
5
|
-
owlplanner/plan.py,sha256=
|
|
5
|
+
owlplanner/plan.py,sha256=obzXmEJhtkIhmTFOR02Q6g1deigUkFnufyv54EqvD5o,117672
|
|
6
6
|
owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
|
|
7
7
|
owlplanner/rates.py,sha256=gJaoe-gJqWCQV5qVLlHp-Yn9TSJs-PJzeTbOwMCbqWs,15682
|
|
8
8
|
owlplanner/tax2025.py,sha256=HEXfL0HfwUvZOQRjivXO2jFeoVZ5m_yk_hoMiVi-hR0,7745
|
|
9
9
|
owlplanner/timelists.py,sha256=tYieZU67FT6TCcQQis36JaXGI7dT6NqD7RvdEjgJL4M,4026
|
|
10
10
|
owlplanner/utils.py,sha256=WpJgn79YZfH8UCkcmhd-AZlxlGuz1i1-UDBRXImsY6I,2485
|
|
11
|
-
owlplanner/version.py,sha256=
|
|
11
|
+
owlplanner/version.py,sha256=TElp9wm8J0rr4k4sU8OgxW2Rf9Zy8iAW808A6RDo1lE,28
|
|
12
12
|
owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
|
|
14
|
-
owlplanner-2025.
|
|
15
|
-
owlplanner-2025.
|
|
16
|
-
owlplanner-2025.
|
|
17
|
-
owlplanner-2025.
|
|
14
|
+
owlplanner-2025.4.2.dist-info/METADATA,sha256=Ro0E2vypz99zMe7EBwOTgJRnoHnEWB9yIt004agJKrA,53800
|
|
15
|
+
owlplanner-2025.4.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
+
owlplanner-2025.4.2.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
17
|
+
owlplanner-2025.4.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|