owlplanner 2025.3.12__py3-none-any.whl → 2025.3.14__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/plan.py CHANGED
@@ -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(
owlplanner/utils.py CHANGED
@@ -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.
owlplanner/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.03.12"
1
+ __version__ = "2025.03.14"
@@ -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
@@ -2,16 +2,16 @@ owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
2
2
  owlplanner/abcapi.py,sha256=LbzW_KcNy0IeHp42MUHwGu_H67B2h_e1_vu-c2ACTkQ,6646
3
3
  owlplanner/config.py,sha256=uJ6jZ9Rx2CB2P5cFRscsUCXOW6uml7I4pta2ozjW0uo,12263
4
4
  owlplanner/logging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
5
- owlplanner/plan.py,sha256=mCcoocK9g1nCUpQDaMVUTiwugKPuDZaEYbrSPPi0sBY,115404
5
+ owlplanner/plan.py,sha256=2uS_N121p-fPhCboVSAthdt_uVw109ltLTvCLeGouuo,116263
6
6
  owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
7
7
  owlplanner/rates.py,sha256=TN407qU4n-bac1oymkQ_n2QKEPwFQxy6JZVGwgIkLQU,15585
8
8
  owlplanner/tax2025.py,sha256=B-A5eU3wxdcAaxRCbT3qI-JEKoD_ZeNbg_86XhNdQEI,7745
9
9
  owlplanner/timelists.py,sha256=tYieZU67FT6TCcQQis36JaXGI7dT6NqD7RvdEjgJL4M,4026
10
- owlplanner/utils.py,sha256=HM70W60qB41zfnbl2LltNwAuLYHyy5XYbwnbNcaa6FE,2351
11
- owlplanner/version.py,sha256=nHgfINF2PnODSOO9bLDEpnzh97BOhFuEmlbblPAAeZY,28
10
+ owlplanner/utils.py,sha256=WpJgn79YZfH8UCkcmhd-AZlxlGuz1i1-UDBRXImsY6I,2485
11
+ owlplanner/version.py,sha256=gPOv802nGWM84uQsfLVWHJKlgbpuozzBAjyW-OgDkzE,28
12
12
  owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
14
- owlplanner-2025.3.12.dist-info/METADATA,sha256=8xb-RhkQGUz2oxEs7Ux_QgQ4f31gaeFdPA_CJ5rOjrw,53801
15
- owlplanner-2025.3.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- owlplanner-2025.3.12.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
17
- owlplanner-2025.3.12.dist-info/RECORD,,
14
+ owlplanner-2025.3.14.dist-info/METADATA,sha256=C5BlpAPu2CwtVQmuBUPDTYi7mXg86T3b0dYR5anPUak,53801
15
+ owlplanner-2025.3.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ owlplanner-2025.3.14.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
17
+ owlplanner-2025.3.14.dist-info/RECORD,,