owlplanner 2025.5.3__py3-none-any.whl → 2025.5.12__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/__init__.py +8 -6
- owlplanner/abcapi.py +14 -7
- owlplanner/config.py +7 -7
- owlplanner/plan.py +435 -829
- owlplanner/plotting/__init__.py +7 -0
- owlplanner/plotting/base.py +76 -0
- owlplanner/plotting/factory.py +32 -0
- owlplanner/plotting/matplotlib_backend.py +432 -0
- owlplanner/plotting/plotly_backend.py +980 -0
- owlplanner/rates.py +40 -68
- owlplanner/tax2025.py +2 -1
- owlplanner/timelists.py +3 -8
- owlplanner/version.py +1 -1
- {owlplanner-2025.5.3.dist-info → owlplanner-2025.5.12.dist-info}/METADATA +8 -5
- owlplanner-2025.5.12.dist-info/RECORD +22 -0
- owlplanner-2025.5.3.dist-info/RECORD +0 -17
- /owlplanner/{logging.py → mylogging.py} +0 -0
- {owlplanner-2025.5.3.dist-info → owlplanner-2025.5.12.dist-info}/WHEEL +0 -0
- {owlplanner-2025.5.3.dist-info → owlplanner-2025.5.12.dist-info}/licenses/LICENSE +0 -0
owlplanner/plan.py
CHANGED
|
@@ -16,26 +16,21 @@ Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
|
16
16
|
###########################################################################
|
|
17
17
|
import numpy as np
|
|
18
18
|
import pandas as pd
|
|
19
|
-
import matplotlib.pyplot as plt
|
|
20
19
|
from datetime import date, datetime
|
|
21
20
|
from functools import wraps
|
|
22
21
|
from openpyxl import Workbook
|
|
23
22
|
from openpyxl.utils.dataframe import dataframe_to_rows
|
|
24
23
|
import time
|
|
25
|
-
import io
|
|
26
24
|
|
|
27
|
-
from
|
|
28
|
-
from
|
|
29
|
-
from
|
|
30
|
-
from
|
|
31
|
-
from
|
|
32
|
-
from
|
|
33
|
-
from
|
|
34
|
-
from
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# This makes all graphs to have the same height.
|
|
38
|
-
plt.rcParams.update({'figure.autolayout': True})
|
|
25
|
+
from . import utils as u
|
|
26
|
+
from . import tax2025 as tx
|
|
27
|
+
from . import abcapi as abc
|
|
28
|
+
from . import rates
|
|
29
|
+
from . import config
|
|
30
|
+
from . import timelists
|
|
31
|
+
from . import mylogging as log
|
|
32
|
+
from . import progress
|
|
33
|
+
from .plotting.factory import PlotFactory
|
|
39
34
|
|
|
40
35
|
|
|
41
36
|
def _genGamma_n(tau):
|
|
@@ -246,14 +241,20 @@ class Plan(object):
|
|
|
246
241
|
self._description = ''
|
|
247
242
|
self.defaultPlots = "nominal"
|
|
248
243
|
self.defaultSolver = "HiGHS"
|
|
244
|
+
self._plotterName = None
|
|
245
|
+
self.setPlotBackend("matplotlib")
|
|
249
246
|
|
|
250
247
|
self.N_i = len(yobs)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
248
|
+
if not (0 <= self.N_i <= 2):
|
|
249
|
+
raise ValueError(f"Cannot support {self.N_i} individuals.")
|
|
250
|
+
if self.N_i != len(expectancy):
|
|
251
|
+
raise ValueError(f"Expectancy must have {self.N_i} entries.")
|
|
252
|
+
if self.N_i != len(inames):
|
|
253
|
+
raise ValueError(f"Names for individuals must have {self.N_i} entries.")
|
|
254
|
+
if inames[0] == "" or (self.N_i == 2 and inames[1] == ""):
|
|
255
|
+
raise ValueError("Name for each individual must be provided.")
|
|
256
|
+
|
|
257
|
+
self.filingStatus = ("single", "married")[self.N_i - 1]
|
|
257
258
|
# Default year TCJA is speculated to expire.
|
|
258
259
|
self.yTCJA = 2026
|
|
259
260
|
self.inames = inames
|
|
@@ -263,10 +264,9 @@ class Plan(object):
|
|
|
263
264
|
# Reference time is starting date in the current year and all passings are assumed at the end.
|
|
264
265
|
thisyear = date.today().year
|
|
265
266
|
self.horizons = self.yobs + self.expectancy - thisyear + 1
|
|
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.
|
|
269
|
+
# Year index in the plan (if any) where individuals turn 59. For 10% withdrawal penalty.
|
|
270
270
|
self.n59 = 59 - thisyear + self.yobs
|
|
271
271
|
self.n59[self.n59 < 0] = 0
|
|
272
272
|
# Handle passing of one spouse before the other.
|
|
@@ -311,7 +311,7 @@ class Plan(object):
|
|
|
311
311
|
self.lambdha = 0
|
|
312
312
|
|
|
313
313
|
# Scenario starts at the beginning of this year and ends at the end of the last year.
|
|
314
|
-
s =
|
|
314
|
+
s = ("", "s")[self.N_i - 1]
|
|
315
315
|
self.mylog.vprint(f"Preparing scenario of {self.N_n} years for {self.N_i} individual{s}.")
|
|
316
316
|
for i in range(self.N_i):
|
|
317
317
|
endyear = thisyear + self.horizons[i] - 1
|
|
@@ -346,7 +346,7 @@ class Plan(object):
|
|
|
346
346
|
self.mylog = logger
|
|
347
347
|
|
|
348
348
|
def setLogstreams(self, verbose, logstreams):
|
|
349
|
-
self.mylog =
|
|
349
|
+
self.mylog = log.Logger(verbose, logstreams)
|
|
350
350
|
# self.mylog.vprint(f"Setting logstreams to {logstreams}.")
|
|
351
351
|
|
|
352
352
|
def logger(self):
|
|
@@ -400,13 +400,11 @@ class Plan(object):
|
|
|
400
400
|
if value is None:
|
|
401
401
|
return self.defaultPlots
|
|
402
402
|
|
|
403
|
-
opts =
|
|
404
|
-
if value in opts:
|
|
405
|
-
|
|
403
|
+
opts = ("nominal", "today")
|
|
404
|
+
if value not in opts:
|
|
405
|
+
raise ValueError(f"Value type must be one of: {opts}")
|
|
406
406
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return None
|
|
407
|
+
return value
|
|
410
408
|
|
|
411
409
|
def rename(self, newname):
|
|
412
410
|
"""
|
|
@@ -433,7 +431,8 @@ class Plan(object):
|
|
|
433
431
|
where s_n is the surplus amount. Here d_0n is the taxable account
|
|
434
432
|
deposit for the first spouse while d_1n is for the second spouse.
|
|
435
433
|
"""
|
|
436
|
-
|
|
434
|
+
if not (0 <= eta <= 1):
|
|
435
|
+
raise ValueError("Fraction must be between 0 and 1.")
|
|
437
436
|
if self.N_i != 2:
|
|
438
437
|
self.mylog.vprint("Deposit fraction can only be 0 for single individuals.")
|
|
439
438
|
eta = 0
|
|
@@ -450,13 +449,26 @@ class Plan(object):
|
|
|
450
449
|
self.defaultPlots = self._checkValue(value)
|
|
451
450
|
self.mylog.vprint(f"Setting plots default value to {value}.")
|
|
452
451
|
|
|
452
|
+
def setPlotBackend(self, backend: str):
|
|
453
|
+
"""
|
|
454
|
+
Set plotting backend.
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
if backend not in ("matplotlib", "plotly"):
|
|
458
|
+
raise ValueError(f"Backend {backend} not a valid option.")
|
|
459
|
+
|
|
460
|
+
if backend != self._plotterName:
|
|
461
|
+
self._plotter = PlotFactory.createBackend(backend)
|
|
462
|
+
self.mylog.vprint(f"Setting plotting backend to {backend}.")
|
|
463
|
+
|
|
453
464
|
def setDividendRate(self, mu):
|
|
454
465
|
"""
|
|
455
|
-
Set dividend rate
|
|
466
|
+
Set dividend tax rate. Rate is in percent. Default 2%.
|
|
456
467
|
"""
|
|
457
|
-
|
|
468
|
+
if not (0 <= mu <= 100):
|
|
469
|
+
raise ValueError("Rate must be between 0 and 100.")
|
|
458
470
|
mu /= 100
|
|
459
|
-
self.mylog.vprint(f"Dividend
|
|
471
|
+
self.mylog.vprint(f"Dividend tax rate set to {u.pc(mu, f=0)}.")
|
|
460
472
|
self.mu = mu
|
|
461
473
|
self.caseStatus = "modified"
|
|
462
474
|
|
|
@@ -473,7 +485,8 @@ class Plan(object):
|
|
|
473
485
|
"""
|
|
474
486
|
Set long-term income tax rate. Rate is in percent. Default 15%.
|
|
475
487
|
"""
|
|
476
|
-
|
|
488
|
+
if not (0 <= psi <= 100):
|
|
489
|
+
raise ValueError("Rate must be between 0 and 100.")
|
|
477
490
|
psi /= 100
|
|
478
491
|
self.mylog.vprint(f"Long-term capital gain income tax set to {u.pc(psi, f=0)}.")
|
|
479
492
|
self.psi = psi
|
|
@@ -484,10 +497,11 @@ class Plan(object):
|
|
|
484
497
|
Set fractions of savings accounts that is left to surviving spouse.
|
|
485
498
|
Default is [1, 1, 1] for taxable, tax-deferred, adn tax-exempt accounts.
|
|
486
499
|
"""
|
|
487
|
-
|
|
500
|
+
if len(phi) != self.N_j:
|
|
501
|
+
raise ValueError(f"Fractions must have {self.N_j} entries.")
|
|
488
502
|
for j in range(self.N_j):
|
|
489
|
-
|
|
490
|
-
|
|
503
|
+
if not (0 <= phi[j] <= 1):
|
|
504
|
+
raise ValueError("Fractions must be between 0 and 1.")
|
|
491
505
|
self.phi_j = np.array(phi, dtype=np.float32)
|
|
492
506
|
self.mylog.vprint("Spousal beneficiary fractions set to",
|
|
493
507
|
["{:.2f}".format(self.phi_j[j]) for j in range(self.N_j)])
|
|
@@ -502,20 +516,24 @@ class Plan(object):
|
|
|
502
516
|
Set the heirs tax rate on the tax-deferred portion of the estate.
|
|
503
517
|
Rate is in percent. Default is 30%.
|
|
504
518
|
"""
|
|
505
|
-
|
|
519
|
+
if not (0 <= nu <= 100):
|
|
520
|
+
raise ValueError("Rate must be between 0 and 100.")
|
|
506
521
|
nu /= 100
|
|
507
522
|
self.mylog.vprint(f"Heirs tax rate on tax-deferred portion of estate set to {u.pc(nu, f=0)}.")
|
|
508
523
|
self.nu = nu
|
|
509
524
|
self.caseStatus = "modified"
|
|
510
525
|
|
|
511
|
-
def setPension(self, amounts, ages, indexed=
|
|
526
|
+
def setPension(self, amounts, ages, indexed=(False, False), units="k"):
|
|
512
527
|
"""
|
|
513
528
|
Set value of pension for each individual and commencement age.
|
|
514
529
|
Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
515
530
|
"""
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
531
|
+
if len(amounts) != self.N_i:
|
|
532
|
+
raise ValueError(f"Amounts must have {self.N_i} entries.")
|
|
533
|
+
if len(ages) != self.N_i:
|
|
534
|
+
raise ValueError(f"Ages must have {self.N_i} entries.")
|
|
535
|
+
if len(indexed) < self.N_i:
|
|
536
|
+
raise ValueError(f"Indexed list must have at least {self.N_i} entries.")
|
|
519
537
|
|
|
520
538
|
fac = u.getUnits(units)
|
|
521
539
|
amounts = u.rescale(amounts, fac)
|
|
@@ -546,8 +564,10 @@ class Plan(object):
|
|
|
546
564
|
Set value of social security for each individual and commencement age.
|
|
547
565
|
Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
548
566
|
"""
|
|
549
|
-
|
|
550
|
-
|
|
567
|
+
if len(amounts) != self.N_i:
|
|
568
|
+
raise ValueError(f"Amounts must have {self.N_i} entries.")
|
|
569
|
+
if len(ages) != self.N_i:
|
|
570
|
+
raise ValueError(f"Ages must have {self.N_i} entries.")
|
|
551
571
|
|
|
552
572
|
fac = u.getUnits(units)
|
|
553
573
|
amounts = u.rescale(amounts, fac)
|
|
@@ -582,10 +602,14 @@ class Plan(object):
|
|
|
582
602
|
as a second argument. Default value is 60%.
|
|
583
603
|
Dip and increase are percent changes in the smile profile.
|
|
584
604
|
"""
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
605
|
+
if not (0 <= percent <= 100):
|
|
606
|
+
raise ValueError(f"Survivor value {percent} outside range.")
|
|
607
|
+
if not (0 <= dip <= 100):
|
|
608
|
+
raise ValueError(f"Dip value {dip} outside range.")
|
|
609
|
+
if not (-100 <= increase <= 100):
|
|
610
|
+
raise ValueError(f"Increase value {increase} outside range.")
|
|
611
|
+
if not (0 <= delay <= self.N_n - 2):
|
|
612
|
+
raise ValueError(f"Delay value {delay} outside year range.")
|
|
589
613
|
|
|
590
614
|
self.chi = percent / 100
|
|
591
615
|
|
|
@@ -676,7 +700,8 @@ class Plan(object):
|
|
|
676
700
|
raise RuntimeError("A rate method needs to be first selected using setRates(...).")
|
|
677
701
|
|
|
678
702
|
thisyear = date.today().year
|
|
679
|
-
|
|
703
|
+
if year <= thisyear:
|
|
704
|
+
raise RuntimeError("Internal error in forwardValue().")
|
|
680
705
|
span = year - thisyear
|
|
681
706
|
|
|
682
707
|
return amount * self.gamma_n[span]
|
|
@@ -688,9 +713,12 @@ class Plan(object):
|
|
|
688
713
|
one entry. Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
|
|
689
714
|
"""
|
|
690
715
|
plurals = ["", "y", "ies"][self.N_i]
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
716
|
+
if len(taxable) != self.N_i:
|
|
717
|
+
raise ValueError(f"taxable must have {self.N_i} entr{plurals}.")
|
|
718
|
+
if len(taxDeferred) != self.N_i:
|
|
719
|
+
raise ValueError(f"taxDeferred must have {self.N_i} entr{plurals}.")
|
|
720
|
+
if len(taxFree) != self.N_i:
|
|
721
|
+
raise ValueError(f"taxFree must have {self.N_i} entr{plurals}.")
|
|
694
722
|
|
|
695
723
|
fac = u.getUnits(units)
|
|
696
724
|
taxable = u.rescale(taxable, fac)
|
|
@@ -766,13 +794,17 @@ class Plan(object):
|
|
|
766
794
|
if allocType == "account":
|
|
767
795
|
# Make sure we have proper input.
|
|
768
796
|
for item in [taxable, taxDeferred, taxFree]:
|
|
769
|
-
|
|
797
|
+
if len(item) != self.N_i:
|
|
798
|
+
raise ValueError(f"{item} must have one entry per individual.")
|
|
770
799
|
for i in range(self.N_i):
|
|
771
800
|
# Initial and final.
|
|
772
|
-
|
|
801
|
+
if len(item[i]) != 2:
|
|
802
|
+
raise ValueError(f"{item}[{i}] must have 2 lists (initial and final).")
|
|
773
803
|
for z in range(2):
|
|
774
|
-
|
|
775
|
-
|
|
804
|
+
if len(item[i][z]) != self.N_k:
|
|
805
|
+
raise ValueError(f"{item}[{i}][{z}] must have {self.N_k} entries.")
|
|
806
|
+
if abs(sum(item[i][z]) - 100) > 0.01:
|
|
807
|
+
raise ValueError("Sum of percentages must add to 100.")
|
|
776
808
|
|
|
777
809
|
for i in range(self.N_i):
|
|
778
810
|
self.mylog.vprint(f"{self.inames[i]}: Setting gliding allocation ratios (%) to {allocType}.")
|
|
@@ -799,13 +831,17 @@ class Plan(object):
|
|
|
799
831
|
self.boundsAR["tax-free"] = taxFree
|
|
800
832
|
|
|
801
833
|
elif allocType == "individual":
|
|
802
|
-
|
|
834
|
+
if len(generic) != self.N_i:
|
|
835
|
+
raise ValueError("generic must have one list per individual.")
|
|
803
836
|
for i in range(self.N_i):
|
|
804
837
|
# Initial and final.
|
|
805
|
-
|
|
838
|
+
if len(generic[i]) != 2:
|
|
839
|
+
raise ValueError(f"generic[{i}] must have 2 lists (initial and final).")
|
|
806
840
|
for z in range(2):
|
|
807
|
-
|
|
808
|
-
|
|
841
|
+
if len(generic[i][z]) != self.N_k:
|
|
842
|
+
raise ValueError(f"generic[{i}][{z}] must have {self.N_k} entries.")
|
|
843
|
+
if abs(sum(generic[i][z]) - 100) > 0.01:
|
|
844
|
+
raise ValueError("Sum of percentages must add to 100.")
|
|
809
845
|
|
|
810
846
|
for i in range(self.N_i):
|
|
811
847
|
self.mylog.vprint(f"{self.inames[i]}: Setting gliding allocation ratios (%) to {allocType}.")
|
|
@@ -817,30 +853,26 @@ class Plan(object):
|
|
|
817
853
|
start = generic[i][0][k] / 100
|
|
818
854
|
end = generic[i][1][k] / 100
|
|
819
855
|
dat = self._interpolator(start, end, Nin)
|
|
820
|
-
|
|
821
|
-
self.alpha_ijkn[i, j, k, :Nin] = dat[:]
|
|
856
|
+
self.alpha_ijkn[i, :, k, :Nin] = dat[:]
|
|
822
857
|
|
|
823
858
|
self.boundsAR["generic"] = generic
|
|
824
859
|
|
|
825
860
|
elif allocType == "spouses":
|
|
826
|
-
|
|
861
|
+
if len(generic) != 2:
|
|
862
|
+
raise ValueError("generic must have 2 entries (initial and final).")
|
|
827
863
|
for z in range(2):
|
|
828
|
-
|
|
829
|
-
|
|
864
|
+
if len(generic[z]) != self.N_k:
|
|
865
|
+
raise ValueError(f"generic[{z}] must have {self.N_k} entries.")
|
|
866
|
+
if abs(sum(generic[z]) - 100) > 0.01:
|
|
867
|
+
raise ValueError("Sum of percentages must add to 100.")
|
|
830
868
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
start = generic[0][k] / 100
|
|
839
|
-
end = generic[1][k] / 100
|
|
840
|
-
dat = self._interpolator(start, end, Nxn)
|
|
841
|
-
for i in range(self.N_i):
|
|
842
|
-
for j in range(self.N_j):
|
|
843
|
-
self.alpha_ijkn[i, j, k, :Nxn] = dat[:]
|
|
869
|
+
for i in range(self.N_i):
|
|
870
|
+
Nin = self.horizons[i] + 1
|
|
871
|
+
for k in range(self.N_k):
|
|
872
|
+
start = generic[0][k] / 100
|
|
873
|
+
end = generic[1][k] / 100
|
|
874
|
+
dat = self._interpolator(start, end, Nin)
|
|
875
|
+
self.alpha_ijkn[i, :, k, :Nin] = dat[:]
|
|
844
876
|
|
|
845
877
|
self.boundsAR["generic"] = generic
|
|
846
878
|
|
|
@@ -873,8 +905,7 @@ class Plan(object):
|
|
|
873
905
|
try:
|
|
874
906
|
filename, self.timeLists = timelists.read(filename, self.inames, self.horizons, self.mylog)
|
|
875
907
|
except Exception as e:
|
|
876
|
-
raise Exception(f"Unsuccessful read of contributions: {e}")
|
|
877
|
-
return False
|
|
908
|
+
raise Exception(f"Unsuccessful read of contributions: {e}") from e
|
|
878
909
|
|
|
879
910
|
self.timeListsFileName = filename
|
|
880
911
|
self.setContributions()
|
|
@@ -971,9 +1002,7 @@ class Plan(object):
|
|
|
971
1002
|
a linear interpolation.
|
|
972
1003
|
"""
|
|
973
1004
|
# num goes one more year as endpoint=True.
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
return dat
|
|
1005
|
+
return np.linspace(a, b, numPoints)
|
|
977
1006
|
|
|
978
1007
|
def _tanhInterp(self, a, b, numPoints):
|
|
979
1008
|
"""
|
|
@@ -1051,259 +1080,217 @@ class Plan(object):
|
|
|
1051
1080
|
def _buildConstraints(self, objective, options):
|
|
1052
1081
|
"""
|
|
1053
1082
|
Utility function that builds constraint matrix and vectors.
|
|
1054
|
-
|
|
1055
|
-
"""
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1083
|
+
Refactored for clarity and maintainability.
|
|
1084
|
+
"""
|
|
1085
|
+
self._setup_constraint_shortcuts(options)
|
|
1086
|
+
|
|
1087
|
+
self.A = abc.ConstraintMatrix(self.nvars)
|
|
1088
|
+
self.B = abc.Bounds(self.nvars, self.nbins)
|
|
1089
|
+
|
|
1090
|
+
self._add_rmd_inequalities()
|
|
1091
|
+
self._add_tax_bracket_bounds()
|
|
1092
|
+
self._add_standard_exemption_bounds()
|
|
1093
|
+
self._add_defunct_constraints()
|
|
1094
|
+
self._add_roth_conversion_constraints(options)
|
|
1095
|
+
self._add_withdrawal_limits()
|
|
1096
|
+
self._add_conversion_limits()
|
|
1097
|
+
self._add_objective_constraints(objective, options)
|
|
1098
|
+
self._add_initial_balances()
|
|
1099
|
+
self._add_surplus_deposit_linking()
|
|
1100
|
+
self._add_account_balance_carryover()
|
|
1101
|
+
self._add_net_cash_flow()
|
|
1102
|
+
self._add_income_profile()
|
|
1103
|
+
self._add_taxable_income()
|
|
1104
|
+
self._configure_binary_variables(options)
|
|
1105
|
+
self._build_objective_vector(objective)
|
|
1059
1106
|
|
|
1060
|
-
|
|
1061
|
-
Ni = self.N_i
|
|
1062
|
-
Nj = self.N_j
|
|
1063
|
-
Nk = self.N_k
|
|
1064
|
-
Nn = self.N_n
|
|
1065
|
-
Nt = self.N_t
|
|
1066
|
-
Nz = self.N_z
|
|
1067
|
-
i_d = self.i_d
|
|
1068
|
-
i_s = self.i_s
|
|
1069
|
-
n_d = self.n_d
|
|
1070
|
-
|
|
1071
|
-
Cb = self.C["b"]
|
|
1072
|
-
Cd = self.C["d"]
|
|
1073
|
-
Ce = self.C["e"]
|
|
1074
|
-
CF = self.C["F"]
|
|
1075
|
-
Cg = self.C["g"]
|
|
1076
|
-
Cs = self.C["s"]
|
|
1077
|
-
Cw = self.C["w"]
|
|
1078
|
-
Cx = self.C["x"]
|
|
1079
|
-
Cz = self.C["z"]
|
|
1080
|
-
|
|
1081
|
-
spLo = 1 - self.lambdha
|
|
1082
|
-
spHi = 1 + self.lambdha
|
|
1107
|
+
return None
|
|
1083
1108
|
|
|
1109
|
+
def _setup_constraint_shortcuts(self, options):
|
|
1110
|
+
# Set up all the local variables as attributes for use in helpers.
|
|
1084
1111
|
oppCostX = options.get("oppCostX", 0.)
|
|
1085
|
-
xnet = 1 - oppCostX/100.
|
|
1112
|
+
self.xnet = 1 - oppCostX / 100.
|
|
1113
|
+
self.optionsUnits = u.getUnits(options.get("units", "k"))
|
|
1086
1114
|
|
|
1087
|
-
|
|
1088
|
-
for i in range(
|
|
1089
|
-
for j in range(Nj):
|
|
1090
|
-
for n in range(Nn):
|
|
1091
|
-
tau_ijn[i, j, n] = np.sum(self.alpha_ijkn[i, j, :, n] * self.tau_kn[:, n], axis=0)
|
|
1092
|
-
|
|
1093
|
-
# Weights are normalized on k: sum_k[alpha*(1 + tau)] = 1 + sum_k(alpha*tau).
|
|
1094
|
-
Tau1_ijn = 1 + tau_ijn
|
|
1095
|
-
Tauh_ijn = 1 + tau_ijn / 2
|
|
1096
|
-
|
|
1097
|
-
units = u.getUnits(options.get("units", "k"))
|
|
1098
|
-
# No units for bigM.
|
|
1099
|
-
bigM = options.get("bigM", 5e6)
|
|
1100
|
-
assert isinstance(bigM, (int, float)), f"bigM {bigM} is not a number."
|
|
1101
|
-
|
|
1102
|
-
###################################################################
|
|
1103
|
-
# Inequality constraint matrix with upper and lower bound vectors.
|
|
1104
|
-
A = abc.ConstraintMatrix(self.nvars)
|
|
1105
|
-
B = abc.Bounds(self.nvars, self.nbins)
|
|
1106
|
-
|
|
1107
|
-
# RMDs inequalities, only if there is an initial balance in tax-deferred account.
|
|
1108
|
-
for i in range(Ni):
|
|
1115
|
+
def _add_rmd_inequalities(self):
|
|
1116
|
+
for i in range(self.N_i):
|
|
1109
1117
|
if self.beta_ij[i, 1] > 0:
|
|
1110
1118
|
for n in range(self.horizons[i]):
|
|
1111
1119
|
rowDic = {
|
|
1112
|
-
_q3(
|
|
1113
|
-
_q3(
|
|
1120
|
+
_q3(self.C["w"], i, 1, n, self.N_i, self.N_j, self.N_n): 1,
|
|
1121
|
+
_q3(self.C["b"], i, 1, n, self.N_i, self.N_j, self.N_n + 1): -self.rho_in[i, n],
|
|
1114
1122
|
}
|
|
1115
|
-
A.addNewRow(rowDic,
|
|
1123
|
+
self.A.addNewRow(rowDic, 0, np.inf)
|
|
1116
1124
|
|
|
1117
|
-
|
|
1118
|
-
for t in range(
|
|
1119
|
-
for n in range(
|
|
1120
|
-
B.setRange(_q2(
|
|
1125
|
+
def _add_tax_bracket_bounds(self):
|
|
1126
|
+
for t in range(self.N_t):
|
|
1127
|
+
for n in range(self.N_n):
|
|
1128
|
+
self.B.setRange(_q2(self.C["F"], t, n, self.N_t, self.N_n), 0, self.DeltaBar_tn[t, n])
|
|
1121
1129
|
|
|
1122
|
-
|
|
1123
|
-
for n in range(
|
|
1124
|
-
B.setRange(_q1(
|
|
1130
|
+
def _add_standard_exemption_bounds(self):
|
|
1131
|
+
for n in range(self.N_n):
|
|
1132
|
+
self.B.setRange(_q1(self.C["e"], n, self.N_n), 0, self.sigmaBar_n[n])
|
|
1125
1133
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
for n in range(self.
|
|
1129
|
-
B.setRange(_q2(
|
|
1130
|
-
B.setRange(_q2(
|
|
1131
|
-
for j in range(
|
|
1132
|
-
B.setRange(_q3(
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
# This condition supercedes everything else.
|
|
1134
|
+
def _add_defunct_constraints(self):
|
|
1135
|
+
if self.N_i == 2:
|
|
1136
|
+
for n in range(self.n_d, self.N_n):
|
|
1137
|
+
self.B.setRange(_q2(self.C["d"], self.i_d, n, self.N_i, self.N_n), 0, 0)
|
|
1138
|
+
self.B.setRange(_q2(self.C["x"], self.i_d, n, self.N_i, self.N_n), 0, 0)
|
|
1139
|
+
for j in range(self.N_j):
|
|
1140
|
+
self.B.setRange(_q3(self.C["w"], self.i_d, j, n, self.N_i, self.N_j, self.N_n), 0, 0)
|
|
1141
|
+
|
|
1142
|
+
def _add_roth_conversion_constraints(self, options):
|
|
1136
1143
|
if "maxRothConversion" in options and options["maxRothConversion"] == "file":
|
|
1137
|
-
|
|
1138
|
-
for i in range(Ni):
|
|
1144
|
+
for i in range(self.N_i):
|
|
1139
1145
|
for n in range(self.horizons[i]):
|
|
1140
1146
|
rhs = self.myRothX_in[i][n]
|
|
1141
|
-
B.setRange(_q2(
|
|
1147
|
+
self.B.setRange(_q2(self.C["x"], i, n, self.N_i, self.N_n), rhs, rhs)
|
|
1142
1148
|
else:
|
|
1143
1149
|
if "maxRothConversion" in options:
|
|
1144
1150
|
rhsopt = options["maxRothConversion"]
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
# self.mylog.vprint('Limiting Roth conversions to:', u.d(rhsopt))
|
|
1152
|
-
for i in range(Ni):
|
|
1151
|
+
if not isinstance(rhsopt, (int, float)):
|
|
1152
|
+
raise ValueError(f"Specified maxRothConversion {rhsopt} is not a number.")
|
|
1153
|
+
|
|
1154
|
+
if rhsopt >= 0:
|
|
1155
|
+
rhsopt *= self.optionsUnits
|
|
1156
|
+
for i in range(self.N_i):
|
|
1153
1157
|
for n in range(self.horizons[i]):
|
|
1154
|
-
|
|
1155
|
-
# Should we adjust Roth conversion cap with inflation?
|
|
1156
|
-
B.setRange(_q2(Cx, i, n, Ni, Nn), zero, rhsopt + 0.01)
|
|
1158
|
+
self.B.setRange(_q2(self.C["x"], i, n, self.N_i, self.N_n), 0, rhsopt + 0.01)
|
|
1157
1159
|
|
|
1158
|
-
# Process startRothConversions option.
|
|
1159
1160
|
if "startRothConversions" in options:
|
|
1160
1161
|
rhsopt = options["startRothConversions"]
|
|
1161
|
-
|
|
1162
|
+
if not isinstance(rhsopt, (int, float)):
|
|
1163
|
+
raise ValueError(f"Specified startRothConversions {rhsopt} is not a number.")
|
|
1162
1164
|
thisyear = date.today().year
|
|
1163
1165
|
yearn = max(rhsopt - thisyear, 0)
|
|
1164
|
-
|
|
1165
|
-
for i in range(Ni):
|
|
1166
|
+
for i in range(self.N_i):
|
|
1166
1167
|
nstart = min(yearn, self.horizons[i])
|
|
1167
1168
|
for n in range(0, nstart):
|
|
1168
|
-
B.setRange(_q2(
|
|
1169
|
+
self.B.setRange(_q2(self.C["x"], i, n, self.N_i, self.N_n), 0, 0)
|
|
1169
1170
|
|
|
1170
|
-
# Process noRothConversions option. Also valid when N_i == 1, why not?
|
|
1171
1171
|
if "noRothConversions" in options and options["noRothConversions"] != "None":
|
|
1172
1172
|
rhsopt = options["noRothConversions"]
|
|
1173
1173
|
try:
|
|
1174
1174
|
i_x = self.inames.index(rhsopt)
|
|
1175
|
-
except ValueError:
|
|
1176
|
-
raise ValueError(f"Unknown individual {rhsopt} for noRothConversions:")
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
B.setRange(_q2(Cx, i_x, n, Ni, Nn), zero, zero)
|
|
1175
|
+
except ValueError as e:
|
|
1176
|
+
raise ValueError(f"Unknown individual {rhsopt} for noRothConversions:") from e
|
|
1177
|
+
for n in range(self.N_n):
|
|
1178
|
+
self.B.setRange(_q2(self.C["x"], i_x, n, self.N_i, self.N_n), 0, 0)
|
|
1180
1179
|
|
|
1181
|
-
|
|
1182
|
-
for i in range(
|
|
1180
|
+
def _add_withdrawal_limits(self):
|
|
1181
|
+
for i in range(self.N_i):
|
|
1183
1182
|
for j in [0, 2]:
|
|
1184
|
-
for n in range(
|
|
1185
|
-
rowDic = {_q3(
|
|
1186
|
-
|
|
1183
|
+
for n in range(self.N_n):
|
|
1184
|
+
rowDic = {_q3(self.C["w"], i, j, n, self.N_i, self.N_j, self.N_n): -1,
|
|
1185
|
+
_q3(self.C["b"], i, j, n, self.N_i, self.N_j, self.N_n + 1): 1}
|
|
1186
|
+
self.A.addNewRow(rowDic, 0, np.inf)
|
|
1187
1187
|
|
|
1188
|
-
|
|
1189
|
-
for i in range(
|
|
1190
|
-
for n in range(
|
|
1188
|
+
def _add_conversion_limits(self):
|
|
1189
|
+
for i in range(self.N_i):
|
|
1190
|
+
for n in range(self.N_n):
|
|
1191
1191
|
rowDic = {
|
|
1192
|
-
_q2(
|
|
1193
|
-
_q3(
|
|
1194
|
-
_q3(
|
|
1192
|
+
_q2(self.C["x"], i, n, self.N_i, self.N_n): -1,
|
|
1193
|
+
_q3(self.C["w"], i, 1, n, self.N_i, self.N_j, self.N_n): -1,
|
|
1194
|
+
_q3(self.C["b"], i, 1, n, self.N_i, self.N_j, self.N_n + 1): 1,
|
|
1195
1195
|
}
|
|
1196
|
-
A.addNewRow(rowDic,
|
|
1196
|
+
self.A.addNewRow(rowDic, 0, np.inf)
|
|
1197
1197
|
|
|
1198
|
-
|
|
1198
|
+
def _add_objective_constraints(self, objective, options):
|
|
1199
1199
|
if objective == "maxSpending":
|
|
1200
|
-
# Impose optional constraint on final bequest requested in today's $.
|
|
1201
1200
|
if "bequest" in options:
|
|
1202
1201
|
bequest = options["bequest"]
|
|
1203
|
-
|
|
1204
|
-
|
|
1202
|
+
if not isinstance(bequest, (int, float)):
|
|
1203
|
+
raise ValueError(f"Desired bequest {bequest} is not a number.")
|
|
1204
|
+
bequest *= self.optionsUnits * self.gamma_n[-1]
|
|
1205
1205
|
else:
|
|
1206
|
-
# If not specified, defaults to $1 (nominal $).
|
|
1207
1206
|
bequest = 1
|
|
1208
1207
|
|
|
1209
|
-
row = A.newRow()
|
|
1210
|
-
for i in range(
|
|
1211
|
-
row.addElem(_q3(
|
|
1212
|
-
row.addElem(_q3(
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
row.addElem(_q3(Cb, i, 2, Nn, Ni, Nj, Nn + 1), 1)
|
|
1216
|
-
A.addRow(row, bequest, bequest)
|
|
1217
|
-
# self.mylog.vprint('Adding bequest constraint of:', u.d(bequest))
|
|
1208
|
+
row = self.A.newRow()
|
|
1209
|
+
for i in range(self.N_i):
|
|
1210
|
+
row.addElem(_q3(self.C["b"], i, 0, self.N_n, self.N_i, self.N_j, self.N_n + 1), 1)
|
|
1211
|
+
row.addElem(_q3(self.C["b"], i, 1, self.N_n, self.N_i, self.N_j, self.N_n + 1), 1 - self.nu)
|
|
1212
|
+
row.addElem(_q3(self.C["b"], i, 2, self.N_n, self.N_i, self.N_j, self.N_n + 1), 1)
|
|
1213
|
+
self.A.addRow(row, bequest, bequest)
|
|
1218
1214
|
elif objective == "maxBequest":
|
|
1219
1215
|
spending = options["netSpending"]
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
spending *=
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
# Set initial balances through bounds or constraints.
|
|
1229
|
-
for i in range(Ni):
|
|
1230
|
-
for j in range(Nj):
|
|
1216
|
+
if not isinstance(spending, (int, float)):
|
|
1217
|
+
raise ValueError(f"Desired spending provided {spending} is not a number.")
|
|
1218
|
+
spending *= self.optionsUnits * self.yearFracLeft
|
|
1219
|
+
self.B.setRange(_q1(self.C["g"], 0, self.N_n), spending, spending)
|
|
1220
|
+
|
|
1221
|
+
def _add_initial_balances(self):
|
|
1222
|
+
for i in range(self.N_i):
|
|
1223
|
+
for j in range(self.N_j):
|
|
1231
1224
|
rhs = self.beta_ij[i, j]
|
|
1232
|
-
|
|
1233
|
-
B.setRange(_q3(Cb, i, j, 0, Ni, Nj, Nn + 1), rhs, rhs)
|
|
1225
|
+
self.B.setRange(_q3(self.C["b"], i, j, 0, self.N_i, self.N_j, self.N_n + 1), rhs, rhs)
|
|
1234
1226
|
|
|
1235
|
-
|
|
1236
|
-
for i in range(
|
|
1227
|
+
def _add_surplus_deposit_linking(self):
|
|
1228
|
+
for i in range(self.N_i):
|
|
1237
1229
|
fac1 = u.krond(i, 0) * (1 - self.eta) + u.krond(i, 1) * self.eta
|
|
1238
|
-
for n in range(n_d):
|
|
1239
|
-
rowDic = {_q2(
|
|
1240
|
-
A.addNewRow(rowDic,
|
|
1230
|
+
for n in range(self.n_d):
|
|
1231
|
+
rowDic = {_q2(self.C["d"], i, n, self.N_i, self.N_n): 1, _q1(self.C["s"], n, self.N_n): -fac1}
|
|
1232
|
+
self.A.addNewRow(rowDic, 0, 0)
|
|
1241
1233
|
fac2 = u.krond(self.i_s, i)
|
|
1242
|
-
for n in range(n_d,
|
|
1243
|
-
rowDic = {_q2(
|
|
1244
|
-
A.addNewRow(rowDic,
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
# Considering spousal asset transfer at passing of a spouse.
|
|
1264
|
-
# Using hybrid approach with 'if' statement and Kronecker deltas.
|
|
1265
|
-
for i in range(Ni):
|
|
1266
|
-
for j in range(Nj):
|
|
1267
|
-
for n in range(Nn):
|
|
1268
|
-
if Ni == 2 and n_d < Nn and i == i_d and n == n_d - 1:
|
|
1269
|
-
# fac1 = 1 - (u.krond(n, n_d - 1) * u.krond(i, i_d))
|
|
1234
|
+
for n in range(self.n_d, self.N_n):
|
|
1235
|
+
rowDic = {_q2(self.C["d"], i, n, self.N_i, self.N_n): 1, _q1(self.C["s"], n, self.N_n): -fac2}
|
|
1236
|
+
self.A.addNewRow(rowDic, 0, 0)
|
|
1237
|
+
# Prevent surplus on last year.
|
|
1238
|
+
self.B.setRange(_q1(self.C["s"], self.N_n - 1, self.N_n), 0, 0)
|
|
1239
|
+
|
|
1240
|
+
def _add_account_balance_carryover(self):
|
|
1241
|
+
tau_ijn = np.zeros((self.N_i, self.N_j, self.N_n))
|
|
1242
|
+
for i in range(self.N_i):
|
|
1243
|
+
for j in range(self.N_j):
|
|
1244
|
+
for n in range(self.N_n):
|
|
1245
|
+
tau_ijn[i, j, n] = np.sum(self.alpha_ijkn[i, j, :, n] * self.tau_kn[:, n], axis=0)
|
|
1246
|
+
|
|
1247
|
+
# Weights are normalized on k: sum_k[alpha*(1 + tau)] = 1 + sum_k[alpha*tau]
|
|
1248
|
+
Tau1_ijn = 1 + tau_ijn
|
|
1249
|
+
Tauh_ijn = 1 + tau_ijn / 2
|
|
1250
|
+
|
|
1251
|
+
for i in range(self.N_i):
|
|
1252
|
+
for j in range(self.N_j):
|
|
1253
|
+
for n in range(self.N_n):
|
|
1254
|
+
if self.N_i == 2 and self.n_d < self.N_n and i == self.i_d and n == self.n_d - 1:
|
|
1270
1255
|
fac1 = 0
|
|
1271
1256
|
else:
|
|
1272
1257
|
fac1 = 1
|
|
1273
1258
|
|
|
1274
1259
|
rhs = fac1 * self.kappa_ijn[i, j, n] * Tauh_ijn[i, j, n]
|
|
1275
1260
|
|
|
1276
|
-
row = A.newRow()
|
|
1277
|
-
row.addElem(_q3(
|
|
1278
|
-
row.addElem(_q3(
|
|
1279
|
-
row.addElem(_q3(
|
|
1280
|
-
row.addElem(_q2(
|
|
1261
|
+
row = self.A.newRow()
|
|
1262
|
+
row.addElem(_q3(self.C["b"], i, j, n + 1, self.N_i, self.N_j, self.N_n + 1), 1)
|
|
1263
|
+
row.addElem(_q3(self.C["b"], i, j, n, self.N_i, self.N_j, self.N_n + 1), -fac1 * Tau1_ijn[i, j, n])
|
|
1264
|
+
row.addElem(_q3(self.C["w"], i, j, n, self.N_i, self.N_j, self.N_n), fac1 * Tau1_ijn[i, j, n])
|
|
1265
|
+
row.addElem(_q2(self.C["d"], i, n, self.N_i, self.N_n), -fac1 * u.krond(j, 0) * Tau1_ijn[i, 0, n])
|
|
1281
1266
|
row.addElem(
|
|
1282
|
-
_q2(
|
|
1283
|
-
-fac1 * (xnet*u.krond(j, 2) - u.krond(j, 1)) * Tau1_ijn[i, j, n],
|
|
1267
|
+
_q2(self.C["x"], i, n, self.N_i, self.N_n),
|
|
1268
|
+
-fac1 * (self.xnet * u.krond(j, 2) - u.krond(j, 1)) * Tau1_ijn[i, j, n],
|
|
1284
1269
|
)
|
|
1285
1270
|
|
|
1286
|
-
if
|
|
1271
|
+
if self.N_i == 2 and self.n_d < self.N_n and i == self.i_s and n == self.n_d - 1:
|
|
1287
1272
|
fac2 = self.phi_j[j]
|
|
1288
|
-
rhs += fac2 * self.kappa_ijn[i_d, j, n] * Tauh_ijn[i_d, j, n]
|
|
1289
|
-
row.addElem(_q3(
|
|
1290
|
-
|
|
1291
|
-
row.addElem(
|
|
1273
|
+
rhs += fac2 * self.kappa_ijn[self.i_d, j, n] * Tauh_ijn[self.i_d, j, n]
|
|
1274
|
+
row.addElem(_q3(self.C["b"], self.i_d, j, n, self.N_i, self.N_j, self.N_n + 1),
|
|
1275
|
+
-fac2 * Tau1_ijn[self.i_d, j, n])
|
|
1276
|
+
row.addElem(_q3(self.C["w"], self.i_d, j, n, self.N_i, self.N_j, self.N_n),
|
|
1277
|
+
fac2 * Tau1_ijn[self.i_d, j, n])
|
|
1278
|
+
row.addElem(_q2(self.C["d"], self.i_d, n, self.N_i, self.N_n),
|
|
1279
|
+
-fac2 * u.krond(j, 0) * Tau1_ijn[self.i_d, 0, n])
|
|
1292
1280
|
row.addElem(
|
|
1293
|
-
_q2(
|
|
1294
|
-
-fac2 * (xnet*u.krond(j, 2) - u.krond(j, 1)) * Tau1_ijn[i_d, j, n],
|
|
1281
|
+
_q2(self.C["x"], self.i_d, n, self.N_i, self.N_n),
|
|
1282
|
+
-fac2 * (self.xnet * u.krond(j, 2) - u.krond(j, 1)) * Tau1_ijn[self.i_d, j, n],
|
|
1295
1283
|
)
|
|
1296
|
-
A.addRow(row, rhs, rhs)
|
|
1284
|
+
self.A.addRow(row, rhs, rhs)
|
|
1297
1285
|
|
|
1286
|
+
def _add_net_cash_flow(self):
|
|
1298
1287
|
tau_0prev = np.roll(self.tau_kn[0, :], 1)
|
|
1299
1288
|
tau_0prev[tau_0prev < 0] = 0
|
|
1300
|
-
|
|
1301
|
-
# Net cash flow.
|
|
1302
|
-
for n in range(Nn):
|
|
1289
|
+
for n in range(self.N_n):
|
|
1303
1290
|
rhs = -self.M_n[n]
|
|
1304
|
-
row = A.newRow({_q1(
|
|
1305
|
-
row.addElem(_q1(
|
|
1306
|
-
for i in range(
|
|
1291
|
+
row = self.A.newRow({_q1(self.C["g"], n, self.N_n): 1})
|
|
1292
|
+
row.addElem(_q1(self.C["s"], n, self.N_n), 1)
|
|
1293
|
+
for i in range(self.N_i):
|
|
1307
1294
|
fac = self.psi * self.alpha_ijkn[i, 0, 0, n]
|
|
1308
1295
|
rhs += (
|
|
1309
1296
|
self.omega_in[i, n]
|
|
@@ -1312,122 +1299,113 @@ class Plan(object):
|
|
|
1312
1299
|
+ self.Lambda_in[i, n]
|
|
1313
1300
|
- 0.5 * fac * self.mu * self.kappa_ijn[i, 0, n]
|
|
1314
1301
|
)
|
|
1315
|
-
|
|
1316
|
-
row.addElem(_q3(
|
|
1317
|
-
# Minus capital gains on taxable withdrawals using last year's rate if >=0.
|
|
1318
|
-
# Plus taxable account withdrawals, and all other withdrawals.
|
|
1319
|
-
row.addElem(_q3(Cw, i, 0, n, Ni, Nj, Nn), fac * (tau_0prev[n] - self.mu) - 1)
|
|
1302
|
+
row.addElem(_q3(self.C["b"], i, 0, n, self.N_i, self.N_j, self.N_n + 1), fac * self.mu)
|
|
1303
|
+
row.addElem(_q3(self.C["w"], i, 0, n, self.N_i, self.N_j, self.N_n), fac * (tau_0prev[n] - self.mu) - 1)
|
|
1320
1304
|
penalty = 0.1 if n < self.n59[i] else 0
|
|
1321
|
-
row.addElem(_q3(
|
|
1322
|
-
row.addElem(_q3(
|
|
1323
|
-
row.addElem(_q2(
|
|
1324
|
-
|
|
1325
|
-
# Minus tax on ordinary income, T_n.
|
|
1326
|
-
for t in range(Nt):
|
|
1327
|
-
row.addElem(_q2(CF, t, n, Nt, Nn), self.theta_tn[t, n])
|
|
1305
|
+
row.addElem(_q3(self.C["w"], i, 1, n, self.N_i, self.N_j, self.N_n), -1 + penalty)
|
|
1306
|
+
row.addElem(_q3(self.C["w"], i, 2, n, self.N_i, self.N_j, self.N_n), -1 + penalty)
|
|
1307
|
+
row.addElem(_q2(self.C["d"], i, n, self.N_i, self.N_n), fac * self.mu)
|
|
1328
1308
|
|
|
1329
|
-
|
|
1309
|
+
for t in range(self.N_t):
|
|
1310
|
+
row.addElem(_q2(self.C["F"], t, n, self.N_t, self.N_n), self.theta_tn[t, n])
|
|
1330
1311
|
|
|
1331
|
-
|
|
1332
|
-
for n in range(1, Nn):
|
|
1333
|
-
rowDic = {_q1(Cg, 0, Nn): spLo * self.xiBar_n[n], _q1(Cg, n, Nn): -self.xiBar_n[0]}
|
|
1334
|
-
A.addNewRow(rowDic, -inf, zero)
|
|
1335
|
-
rowDic = {_q1(Cg, 0, Nn): spHi * self.xiBar_n[n], _q1(Cg, n, Nn): -self.xiBar_n[0]}
|
|
1336
|
-
A.addNewRow(rowDic, zero, inf)
|
|
1312
|
+
self.A.addRow(row, rhs, rhs)
|
|
1337
1313
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1314
|
+
def _add_income_profile(self):
|
|
1315
|
+
spLo = 1 - self.lambdha
|
|
1316
|
+
spHi = 1 + self.lambdha
|
|
1317
|
+
for n in range(1, self.N_n):
|
|
1318
|
+
rowDic = {_q1(self.C["g"], 0, self.N_n): spLo * self.xiBar_n[n],
|
|
1319
|
+
_q1(self.C["g"], n, self.N_n): -self.xiBar_n[0]}
|
|
1320
|
+
self.A.addNewRow(rowDic, -np.inf, 0)
|
|
1321
|
+
rowDic = {_q1(self.C["g"], 0, self.N_n): spHi * self.xiBar_n[n],
|
|
1322
|
+
_q1(self.C["g"], n, self.N_n): -self.xiBar_n[0]}
|
|
1323
|
+
self.A.addNewRow(rowDic, 0, np.inf)
|
|
1324
|
+
|
|
1325
|
+
def _add_taxable_income(self):
|
|
1326
|
+
for n in range(self.N_n):
|
|
1340
1327
|
rhs = 0
|
|
1341
|
-
row = A.newRow()
|
|
1342
|
-
row.addElem(_q1(
|
|
1343
|
-
for i in range(
|
|
1328
|
+
row = self.A.newRow()
|
|
1329
|
+
row.addElem(_q1(self.C["e"], n, self.N_n), 1)
|
|
1330
|
+
for i in range(self.N_i):
|
|
1344
1331
|
rhs += self.omega_in[i, n] + 0.85 * self.zetaBar_in[i, n] + self.piBar_in[i, n]
|
|
1345
|
-
|
|
1346
|
-
row.addElem(
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
# Taxable returns on securities in taxable account.
|
|
1350
|
-
fak = np.sum(self.tau_kn[1:Nk, n] * self.alpha_ijkn[i, 0, 1:Nk, n], axis=0)
|
|
1332
|
+
row.addElem(_q3(self.C["w"], i, 1, n, self.N_i, self.N_j, self.N_n), -1)
|
|
1333
|
+
row.addElem(_q2(self.C["x"], i, n, self.N_i, self.N_n), -1)
|
|
1334
|
+
fak = np.sum(self.tau_kn[1:self.N_k, n] * self.alpha_ijkn[i, 0, 1:self.N_k, n], axis=0)
|
|
1351
1335
|
rhs += 0.5 * fak * self.kappa_ijn[i, 0, n]
|
|
1352
|
-
row.addElem(_q3(
|
|
1353
|
-
row.addElem(_q3(
|
|
1354
|
-
row.addElem(_q2(
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1336
|
+
row.addElem(_q3(self.C["b"], i, 0, n, self.N_i, self.N_j, self.N_n + 1), -fak)
|
|
1337
|
+
row.addElem(_q3(self.C["w"], i, 0, n, self.N_i, self.N_j, self.N_n), fak)
|
|
1338
|
+
row.addElem(_q2(self.C["d"], i, n, self.N_i, self.N_n), -fak)
|
|
1339
|
+
for t in range(self.N_t):
|
|
1340
|
+
row.addElem(_q2(self.C["F"], t, n, self.N_t, self.N_n), 1)
|
|
1341
|
+
self.A.addRow(row, rhs, rhs)
|
|
1342
|
+
|
|
1343
|
+
def _configure_binary_variables(self, options):
|
|
1344
|
+
bigM = options.get("bigM", 5e6)
|
|
1345
|
+
if not isinstance(bigM, (int, float)):
|
|
1346
|
+
raise ValueError(f"bigM {bigM} is not a number.")
|
|
1360
1347
|
|
|
1361
|
-
|
|
1362
|
-
for i in range(Ni):
|
|
1348
|
+
for i in range(self.N_i):
|
|
1363
1349
|
for n in range(self.horizons[i]):
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
A.addNewRow(
|
|
1369
|
-
{_q3(Cz, i, n, 0, Ni, Nn, Nz): bigM, _q1(Cs, n, Nn): -1},
|
|
1370
|
-
zero,
|
|
1350
|
+
self.A.addNewRow(
|
|
1351
|
+
{_q3(self.C["z"], i, n, 0, self.N_i, self.N_n, self.N_z): bigM,
|
|
1352
|
+
_q1(self.C["s"], n, self.N_n): -1},
|
|
1353
|
+
0,
|
|
1371
1354
|
bigM,
|
|
1372
1355
|
)
|
|
1373
|
-
|
|
1374
|
-
A.addNewRow(
|
|
1356
|
+
self.A.addNewRow(
|
|
1375
1357
|
{
|
|
1376
|
-
_q3(
|
|
1377
|
-
_q3(
|
|
1378
|
-
_q3(
|
|
1358
|
+
_q3(self.C["z"], i, n, 0, self.N_i, self.N_n, self.N_z): bigM,
|
|
1359
|
+
_q3(self.C["w"], i, 0, n, self.N_i, self.N_j, self.N_n): 1,
|
|
1360
|
+
_q3(self.C["w"], i, 2, n, self.N_i, self.N_j, self.N_n): 1,
|
|
1379
1361
|
},
|
|
1380
|
-
|
|
1362
|
+
0,
|
|
1381
1363
|
bigM,
|
|
1382
1364
|
)
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
zero,
|
|
1365
|
+
self.A.addNewRow(
|
|
1366
|
+
{_q3(self.C["z"], i, n, 1, self.N_i, self.N_n, self.N_z): bigM,
|
|
1367
|
+
_q2(self.C["x"], i, n, self.N_i, self.N_n): -1},
|
|
1368
|
+
0,
|
|
1388
1369
|
bigM,
|
|
1389
1370
|
)
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1371
|
+
self.A.addNewRow(
|
|
1372
|
+
{_q3(self.C["z"], i, n, 1, self.N_i, self.N_n, self.N_z): bigM,
|
|
1373
|
+
_q3(self.C["w"], i, 2, n, self.N_i, self.N_j, self.N_n): 1},
|
|
1374
|
+
0,
|
|
1394
1375
|
bigM,
|
|
1395
1376
|
)
|
|
1377
|
+
for n in range(self.horizons[i], self.N_n):
|
|
1378
|
+
self.B.setRange(_q3(self.C["z"], i, n, 0, self.N_i, self.N_n, self.N_z), 0, 0)
|
|
1379
|
+
self.B.setRange(_q3(self.C["z"], i, n, 1, self.N_i, self.N_n, self.N_z), 0, 0)
|
|
1396
1380
|
|
|
1397
|
-
|
|
1398
|
-
B.setRange(_q3(Cz, i, n, 0, Ni, Nn, Nz), zero, zero)
|
|
1399
|
-
B.setRange(_q3(Cz, i, n, 1, Ni, Nn, Nz), zero, zero)
|
|
1400
|
-
|
|
1401
|
-
# Now build a solver-neutral objective vector.
|
|
1381
|
+
def _build_objective_vector(self, objective):
|
|
1402
1382
|
c = abc.Objective(self.nvars)
|
|
1403
1383
|
if objective == "maxSpending":
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
c.setElem(_q1(Cg, n, Nn), -1/self.gamma_n[n])
|
|
1384
|
+
for n in range(self.N_n):
|
|
1385
|
+
c.setElem(_q1(self.C["g"], n, self.N_n), -1/self.gamma_n[n])
|
|
1407
1386
|
elif objective == "maxBequest":
|
|
1408
|
-
for i in range(
|
|
1409
|
-
c.setElem(_q3(
|
|
1410
|
-
c.setElem(_q3(
|
|
1411
|
-
c.setElem(_q3(
|
|
1387
|
+
for i in range(self.N_i):
|
|
1388
|
+
c.setElem(_q3(self.C["b"], i, 0, self.N_n, self.N_i, self.N_j, self.N_n + 1), -1)
|
|
1389
|
+
c.setElem(_q3(self.C["b"], i, 1, self.N_n, self.N_i, self.N_j, self.N_n + 1), -(1 - self.nu))
|
|
1390
|
+
c.setElem(_q3(self.C["b"], i, 2, self.N_n, self.N_i, self.N_j, self.N_n + 1), -1)
|
|
1412
1391
|
else:
|
|
1413
1392
|
raise RuntimeError("Internal error in objective function.")
|
|
1414
|
-
|
|
1415
|
-
self.A = A
|
|
1416
|
-
self.B = B
|
|
1417
1393
|
self.c = c
|
|
1418
1394
|
|
|
1419
|
-
return None
|
|
1420
|
-
|
|
1421
1395
|
@_timer
|
|
1422
1396
|
def runHistoricalRange(self, objective, options, ystart, yend, *, verbose=False, figure=False, progcall=None):
|
|
1423
1397
|
"""
|
|
1424
1398
|
Run historical scenarios on plan over a range of years.
|
|
1425
1399
|
"""
|
|
1400
|
+
|
|
1426
1401
|
if yend + self.N_n > self.year_n[0]:
|
|
1427
1402
|
yend = self.year_n[0] - self.N_n - 1
|
|
1428
1403
|
self.mylog.vprint(f"Warning: Upper bound for year range re-adjusted to {yend}.")
|
|
1429
|
-
N = yend - ystart + 1
|
|
1430
1404
|
|
|
1405
|
+
if yend < ystart:
|
|
1406
|
+
raise ValueError(f"Starting year is too large to support a lifespan of {self.N_n} years.")
|
|
1407
|
+
|
|
1408
|
+
N = yend - ystart + 1
|
|
1431
1409
|
self.mylog.vprint(f"Running historical range from {ystart} to {yend}.")
|
|
1432
1410
|
|
|
1433
1411
|
self.mylog.setVerbose(verbose)
|
|
@@ -1438,7 +1416,7 @@ class Plan(object):
|
|
|
1438
1416
|
columns = ["partial", "final"]
|
|
1439
1417
|
else:
|
|
1440
1418
|
self.mylog.print(f"Invalid objective {objective}.")
|
|
1441
|
-
|
|
1419
|
+
raise ValueError(f"Invalid objective {objective}.")
|
|
1442
1420
|
|
|
1443
1421
|
df = pd.DataFrame(columns=columns)
|
|
1444
1422
|
|
|
@@ -1461,7 +1439,9 @@ class Plan(object):
|
|
|
1461
1439
|
|
|
1462
1440
|
progcall.finish()
|
|
1463
1441
|
self.mylog.resetVerbose()
|
|
1464
|
-
|
|
1442
|
+
|
|
1443
|
+
fig, description = self._plotter.plot_histogram_results(objective, df, N, self.year_n,
|
|
1444
|
+
self.n_d, self.N_i, self.phi_j)
|
|
1465
1445
|
self.mylog.print(description.getvalue())
|
|
1466
1446
|
|
|
1467
1447
|
if figure:
|
|
@@ -1474,7 +1454,7 @@ class Plan(object):
|
|
|
1474
1454
|
"""
|
|
1475
1455
|
Run Monte Carlo simulations on plan.
|
|
1476
1456
|
"""
|
|
1477
|
-
if self.rateMethod not in
|
|
1457
|
+
if self.rateMethod not in ("stochastic", "histochastic"):
|
|
1478
1458
|
self.mylog.print("It is pointless to run Monte Carlo simulations with fixed rates.")
|
|
1479
1459
|
return
|
|
1480
1460
|
|
|
@@ -1517,7 +1497,9 @@ class Plan(object):
|
|
|
1517
1497
|
|
|
1518
1498
|
progcall.finish()
|
|
1519
1499
|
self.mylog.resetVerbose()
|
|
1520
|
-
|
|
1500
|
+
|
|
1501
|
+
fig, description = self._plotter.plot_histogram_results(objective, df, N, self.year_n,
|
|
1502
|
+
self.n_d, self.N_i, self.phi_j)
|
|
1521
1503
|
self.mylog.print(description.getvalue())
|
|
1522
1504
|
|
|
1523
1505
|
if figure:
|
|
@@ -1525,90 +1507,6 @@ class Plan(object):
|
|
|
1525
1507
|
|
|
1526
1508
|
return N, df
|
|
1527
1509
|
|
|
1528
|
-
def _showResults(self, objective, df, N, figure):
|
|
1529
|
-
"""
|
|
1530
|
-
Show a histogram of values from runMC() and runHistoricalRange().
|
|
1531
|
-
"""
|
|
1532
|
-
import seaborn as sbn
|
|
1533
|
-
|
|
1534
|
-
description = io.StringIO()
|
|
1535
|
-
|
|
1536
|
-
pSuccess = u.pc(len(df) / N)
|
|
1537
|
-
print(f"Success rate: {pSuccess} on {N} samples.", file=description)
|
|
1538
|
-
title = f"$N$ = {N}, $P$ = {pSuccess}"
|
|
1539
|
-
means = df.mean(axis=0, numeric_only=True)
|
|
1540
|
-
medians = df.median(axis=0, numeric_only=True)
|
|
1541
|
-
|
|
1542
|
-
my = 2 * [self.year_n[-1]]
|
|
1543
|
-
if self.N_i == 2 and self.n_d < self.N_n:
|
|
1544
|
-
my[0] = self.year_n[self.n_d - 1]
|
|
1545
|
-
|
|
1546
|
-
# Don't show partial bequest of zero if spouse is full beneficiary,
|
|
1547
|
-
# or if solution led to empty accounts at the end of first spouse's life.
|
|
1548
|
-
if np.all(self.phi_j == 1) or medians.iloc[0] < 1:
|
|
1549
|
-
if medians.iloc[0] < 1:
|
|
1550
|
-
print(f"Optimized solutions all have null partial bequest in year {my[0]}.", file=description)
|
|
1551
|
-
df.drop("partial", axis=1, inplace=True)
|
|
1552
|
-
means = df.mean(axis=0, numeric_only=True)
|
|
1553
|
-
medians = df.median(axis=0, numeric_only=True)
|
|
1554
|
-
|
|
1555
|
-
df /= 1000
|
|
1556
|
-
if len(df) > 0:
|
|
1557
|
-
thisyear = self.year_n[0]
|
|
1558
|
-
if objective == "maxBequest":
|
|
1559
|
-
fig, axes = plt.subplots()
|
|
1560
|
-
# Show both partial and final bequests in the same histogram.
|
|
1561
|
-
sbn.histplot(df, multiple="dodge", kde=True, ax=axes)
|
|
1562
|
-
legend = []
|
|
1563
|
-
# Don't know why but legend is reversed from df.
|
|
1564
|
-
for q in range(len(means) - 1, -1, -1):
|
|
1565
|
-
dmedian = u.d(medians.iloc[q], latex=True)
|
|
1566
|
-
dmean = u.d(means.iloc[q], latex=True)
|
|
1567
|
-
legend.append(f"{my[q]}: $M$: {dmedian}, $\\bar{{x}}$: {dmean}")
|
|
1568
|
-
plt.legend(legend, shadow=True)
|
|
1569
|
-
plt.xlabel(f"{thisyear} $k")
|
|
1570
|
-
plt.title(objective)
|
|
1571
|
-
leads = [f"partial {my[0]}", f" final {my[1]}"]
|
|
1572
|
-
elif len(means) == 2:
|
|
1573
|
-
# Show partial bequest and net spending as two separate histograms.
|
|
1574
|
-
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
|
|
1575
|
-
cols = ["partial", objective]
|
|
1576
|
-
leads = [f"partial {my[0]}", objective]
|
|
1577
|
-
for q in range(2):
|
|
1578
|
-
sbn.histplot(df[cols[q]], kde=True, ax=axes[q])
|
|
1579
|
-
dmedian = u.d(medians.iloc[q], latex=True)
|
|
1580
|
-
dmean = u.d(means.iloc[q], latex=True)
|
|
1581
|
-
legend = [f"$M$: {dmedian}, $\\bar{{x}}$: {dmean}"]
|
|
1582
|
-
axes[q].set_label(legend)
|
|
1583
|
-
axes[q].legend(labels=legend)
|
|
1584
|
-
axes[q].set_title(leads[q])
|
|
1585
|
-
axes[q].set_xlabel(f"{thisyear} $k")
|
|
1586
|
-
else:
|
|
1587
|
-
# Show net spending as single histogram.
|
|
1588
|
-
fig, axes = plt.subplots()
|
|
1589
|
-
sbn.histplot(df[objective], kde=True, ax=axes)
|
|
1590
|
-
dmedian = u.d(medians.iloc[0], latex=True)
|
|
1591
|
-
dmean = u.d(means.iloc[0], latex=True)
|
|
1592
|
-
legend = [f"$M$: {dmedian}, $\\bar{{x}}$: {dmean}"]
|
|
1593
|
-
plt.legend(legend, shadow=True)
|
|
1594
|
-
plt.xlabel(f"{thisyear} $k")
|
|
1595
|
-
plt.title(objective)
|
|
1596
|
-
leads = [objective]
|
|
1597
|
-
|
|
1598
|
-
plt.suptitle(title)
|
|
1599
|
-
# plt.show()
|
|
1600
|
-
|
|
1601
|
-
for q in range(len(means)):
|
|
1602
|
-
print(f"{leads[q]:>12}: Median ({thisyear} $): {u.d(medians.iloc[q])}", file=description)
|
|
1603
|
-
print(f"{leads[q]:>12}: Mean ({thisyear} $): {u.d(means.iloc[q])}", file=description)
|
|
1604
|
-
mmin = 1000 * df.iloc[:, q].min()
|
|
1605
|
-
mmax = 1000 * df.iloc[:, q].max()
|
|
1606
|
-
print(f"{leads[q]:>12}: Range: {u.d(mmin)} - {u.d(mmax)}", file=description)
|
|
1607
|
-
nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
|
|
1608
|
-
print(f"{leads[q]:>12}: N zero solns: {nzeros}", file=description)
|
|
1609
|
-
|
|
1610
|
-
return fig, description
|
|
1611
|
-
|
|
1612
1510
|
def resolve(self):
|
|
1613
1511
|
"""
|
|
1614
1512
|
Solve a plan using saved options.
|
|
@@ -1619,7 +1517,7 @@ class Plan(object):
|
|
|
1619
1517
|
|
|
1620
1518
|
@_checkConfiguration
|
|
1621
1519
|
@_timer
|
|
1622
|
-
def solve(self, objective, options=
|
|
1520
|
+
def solve(self, objective, options=None):
|
|
1623
1521
|
"""
|
|
1624
1522
|
This function builds the necessary constaints and
|
|
1625
1523
|
runs the optimizer.
|
|
@@ -1661,6 +1559,7 @@ class Plan(object):
|
|
|
1661
1559
|
"oppCostX",
|
|
1662
1560
|
]
|
|
1663
1561
|
# We might modify options if required.
|
|
1562
|
+
options = {} if options is None else options
|
|
1664
1563
|
myoptions = dict(options)
|
|
1665
1564
|
|
|
1666
1565
|
for opt in myoptions:
|
|
@@ -1739,7 +1638,8 @@ class Plan(object):
|
|
|
1739
1638
|
while True:
|
|
1740
1639
|
solution, xx, solverSuccess, solverMsg = solverMethod(objective, options)
|
|
1741
1640
|
|
|
1742
|
-
if not solverSuccess:
|
|
1641
|
+
if not solverSuccess or solution is None:
|
|
1642
|
+
self.mylog.vprint("Solver failed:", solverMsg, solverSuccess)
|
|
1743
1643
|
break
|
|
1744
1644
|
|
|
1745
1645
|
if not withMedicare:
|
|
@@ -1789,8 +1689,13 @@ class Plan(object):
|
|
|
1789
1689
|
"""
|
|
1790
1690
|
from scipy import optimize
|
|
1791
1691
|
|
|
1792
|
-
#
|
|
1793
|
-
milpOptions = {
|
|
1692
|
+
# Optimize solver parameters
|
|
1693
|
+
milpOptions = {
|
|
1694
|
+
"disp": False,
|
|
1695
|
+
"mip_rel_gap": 1e-7,
|
|
1696
|
+
"presolve": True,
|
|
1697
|
+
"node_limit": 10000 # Limit search nodes for faster solutions
|
|
1698
|
+
}
|
|
1794
1699
|
|
|
1795
1700
|
self._buildConstraints(objective, options)
|
|
1796
1701
|
Alu, lbvec, ubvec = self.A.arrays()
|
|
@@ -2161,10 +2066,10 @@ class Plan(object):
|
|
|
2161
2066
|
dic[f"Net spending for year {now}"] = u.d(self.g_n[0] / self.yearFracLeft)
|
|
2162
2067
|
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
|
|
2163
2068
|
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
dic["Total net spending"] = f"{u.d(
|
|
2167
|
-
dic["[Total net spending]"] = f"{u.d(
|
|
2069
|
+
totSpending = np.sum(self.g_n, axis=0)
|
|
2070
|
+
totSpendingNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
|
|
2071
|
+
dic["Total net spending"] = f"{u.d(totSpendingNow)}"
|
|
2072
|
+
dic["[Total net spending]"] = f"{u.d(totSpending)}"
|
|
2168
2073
|
|
|
2169
2074
|
totRoth = np.sum(self.x_in, axis=(0, 1))
|
|
2170
2075
|
totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
|
|
@@ -2260,58 +2165,29 @@ class Plan(object):
|
|
|
2260
2165
|
|
|
2261
2166
|
A tag string can be set to add information to the title of the plot.
|
|
2262
2167
|
"""
|
|
2263
|
-
import seaborn as sbn
|
|
2264
|
-
|
|
2265
2168
|
if self.rateMethod in [None, "user", "historical average", "conservative"]:
|
|
2266
2169
|
self.mylog.vprint(f"Warning: Cannot plot correlations for {self.rateMethod} rate method.")
|
|
2267
2170
|
return None
|
|
2268
2171
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
"Baa Corp. Bonds",
|
|
2272
|
-
"10-y T-Notes",
|
|
2273
|
-
"Inflation",
|
|
2274
|
-
]
|
|
2172
|
+
fig = self._plotter.plot_rates_correlations(self._name, self.tau_kn, self.N_n, self.rateMethod,
|
|
2173
|
+
self.rateFrm, self.rateTo, tag, shareRange)
|
|
2275
2174
|
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
data = 100 * self.tau_kn[k]
|
|
2279
|
-
df[name] = data
|
|
2280
|
-
|
|
2281
|
-
g = sbn.PairGrid(df, diag_sharey=False, height=1.8, aspect=1)
|
|
2282
|
-
if shareRange:
|
|
2283
|
-
minval = df.min().min() - 5
|
|
2284
|
-
maxval = df.max().max() + 5
|
|
2285
|
-
g.set(xlim=(minval, maxval), ylim=(minval, maxval))
|
|
2286
|
-
g.map_upper(sbn.scatterplot)
|
|
2287
|
-
g.map_lower(sbn.kdeplot)
|
|
2288
|
-
# g.map_diag(sbn.kdeplot)
|
|
2289
|
-
g.map_diag(sbn.histplot, color="orange")
|
|
2290
|
-
|
|
2291
|
-
# Put zero axes on off-diagonal plots.
|
|
2292
|
-
imod = len(rateNames) + 1
|
|
2293
|
-
for i, ax in enumerate(g.axes.flat):
|
|
2294
|
-
ax.axvline(x=0, color="grey", linewidth=1, linestyle=":")
|
|
2295
|
-
if i % imod != 0:
|
|
2296
|
-
ax.axhline(y=0, color="grey", linewidth=1, linestyle=":")
|
|
2297
|
-
# ax.tick_params(axis='both', labelleft=True, labelbottom=True)
|
|
2298
|
-
|
|
2299
|
-
# plt.subplots_adjust(wspace=0.3, hspace=0.3)
|
|
2300
|
-
|
|
2301
|
-
title = self._name + "\n"
|
|
2302
|
-
title += f"Rates Correlations (N={self.N_n}) {self.rateMethod}"
|
|
2303
|
-
if self.rateMethod in ["historical", "histochastic"]:
|
|
2304
|
-
title += " (" + str(self.rateFrm) + "-" + str(self.rateTo) + ")"
|
|
2305
|
-
|
|
2306
|
-
if tag != "":
|
|
2307
|
-
title += " - " + tag
|
|
2175
|
+
if figure:
|
|
2176
|
+
return fig
|
|
2308
2177
|
|
|
2309
|
-
|
|
2178
|
+
self._plotter.jupyter_renderer(fig)
|
|
2179
|
+
return None
|
|
2310
2180
|
|
|
2181
|
+
def showRatesDistributions(self, frm=rates.FROM, to=rates.TO, figure=False):
|
|
2182
|
+
"""
|
|
2183
|
+
Plot histograms of the rates distributions.
|
|
2184
|
+
"""
|
|
2185
|
+
fig = self._plotter.plot_rates_distributions(frm, to, rates.SP500, rates.BondsBaa,
|
|
2186
|
+
rates.TNotes, rates.Inflation, rates.FROM)
|
|
2311
2187
|
if figure:
|
|
2312
|
-
return
|
|
2188
|
+
return fig
|
|
2313
2189
|
|
|
2314
|
-
|
|
2190
|
+
self._plotter.jupyter_renderer(fig)
|
|
2315
2191
|
return None
|
|
2316
2192
|
|
|
2317
2193
|
def showRates(self, tag="", figure=False):
|
|
@@ -2320,54 +2196,17 @@ class Plan(object):
|
|
|
2320
2196
|
|
|
2321
2197
|
A tag string can be set to add information to the title of the plot.
|
|
2322
2198
|
"""
|
|
2323
|
-
import matplotlib.ticker as tk
|
|
2324
|
-
|
|
2325
2199
|
if self.rateMethod is None:
|
|
2326
2200
|
self.mylog.vprint("Warning: Rate method must be selected before plotting.")
|
|
2327
2201
|
return None
|
|
2328
2202
|
|
|
2329
|
-
fig
|
|
2330
|
-
|
|
2331
|
-
title = self._name + "\nReturn & Inflation Rates (" + str(self.rateMethod)
|
|
2332
|
-
if self.rateMethod in ["historical", "histochastic", "historical average"]:
|
|
2333
|
-
title += " " + str(self.rateFrm) + "-" + str(self.rateTo)
|
|
2334
|
-
title += ")"
|
|
2335
|
-
|
|
2336
|
-
if tag != "":
|
|
2337
|
-
title += " - " + tag
|
|
2338
|
-
|
|
2339
|
-
rateName = [
|
|
2340
|
-
"S&P500 (incl. div.)",
|
|
2341
|
-
"Baa Corp. Bonds",
|
|
2342
|
-
"10-y T-Notes",
|
|
2343
|
-
"Inflation",
|
|
2344
|
-
]
|
|
2345
|
-
ltype = ["-", "-.", ":", "--"]
|
|
2346
|
-
for k in range(self.N_k):
|
|
2347
|
-
if self.yearFracLeft == 1:
|
|
2348
|
-
data = 100 * self.tau_kn[k]
|
|
2349
|
-
years = self.year_n
|
|
2350
|
-
else:
|
|
2351
|
-
data = 100 * self.tau_kn[k, 1:]
|
|
2352
|
-
years = self.year_n[1:]
|
|
2353
|
-
|
|
2354
|
-
# Use ddof=1 to match pandas.
|
|
2355
|
-
label = (
|
|
2356
|
-
rateName[k] + " <" + "{:.1f}".format(np.mean(data)) + " +/- {:.1f}".format(np.std(data, ddof=1)) + "%>"
|
|
2357
|
-
)
|
|
2358
|
-
ax.plot(years, data, label=label, ls=ltype[k % self.N_k])
|
|
2359
|
-
|
|
2360
|
-
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
2361
|
-
ax.legend(loc="best", reverse=False, fontsize=8, framealpha=0.7)
|
|
2362
|
-
# ax.legend(loc='upper left')
|
|
2363
|
-
ax.set_title(title)
|
|
2364
|
-
ax.set_xlabel("year")
|
|
2365
|
-
ax.set_ylabel("%")
|
|
2203
|
+
fig = self._plotter.plot_rates(self._name, self.tau_kn, self.year_n, self.yearFracLeft,
|
|
2204
|
+
self.N_k, self.rateMethod, self.rateFrm, self.rateTo, tag)
|
|
2366
2205
|
|
|
2367
2206
|
if figure:
|
|
2368
2207
|
return fig
|
|
2369
2208
|
|
|
2370
|
-
|
|
2209
|
+
self._plotter.jupyter_renderer(fig)
|
|
2371
2210
|
return None
|
|
2372
2211
|
|
|
2373
2212
|
def showProfile(self, tag="", figure=False):
|
|
@@ -2379,20 +2218,15 @@ class Plan(object):
|
|
|
2379
2218
|
if self.xi_n is None:
|
|
2380
2219
|
self.mylog.vprint("Warning: Profile must be selected before plotting.")
|
|
2381
2220
|
return None
|
|
2382
|
-
|
|
2383
2221
|
title = self._name + "\nSpending Profile"
|
|
2384
|
-
if tag
|
|
2222
|
+
if tag:
|
|
2385
2223
|
title += " - " + tag
|
|
2386
|
-
|
|
2387
|
-
# style = {'net': '-', 'target': ':'}
|
|
2388
|
-
style = {"profile": "-"}
|
|
2389
|
-
series = {"profile": self.xi_n}
|
|
2390
|
-
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat="$\\xi$")
|
|
2224
|
+
fig = self._plotter.plot_profile(self.year_n, self.xi_n, title, self.inames)
|
|
2391
2225
|
|
|
2392
2226
|
if figure:
|
|
2393
2227
|
return fig
|
|
2394
2228
|
|
|
2395
|
-
|
|
2229
|
+
self._plotter.jupyter_renderer(fig)
|
|
2396
2230
|
return None
|
|
2397
2231
|
|
|
2398
2232
|
@_checkCaseStatus
|
|
@@ -2406,28 +2240,15 @@ class Plan(object):
|
|
|
2406
2240
|
the default behavior of setDefaultPlots().
|
|
2407
2241
|
"""
|
|
2408
2242
|
value = self._checkValue(value)
|
|
2409
|
-
|
|
2410
2243
|
title = self._name + "\nNet Available Spending"
|
|
2411
|
-
if tag
|
|
2244
|
+
if tag:
|
|
2412
2245
|
title += " - " + tag
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
if value == "nominal":
|
|
2416
|
-
series = {"net": self.g_n, "target": (self.g_n[0] / self.xi_n[0]) * self.xiBar_n}
|
|
2417
|
-
yformat = "\\$k (nominal)"
|
|
2418
|
-
else:
|
|
2419
|
-
series = {
|
|
2420
|
-
"net": self.g_n / self.gamma_n[:-1],
|
|
2421
|
-
"target": (self.g_n[0] / self.xi_n[0]) * self.xi_n,
|
|
2422
|
-
}
|
|
2423
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2424
|
-
|
|
2425
|
-
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
|
|
2426
|
-
|
|
2246
|
+
fig = self._plotter.plot_net_spending(self.year_n, self.g_n, self.xi_n, self.xiBar_n,
|
|
2247
|
+
self.gamma_n, value, title, self.inames)
|
|
2427
2248
|
if figure:
|
|
2428
2249
|
return fig
|
|
2429
2250
|
|
|
2430
|
-
|
|
2251
|
+
self._plotter.jupyter_renderer(fig)
|
|
2431
2252
|
return None
|
|
2432
2253
|
|
|
2433
2254
|
@_checkCaseStatus
|
|
@@ -2444,42 +2265,37 @@ class Plan(object):
|
|
|
2444
2265
|
the default behavior of setDefaultPlots().
|
|
2445
2266
|
"""
|
|
2446
2267
|
value = self._checkValue(value)
|
|
2268
|
+
figures = self._plotter.plot_asset_distribution(self.year_n, self.inames, self.b_ijkn,
|
|
2269
|
+
self.gamma_n, value, self._name, tag)
|
|
2270
|
+
if figure:
|
|
2271
|
+
return figures
|
|
2447
2272
|
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
else:
|
|
2452
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2453
|
-
infladjust = self.gamma_n
|
|
2454
|
-
|
|
2455
|
-
years_n = np.array(self.year_n)
|
|
2456
|
-
years_n = np.append(years_n, [years_n[-1] + 1])
|
|
2457
|
-
y2stack = {}
|
|
2458
|
-
jDic = {"taxable": 0, "tax-deferred": 1, "tax-free": 2}
|
|
2459
|
-
kDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
2460
|
-
figures = []
|
|
2461
|
-
for jkey in jDic:
|
|
2462
|
-
stackNames = []
|
|
2463
|
-
for kkey in kDic:
|
|
2464
|
-
name = kkey + " / " + jkey
|
|
2465
|
-
stackNames.append(name)
|
|
2466
|
-
y2stack[name] = np.zeros((self.N_i, self.N_n + 1))
|
|
2467
|
-
for i in range(self.N_i):
|
|
2468
|
-
y2stack[name][i][:] = self.b_ijkn[i][jDic[jkey]][kDic[kkey]][:] / infladjust
|
|
2273
|
+
for fig in figures:
|
|
2274
|
+
self._plotter.jupyter_renderer(fig)
|
|
2275
|
+
return None
|
|
2469
2276
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2277
|
+
@_checkCaseStatus
|
|
2278
|
+
def showGrossIncome(self, tag="", value=None, figure=False):
|
|
2279
|
+
"""
|
|
2280
|
+
Plot income tax and taxable income over time horizon.
|
|
2473
2281
|
|
|
2474
|
-
|
|
2475
|
-
years_n, self.inames, title, range(self.N_i), y2stack, stackNames, "upper left", yformat
|
|
2476
|
-
)
|
|
2477
|
-
figures.append(fig)
|
|
2282
|
+
A tag string can be set to add information to the title of the plot.
|
|
2478
2283
|
|
|
2284
|
+
The value parameter can be set to *nominal* or *today*, overriding
|
|
2285
|
+
the default behavior of setDefaultPlots().
|
|
2286
|
+
"""
|
|
2287
|
+
value = self._checkValue(value)
|
|
2288
|
+
tax_brackets = tx.taxBrackets(self.N_i, self.n_d, self.N_n, self.yTCJA)
|
|
2289
|
+
title = self._name + "\nTaxable Ordinary Income vs. Tax Brackets"
|
|
2290
|
+
if tag:
|
|
2291
|
+
title += " - " + tag
|
|
2292
|
+
fig = self._plotter.plot_gross_income(
|
|
2293
|
+
self.year_n, self.G_n, self.gamma_n, value, title, tax_brackets
|
|
2294
|
+
)
|
|
2479
2295
|
if figure:
|
|
2480
|
-
return
|
|
2296
|
+
return fig
|
|
2481
2297
|
|
|
2482
|
-
|
|
2298
|
+
self._plotter.jupyter_renderer(fig)
|
|
2483
2299
|
return None
|
|
2484
2300
|
|
|
2485
2301
|
def showAllocations(self, tag="", figure=False):
|
|
@@ -2490,45 +2306,16 @@ class Plan(object):
|
|
|
2490
2306
|
|
|
2491
2307
|
A tag string can be set to add information to the title of the plot.
|
|
2492
2308
|
"""
|
|
2493
|
-
|
|
2494
|
-
if
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
acList = [self.ARCoord]
|
|
2499
|
-
elif self.ARCoord == "account":
|
|
2500
|
-
acList = ["taxable", "tax-deferred", "tax-free"]
|
|
2501
|
-
else:
|
|
2502
|
-
raise ValueError(f"Unknown coordination {self.ARCoord}.")
|
|
2503
|
-
|
|
2504
|
-
figures = []
|
|
2505
|
-
assetDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
|
|
2506
|
-
for i in range(count):
|
|
2507
|
-
y2stack = {}
|
|
2508
|
-
for acType in acList:
|
|
2509
|
-
stackNames = []
|
|
2510
|
-
for key in assetDic:
|
|
2511
|
-
aname = key + " / " + acType
|
|
2512
|
-
stackNames.append(aname)
|
|
2513
|
-
y2stack[aname] = np.zeros((count, self.N_n))
|
|
2514
|
-
y2stack[aname][i][:] = self.alpha_ijkn[i, acList.index(acType), assetDic[key], : self.N_n]
|
|
2515
|
-
|
|
2516
|
-
title = self._name + "\nAsset Allocation (%) - " + acType
|
|
2517
|
-
if self.ARCoord == "spouses":
|
|
2518
|
-
title += " spouses"
|
|
2519
|
-
else:
|
|
2520
|
-
title += " " + self.inames[i]
|
|
2521
|
-
|
|
2522
|
-
if tag != "":
|
|
2523
|
-
title += " - " + tag
|
|
2524
|
-
|
|
2525
|
-
fig, ax = _stackPlot(self.year_n, self.inames, title, [i], y2stack, stackNames, "upper left", "percent")
|
|
2526
|
-
figures.append(fig)
|
|
2527
|
-
|
|
2309
|
+
title = self._name + "\nAsset Allocation"
|
|
2310
|
+
if tag:
|
|
2311
|
+
title += " - " + tag
|
|
2312
|
+
figures = self._plotter.plot_allocations(self.year_n, self.inames, self.alpha_ijkn,
|
|
2313
|
+
self.ARCoord, title)
|
|
2528
2314
|
if figure:
|
|
2529
2315
|
return figures
|
|
2530
2316
|
|
|
2531
|
-
|
|
2317
|
+
for fig in figures:
|
|
2318
|
+
self._plotter.jupyter_renderer(fig)
|
|
2532
2319
|
return None
|
|
2533
2320
|
|
|
2534
2321
|
@_checkCaseStatus
|
|
@@ -2542,30 +2329,15 @@ class Plan(object):
|
|
|
2542
2329
|
the default behavior of setDefaultPlots().
|
|
2543
2330
|
"""
|
|
2544
2331
|
value = self._checkValue(value)
|
|
2545
|
-
|
|
2546
2332
|
title = self._name + "\nSavings Balance"
|
|
2547
|
-
if tag
|
|
2333
|
+
if tag:
|
|
2548
2334
|
title += " - " + tag
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
# Add one year for estate.
|
|
2552
|
-
year_n = np.append(self.year_n, [self.year_n[-1] + 1])
|
|
2553
|
-
|
|
2554
|
-
if value == "nominal":
|
|
2555
|
-
yformat = "\\$k (nominal)"
|
|
2556
|
-
savings_in = self.savings_in
|
|
2557
|
-
else:
|
|
2558
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2559
|
-
savings_in = {}
|
|
2560
|
-
for key in self.savings_in:
|
|
2561
|
-
savings_in[key] = self.savings_in[key] / self.gamma_n
|
|
2562
|
-
|
|
2563
|
-
fig, ax = _stackPlot(year_n, self.inames, title, range(self.N_i), savings_in, stypes, "upper left", yformat)
|
|
2564
|
-
|
|
2335
|
+
fig = self._plotter.plot_accounts(self.year_n, self.savings_in, self.gamma_n,
|
|
2336
|
+
value, title, self.inames)
|
|
2565
2337
|
if figure:
|
|
2566
2338
|
return fig
|
|
2567
2339
|
|
|
2568
|
-
|
|
2340
|
+
self._plotter.jupyter_renderer(fig)
|
|
2569
2341
|
return None
|
|
2570
2342
|
|
|
2571
2343
|
@_checkCaseStatus
|
|
@@ -2579,58 +2351,15 @@ class Plan(object):
|
|
|
2579
2351
|
the default behavior of setDefaultPlots().
|
|
2580
2352
|
"""
|
|
2581
2353
|
value = self._checkValue(value)
|
|
2582
|
-
|
|
2583
2354
|
title = self._name + "\nRaw Income Sources"
|
|
2584
|
-
|
|
2585
|
-
# stypes = [item for item in stypes if "RothX" not in item]
|
|
2586
|
-
|
|
2587
|
-
if tag != "":
|
|
2355
|
+
if tag:
|
|
2588
2356
|
title += " - " + tag
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
yformat = "\\$k (nominal)"
|
|
2592
|
-
sources_in = self.sources_in
|
|
2593
|
-
else:
|
|
2594
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2595
|
-
sources_in = {}
|
|
2596
|
-
for key in stypes:
|
|
2597
|
-
sources_in[key] = self.sources_in[key] / self.gamma_n[:-1]
|
|
2598
|
-
|
|
2599
|
-
fig, ax = _stackPlot(
|
|
2600
|
-
self.year_n, self.inames, title, range(self.N_i), sources_in, stypes, "upper left", yformat
|
|
2601
|
-
)
|
|
2602
|
-
|
|
2357
|
+
fig = self._plotter.plot_sources(self.year_n, self.sources_in, self.gamma_n,
|
|
2358
|
+
value, title, self.inames)
|
|
2603
2359
|
if figure:
|
|
2604
2360
|
return fig
|
|
2605
2361
|
|
|
2606
|
-
|
|
2607
|
-
return None
|
|
2608
|
-
|
|
2609
|
-
@_checkCaseStatus
|
|
2610
|
-
def _showFeff(self, tag=""):
|
|
2611
|
-
"""
|
|
2612
|
-
Plot income tax paid over time.
|
|
2613
|
-
|
|
2614
|
-
A tag string can be set to add information to the title of the plot.
|
|
2615
|
-
"""
|
|
2616
|
-
title = self._name + "\nEff f "
|
|
2617
|
-
if tag != "":
|
|
2618
|
-
title += " - " + tag
|
|
2619
|
-
|
|
2620
|
-
various = ["-", "--", "-.", ":"]
|
|
2621
|
-
style = {}
|
|
2622
|
-
series = {}
|
|
2623
|
-
q = 0
|
|
2624
|
-
for t in range(self.N_t):
|
|
2625
|
-
key = "f " + str(t)
|
|
2626
|
-
series[key] = self.F_tn[t] / self.DeltaBar_tn[t]
|
|
2627
|
-
# print(key, series[key])
|
|
2628
|
-
style[key] = various[q % len(various)]
|
|
2629
|
-
q += 1
|
|
2630
|
-
|
|
2631
|
-
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat="")
|
|
2632
|
-
|
|
2633
|
-
plt.show()
|
|
2362
|
+
self._plotter.jupyter_renderer(fig)
|
|
2634
2363
|
return None
|
|
2635
2364
|
|
|
2636
2365
|
@_checkCaseStatus
|
|
@@ -2644,87 +2373,17 @@ class Plan(object):
|
|
|
2644
2373
|
the default behavior of setDefaultPlots().
|
|
2645
2374
|
"""
|
|
2646
2375
|
value = self._checkValue(value)
|
|
2647
|
-
|
|
2648
|
-
style = {"income taxes": "-", "Medicare": "-."}
|
|
2649
|
-
|
|
2650
|
-
if value == "nominal":
|
|
2651
|
-
series = {"income taxes": self.T_n, "Medicare": self.M_n}
|
|
2652
|
-
yformat = "\\$k (nominal)"
|
|
2653
|
-
else:
|
|
2654
|
-
series = {
|
|
2655
|
-
"income taxes": self.T_n / self.gamma_n[:-1],
|
|
2656
|
-
"Medicare": self.M_n / self.gamma_n[:-1],
|
|
2657
|
-
}
|
|
2658
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2659
|
-
|
|
2660
2376
|
title = self._name + "\nIncome Tax"
|
|
2661
|
-
if tag
|
|
2377
|
+
if tag:
|
|
2662
2378
|
title += " - " + tag
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2379
|
+
fig = self._plotter.plot_taxes(self.year_n, self.T_n, self.M_n, self.gamma_n,
|
|
2380
|
+
value, title, self.inames)
|
|
2666
2381
|
if figure:
|
|
2667
2382
|
return fig
|
|
2668
2383
|
|
|
2669
|
-
|
|
2384
|
+
self._plotter.jupyter_renderer(fig)
|
|
2670
2385
|
return None
|
|
2671
2386
|
|
|
2672
|
-
@_checkCaseStatus
|
|
2673
|
-
def showGrossIncome(self, tag="", value=None, figure=False):
|
|
2674
|
-
"""
|
|
2675
|
-
Plot income tax and taxable income over time horizon.
|
|
2676
|
-
|
|
2677
|
-
A tag string can be set to add information to the title of the plot.
|
|
2678
|
-
|
|
2679
|
-
The value parameter can be set to *nominal* or *today*, overriding
|
|
2680
|
-
the default behavior of setDefaultPlots().
|
|
2681
|
-
"""
|
|
2682
|
-
value = self._checkValue(value)
|
|
2683
|
-
|
|
2684
|
-
style = {"taxable income": "-"}
|
|
2685
|
-
|
|
2686
|
-
if value == "nominal":
|
|
2687
|
-
series = {"taxable income": self.G_n}
|
|
2688
|
-
yformat = "\\$k (nominal)"
|
|
2689
|
-
infladjust = self.gamma_n[:-1]
|
|
2690
|
-
else:
|
|
2691
|
-
series = {"taxable income": self.G_n / self.gamma_n[:-1]}
|
|
2692
|
-
yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
|
|
2693
|
-
infladjust = 1
|
|
2694
|
-
|
|
2695
|
-
title = self._name + "\nTaxable Ordinary Income vs. Tax Brackets"
|
|
2696
|
-
if tag != "":
|
|
2697
|
-
title += " - " + tag
|
|
2698
|
-
|
|
2699
|
-
fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
|
|
2700
|
-
|
|
2701
|
-
data = tx.taxBrackets(self.N_i, self.n_d, self.N_n, self.yTCJA)
|
|
2702
|
-
for key in data:
|
|
2703
|
-
data_adj = data[key] * infladjust
|
|
2704
|
-
ax.plot(self.year_n, data_adj, label=key, ls=":")
|
|
2705
|
-
|
|
2706
|
-
plt.grid(visible="both")
|
|
2707
|
-
ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
|
|
2708
|
-
|
|
2709
|
-
if figure:
|
|
2710
|
-
return fig
|
|
2711
|
-
|
|
2712
|
-
plt.show()
|
|
2713
|
-
return None
|
|
2714
|
-
|
|
2715
|
-
# @_checkCaseStatus
|
|
2716
|
-
def saveConfig(self, basename=None):
|
|
2717
|
-
"""
|
|
2718
|
-
Save parameters in a configuration file.
|
|
2719
|
-
"""
|
|
2720
|
-
if basename is None:
|
|
2721
|
-
basename = "case_" + self._name
|
|
2722
|
-
|
|
2723
|
-
config.saveConfig(self, basename, self.mylog)
|
|
2724
|
-
|
|
2725
|
-
return None
|
|
2726
|
-
|
|
2727
|
-
@_checkCaseStatus
|
|
2728
2387
|
def saveWorkbook(self, overwrite=False, *, basename=None, saveToFile=True):
|
|
2729
2388
|
"""
|
|
2730
2389
|
Save instance in an Excel spreadsheet.
|
|
@@ -2826,7 +2485,7 @@ class Plan(object):
|
|
|
2826
2485
|
for i in range(self.N_i):
|
|
2827
2486
|
sname = self.inames[i] + "'s Sources"
|
|
2828
2487
|
ws = wb.create_sheet(sname)
|
|
2829
|
-
fillsheet(ws, srcDic, "currency", op=lambda x: x[i])
|
|
2488
|
+
fillsheet(ws, srcDic, "currency", op=lambda x: x[i]) # noqa: B023
|
|
2830
2489
|
|
|
2831
2490
|
# Account balances except final year.
|
|
2832
2491
|
accDic = {
|
|
@@ -2846,7 +2505,7 @@ class Plan(object):
|
|
|
2846
2505
|
for i in range(self.N_i):
|
|
2847
2506
|
sname = self.inames[i] + "'s Accounts"
|
|
2848
2507
|
ws = wb.create_sheet(sname)
|
|
2849
|
-
fillsheet(ws, accDic, "currency", op=lambda x: x[i])
|
|
2508
|
+
fillsheet(ws, accDic, "currency", op=lambda x: x[i]) # noqa: B023
|
|
2850
2509
|
# Add final balances.
|
|
2851
2510
|
lastRow = [
|
|
2852
2511
|
self.year_n[-1] + 1,
|
|
@@ -2963,73 +2622,20 @@ class Plan(object):
|
|
|
2963
2622
|
if key == "n":
|
|
2964
2623
|
break
|
|
2965
2624
|
except Exception as e:
|
|
2966
|
-
raise Exception(f"Unanticipated exception: {e}.")
|
|
2625
|
+
raise Exception(f"Unanticipated exception: {e}.") from e
|
|
2967
2626
|
|
|
2968
2627
|
return None
|
|
2969
2628
|
|
|
2629
|
+
def saveConfig(self, basename=None):
|
|
2630
|
+
"""
|
|
2631
|
+
Save parameters in a configuration file.
|
|
2632
|
+
"""
|
|
2633
|
+
if basename is None:
|
|
2634
|
+
basename = "case_" + self._name
|
|
2970
2635
|
|
|
2971
|
-
|
|
2972
|
-
"""
|
|
2973
|
-
Core line plotter function.
|
|
2974
|
-
"""
|
|
2975
|
-
import matplotlib.ticker as tk
|
|
2976
|
-
|
|
2977
|
-
fig, ax = plt.subplots(figsize=(6, 4))
|
|
2978
|
-
plt.grid(visible="both")
|
|
2979
|
-
|
|
2980
|
-
for sname in series:
|
|
2981
|
-
ax.plot(x, series[sname], label=sname, ls=style[sname])
|
|
2982
|
-
|
|
2983
|
-
ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
|
|
2984
|
-
ax.set_title(title)
|
|
2985
|
-
ax.set_xlabel("year")
|
|
2986
|
-
ax.set_ylabel(yformat)
|
|
2987
|
-
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
2988
|
-
if "k" in yformat:
|
|
2989
|
-
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
|
|
2990
|
-
# Give range to y values in unindexed flat profiles.
|
|
2991
|
-
ymin, ymax = ax.get_ylim()
|
|
2992
|
-
if ymax - ymin < 5000:
|
|
2993
|
-
ax.set_ylim((ymin * 0.95, ymax * 1.05))
|
|
2994
|
-
|
|
2995
|
-
return fig, ax
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
def _stackPlot(x, inames, title, irange, series, snames, location, yformat="\\$k"):
|
|
2999
|
-
"""
|
|
3000
|
-
Core function for stacked plots.
|
|
3001
|
-
"""
|
|
3002
|
-
import matplotlib.ticker as tk
|
|
3003
|
-
|
|
3004
|
-
nonzeroSeries = {}
|
|
3005
|
-
for sname in snames:
|
|
3006
|
-
for i in irange:
|
|
3007
|
-
tmp = series[sname][i]
|
|
3008
|
-
if sum(tmp) > 1.0:
|
|
3009
|
-
nonzeroSeries[sname + " " + inames[i]] = tmp
|
|
3010
|
-
|
|
3011
|
-
if len(nonzeroSeries) == 0:
|
|
3012
|
-
# print('Nothing to plot for', title)
|
|
3013
|
-
return None, None
|
|
3014
|
-
|
|
3015
|
-
fig, ax = plt.subplots(figsize=(6, 4))
|
|
3016
|
-
plt.grid(visible="both")
|
|
3017
|
-
|
|
3018
|
-
ax.stackplot(x, nonzeroSeries.values(), labels=nonzeroSeries.keys(), alpha=0.6)
|
|
3019
|
-
ax.legend(loc=location, reverse=True, fontsize=8, ncol=2, framealpha=0.5)
|
|
3020
|
-
ax.set_title(title)
|
|
3021
|
-
ax.set_xlabel("year")
|
|
3022
|
-
ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
|
|
3023
|
-
if "k" in yformat:
|
|
3024
|
-
ax.set_ylabel(yformat)
|
|
3025
|
-
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
|
|
3026
|
-
elif yformat == "percent":
|
|
3027
|
-
ax.set_ylabel("%")
|
|
3028
|
-
ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x), ",")))
|
|
3029
|
-
else:
|
|
3030
|
-
raise RuntimeError(f"Unknown yformat: {yformat}.")
|
|
2636
|
+
config.saveConfig(self, basename, self.mylog)
|
|
3031
2637
|
|
|
3032
|
-
|
|
2638
|
+
return None
|
|
3033
2639
|
|
|
3034
2640
|
|
|
3035
2641
|
def _saveWorkbook(wb, basename, overwrite, mylog):
|
|
@@ -3051,7 +2657,7 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
|
|
|
3051
2657
|
mylog.vprint("Skipping save and returning.")
|
|
3052
2658
|
return None
|
|
3053
2659
|
|
|
3054
|
-
|
|
2660
|
+
for _ in range(3):
|
|
3055
2661
|
try:
|
|
3056
2662
|
mylog.vprint(f'Saving plan as "{fname}".')
|
|
3057
2663
|
wb.save(fname)
|
|
@@ -3062,7 +2668,7 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
|
|
|
3062
2668
|
if key == "n":
|
|
3063
2669
|
break
|
|
3064
2670
|
except Exception as e:
|
|
3065
|
-
raise Exception(f"Unanticipated exception {e}.")
|
|
2671
|
+
raise Exception(f"Unanticipated exception {e}.") from e
|
|
3066
2672
|
|
|
3067
2673
|
return None
|
|
3068
2674
|
|