owlplanner 2025.2.8__py3-none-any.whl → 2025.2.9__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
@@ -63,10 +63,10 @@ def _genXi_n(profile, fraction, n_d, N_n, a, b, c):
63
63
  Series is unadjusted for inflation.
64
64
  """
65
65
  xi = np.ones(N_n)
66
- if profile == 'flat':
66
+ if profile == "flat":
67
67
  if n_d < N_n:
68
68
  xi[n_d:] *= fraction
69
- elif profile == 'smile':
69
+ elif profile == "smile":
70
70
  span = N_n - 1 - c
71
71
  x = np.linspace(0, span, N_n - c)
72
72
  a /= 100
@@ -82,7 +82,7 @@ def _genXi_n(profile, fraction, n_d, N_n, a, b, c):
82
82
  xi[n_d:] *= fraction
83
83
  xi *= neutralSum / xi.sum()
84
84
  else:
85
- raise ValueError('Unknown profile type %s.' % profile)
85
+ raise ValueError("Unknown profile type %s." % profile)
86
86
 
87
87
  return xi
88
88
 
@@ -142,7 +142,7 @@ def clone(plan, newname=None, *, verbose=True, logstreams=None):
142
142
  newplan.setLogstreams(verbose, logstreams)
143
143
 
144
144
  if newname is None:
145
- newplan.rename(plan._name + ' (copy)')
145
+ newplan.rename(plan._name + " (copy)")
146
146
  else:
147
147
  newplan.rename(newname)
148
148
 
@@ -160,8 +160,8 @@ def _checkCaseStatus(func):
160
160
 
161
161
  @wraps(func)
162
162
  def wrapper(self, *args, **kwargs):
163
- if self.caseStatus != 'solved':
164
- self.mylog.vprint('Preventing to run method %s() while case is %s.' % (func.__name__, self.caseStatus))
163
+ if self.caseStatus != "solved":
164
+ self.mylog.vprint("Preventing to run method %s() while case is %s." % (func.__name__, self.caseStatus))
165
165
  return None
166
166
  return func(self, *args, **kwargs)
167
167
 
@@ -177,11 +177,11 @@ def _checkConfiguration(func):
177
177
  @wraps(func)
178
178
  def wrapper(self, *args, **kwargs):
179
179
  if self.xi_n is None:
180
- msg = 'You must define a spending profile before calling %s().' % func.__name__
180
+ msg = "You must define a spending profile before calling %s()." % func.__name__
181
181
  self.mylog.vprint(msg)
182
182
  raise RuntimeError(msg)
183
183
  if self.alpha_ijkn is None:
184
- msg = 'You must define an allocation profile before calling %s().' % func.__name__
184
+ msg = "You must define an allocation profile before calling %s()." % func.__name__
185
185
  self.mylog.vprint(msg)
186
186
  raise RuntimeError(msg)
187
187
  return func(self, *args, **kwargs)
@@ -201,8 +201,9 @@ def _timer(func):
201
201
  result = func(self, *args, **kwargs)
202
202
  pt = time.process_time() - pt0
203
203
  rt = time.time() - rt0
204
- self.mylog.vprint('CPU time used: %dm%.1fs, Wall time: %dm%.1fs.'
205
- % (int(pt / 60), pt % 60, int(rt / 60), rt % 60))
204
+ self.mylog.vprint(
205
+ "CPU time used: %dm%.1fs, Wall time: %dm%.1fs." % (int(pt / 60), pt % 60, int(rt / 60), rt % 60)
206
+ )
206
207
  return result
207
208
 
208
209
  return wrapper
@@ -221,8 +222,8 @@ class Plan(object):
221
222
  and the third the life expectancy. Last argument is a name for
222
223
  the plan.
223
224
  """
224
- if name == '':
225
- raise ValueError('Plan must have a name')
225
+ if name == "":
226
+ raise ValueError("Plan must have a name")
226
227
 
227
228
  self._name = name
228
229
  self.setLogstreams(verbose, logstreams)
@@ -235,21 +236,21 @@ class Plan(object):
235
236
  self.N_z = 2
236
237
 
237
238
  # Default interpolation parameters for allocation ratios.
238
- self.interpMethod = 'linear'
239
+ self.interpMethod = "linear"
239
240
  self._interpolator = self._linInterp
240
241
  self.interpCenter = 15
241
242
  self.interpWidth = 5
242
243
 
243
- self.defaultPlots = 'nominal'
244
- self.defaultSolver = 'HiGHS'
244
+ self.defaultPlots = "nominal"
245
+ self.defaultSolver = "HiGHS"
245
246
 
246
247
  self.N_i = len(yobs)
247
- assert 0 < self.N_i and self.N_i <= 2, 'Cannot support %d individuals.' % self.N_i
248
- assert self.N_i == len(expectancy), 'Expectancy must have %d entries.' % self.N_i
249
- assert self.N_i == len(inames), 'Names for individuals must have %d entries.' % self.N_i
250
- assert inames[0] != '' or (self.N_i == 2 and inames[1] == ''), 'Name for each individual must be provided.'
248
+ assert 0 < self.N_i and self.N_i <= 2, "Cannot support %d individuals." % self.N_i
249
+ assert self.N_i == len(expectancy), "Expectancy must have %d entries." % self.N_i
250
+ assert self.N_i == len(inames), "Names for individuals must have %d entries." % self.N_i
251
+ assert inames[0] != "" or (self.N_i == 2 and inames[1] == ""), "Name for each individual must be provided."
251
252
 
252
- self.filingStatus = ['single', 'married'][self.N_i - 1]
253
+ self.filingStatus = ["single", "married"][self.N_i - 1]
253
254
 
254
255
  self.inames = inames
255
256
  self.yobs = np.array(yobs, dtype=np.int32)
@@ -300,11 +301,13 @@ class Plan(object):
300
301
  self.prevMAGI = np.zeros((2))
301
302
 
302
303
  # Scenario starts at the beginning of this year and ends at the end of the last year.
303
- self.mylog.vprint('Preparing scenario of %d years for %d individual%s.'
304
- % (self.N_n, self.N_i, ['', 's'][self.N_i - 1]))
304
+ self.mylog.vprint(
305
+ "Preparing scenario of %d years for %d individual%s." % (self.N_n, self.N_i, ["", "s"][self.N_i - 1])
306
+ )
305
307
  for i in range(self.N_i):
306
- self.mylog.vprint('%14s: life horizon from %d -> %d.'
307
- % (self.inames[i], thisyear, thisyear + self.horizons[i] - 1))
308
+ self.mylog.vprint(
309
+ "%14s: life horizon from %d -> %d." % (self.inames[i], thisyear, thisyear + self.horizons[i] - 1)
310
+ )
308
311
 
309
312
  # Prepare income tax and RMD time series.
310
313
  self.rho_in = tx.rho_in(self.yobs, self.N_n)
@@ -320,11 +323,11 @@ class Plan(object):
320
323
  self.timeListsFileName = "None"
321
324
  self.timeLists = {}
322
325
  self.zeroContributions()
323
- self.caseStatus = 'unsolved'
326
+ self.caseStatus = "unsolved"
324
327
  self.rateMethod = None
325
328
 
326
329
  self.ARCoord = None
327
- self.objective = 'unknown'
330
+ self.objective = "unknown"
328
331
 
329
332
  # Placeholders to check if properly configured.
330
333
  self.xi_n = None
@@ -361,13 +364,13 @@ class Plan(object):
361
364
  thisyear = date.today().year
362
365
 
363
366
  if isinstance(mydate, date):
364
- mydate = mydate.strftime('%Y-%m-%d')
367
+ mydate = mydate.strftime("%Y-%m-%d")
365
368
 
366
- if mydate is None or mydate == 'today':
369
+ if mydate is None or mydate == "today":
367
370
  refdate = date.today()
368
- self.startDate = refdate.strftime('%Y-%m-%d')
371
+ self.startDate = refdate.strftime("%Y-%m-%d")
369
372
  else:
370
- mydatelist = mydate.split('-')
373
+ mydatelist = mydate.split("-")
371
374
  if len(mydatelist) == 2 or len(mydatelist) == 3:
372
375
  self.startDate = mydate
373
376
  # Ignore the year provided.
@@ -379,7 +382,7 @@ class Plan(object):
379
382
  # Take midnight as the reference.
380
383
  self.yearFracLeft = 1 - (refdate.timetuple().tm_yday - 1) / (365 + lp)
381
384
 
382
- self.mylog.vprint('Setting 1st-year starting date to %s.' % (self.startDate))
385
+ self.mylog.vprint("Setting 1st-year starting date to %s." % (self.startDate))
383
386
 
384
387
  return None
385
388
 
@@ -390,11 +393,11 @@ class Plan(object):
390
393
  if value is None:
391
394
  return self.defaultPlots
392
395
 
393
- opts = ['nominal', 'today']
396
+ opts = ["nominal", "today"]
394
397
  if value in opts:
395
398
  return value
396
399
 
397
- raise ValueError('Value type must be one of: %r' % opts)
400
+ raise ValueError("Value type must be one of: %r" % opts)
398
401
 
399
402
  return None
400
403
 
@@ -404,7 +407,7 @@ class Plan(object):
404
407
  to distinguish graph outputs and as base name for
405
408
  saving configurations and workbooks.
406
409
  """
407
- self.mylog.vprint('Renaming plan %s -> %s.' % (self._name, newname))
410
+ self.mylog.vprint("Renaming plan %s -> %s." % (self._name, newname))
408
411
  self._name = newname
409
412
 
410
413
  def setSpousalDepositFraction(self, eta):
@@ -417,13 +420,13 @@ class Plan(object):
417
420
  where s_n is the surplus amount. Here d_0n is the taxable account
418
421
  deposit for the first spouse while d_1n is for the second spouse.
419
422
  """
420
- assert 0 <= eta and eta <= 1, 'Fraction must be between 0 and 1.'
423
+ assert 0 <= eta and eta <= 1, "Fraction must be between 0 and 1."
421
424
  if self.N_i != 2:
422
- self.mylog.vprint('Deposit fraction can only be 0 for single individuals.')
425
+ self.mylog.vprint("Deposit fraction can only be 0 for single individuals.")
423
426
  eta = 0
424
427
  else:
425
- self.mylog.vprint('Setting spousal surplus deposit fraction to %.1f.' % eta)
426
- self.mylog.vprint('\t%s: %.1f, %s: %.1f' % (self.inames[0], (1 - eta), self.inames[1], eta))
428
+ self.mylog.vprint("Setting spousal surplus deposit fraction to %.1f." % eta)
429
+ self.mylog.vprint("\t%s: %.1f, %s: %.1f" % (self.inames[0], (1 - eta), self.inames[1], eta))
427
430
  self.eta = eta
428
431
 
429
432
  def setDefaultPlots(self, value):
@@ -432,69 +435,69 @@ class Plan(object):
432
435
  """
433
436
 
434
437
  self.defaultPlots = self._checkValue(value)
435
- self.mylog.vprint('Setting plots default value to %s.' % value)
438
+ self.mylog.vprint("Setting plots default value to %s." % value)
436
439
 
437
440
  def setDividendRate(self, mu):
438
441
  """
439
442
  Set dividend rate on equities. Rate is in percent. Default 2%.
440
443
  """
441
- assert 0 <= mu and mu <= 100, 'Rate must be between 0 and 100.'
444
+ assert 0 <= mu and mu <= 100, "Rate must be between 0 and 100."
442
445
  mu /= 100
443
- self.mylog.vprint('Dividend return rate on equities set to %s.' % u.pc(mu, f=1))
446
+ self.mylog.vprint("Dividend return rate on equities set to %s." % u.pc(mu, f=1))
444
447
  self.mu = mu
445
- self.caseStatus = 'modified'
448
+ self.caseStatus = "modified"
446
449
 
447
450
  def setLongTermCapitalTaxRate(self, psi):
448
451
  """
449
452
  Set long-term income tax rate. Rate is in percent. Default 15%.
450
453
  """
451
- assert 0 <= psi and psi <= 100, 'Rate must be between 0 and 100.'
454
+ assert 0 <= psi and psi <= 100, "Rate must be between 0 and 100."
452
455
  psi /= 100
453
- self.mylog.vprint('Long-term capital gain income tax set to %s.' % u.pc(psi, f=0))
456
+ self.mylog.vprint("Long-term capital gain income tax set to %s." % u.pc(psi, f=0))
454
457
  self.psi = psi
455
- self.caseStatus = 'modified'
458
+ self.caseStatus = "modified"
456
459
 
457
460
  def setBeneficiaryFractions(self, phi):
458
461
  """
459
462
  Set fractions of savings accounts that is left to surviving spouse.
460
463
  Default is [1, 1, 1] for taxable, tax-deferred, adn tax-exempt accounts.
461
464
  """
462
- assert len(phi) == self.N_j, 'Fractions must have %d entries.' % self.N_j
465
+ assert len(phi) == self.N_j, "Fractions must have %d entries." % self.N_j
463
466
  for j in range(self.N_j):
464
- assert 0 <= phi[j] <= 1, 'Fractions must be between 0 and 1.'
467
+ assert 0 <= phi[j] <= 1, "Fractions must be between 0 and 1."
465
468
 
466
469
  self.phi_j = np.array(phi, dtype=np.float32)
467
- self.mylog.vprint('Spousal beneficiary fractions set to', phi)
468
- self.caseStatus = 'modified'
470
+ self.mylog.vprint("Spousal beneficiary fractions set to", phi)
471
+ self.caseStatus = "modified"
469
472
 
470
473
  if np.any(self.phi_j != 1):
471
- self.mylog.vprint('Consider changing spousal deposit fraction for better convergence.')
472
- self.mylog.vprint('\tRecommended: setSpousalDepositFraction(%d)' % self.i_d)
474
+ self.mylog.vprint("Consider changing spousal deposit fraction for better convergence.")
475
+ self.mylog.vprint("\tRecommended: setSpousalDepositFraction(%d)" % self.i_d)
473
476
 
474
477
  def setHeirsTaxRate(self, nu):
475
478
  """
476
479
  Set the heirs tax rate on the tax-deferred portion of the estate.
477
480
  Rate is in percent. Default is 30%.
478
481
  """
479
- assert 0 <= nu and nu <= 100, 'Rate must be between 0 and 100.'
482
+ assert 0 <= nu and nu <= 100, "Rate must be between 0 and 100."
480
483
  nu /= 100
481
- self.mylog.vprint('Heirs tax rate on tax-deferred portion of estate set to %s.' % u.pc(nu, f=0))
484
+ self.mylog.vprint("Heirs tax rate on tax-deferred portion of estate set to %s." % u.pc(nu, f=0))
482
485
  self.nu = nu
483
- self.caseStatus = 'modified'
486
+ self.caseStatus = "modified"
484
487
 
485
- def setPension(self, amounts, ages, indexed=[False, False], units='k'):
488
+ def setPension(self, amounts, ages, indexed=[False, False], units="k"):
486
489
  """
487
490
  Set value of pension for each individual and commencement age.
488
491
  Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
489
492
  """
490
- assert len(amounts) == self.N_i, 'Amounts must have %d entries.' % self.N_i
491
- assert len(ages) == self.N_i, 'Ages must have %d entries.' % self.N_i
492
- assert len(indexed) >= self.N_i, 'Indexed list must have at least %d entries.' % self.N_i
493
+ assert len(amounts) == self.N_i, "Amounts must have %d entries." % self.N_i
494
+ assert len(ages) == self.N_i, "Ages must have %d entries." % self.N_i
495
+ assert len(indexed) >= self.N_i, "Indexed list must have at least %d entries." % self.N_i
493
496
 
494
497
  fac = u.getUnits(units)
495
498
  amounts = u.rescale(amounts, fac)
496
499
 
497
- self.mylog.vprint('Setting pension of', [u.d(amounts[i]) for i in range(self.N_i)], 'at age(s)', ages)
500
+ self.mylog.vprint("Setting pension of", [u.d(amounts[i]) for i in range(self.N_i)], "at age(s)", ages)
498
501
 
499
502
  thisyear = date.today().year
500
503
  # Use zero array freshly initialized.
@@ -511,24 +514,24 @@ class Plan(object):
511
514
  self.pensionAmounts = np.array(amounts)
512
515
  self.pensionAges = np.array(ages, dtype=np.int32)
513
516
  self.pensionIndexed = indexed
514
- self.caseStatus = 'modified'
517
+ self.caseStatus = "modified"
515
518
  self._adjustedParameters = False
516
519
 
517
- def setSocialSecurity(self, amounts, ages, units='k'):
520
+ def setSocialSecurity(self, amounts, ages, units="k"):
518
521
  """
519
522
  Set value of social security for each individual and commencement age.
520
523
  Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
521
524
  """
522
- assert len(amounts) == self.N_i, 'Amounts must have %d entries.' % self.N_i
523
- assert len(ages) == self.N_i, 'Ages must have %d entries.' % self.N_i
525
+ assert len(amounts) == self.N_i, "Amounts must have %d entries." % self.N_i
526
+ assert len(ages) == self.N_i, "Ages must have %d entries." % self.N_i
524
527
 
525
528
  fac = u.getUnits(units)
526
529
  amounts = u.rescale(amounts, fac)
527
530
 
528
531
  self.mylog.vprint(
529
- 'Setting social security benefits of',
532
+ "Setting social security benefits of",
530
533
  [u.d(amounts[i]) for i in range(self.N_i)],
531
- 'at age(s)',
534
+ "at age(s)",
532
535
  ages,
533
536
  )
534
537
 
@@ -544,11 +547,11 @@ class Plan(object):
544
547
 
545
548
  if self.N_i == 2:
546
549
  # Approximate calculation for spousal benefit (only valid at FRA).
547
- self.zeta_in[self.i_s, self.n_d:] = max(amounts[self.i_s], amounts[self.i_d])
550
+ self.zeta_in[self.i_s, self.n_d :] = max(amounts[self.i_s], amounts[self.i_d])
548
551
 
549
552
  self.ssecAmounts = np.array(amounts)
550
553
  self.ssecAges = np.array(ages, dtype=np.int32)
551
- self.caseStatus = 'modified'
554
+ self.caseStatus = "modified"
552
555
  self._adjustedParameters = False
553
556
 
554
557
  def setSpendingProfile(self, profile, percent=60, dip=15, increase=12, delay=0):
@@ -557,16 +560,16 @@ class Plan(object):
557
560
  as a second argument. Default value is 60%.
558
561
  Dip and increase are percent changes in the smile profile.
559
562
  """
560
- assert 0 <= percent and percent <= 100, 'Survivor value %r outside range.' % percent
561
- assert 0 <= dip and dip <= 100, 'Dip value %r outside range.' % dip
562
- assert -100 <= increase and increase <= 100, 'Increase value %r outside range.' % dip
563
- assert 0 <= delay and delay <= self.N_n - 2 , 'Delay value %r outside range.' % delay
563
+ assert 0 <= percent and percent <= 100, "Survivor value %r outside range." % percent
564
+ assert 0 <= dip and dip <= 100, "Dip value %r outside range." % dip
565
+ assert -100 <= increase and increase <= 100, "Increase value %r outside range." % dip
566
+ assert 0 <= delay and delay <= self.N_n - 2, "Delay value %r outside range." % delay
564
567
 
565
568
  self.chi = percent / 100
566
569
 
567
- self.mylog.vprint('Setting', profile, 'spending profile.')
570
+ self.mylog.vprint("Setting", profile, "spending profile.")
568
571
  if self.N_i == 2:
569
- self.mylog.vprint('Securing', u.pc(self.chi, f=0), 'of spending amount for surviving spouse.')
572
+ self.mylog.vprint("Securing", u.pc(self.chi, f=0), "of spending amount for surviving spouse.")
570
573
 
571
574
  self.xi_n = _genXi_n(profile, self.chi, self.n_d, self.N_n, dip, increase, delay)
572
575
  # Account for time elapsed in the current year.
@@ -576,7 +579,7 @@ class Plan(object):
576
579
  self.smileDip = dip
577
580
  self.smileIncrease = increase
578
581
  self.smileDelay = delay
579
- self.caseStatus = 'modified'
582
+ self.caseStatus = "modified"
580
583
 
581
584
  def setRates(self, method, frm=None, to=None, values=None, stdev=None, corr=None):
582
585
  """
@@ -604,11 +607,11 @@ class Plan(object):
604
607
  self.rateTo = to
605
608
  self.tau_kn = dr.genSeries(self.N_n).transpose()
606
609
  self.mylog.vprint(
607
- 'Generating rate series of',
610
+ "Generating rate series of",
608
611
  len(self.tau_kn[0]),
609
- 'years using',
612
+ "years using",
610
613
  method,
611
- 'method.',
614
+ "method.",
612
615
  )
613
616
 
614
617
  # Account for how late we are now in the first year and reduce rate accordingly.
@@ -617,7 +620,7 @@ class Plan(object):
617
620
  # Once rates are selected, (re)build cumulative inflation multipliers.
618
621
  self.gamma_n = _genGamma_n(self.tau_kn)
619
622
  self._adjustedParameters = False
620
- self.caseStatus = 'modified'
623
+ self.caseStatus = "modified"
621
624
 
622
625
  def regenRates(self):
623
626
  """
@@ -654,24 +657,24 @@ class Plan(object):
654
657
  to the beginning of the year provided.
655
658
  """
656
659
  if self.rateMethod is None:
657
- raise RuntimeError('A rate method needs to be first selected using setRates(...).')
660
+ raise RuntimeError("A rate method needs to be first selected using setRates(...).")
658
661
 
659
662
  thisyear = date.today().year
660
- assert year > thisyear, 'Internal error in forwardValue().'
663
+ assert year > thisyear, "Internal error in forwardValue()."
661
664
  span = year - thisyear
662
665
 
663
666
  return amount * self.gamma_n[span]
664
667
 
665
- def setAccountBalances(self, *, taxable, taxDeferred, taxFree, units='k'):
668
+ def setAccountBalances(self, *, taxable, taxDeferred, taxFree, units="k"):
666
669
  """
667
670
  Three lists containing the balance of all assets in each category for
668
671
  each spouse. For single individuals, these lists will contain only
669
672
  one entry. Units are in $k, unless specified otherwise: 'k', 'M', or '1'.
670
673
  """
671
- plurals = ['', 'y', 'ies']
672
- assert len(taxable) == self.N_i, 'taxable must have %d entr%s.' % (self.N_i, plurals[self.N_i])
673
- assert len(taxDeferred) == self.N_i, 'taxDeferred must have %d entr%s.' % (self.N_i, plurals[self.N_i])
674
- assert len(taxFree) == self.N_i, 'taxFree must have %d entr%s.' % (self.N_i, plurals[self.N_i])
674
+ plurals = ["", "y", "ies"]
675
+ assert len(taxable) == self.N_i, "taxable must have %d entr%s." % (self.N_i, plurals[self.N_i])
676
+ assert len(taxDeferred) == self.N_i, "taxDeferred must have %d entr%s." % (self.N_i, plurals[self.N_i])
677
+ assert len(taxFree) == self.N_i, "taxFree must have %d entr%s." % (self.N_i, plurals[self.N_i])
675
678
 
676
679
  fac = u.getUnits(units)
677
680
  taxable = u.rescale(taxable, fac)
@@ -683,14 +686,14 @@ class Plan(object):
683
686
  self.b_ji[1][:] = taxDeferred
684
687
  self.b_ji[2][:] = taxFree
685
688
  self.beta_ij = self.b_ji.transpose()
686
- self.caseStatus = 'modified'
689
+ self.caseStatus = "modified"
687
690
 
688
- self.mylog.vprint('Taxable balances:', *[u.d(taxable[i]) for i in range(self.N_i)])
689
- self.mylog.vprint('Tax-deferred balances:', *[u.d(taxDeferred[i]) for i in range(self.N_i)])
690
- self.mylog.vprint('Tax-free balances:', *[u.d(taxFree[i]) for i in range(self.N_i)])
691
- self.mylog.vprint('Sum of all savings accounts:', u.d(np.sum(taxable) + np.sum(taxDeferred) + np.sum(taxFree)))
691
+ self.mylog.vprint("Taxable balances:", *[u.d(taxable[i]) for i in range(self.N_i)])
692
+ self.mylog.vprint("Tax-deferred balances:", *[u.d(taxDeferred[i]) for i in range(self.N_i)])
693
+ self.mylog.vprint("Tax-free balances:", *[u.d(taxFree[i]) for i in range(self.N_i)])
694
+ self.mylog.vprint("Sum of all savings accounts:", u.d(np.sum(taxable) + np.sum(taxDeferred) + np.sum(taxFree)))
692
695
  self.mylog.vprint(
693
- 'Post-tax total wealth of approximately',
696
+ "Post-tax total wealth of approximately",
694
697
  u.d(np.sum(taxable) + 0.7 * np.sum(taxDeferred) + np.sum(taxFree)),
695
698
  )
696
699
 
@@ -706,19 +709,19 @@ class Plan(object):
706
709
  5 years. This means that the transition from initial to final
707
710
  will start occuring in 10 years (15-5) and will end in 20 years (15+5).
708
711
  """
709
- if method == 'linear':
712
+ if method == "linear":
710
713
  self._interpolator = self._linInterp
711
- elif method == 's-curve':
714
+ elif method == "s-curve":
712
715
  self._interpolator = self._tanhInterp
713
716
  self.interpCenter = center
714
717
  self.interpWidth = width
715
718
  else:
716
- raise ValueError('Method %s not supported.' % method)
719
+ raise ValueError("Method %s not supported." % method)
717
720
 
718
721
  self.interpMethod = method
719
- self.caseStatus = 'modified'
722
+ self.caseStatus = "modified"
720
723
 
721
- self.mylog.vprint('Asset allocation interpolation method set to %s.' % method)
724
+ self.mylog.vprint("Asset allocation interpolation method set to %s." % method)
722
725
 
723
726
  def setAllocationRatios(self, allocType, taxable=None, taxDeferred=None, taxFree=None, generic=None):
724
727
  """
@@ -744,34 +747,34 @@ class Plan(object):
744
747
  """
745
748
  self.boundsAR = {}
746
749
  self.alpha_ijkn = np.zeros((self.N_i, self.N_j, self.N_k, self.N_n + 1))
747
- if allocType == 'account':
750
+ if allocType == "account":
748
751
  # Make sure we have proper input.
749
752
  for item in [taxable, taxDeferred, taxFree]:
750
- assert len(item) == self.N_i, '%s must one entry per individual.' % (item)
753
+ assert len(item) == self.N_i, "%s must one entry per individual." % (item)
751
754
  for i in range(self.N_i):
752
755
  # Initial and final.
753
- assert len(item[i]) == 2, '%s[%d] must have 2 lists (initial and final).' % (
756
+ assert len(item[i]) == 2, "%s[%d] must have 2 lists (initial and final)." % (
754
757
  item,
755
758
  i,
756
759
  )
757
760
  for z in range(2):
758
- assert len(item[i][z]) == self.N_k, '%s[%d][%d] must have %d entries.' % (
761
+ assert len(item[i][z]) == self.N_k, "%s[%d][%d] must have %d entries." % (
759
762
  item,
760
763
  i,
761
764
  z,
762
765
  self.N_k,
763
766
  )
764
- assert abs(sum(item[i][z]) - 100) < 0.01, 'Sum of percentages must add to 100.'
767
+ assert abs(sum(item[i][z]) - 100) < 0.01, "Sum of percentages must add to 100."
765
768
 
766
769
  for i in range(self.N_i):
767
770
  self.mylog.vprint(
768
771
  self.inames[i],
769
- ': Setting gliding allocation ratios (%) to',
772
+ ": Setting gliding allocation ratios (%) to",
770
773
  allocType,
771
774
  )
772
- self.mylog.vprint(' taxable:', taxable[i][0], '->', taxable[i][1])
773
- self.mylog.vprint(' taxDeferred:', taxDeferred[i][0], '->', taxDeferred[i][1])
774
- self.mylog.vprint(' taxFree:', taxFree[i][0], '->', taxFree[i][1])
775
+ self.mylog.vprint(" taxable:", taxable[i][0], "->", taxable[i][1])
776
+ self.mylog.vprint(" taxDeferred:", taxDeferred[i][0], "->", taxDeferred[i][1])
777
+ self.mylog.vprint(" taxFree:", taxFree[i][0], "->", taxFree[i][1])
775
778
 
776
779
  # Order in alpha is j, i, 0/1, k.
777
780
  alpha = {}
@@ -787,30 +790,30 @@ class Plan(object):
787
790
  dat = self._interpolator(start, end, Nin)
788
791
  self.alpha_ijkn[i, j, k, :Nin] = dat[:]
789
792
 
790
- self.boundsAR['taxable'] = taxable
791
- self.boundsAR['tax-deferred'] = taxDeferred
792
- self.boundsAR['tax-free'] = taxFree
793
+ self.boundsAR["taxable"] = taxable
794
+ self.boundsAR["tax-deferred"] = taxDeferred
795
+ self.boundsAR["tax-free"] = taxFree
793
796
 
794
- elif allocType == 'individual':
795
- assert len(generic) == self.N_i, 'generic must have one list per individual.'
797
+ elif allocType == "individual":
798
+ assert len(generic) == self.N_i, "generic must have one list per individual."
796
799
  for i in range(self.N_i):
797
800
  # Initial and final.
798
- assert len(generic[i]) == 2, 'generic[%d] must have 2 lists (initial and final).' % i
801
+ assert len(generic[i]) == 2, "generic[%d] must have 2 lists (initial and final)." % i
799
802
  for z in range(2):
800
- assert len(generic[i][z]) == self.N_k, 'generic[%d][%d] must have %d entries.' % (
803
+ assert len(generic[i][z]) == self.N_k, "generic[%d][%d] must have %d entries." % (
801
804
  i,
802
805
  z,
803
806
  self.N_k,
804
807
  )
805
- assert abs(sum(generic[i][z]) - 100) < 0.01, 'Sum of percentages must add to 100.'
808
+ assert abs(sum(generic[i][z]) - 100) < 0.01, "Sum of percentages must add to 100."
806
809
 
807
810
  for i in range(self.N_i):
808
811
  self.mylog.vprint(
809
812
  self.inames[i],
810
- ': Setting gliding allocation ratios (%) to',
813
+ ": Setting gliding allocation ratios (%) to",
811
814
  allocType,
812
815
  )
813
- self.mylog.vprint('\t', generic[i][0], '->', generic[i][1])
816
+ self.mylog.vprint("\t", generic[i][0], "->", generic[i][1])
814
817
 
815
818
  for i in range(self.N_i):
816
819
  Nin = self.horizons[i] + 1
@@ -821,19 +824,19 @@ class Plan(object):
821
824
  for j in range(self.N_j):
822
825
  self.alpha_ijkn[i, j, k, :Nin] = dat[:]
823
826
 
824
- self.boundsAR['generic'] = generic
827
+ self.boundsAR["generic"] = generic
825
828
 
826
- elif allocType == 'spouses':
827
- assert len(generic) == 2, 'generic must have 2 entries (initial and final).'
829
+ elif allocType == "spouses":
830
+ assert len(generic) == 2, "generic must have 2 entries (initial and final)."
828
831
  for z in range(2):
829
- assert len(generic[z]) == self.N_k, 'generic[%d] must have %d entries.' % (
832
+ assert len(generic[z]) == self.N_k, "generic[%d] must have %d entries." % (
830
833
  z,
831
834
  self.N_k,
832
835
  )
833
- assert abs(sum(generic[z]) - 100) < 0.01, 'Sum of percentages must add to 100.'
836
+ assert abs(sum(generic[z]) - 100) < 0.01, "Sum of percentages must add to 100."
834
837
 
835
- self.mylog.vprint('Setting gliding allocation ratios (%) to', allocType)
836
- self.mylog.vprint('\t', generic[0], '->', generic[1])
838
+ self.mylog.vprint("Setting gliding allocation ratios (%) to", allocType)
839
+ self.mylog.vprint("\t", generic[0], "->", generic[1])
837
840
 
838
841
  # Use longest-lived spouse for both time scales.
839
842
  Nxn = max(self.horizons) + 1
@@ -846,12 +849,12 @@ class Plan(object):
846
849
  for j in range(self.N_j):
847
850
  self.alpha_ijkn[i, j, k, :Nxn] = dat[:]
848
851
 
849
- self.boundsAR['generic'] = generic
852
+ self.boundsAR["generic"] = generic
850
853
 
851
854
  self.ARCoord = allocType
852
- self.caseStatus = 'modified'
855
+ self.caseStatus = "modified"
853
856
 
854
- self.mylog.vprint('Interpolating assets allocation ratios using', self.interpMethod, 'method.')
857
+ self.mylog.vprint("Interpolating assets allocation ratios using", self.interpMethod, "method.")
855
858
 
856
859
  def readContributions(self, filename):
857
860
  """
@@ -877,7 +880,7 @@ class Plan(object):
877
880
  try:
878
881
  filename, self.timeLists = timelists.read(filename, self.inames, self.horizons, self.mylog)
879
882
  except Exception as e:
880
- raise Exception('Unsuccessful read of contributions: %s' % e)
883
+ raise Exception("Unsuccessful read of contributions: %s" % e)
881
884
  return False
882
885
 
883
886
  self.timeListsFileName = filename
@@ -893,20 +896,20 @@ class Plan(object):
893
896
  # Now fill in parameters which are in $.
894
897
  for i, iname in enumerate(self.inames):
895
898
  h = self.horizons[i]
896
- self.omega_in[i, :h] = self.timeLists[iname]['anticipated wages'].iloc[:h]
897
- self.kappa_ijn[i, 0, :h] = self.timeLists[iname]['taxable ctrb'].iloc[:h]
898
- self.kappa_ijn[i, 1, :h] = self.timeLists[iname]['401k ctrb'].iloc[:h]
899
- self.kappa_ijn[i, 2, :h] = self.timeLists[iname]['Roth 401k ctrb'].iloc[:h]
900
- self.kappa_ijn[i, 1, :h] += self.timeLists[iname]['IRA ctrb'].iloc[:h]
901
- self.kappa_ijn[i, 2, :h] += self.timeLists[iname]['Roth IRA ctrb'].iloc[:h]
902
- self.myRothX_in[i, :h] = self.timeLists[iname]['Roth conv'].iloc[:h]
903
- self.Lambda_in[i, :h] = self.timeLists[iname]['big-ticket items'].iloc[:h]
899
+ self.omega_in[i, :h] = self.timeLists[iname]["anticipated wages"].iloc[:h]
900
+ self.kappa_ijn[i, 0, :h] = self.timeLists[iname]["taxable ctrb"].iloc[:h]
901
+ self.kappa_ijn[i, 1, :h] = self.timeLists[iname]["401k ctrb"].iloc[:h]
902
+ self.kappa_ijn[i, 2, :h] = self.timeLists[iname]["Roth 401k ctrb"].iloc[:h]
903
+ self.kappa_ijn[i, 1, :h] += self.timeLists[iname]["IRA ctrb"].iloc[:h]
904
+ self.kappa_ijn[i, 2, :h] += self.timeLists[iname]["Roth IRA ctrb"].iloc[:h]
905
+ self.myRothX_in[i, :h] = self.timeLists[iname]["Roth conv"].iloc[:h]
906
+ self.Lambda_in[i, :h] = self.timeLists[iname]["big-ticket items"].iloc[:h]
904
907
 
905
908
  # In 1st year, reduce wages and contributions depending on starting date.
906
909
  self.omega_in[:, 0] *= self.yearFracLeft
907
910
  self.kappa_ijn[:, :, 0] *= self.yearFracLeft
908
911
 
909
- self.caseStatus = 'modified'
912
+ self.caseStatus = "modified"
910
913
 
911
914
  return self.timeLists
912
915
 
@@ -917,14 +920,14 @@ class Plan(object):
917
920
  if self.timeLists is None:
918
921
  return None
919
922
 
920
- self.mylog.vprint('Preparing wages and contributions workbook.')
923
+ self.mylog.vprint("Preparing wages and contributions workbook.")
921
924
 
922
925
  def fillsheet(sheet, i):
923
926
  sheet.title = self.inames[i]
924
927
  df = self.timeLists[self.inames[i]]
925
928
  for row in dataframe_to_rows(df, index=False, header=True):
926
929
  sheet.append(row)
927
- _formatSpreadsheet(sheet, 'currency')
930
+ _formatSpreadsheet(sheet, "currency")
928
931
 
929
932
  wb = Workbook()
930
933
  ws = wb.active
@@ -940,23 +943,32 @@ class Plan(object):
940
943
  """
941
944
  Reset all contributions variables to zero.
942
945
  """
943
- self.mylog.vprint('Resetting wages and contributions to zero.')
946
+ self.mylog.vprint("Resetting wages and contributions to zero.")
944
947
 
945
948
  # Reset parameters with zeros.
946
- self.omega_in[:, :] = 0.
947
- self.Lambda_in[:, :] = 0.
948
- self.myRothX_in[:, :] = 0.
949
- self.kappa_ijn[:, :, :] = 0.
950
-
951
- cols = ['year', 'anticipated wages', 'taxable ctrb', '401k ctrb',
952
- 'Roth 401k ctrb', 'IRA ctrb', 'Roth IRA ctrb', 'Roth conv', 'big-ticket items']
949
+ self.omega_in[:, :] = 0.0
950
+ self.Lambda_in[:, :] = 0.0
951
+ self.myRothX_in[:, :] = 0.0
952
+ self.kappa_ijn[:, :, :] = 0.0
953
+
954
+ cols = [
955
+ "year",
956
+ "anticipated wages",
957
+ "taxable ctrb",
958
+ "401k ctrb",
959
+ "Roth 401k ctrb",
960
+ "IRA ctrb",
961
+ "Roth IRA ctrb",
962
+ "Roth conv",
963
+ "big-ticket items",
964
+ ]
953
965
  for i, iname in enumerate(self.inames):
954
966
  h = self.horizons[i]
955
967
  df = pd.DataFrame(0, index=np.arange(h), columns=cols)
956
- df['year'] = self.year_n[:h]
968
+ df["year"] = self.year_n[:h]
957
969
  self.timeLists[iname] = df
958
970
 
959
- self.caseStatus = 'modified'
971
+ self.caseStatus = "modified"
960
972
 
961
973
  return self.timeLists
962
974
 
@@ -977,7 +989,7 @@ class Plan(object):
977
989
  is happening, and "w" is the width of the transition.
978
990
  """
979
991
  c = self.interpCenter
980
- w = self.interpWidth + 0.0001 # Avoid division by zero.
992
+ w = self.interpWidth + 0.0001 # Avoid division by zero.
981
993
  t = np.linspace(0, numPoints, numPoints)
982
994
  # Solve 2x2 system to match end points exactly.
983
995
  th0 = np.tanh((t[0] - c) / w)
@@ -997,10 +1009,10 @@ class Plan(object):
997
1009
  Adjust parameters that follow inflation.
998
1010
  """
999
1011
  if self.rateMethod is None:
1000
- raise RuntimeError('A rate method needs to be first selected using setRates(...).')
1012
+ raise RuntimeError("A rate method needs to be first selected using setRates(...).")
1001
1013
 
1002
1014
  if not self._adjustedParameters:
1003
- self.mylog.vprint('Adjusting parameters for inflation.')
1015
+ self.mylog.vprint("Adjusting parameters for inflation.")
1004
1016
  self.DeltaBar_tn = self.Delta_tn * self.gamma_n[:-1]
1005
1017
  self.zetaBar_in = self.zeta_in * self.gamma_n[:-1]
1006
1018
  self.sigmaBar_n = self.sigma_n * self.gamma_n[:-1]
@@ -1021,24 +1033,24 @@ class Plan(object):
1021
1033
  """
1022
1034
  # Stack all variables in a single block vector.
1023
1035
  C = {}
1024
- C['b'] = 0
1025
- C['d'] = _qC(C['b'], self.N_i, self.N_j, self.N_n + 1)
1026
- C['e'] = _qC(C['d'], self.N_i, self.N_n)
1027
- C['F'] = _qC(C['e'], self.N_n)
1028
- C['g'] = _qC(C['F'], self.N_t, self.N_n)
1029
- C['s'] = _qC(C['g'], self.N_n)
1030
- C['w'] = _qC(C['s'], self.N_n)
1031
- C['x'] = _qC(C['w'], self.N_i, self.N_j, self.N_n)
1032
- C['z'] = _qC(C['x'], self.N_i, self.N_n)
1033
- self.nvars = _qC(C['z'], self.N_i, self.N_n, self.N_z)
1036
+ C["b"] = 0
1037
+ C["d"] = _qC(C["b"], self.N_i, self.N_j, self.N_n + 1)
1038
+ C["e"] = _qC(C["d"], self.N_i, self.N_n)
1039
+ C["F"] = _qC(C["e"], self.N_n)
1040
+ C["g"] = _qC(C["F"], self.N_t, self.N_n)
1041
+ C["s"] = _qC(C["g"], self.N_n)
1042
+ C["w"] = _qC(C["s"], self.N_n)
1043
+ C["x"] = _qC(C["w"], self.N_i, self.N_j, self.N_n)
1044
+ C["z"] = _qC(C["x"], self.N_i, self.N_n)
1045
+ self.nvars = _qC(C["z"], self.N_i, self.N_n, self.N_z)
1034
1046
 
1035
1047
  self.C = C
1036
1048
  self.mylog.vprint(
1037
- 'Problem has',
1049
+ "Problem has",
1038
1050
  len(C),
1039
- 'distinct time series forming',
1051
+ "distinct time series forming",
1040
1052
  self.nvars,
1041
- 'decision variables.',
1053
+ "decision variables.",
1042
1054
  )
1043
1055
 
1044
1056
  return None
@@ -1063,15 +1075,15 @@ class Plan(object):
1063
1075
  i_s = self.i_s
1064
1076
  n_d = self.n_d
1065
1077
 
1066
- Cb = self.C['b']
1067
- Cd = self.C['d']
1068
- Ce = self.C['e']
1069
- CF = self.C['F']
1070
- Cg = self.C['g']
1071
- Cs = self.C['s']
1072
- Cw = self.C['w']
1073
- Cx = self.C['x']
1074
- Cz = self.C['z']
1078
+ Cb = self.C["b"]
1079
+ Cd = self.C["d"]
1080
+ Ce = self.C["e"]
1081
+ CF = self.C["F"]
1082
+ Cg = self.C["g"]
1083
+ Cs = self.C["s"]
1084
+ Cw = self.C["w"]
1085
+ Cx = self.C["x"]
1086
+ Cz = self.C["z"]
1075
1087
 
1076
1088
  tau_ijn = np.zeros((Ni, Nj, Nn))
1077
1089
  for i in range(Ni):
@@ -1083,15 +1095,15 @@ class Plan(object):
1083
1095
  Tau1_ijn = 1 + tau_ijn
1084
1096
  Tauh_ijn = 1 + tau_ijn / 2
1085
1097
 
1086
- if 'units' in options:
1087
- units = u.getUnits(options['units'])
1098
+ if "units" in options:
1099
+ units = u.getUnits(options["units"])
1088
1100
  else:
1089
1101
  units = 1000
1090
1102
 
1091
1103
  bigM = 5e6
1092
- if 'bigM' in options:
1104
+ if "bigM" in options:
1093
1105
  # No units for bigM.
1094
- bigM = options['bigM']
1106
+ bigM = options["bigM"]
1095
1107
 
1096
1108
  ###################################################################
1097
1109
  # Inequality constraint matrix with upper and lower bound vectors.
@@ -1118,16 +1130,16 @@ class Plan(object):
1118
1130
  B.set0_Ub(_q1(Ce, n, Nn), self.sigmaBar_n[n])
1119
1131
 
1120
1132
  # Roth conversions equalities/inequalities.
1121
- if 'maxRothConversion' in options:
1122
- if options['maxRothConversion'] == 'file':
1133
+ if "maxRothConversion" in options:
1134
+ if options["maxRothConversion"] == "file":
1123
1135
  # self.mylog.vprint('Fixing Roth conversions to those from file %s.' % self.timeListsFileName)
1124
1136
  for i in range(Ni):
1125
1137
  for n in range(self.horizons[i]):
1126
1138
  rhs = self.myRothX_in[i][n]
1127
1139
  B.setRange(_q2(Cx, i, n, Ni, Nn), rhs, rhs)
1128
1140
  else:
1129
- rhsopt = options['maxRothConversion']
1130
- assert isinstance(rhsopt, (int, float)), 'Specified maxConversion is not a number.'
1141
+ rhsopt = options["maxRothConversion"]
1142
+ assert isinstance(rhsopt, (int, float)), "Specified maxConversion is not a number."
1131
1143
  rhsopt *= units
1132
1144
  if rhsopt < 0:
1133
1145
  # self.mylog.vprint('Unlimited Roth conversions (<0)')
@@ -1140,12 +1152,12 @@ class Plan(object):
1140
1152
  B.set0_Ub(_q2(Cx, i, n, Ni, Nn), rhsopt)
1141
1153
 
1142
1154
  # Process noRothConversions option. Also valid when N_i == 1, why not?
1143
- if 'noRothConversions' in options and options['noRothConversions'] != 'None':
1144
- rhsopt = options['noRothConversions']
1155
+ if "noRothConversions" in options and options["noRothConversions"] != "None":
1156
+ rhsopt = options["noRothConversions"]
1145
1157
  try:
1146
1158
  i_x = self.inames.index(rhsopt)
1147
1159
  except ValueError:
1148
- raise ValueError('Unknown individual %s for noRothConversions:' % rhsopt)
1160
+ raise ValueError("Unknown individual %s for noRothConversions:" % rhsopt)
1149
1161
 
1150
1162
  for n in range(Nn):
1151
1163
  B.set0_Ub(_q2(Cx, i_x, n, Ni, Nn), zero)
@@ -1168,11 +1180,11 @@ class Plan(object):
1168
1180
  A.addNewRow(rowDic, zero, inf)
1169
1181
 
1170
1182
  # Constraints depending on objective function.
1171
- if objective == 'maxSpending':
1183
+ if objective == "maxSpending":
1172
1184
  # Impose optional constraint on final bequest requested in today's $.
1173
- if 'bequest' in options:
1174
- bequest = options['bequest']
1175
- assert isinstance(bequest, (int, float)), 'Desired bequest is not a number.'
1185
+ if "bequest" in options:
1186
+ bequest = options["bequest"]
1187
+ assert isinstance(bequest, (int, float)), "Desired bequest is not a number."
1176
1188
  bequest *= units * self.gamma_n[-1]
1177
1189
  else:
1178
1190
  # If not specified, defaults to $1 (nominal $).
@@ -1187,9 +1199,9 @@ class Plan(object):
1187
1199
  row.addElem(_q3(Cb, i, 2, Nn, Ni, Nj, Nn + 1), 1)
1188
1200
  A.addRow(row, bequest, bequest)
1189
1201
  # self.mylog.vprint('Adding bequest constraint of:', u.d(bequest))
1190
- elif objective == 'maxBequest':
1191
- spending = options['netSpending']
1192
- assert isinstance(spending, (int, float)), 'Desired spending provided is not a number.'
1202
+ elif objective == "maxBequest":
1203
+ spending = options["netSpending"]
1204
+ assert isinstance(spending, (int, float)), "Desired spending provided is not a number."
1193
1205
  # Account for time elapsed in the current year.
1194
1206
  spending *= units * self.yearFracLeft
1195
1207
  # self.mylog.vprint('Maximizing bequest with desired net spending of:', u.d(spending))
@@ -1362,15 +1374,15 @@ class Plan(object):
1362
1374
 
1363
1375
  # Now build a solver-neutral objective vector.
1364
1376
  c = abc.Objective(self.nvars)
1365
- if objective == 'maxSpending':
1377
+ if objective == "maxSpending":
1366
1378
  c.setElem(_q1(Cg, 0, Nn), -1)
1367
- elif objective == 'maxBequest':
1379
+ elif objective == "maxBequest":
1368
1380
  for i in range(Ni):
1369
1381
  c.setElem(_q3(Cb, i, 0, Nn, Ni, Nj, Nn + 1), -1)
1370
1382
  c.setElem(_q3(Cb, i, 1, Nn, Ni, Nj, Nn + 1), -(1 - self.nu))
1371
1383
  c.setElem(_q3(Cb, i, 2, Nn, Ni, Nj, Nn + 1), -1)
1372
1384
  else:
1373
- raise RuntimeError('Internal error in objective function.')
1385
+ raise RuntimeError("Internal error in objective function.")
1374
1386
 
1375
1387
  self.A = A
1376
1388
  self.B = B
@@ -1385,19 +1397,19 @@ class Plan(object):
1385
1397
  """
1386
1398
  if yend + self.N_n > self.year_n[0]:
1387
1399
  yend = self.year_n[0] - self.N_n - 1
1388
- self.mylog.vprint('Warning: Upper bound for year range re-adjusted to %d.' % yend)
1400
+ self.mylog.vprint("Warning: Upper bound for year range re-adjusted to %d." % yend)
1389
1401
  N = yend - ystart + 1
1390
1402
 
1391
- self.mylog.vprint('Running historical range from %d to %d.' % (ystart, yend))
1403
+ self.mylog.vprint("Running historical range from %d to %d." % (ystart, yend))
1392
1404
 
1393
1405
  self.mylog.setVerbose(verbose)
1394
1406
 
1395
- if objective == 'maxSpending':
1396
- columns = ['partial', objective]
1397
- elif objective == 'maxBequest':
1398
- columns = ['partial', 'final']
1407
+ if objective == "maxSpending":
1408
+ columns = ["partial", objective]
1409
+ elif objective == "maxBequest":
1410
+ columns = ["partial", "final"]
1399
1411
  else:
1400
- self.mylog.print('Invalid objective %s.' % objective)
1412
+ self.mylog.print("Invalid objective %s." % objective)
1401
1413
  return None
1402
1414
 
1403
1415
  df = pd.DataFrame(columns=columns)
@@ -1409,14 +1421,14 @@ class Plan(object):
1409
1421
  progcall.start()
1410
1422
 
1411
1423
  for year in range(ystart, yend + 1):
1412
- self.setRates('historical', year)
1424
+ self.setRates("historical", year)
1413
1425
  self.solve(objective, options)
1414
1426
  if not verbose:
1415
- progcall.show((year - ystart + 1)/N)
1416
- if self.caseStatus == 'solved':
1417
- if objective == 'maxSpending':
1427
+ progcall.show((year - ystart + 1) / N)
1428
+ if self.caseStatus == "solved":
1429
+ if objective == "maxSpending":
1418
1430
  df.loc[len(df)] = [self.partialBequest, self.basis]
1419
- elif objective == 'maxBequest':
1431
+ elif objective == "maxBequest":
1420
1432
  df.loc[len(df)] = [self.partialBequest, self.bequest]
1421
1433
 
1422
1434
  progcall.finish()
@@ -1434,26 +1446,26 @@ class Plan(object):
1434
1446
  """
1435
1447
  Run Monte Carlo simulations on plan.
1436
1448
  """
1437
- if self.rateMethod not in ['stochastic', 'histochastic']:
1438
- self.mylog.print('It is pointless to run Monte Carlo simulations with fixed rates.')
1449
+ if self.rateMethod not in ["stochastic", "histochastic"]:
1450
+ self.mylog.print("It is pointless to run Monte Carlo simulations with fixed rates.")
1439
1451
  return
1440
1452
 
1441
- self.mylog.vprint('Running %d Monte Carlo simulations.' % N)
1453
+ self.mylog.vprint("Running %d Monte Carlo simulations." % N)
1442
1454
  self.mylog.setVerbose(verbose)
1443
1455
 
1444
1456
  # Turn off Medicare by default, unless specified in options.
1445
- if 'withMedicare' not in options:
1457
+ if "withMedicare" not in options:
1446
1458
  myoptions = dict(options)
1447
- myoptions['withMedicare'] = False
1459
+ myoptions["withMedicare"] = False
1448
1460
  else:
1449
1461
  myoptions = options
1450
1462
 
1451
- if objective == 'maxSpending':
1452
- columns = ['partial', objective]
1453
- elif objective == 'maxBequest':
1454
- columns = ['partial', 'final']
1463
+ if objective == "maxSpending":
1464
+ columns = ["partial", objective]
1465
+ elif objective == "maxBequest":
1466
+ columns = ["partial", "final"]
1455
1467
  else:
1456
- self.mylog.print('Invalid objective %s.' % objective)
1468
+ self.mylog.print("Invalid objective %s." % objective)
1457
1469
  return None
1458
1470
 
1459
1471
  df = pd.DataFrame(columns=columns)
@@ -1468,11 +1480,11 @@ class Plan(object):
1468
1480
  self.regenRates()
1469
1481
  self.solve(objective, myoptions)
1470
1482
  if not verbose:
1471
- progcall.show((n+1)/N)
1472
- if self.caseStatus == 'solved':
1473
- if objective == 'maxSpending':
1483
+ progcall.show((n + 1) / N)
1484
+ if self.caseStatus == "solved":
1485
+ if objective == "maxSpending":
1474
1486
  df.loc[len(df)] = [self.partialBequest, self.basis]
1475
- elif objective == 'maxBequest':
1487
+ elif objective == "maxBequest":
1476
1488
  df.loc[len(df)] = [self.partialBequest, self.bequest]
1477
1489
 
1478
1490
  progcall.finish()
@@ -1493,8 +1505,8 @@ class Plan(object):
1493
1505
 
1494
1506
  description = io.StringIO()
1495
1507
 
1496
- print('Success rate: %s on %d samples.' % (u.pc(len(df) / N), N), file=description)
1497
- title = '$N$ = %d, $P$ = %s' % (N, u.pc(len(df) / N))
1508
+ print("Success rate: %s on %d samples." % (u.pc(len(df) / N), N), file=description)
1509
+ title = "$N$ = %d, $P$ = %s" % (N, u.pc(len(df) / N))
1498
1510
  means = df.mean(axis=0, numeric_only=True)
1499
1511
  medians = df.median(axis=0, numeric_only=True)
1500
1512
 
@@ -1506,51 +1518,51 @@ class Plan(object):
1506
1518
  # or if solution led to empty accounts at the end of first spouse's life.
1507
1519
  if np.all(self.phi_j == 1) or medians.iloc[0] < 1:
1508
1520
  if medians.iloc[0] < 1:
1509
- print('Optimized solutions all have null partial bequest in year %d.' % my[0], file=description)
1510
- df.drop('partial', axis=1, inplace=True)
1521
+ print("Optimized solutions all have null partial bequest in year %d." % my[0], file=description)
1522
+ df.drop("partial", axis=1, inplace=True)
1511
1523
  means = df.mean(axis=0, numeric_only=True)
1512
1524
  medians = df.median(axis=0, numeric_only=True)
1513
1525
 
1514
1526
  df /= 1000
1515
1527
  if len(df) > 0:
1516
- if objective == 'maxBequest':
1528
+ if objective == "maxBequest":
1517
1529
  fig, axes = plt.subplots()
1518
1530
  # Show both partial and final bequests in the same histogram.
1519
- sbn.histplot(df, multiple='dodge', kde=True, ax=axes)
1531
+ sbn.histplot(df, multiple="dodge", kde=True, ax=axes)
1520
1532
  legend = []
1521
1533
  # Don't know why but legend is reversed from df.
1522
1534
  for q in range(len(means) - 1, -1, -1):
1523
1535
  legend.append(
1524
- '%d: $M$: %s, $\\bar{x}$: %s'
1536
+ "%d: $M$: %s, $\\bar{x}$: %s"
1525
1537
  % (my[q], u.d(medians.iloc[q], latex=True), u.d(means.iloc[q], latex=True))
1526
1538
  )
1527
1539
  plt.legend(legend, shadow=True)
1528
- plt.xlabel('%d $k' % self.year_n[0])
1540
+ plt.xlabel("%d $k" % self.year_n[0])
1529
1541
  plt.title(objective)
1530
- leads = ['partial %d' % my[0], ' final %d' % my[1]]
1542
+ leads = ["partial %d" % my[0], " final %d" % my[1]]
1531
1543
  elif len(means) == 2:
1532
1544
  # Show partial bequest and net spending as two separate histograms.
1533
1545
  fig, axes = plt.subplots(1, 2, figsize=(10, 5))
1534
- cols = ['partial', objective]
1535
- leads = ['partial %d' % my[0], objective]
1546
+ cols = ["partial", objective]
1547
+ leads = ["partial %d" % my[0], objective]
1536
1548
  for q in range(2):
1537
1549
  sbn.histplot(df[cols[q]], kde=True, ax=axes[q])
1538
1550
  legend = [
1539
- ('$M$: %s, $\\bar{x}$: %s' % (u.d(medians.iloc[q], latex=True), u.d(means.iloc[q], latex=True)))
1551
+ ("$M$: %s, $\\bar{x}$: %s" % (u.d(medians.iloc[q], latex=True), u.d(means.iloc[q], latex=True)))
1540
1552
  ]
1541
1553
  axes[q].set_label(legend)
1542
1554
  axes[q].legend(labels=legend)
1543
1555
  axes[q].set_title(leads[q])
1544
- axes[q].set_xlabel('%d $k' % self.year_n[0])
1556
+ axes[q].set_xlabel("%d $k" % self.year_n[0])
1545
1557
  else:
1546
1558
  # Show net spending as single histogram.
1547
1559
  fig, axes = plt.subplots()
1548
1560
  sbn.histplot(df[objective], kde=True, ax=axes)
1549
1561
  legend = [
1550
- ('$M$: %s, $\\bar{x}$: %s' % (u.d(medians.iloc[0], latex=True), u.d(means.iloc[0], latex=True)))
1562
+ ("$M$: %s, $\\bar{x}$: %s" % (u.d(medians.iloc[0], latex=True), u.d(means.iloc[0], latex=True)))
1551
1563
  ]
1552
1564
  plt.legend(legend, shadow=True)
1553
- plt.xlabel('%d $k' % self.year_n[0])
1565
+ plt.xlabel("%d $k" % self.year_n[0])
1554
1566
  plt.title(objective)
1555
1567
  leads = [objective]
1556
1568
 
@@ -1558,14 +1570,15 @@ class Plan(object):
1558
1570
  # plt.show()
1559
1571
 
1560
1572
  for q in range(len(means)):
1561
- print('%12s: Median (%d $): %s' % (leads[q], self.year_n[0], u.d(medians.iloc[q])), file=description)
1562
- print('%12s: Mean (%d $): %s' % (leads[q], self.year_n[0], u.d(means.iloc[q])), file=description)
1573
+ print("%12s: Median (%d $): %s" % (leads[q], self.year_n[0], u.d(medians.iloc[q])), file=description)
1574
+ print("%12s: Mean (%d $): %s" % (leads[q], self.year_n[0], u.d(means.iloc[q])), file=description)
1563
1575
  print(
1564
- '%12s: Range: %s - %s'
1576
+ "%12s: Range: %s - %s"
1565
1577
  % (leads[q], u.d(1000 * df.iloc[:, q].min()), u.d(1000 * df.iloc[:, q].max())),
1566
- file=description)
1578
+ file=description,
1579
+ )
1567
1580
  nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
1568
- print('%12s: N zero solns: %d' % (leads[q], nzeros), file=description)
1581
+ print("%12s: N zero solns: %d" % (leads[q], nzeros), file=description)
1569
1582
 
1570
1583
  return fig, description
1571
1584
 
@@ -1597,24 +1610,24 @@ class Plan(object):
1597
1610
  Refer to companion document for implementation details.
1598
1611
  """
1599
1612
  if self.rateMethod is None:
1600
- raise RuntimeError('Rate method must be selected before solving.')
1613
+ raise RuntimeError("Rate method must be selected before solving.")
1601
1614
 
1602
1615
  # Assume unsuccessful until problem solved.
1603
- self.caseStatus = 'unsuccessful'
1616
+ self.caseStatus = "unsuccessful"
1604
1617
 
1605
1618
  # Check objective and required options.
1606
- knownObjectives = ['maxBequest', 'maxSpending']
1607
- knownSolvers = ['HiGHS', 'MOSEK']
1619
+ knownObjectives = ["maxBequest", "maxSpending"]
1620
+ knownSolvers = ["HiGHS", "MOSEK"]
1608
1621
  knownOptions = [
1609
- 'units',
1610
- 'maxRothConversion',
1611
- 'netSpending',
1612
- 'bequest',
1613
- 'bigM',
1614
- 'noRothConversions',
1615
- 'withMedicare',
1616
- 'solver',
1617
- 'previousMAGIs',
1622
+ "units",
1623
+ "maxRothConversion",
1624
+ "netSpending",
1625
+ "bequest",
1626
+ "bigM",
1627
+ "noRothConversions",
1628
+ "withMedicare",
1629
+ "solver",
1630
+ "previousMAGIs",
1618
1631
  ]
1619
1632
  # We will modify options if required.
1620
1633
  if options is None:
@@ -1624,48 +1637,48 @@ class Plan(object):
1624
1637
 
1625
1638
  for opt in myoptions:
1626
1639
  if opt not in knownOptions:
1627
- raise ValueError('Option %s is not one of %r.' % (opt, knownOptions))
1640
+ raise ValueError("Option %s is not one of %r." % (opt, knownOptions))
1628
1641
 
1629
1642
  if objective not in knownObjectives:
1630
- raise ValueError('Objective %s is not one of %r.' % (objective, knownObjectives))
1643
+ raise ValueError("Objective %s is not one of %r." % (objective, knownObjectives))
1631
1644
 
1632
- if objective == 'maxBequest' and 'netSpending' not in myoptions:
1633
- raise RuntimeError('Objective %s needs netSpending option.' % objective)
1645
+ if objective == "maxBequest" and "netSpending" not in myoptions:
1646
+ raise RuntimeError("Objective %s needs netSpending option." % objective)
1634
1647
 
1635
- if objective == 'maxBequest' and 'bequest' in myoptions:
1636
- self.mylog.vprint('Ignoring bequest option provided.')
1637
- myoptions.pop('bequest')
1648
+ if objective == "maxBequest" and "bequest" in myoptions:
1649
+ self.mylog.vprint("Ignoring bequest option provided.")
1650
+ myoptions.pop("bequest")
1638
1651
 
1639
- if objective == 'maxSpending' and 'netSpending' in myoptions:
1640
- self.mylog.vprint('Ignoring netSpending option provided.')
1641
- myoptions.pop('netSpending')
1652
+ if objective == "maxSpending" and "netSpending" in myoptions:
1653
+ self.mylog.vprint("Ignoring netSpending option provided.")
1654
+ myoptions.pop("netSpending")
1642
1655
 
1643
- if objective == 'maxSpending' and 'bequest' not in myoptions:
1644
- self.mylog.vprint('Using bequest of $1.')
1656
+ if objective == "maxSpending" and "bequest" not in myoptions:
1657
+ self.mylog.vprint("Using bequest of $1.")
1645
1658
 
1646
- if 'previousMAGIs' in myoptions:
1647
- magi = myoptions['previousMAGIs']
1659
+ if "previousMAGIs" in myoptions:
1660
+ magi = myoptions["previousMAGIs"]
1648
1661
  if len(magi) != 2:
1649
1662
  raise ValueError("previousMAGIs must have two values.")
1650
1663
 
1651
- if 'units' in options:
1652
- units = u.getUnits(options['units'])
1664
+ if "units" in options:
1665
+ units = u.getUnits(options["units"])
1653
1666
  else:
1654
1667
  units = 1000
1655
1668
  self.prevMAGI = units * np.array(magi)
1656
1669
 
1657
1670
  self._adjustParameters()
1658
1671
 
1659
- if 'solver' in options:
1660
- solver = myoptions['solver']
1672
+ if "solver" in options:
1673
+ solver = myoptions["solver"]
1661
1674
  if solver not in knownSolvers:
1662
- raise ValueError('Unknown solver %s.' % solver)
1675
+ raise ValueError("Unknown solver %s." % solver)
1663
1676
  else:
1664
1677
  solver = self.defaultSolver
1665
1678
 
1666
- if solver == 'HiGHS':
1679
+ if solver == "HiGHS":
1667
1680
  self._milpSolve(objective, myoptions)
1668
- elif solver == 'MOSEK':
1681
+ elif solver == "MOSEK":
1669
1682
  self._mosekSolve(objective, myoptions)
1670
1683
 
1671
1684
  self.objective = objective
@@ -1680,16 +1693,16 @@ class Plan(object):
1680
1693
  from scipy import optimize
1681
1694
 
1682
1695
  withMedicare = True
1683
- if 'withMedicare' in options and options['withMedicare'] is False:
1696
+ if "withMedicare" in options and options["withMedicare"] is False:
1684
1697
  withMedicare = False
1685
1698
 
1686
- if objective == 'maxSpending':
1699
+ if objective == "maxSpending":
1687
1700
  objFac = -1 / self.xi_n[0]
1688
1701
  else:
1689
1702
  objFac = -1 / self.gamma_n[-1]
1690
1703
 
1691
1704
  # mip_rel_gap smaller than 1e-6 can lead to oscillatory solutions.
1692
- milpOptions = {'disp': False, 'mip_rel_gap': 1e-6}
1705
+ milpOptions = {"disp": False, "mip_rel_gap": 1e-6}
1693
1706
 
1694
1707
  it = 0
1695
1708
  absdiff = np.inf
@@ -1722,38 +1735,38 @@ class Plan(object):
1722
1735
 
1723
1736
  self._estimateMedicare(solution.x)
1724
1737
 
1725
- self.mylog.vprint('Iteration:', it, 'objective:', u.d(solution.fun * objFac, f=2))
1738
+ self.mylog.vprint("Iteration:", it, "objective:", u.d(solution.fun * objFac, f=2))
1726
1739
 
1727
1740
  delta = solution.x - old_x
1728
1741
  absdiff = np.sum(np.abs(delta), axis=0)
1729
1742
  if absdiff < 1:
1730
- self.mylog.vprint('Converged on full solution.')
1743
+ self.mylog.vprint("Converged on full solution.")
1731
1744
  break
1732
1745
 
1733
1746
  # Avoid oscillatory solutions. Look only at most recent solutions. Within $10.
1734
- isclosenough = abs(-solution.fun - min(old_solutions[int(it / 2):])) < 10 * self.xi_n[0]
1747
+ isclosenough = abs(-solution.fun - min(old_solutions[int(it / 2) :])) < 10 * self.xi_n[0]
1735
1748
  if isclosenough:
1736
- self.mylog.vprint('Converged through selecting minimum oscillating objective.')
1749
+ self.mylog.vprint("Converged through selecting minimum oscillating objective.")
1737
1750
  break
1738
1751
 
1739
1752
  if it > 59:
1740
- self.mylog.vprint('WARNING: Exiting loop on maximum iterations.')
1753
+ self.mylog.vprint("WARNING: Exiting loop on maximum iterations.")
1741
1754
  break
1742
1755
 
1743
1756
  old_solutions.append(-solution.fun)
1744
1757
  old_x = solution.x
1745
1758
 
1746
1759
  if solution.success:
1747
- self.mylog.vprint('Self-consistent Medicare loop returned after %d iterations.' % it)
1760
+ self.mylog.vprint("Self-consistent Medicare loop returned after %d iterations." % it)
1748
1761
  self.mylog.vprint(solution.message)
1749
- self.mylog.vprint('Objective:', u.d(solution.fun * objFac))
1762
+ self.mylog.vprint("Objective:", u.d(solution.fun * objFac))
1750
1763
  # self.mylog.vprint('Upper bound:', u.d(-solution.mip_dual_bound))
1751
1764
  self._aggregateResults(solution.x)
1752
- self._timestamp = datetime.now().strftime('%Y-%m-%d at %H:%M:%S')
1753
- self.caseStatus = 'solved'
1765
+ self._timestamp = datetime.now().strftime("%Y-%m-%d at %H:%M:%S")
1766
+ self.caseStatus = "solved"
1754
1767
  else:
1755
- self.mylog.vprint('WARNING: Optimization failed:', solution.message, solution.success)
1756
- self.caseStatus = 'unsuccessful'
1768
+ self.mylog.vprint("WARNING: Optimization failed:", solution.message, solution.success)
1769
+ self.caseStatus = "unsuccessful"
1757
1770
 
1758
1771
  return None
1759
1772
 
@@ -1764,10 +1777,10 @@ class Plan(object):
1764
1777
  import mosek
1765
1778
 
1766
1779
  withMedicare = True
1767
- if 'withMedicare' in options and options['withMedicare'] is False:
1780
+ if "withMedicare" in options and options["withMedicare"] is False:
1768
1781
  withMedicare = False
1769
1782
 
1770
- if objective == 'maxSpending':
1783
+ if objective == "maxSpending":
1771
1784
  objFac = -1 / self.xi_n[0]
1772
1785
  else:
1773
1786
  objFac = -1 / self.gamma_n[-1]
@@ -1775,11 +1788,11 @@ class Plan(object):
1775
1788
  # mip_rel_gap smaller than 1e-6 can lead to oscillatory solutions.
1776
1789
 
1777
1790
  bdic = {
1778
- 'fx': mosek.boundkey.fx,
1779
- 'fr': mosek.boundkey.fr,
1780
- 'lo': mosek.boundkey.lo,
1781
- 'ra': mosek.boundkey.ra,
1782
- 'up': mosek.boundkey.up,
1791
+ "fx": mosek.boundkey.fx,
1792
+ "fr": mosek.boundkey.fr,
1793
+ "lo": mosek.boundkey.lo,
1794
+ "ra": mosek.boundkey.ra,
1795
+ "up": mosek.boundkey.up,
1783
1796
  }
1784
1797
 
1785
1798
  it = 0
@@ -1834,22 +1847,22 @@ class Plan(object):
1834
1847
 
1835
1848
  self._estimateMedicare(xx)
1836
1849
 
1837
- self.mylog.vprint('Iteration:', it, 'objective:', u.d(solution * objFac, f=2))
1850
+ self.mylog.vprint("Iteration:", it, "objective:", u.d(solution * objFac, f=2))
1838
1851
 
1839
1852
  delta = xx - old_x
1840
1853
  absdiff = np.sum(np.abs(delta), axis=0)
1841
1854
  if absdiff < 1:
1842
- self.mylog.vprint('Converged on full solution.')
1855
+ self.mylog.vprint("Converged on full solution.")
1843
1856
  break
1844
1857
 
1845
1858
  # Avoid oscillatory solutions. Look only at most recent solutions. Within $10.
1846
- isclosenough = abs(-solution - min(old_solutions[int(it / 2):])) < 10 * self.xi_n[0]
1859
+ isclosenough = abs(-solution - min(old_solutions[int(it / 2) :])) < 10 * self.xi_n[0]
1847
1860
  if isclosenough:
1848
- self.mylog.vprint('Converged through selecting minimum oscillating objective.')
1861
+ self.mylog.vprint("Converged through selecting minimum oscillating objective.")
1849
1862
  break
1850
1863
 
1851
1864
  if it > 59:
1852
- self.mylog.vprint('WARNING: Exiting loop on maximum iterations.')
1865
+ self.mylog.vprint("WARNING: Exiting loop on maximum iterations.")
1853
1866
  break
1854
1867
 
1855
1868
  old_solutions.append(-solution)
@@ -1858,17 +1871,17 @@ class Plan(object):
1858
1871
  task.set_Stream(mosek.streamtype.wrn, _streamPrinter)
1859
1872
  # task.writedata(self._name+'.ptf')
1860
1873
  if solsta == mosek.solsta.integer_optimal:
1861
- self.mylog.vprint('Self-consistent Medicare loop returned after %d iterations.' % it)
1874
+ self.mylog.vprint("Self-consistent Medicare loop returned after %d iterations." % it)
1862
1875
  task.solutionsummary(mosek.streamtype.msg)
1863
- self.mylog.vprint('Objective:', u.d(solution * objFac))
1864
- self.caseStatus = 'solved'
1876
+ self.mylog.vprint("Objective:", u.d(solution * objFac))
1877
+ self.caseStatus = "solved"
1865
1878
  # self.mylog.vprint('Upper bound:', u.d(-solution.mip_dual_bound))
1866
1879
  self._aggregateResults(xx)
1867
- self._timestamp = datetime.now().strftime('%Y/%m/%d %H:%M:%S')
1880
+ self._timestamp = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
1868
1881
  else:
1869
- self.mylog.vprint('WARNING: Optimization failed:', 'Infeasible or unbounded.')
1882
+ self.mylog.vprint("WARNING: Optimization failed:", "Infeasible or unbounded.")
1870
1883
  task.solutionsummary(mosek.streamtype.msg)
1871
- self.caseStatus = 'unsuccessful'
1884
+ self.caseStatus = "unsuccessful"
1872
1885
 
1873
1886
  return None
1874
1887
 
@@ -1883,9 +1896,9 @@ class Plan(object):
1883
1896
  if x is None:
1884
1897
  MAGI_n = np.zeros(self.N_n)
1885
1898
  else:
1886
- self.F_tn = np.array(x[self.C['F']:self.C['g']])
1899
+ self.F_tn = np.array(x[self.C["F"] : self.C["g"]])
1887
1900
  self.F_tn = self.F_tn.reshape((self.N_t, self.N_n))
1888
- MAGI_n = np.sum(self.F_tn, axis=0) + np.array(x[self.C['e']:self.C['F']])
1901
+ MAGI_n = np.sum(self.F_tn, axis=0) + np.array(x[self.C["e"] : self.C["F"]])
1889
1902
 
1890
1903
  self.M_n = tx.mediCosts(self.yobs, self.horizons, MAGI_n, self.prevMAGI, self.gamma_n[:-1], self.N_n)
1891
1904
 
@@ -1905,15 +1918,15 @@ class Plan(object):
1905
1918
  # Nz = self.N_z
1906
1919
  n_d = self.n_d
1907
1920
 
1908
- Cb = self.C['b']
1909
- Cd = self.C['d']
1910
- Ce = self.C['e']
1911
- CF = self.C['F']
1912
- Cg = self.C['g']
1913
- Cs = self.C['s']
1914
- Cw = self.C['w']
1915
- Cx = self.C['x']
1916
- Cz = self.C['z']
1921
+ Cb = self.C["b"]
1922
+ Cd = self.C["d"]
1923
+ Ce = self.C["e"]
1924
+ CF = self.C["F"]
1925
+ Cg = self.C["g"]
1926
+ Cs = self.C["s"]
1927
+ Cw = self.C["w"]
1928
+ Cx = self.C["x"]
1929
+ Cz = self.C["z"]
1917
1930
 
1918
1931
  x = u.roundCents(x)
1919
1932
 
@@ -2006,20 +2019,20 @@ class Plan(object):
2006
2019
  ]
2007
2020
  """
2008
2021
  sources = {}
2009
- sources['wages'] = self.omega_in
2010
- sources['ssec'] = self.zetaBar_in
2011
- sources['pension'] = self.piBar_in
2012
- sources['txbl acc wdrwl'] = self.w_ijn[:, 0, :]
2013
- sources['RMD'] = self.rmd_in
2014
- sources['+dist'] = self.dist_in
2015
- sources['RothX'] = self.x_in
2016
- sources['tax-free wdrwl'] = self.w_ijn[:, 2, :]
2017
- sources['BTI'] = self.Lambda_in
2022
+ sources["wages"] = self.omega_in
2023
+ sources["ssec"] = self.zetaBar_in
2024
+ sources["pension"] = self.piBar_in
2025
+ sources["txbl acc wdrwl"] = self.w_ijn[:, 0, :]
2026
+ sources["RMD"] = self.rmd_in
2027
+ sources["+dist"] = self.dist_in
2028
+ sources["RothX"] = self.x_in
2029
+ sources["tax-free wdrwl"] = self.w_ijn[:, 2, :]
2030
+ sources["BTI"] = self.Lambda_in
2018
2031
 
2019
2032
  savings = {}
2020
- savings['taxable'] = self.b_ijn[:, 0, :]
2021
- savings['tax-deferred'] = self.b_ijn[:, 1, :]
2022
- savings['tax-free'] = self.b_ijn[:, 2, :]
2033
+ savings["taxable"] = self.b_ijn[:, 0, :]
2034
+ savings["tax-deferred"] = self.b_ijn[:, 1, :]
2035
+ savings["tax-free"] = self.b_ijn[:, 2, :]
2023
2036
 
2024
2037
  self.sources_in = sources
2025
2038
  self.savings_in = savings
@@ -2039,7 +2052,7 @@ class Plan(object):
2039
2052
  """
2040
2053
  _estate = np.sum(self.b_ijn[:, :, :, self.N_n], axis=(0, 2))
2041
2054
  _estate[1] *= 1 - self.nu
2042
- self.mylog.vprint('Estate value of %s at the end of year %s.' % (u.d(sum(_estate)), self.year_n[-1]))
2055
+ self.mylog.vprint("Estate value of %s at the end of year %s." % (u.d(sum(_estate)), self.year_n[-1]))
2043
2056
 
2044
2057
  return None
2045
2058
 
@@ -2048,11 +2061,11 @@ class Plan(object):
2048
2061
  """
2049
2062
  Print summary in logs.
2050
2063
  """
2051
- self.mylog.print('SUMMARY ================================================================')
2064
+ self.mylog.print("SUMMARY ================================================================")
2052
2065
  dic = self.summaryDic()
2053
2066
  for key, value in dic.items():
2054
2067
  self.mylog.print(f"{key}: {value}")
2055
- self.mylog.print('------------------------------------------------------------------------')
2068
+ self.mylog.print("------------------------------------------------------------------------")
2056
2069
 
2057
2070
  return None
2058
2071
 
@@ -2071,11 +2084,10 @@ class Plan(object):
2071
2084
  """
2072
2085
  Return summary as a string.
2073
2086
  """
2074
- string = 'Synopsis\n'
2087
+ string = "Synopsis\n"
2075
2088
  dic = self.summaryDic()
2076
2089
  for key, value in dic.items():
2077
2090
  string += f"{key:>70}: {value}\n"
2078
- # string += "%60s: %s\n" % (key, value)
2079
2091
 
2080
2092
  return string
2081
2093
 
@@ -2086,39 +2098,29 @@ class Plan(object):
2086
2098
  now = self.year_n[0]
2087
2099
  dic = {}
2088
2100
  # Results
2089
- dic[f"Net yearly spending basis in {now}$"] = (u.d(self.g_n[0] / self.xi_n[0]))
2090
- dic[f"Net yearly spending for year {now}"] = (u.d(self.g_n[0] / self.yearFracLeft))
2101
+ dic[f"Net yearly spending basis in {now}$"] = u.d(self.g_n[0] / self.xi_n[0])
2102
+ dic[f"Net yearly spending for year {now}"] = u.d(self.g_n[0] / self.yearFracLeft)
2091
2103
  dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
2092
2104
 
2093
2105
  totIncome = np.sum(self.g_n, axis=0)
2094
2106
  totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
2095
- dic[f"Total net spending in {now}$"] = (
2096
- "%s (%s nominal)" % (u.d(totIncomeNow), u.d(totIncome))
2097
- )
2107
+ dic[f"Total net spending in {now}$"] = "%s (%s nominal)" % (u.d(totIncomeNow), u.d(totIncome))
2098
2108
 
2099
2109
  totRoth = np.sum(self.x_in, axis=(0, 1))
2100
2110
  totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
2101
- dic[f"Total Roth conversions in {now}$"] = (
2102
- "%s (%s nominal)" % (u.d(totRothNow), u.d(totRoth))
2103
- )
2111
+ dic[f"Total Roth conversions in {now}$"] = "%s (%s nominal)" % (u.d(totRothNow), u.d(totRoth))
2104
2112
 
2105
2113
  taxPaid = np.sum(self.T_n, axis=0)
2106
2114
  taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
2107
- dic[f"Total income tax paid on ordinary income in {now}$"] = (
2108
- "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2109
- )
2115
+ dic[f"Total income tax paid on ordinary income in {now}$"] = "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2110
2116
 
2111
2117
  taxPaid = np.sum(self.U_n, axis=0)
2112
2118
  taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
2113
- dic[f"Total tax paid on gains and dividends in {now}$"] = (
2114
- "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2115
- )
2119
+ dic[f"Total tax paid on gains and dividends in {now}$"] = "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2116
2120
 
2117
2121
  taxPaid = np.sum(self.M_n, axis=0)
2118
2122
  taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
2119
- dic[f"Total Medicare premiums paid in {now}$"] = (
2120
- "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2121
- )
2123
+ dic[f"Total Medicare premiums paid in {now}$"] = "%s (%s nominal)" % (u.d(taxPaidNow), u.d(taxPaid))
2122
2124
 
2123
2125
  if self.N_i == 2 and self.n_d < self.N_n:
2124
2126
  p_j = self.partialEstate_j * (1 - self.phi_j)
@@ -2129,23 +2131,21 @@ class Plan(object):
2129
2131
  q_j = self.partialEstate_j * self.phi_j
2130
2132
  totSpousal = np.sum(q_j)
2131
2133
  totSpousalNow = totSpousal / self.gamma_n[nx + 1]
2132
- dic["Spousal wealth transfer from %s to %s in year %d (nominal)" %
2133
- (self.inames[self.i_d], self.inames[self.i_s], self.year_n[nx])] = (
2134
- "taxable: %s tax-def: %s tax-free: %s" % (u.d(q_j[0]), u.d(q_j[1]), u.d(q_j[2]))
2135
- )
2134
+ dic[
2135
+ "Spousal wealth transfer from %s to %s in year %d (nominal)"
2136
+ % (self.inames[self.i_d], self.inames[self.i_s], self.year_n[nx])
2137
+ ] = "taxable: %s tax-def: %s tax-free: %s" % (u.d(q_j[0]), u.d(q_j[1]), u.d(q_j[2]))
2136
2138
 
2137
- dic["Sum of spousal bequests to %s in year %d in %d$" %
2138
- (self.inames[self.i_s], self.year_n[nx], now)] = (
2139
+ dic["Sum of spousal bequests to %s in year %d in %d$" % (self.inames[self.i_s], self.year_n[nx], now)] = (
2139
2140
  "%s (%s nominal)" % (u.d(totSpousalNow), u.d(totSpousal))
2140
2141
  )
2141
- dic["Post-tax non-spousal bequests from %s in year %d (nominal)" %
2142
- (self.inames[self.i_d], self.year_n[nx])] = (
2143
- "taxable: %s tax-def: %s tax-free: %s" % (u.d(p_j[0]), u.d(p_j[1]), u.d(p_j[2]))
2144
- )
2145
- dic["Sum of post-tax non-spousal bequests from %s in year %d in %d$" %
2146
- (self.inames[self.i_d], self.year_n[nx], now)] = (
2147
- "%s (%s nominal)" % (u.d(totOthersNow), u.d(totOthers))
2148
- )
2142
+ dic[
2143
+ "Post-tax non-spousal bequests from %s in year %d (nominal)" % (self.inames[self.i_d], self.year_n[nx])
2144
+ ] = "taxable: %s tax-def: %s tax-free: %s" % (u.d(p_j[0]), u.d(p_j[1]), u.d(p_j[2]))
2145
+ dic[
2146
+ "Sum of post-tax non-spousal bequests from %s in year %d in %d$"
2147
+ % (self.inames[self.i_d], self.year_n[nx], now)
2148
+ ] = "%s (%s nominal)" % (u.d(totOthersNow), u.d(totOthers))
2149
2149
 
2150
2150
  estate = np.sum(self.b_ijn[:, :, self.N_n], axis=0)
2151
2151
  estate[1] *= 1 - self.nu
@@ -2159,12 +2159,11 @@ class Plan(object):
2159
2159
  "%s (%s nominal)" % (u.d(totEstateNow), u.d(totEstate))
2160
2160
  )
2161
2161
  dic["Plan starting date"] = str(self.startDate)
2162
- dic["Cumulative inflation factor from start date to end of plan"] = (
2163
- "%.2f" % (self.gamma_n[-1])
2164
- )
2162
+ dic["Cumulative inflation factor from start date to end of plan"] = "%.2f" % (self.gamma_n[-1])
2165
2163
  for i in range(self.N_i):
2166
- dic["%12s's %02d-year life horizon" % (self.inames[i], self.horizons[i])] = (
2167
- "%d -> %d" % (now, now + self.horizons[i] - 1)
2164
+ dic["%12s's %02d-year life horizon" % (self.inames[i], self.horizons[i])] = "%d -> %d" % (
2165
+ now,
2166
+ now + self.horizons[i] - 1,
2168
2167
  )
2169
2168
 
2170
2169
  dic["Plan name"] = self._name
@@ -2174,7 +2173,7 @@ class Plan(object):
2174
2173
 
2175
2174
  return dic
2176
2175
 
2177
- def showRatesCorrelations(self, tag='', shareRange=False, figure=False):
2176
+ def showRatesCorrelations(self, tag="", shareRange=False, figure=False):
2178
2177
  """
2179
2178
  Plot correlations between various rates.
2180
2179
 
@@ -2182,15 +2181,15 @@ class Plan(object):
2182
2181
  """
2183
2182
  import seaborn as sbn
2184
2183
 
2185
- if self.rateMethod in [None, 'user', 'historical average', 'conservative']:
2186
- self.mylog.vprint('Warning: Cannot plot correlations for %s rate method.' % self.rateMethod)
2184
+ if self.rateMethod in [None, "user", "historical average", "conservative"]:
2185
+ self.mylog.vprint("Warning: Cannot plot correlations for %s rate method." % self.rateMethod)
2187
2186
  return None
2188
2187
 
2189
2188
  rateNames = [
2190
- 'S&P500 (incl. div.)',
2191
- 'Baa Corp. Bonds',
2192
- '10-y T-Notes',
2193
- 'Inflation',
2189
+ "S&P500 (incl. div.)",
2190
+ "Baa Corp. Bonds",
2191
+ "10-y T-Notes",
2192
+ "Inflation",
2194
2193
  ]
2195
2194
 
2196
2195
  df = pd.DataFrame()
@@ -2206,25 +2205,25 @@ class Plan(object):
2206
2205
  g.map_upper(sbn.scatterplot)
2207
2206
  g.map_lower(sbn.kdeplot)
2208
2207
  # g.map_diag(sbn.kdeplot)
2209
- g.map_diag(sbn.histplot, color='orange')
2208
+ g.map_diag(sbn.histplot, color="orange")
2210
2209
 
2211
2210
  # Put zero axes on off-diagonal plots.
2212
2211
  imod = len(rateNames) + 1
2213
2212
  for i, ax in enumerate(g.axes.flat):
2214
- ax.axvline(x=0, color='grey', linewidth=1, linestyle=':')
2213
+ ax.axvline(x=0, color="grey", linewidth=1, linestyle=":")
2215
2214
  if i % imod != 0:
2216
- ax.axhline(y=0, color='grey', linewidth=1, linestyle=':')
2215
+ ax.axhline(y=0, color="grey", linewidth=1, linestyle=":")
2217
2216
  # ax.tick_params(axis='both', labelleft=True, labelbottom=True)
2218
2217
 
2219
2218
  # plt.subplots_adjust(wspace=0.3, hspace=0.3)
2220
2219
 
2221
- title = self._name + '\n'
2222
- title += 'Rates Correlations (N=%d) %s' % (self.N_n, self.rateMethod)
2223
- if self.rateMethod in ['historical', 'histochastic']:
2224
- title += ' (' + str(self.rateFrm) + '-' + str(self.rateTo) + ')'
2220
+ title = self._name + "\n"
2221
+ title += "Rates Correlations (N=%d) %s" % (self.N_n, self.rateMethod)
2222
+ if self.rateMethod in ["historical", "histochastic"]:
2223
+ title += " (" + str(self.rateFrm) + "-" + str(self.rateTo) + ")"
2225
2224
 
2226
- if tag != '':
2227
- title += ' - ' + tag
2225
+ if tag != "":
2226
+ title += " - " + tag
2228
2227
 
2229
2228
  g.fig.suptitle(title, y=1.08)
2230
2229
 
@@ -2234,7 +2233,7 @@ class Plan(object):
2234
2233
  plt.show()
2235
2234
  return None
2236
2235
 
2237
- def showRates(self, tag='', figure=False):
2236
+ def showRates(self, tag="", figure=False):
2238
2237
  """
2239
2238
  Plot rate values used over the time horizon.
2240
2239
 
@@ -2243,26 +2242,26 @@ class Plan(object):
2243
2242
  import matplotlib.ticker as tk
2244
2243
 
2245
2244
  if self.rateMethod is None:
2246
- self.mylog.vprint('Warning: Rate method must be selected before plotting.')
2245
+ self.mylog.vprint("Warning: Rate method must be selected before plotting.")
2247
2246
  return None
2248
2247
 
2249
2248
  fig, ax = plt.subplots(figsize=(6, 4))
2250
- plt.grid(visible='both')
2251
- title = self._name + '\nReturn & Inflation Rates (' + str(self.rateMethod)
2252
- if self.rateMethod in ['historical', 'histochastic', 'historical average']:
2253
- title += ' ' + str(self.rateFrm) + '-' + str(self.rateTo)
2254
- title += ')'
2249
+ plt.grid(visible="both")
2250
+ title = self._name + "\nReturn & Inflation Rates (" + str(self.rateMethod)
2251
+ if self.rateMethod in ["historical", "histochastic", "historical average"]:
2252
+ title += " " + str(self.rateFrm) + "-" + str(self.rateTo)
2253
+ title += ")"
2255
2254
 
2256
- if tag != '':
2257
- title += ' - ' + tag
2255
+ if tag != "":
2256
+ title += " - " + tag
2258
2257
 
2259
2258
  rateName = [
2260
- 'S&P500 (incl. div.)',
2261
- 'Baa Corp. Bonds',
2262
- '10-y T-Notes',
2263
- 'Inflation',
2259
+ "S&P500 (incl. div.)",
2260
+ "Baa Corp. Bonds",
2261
+ "10-y T-Notes",
2262
+ "Inflation",
2264
2263
  ]
2265
- ltype = ['-', '-.', ':', '--']
2264
+ ltype = ["-", "-.", ":", "--"]
2266
2265
  for k in range(self.N_k):
2267
2266
  if self.yearFracLeft == 1:
2268
2267
  data = 100 * self.tau_kn[k]
@@ -2272,16 +2271,17 @@ class Plan(object):
2272
2271
  years = self.year_n[1:]
2273
2272
 
2274
2273
  # Use ddof=1 to match pandas.
2275
- label = (rateName[k] + ' <' + '{:.1f}'.format(np.mean(data)) +
2276
- ' +/- {:.1f}'.format(np.std(data, ddof=1)) + '%>')
2274
+ label = (
2275
+ rateName[k] + " <" + "{:.1f}".format(np.mean(data)) + " +/- {:.1f}".format(np.std(data, ddof=1)) + "%>"
2276
+ )
2277
2277
  ax.plot(years, data, label=label, ls=ltype[k % self.N_k])
2278
2278
 
2279
2279
  ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
2280
- ax.legend(loc='best', reverse=False, fontsize=8, framealpha=0.7)
2280
+ ax.legend(loc="best", reverse=False, fontsize=8, framealpha=0.7)
2281
2281
  # ax.legend(loc='upper left')
2282
2282
  ax.set_title(title)
2283
- ax.set_xlabel('year')
2284
- ax.set_ylabel('%')
2283
+ ax.set_xlabel("year")
2284
+ ax.set_ylabel("%")
2285
2285
 
2286
2286
  if figure:
2287
2287
  return fig
@@ -2289,24 +2289,24 @@ class Plan(object):
2289
2289
  plt.show()
2290
2290
  return None
2291
2291
 
2292
- def showProfile(self, tag='', figure=False):
2292
+ def showProfile(self, tag="", figure=False):
2293
2293
  """
2294
2294
  Plot spending profile over time.
2295
2295
 
2296
2296
  A tag string can be set to add information to the title of the plot.
2297
2297
  """
2298
2298
  if self.xi_n is None:
2299
- self.mylog.vprint('Warning: Profile must be selected before plotting.')
2299
+ self.mylog.vprint("Warning: Profile must be selected before plotting.")
2300
2300
  return None
2301
2301
 
2302
- title = self._name + '\nSpending Profile'
2303
- if tag != '':
2304
- title += ' - ' + tag
2302
+ title = self._name + "\nSpending Profile"
2303
+ if tag != "":
2304
+ title += " - " + tag
2305
2305
 
2306
2306
  # style = {'net': '-', 'target': ':'}
2307
- style = {'profile': '-'}
2308
- series = {'profile': self.xi_n}
2309
- fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat='xi')
2307
+ style = {"profile": "-"}
2308
+ series = {"profile": self.xi_n}
2309
+ fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat="xi")
2310
2310
 
2311
2311
  if figure:
2312
2312
  return fig
@@ -2315,7 +2315,7 @@ class Plan(object):
2315
2315
  return None
2316
2316
 
2317
2317
  @_checkCaseStatus
2318
- def showNetSpending(self, tag='', value=None, figure=False):
2318
+ def showNetSpending(self, tag="", value=None, figure=False):
2319
2319
  """
2320
2320
  Plot net available spending and target over time.
2321
2321
 
@@ -2326,20 +2326,20 @@ class Plan(object):
2326
2326
  """
2327
2327
  value = self._checkValue(value)
2328
2328
 
2329
- title = self._name + '\nNet Available Spending'
2330
- if tag != '':
2331
- title += ' - ' + tag
2329
+ title = self._name + "\nNet Available Spending"
2330
+ if tag != "":
2331
+ title += " - " + tag
2332
2332
 
2333
- style = {'net': '-', 'target': ':'}
2334
- if value == 'nominal':
2335
- series = {'net': self.g_n, 'target': (self.g_n[0] / self.xi_n[0]) * self.xiBar_n}
2336
- yformat = '\\$k (nominal)'
2333
+ style = {"net": "-", "target": ":"}
2334
+ if value == "nominal":
2335
+ series = {"net": self.g_n, "target": (self.g_n[0] / self.xi_n[0]) * self.xiBar_n}
2336
+ yformat = "\\$k (nominal)"
2337
2337
  else:
2338
2338
  series = {
2339
- 'net': self.g_n / self.gamma_n[:-1],
2340
- 'target': (self.g_n[0] / self.xi_n[0]) * self.xi_n,
2339
+ "net": self.g_n / self.gamma_n[:-1],
2340
+ "target": (self.g_n[0] / self.xi_n[0]) * self.xi_n,
2341
2341
  }
2342
- yformat = '\\$k (' + str(self.year_n[0]) + '\\$)'
2342
+ yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2343
2343
 
2344
2344
  fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
2345
2345
 
@@ -2350,7 +2350,7 @@ class Plan(object):
2350
2350
  return None
2351
2351
 
2352
2352
  @_checkCaseStatus
2353
- def showAssetDistribution(self, tag='', value=None, figure=False):
2353
+ def showAssetDistribution(self, tag="", value=None, figure=False):
2354
2354
  """
2355
2355
  Plot the distribution of each savings account in thousands of dollars
2356
2356
  during the simulation time. This function will generate three
@@ -2364,34 +2364,35 @@ class Plan(object):
2364
2364
  """
2365
2365
  value = self._checkValue(value)
2366
2366
 
2367
- if value == 'nominal':
2368
- yformat = '\\$k (nominal)'
2367
+ if value == "nominal":
2368
+ yformat = "\\$k (nominal)"
2369
2369
  infladjust = 1
2370
2370
  else:
2371
- yformat = '\\$k (' + str(self.year_n[0]) + '\\$)'
2371
+ yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2372
2372
  infladjust = self.gamma_n
2373
2373
 
2374
2374
  years_n = np.array(self.year_n)
2375
2375
  years_n = np.append(years_n, [years_n[-1] + 1])
2376
2376
  y2stack = {}
2377
- jDic = {'taxable': 0, 'tax-deferred': 1, 'tax-free': 2}
2378
- kDic = {'stocks': 0, 'C bonds': 1, 'T notes': 2, 'common': 3}
2377
+ jDic = {"taxable": 0, "tax-deferred": 1, "tax-free": 2}
2378
+ kDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
2379
2379
  figures = []
2380
2380
  for jkey in jDic:
2381
2381
  stackNames = []
2382
2382
  for kkey in kDic:
2383
- name = kkey + ' / ' + jkey
2383
+ name = kkey + " / " + jkey
2384
2384
  stackNames.append(name)
2385
2385
  y2stack[name] = np.zeros((self.N_i, self.N_n + 1))
2386
2386
  for i in range(self.N_i):
2387
2387
  y2stack[name][i][:] = self.b_ijkn[i][jDic[jkey]][kDic[kkey]][:] / infladjust
2388
2388
 
2389
- title = self._name + '\nAssets Distribution - ' + jkey
2390
- if tag != '':
2391
- title += ' - ' + tag
2389
+ title = self._name + "\nAssets Distribution - " + jkey
2390
+ if tag != "":
2391
+ title += " - " + tag
2392
2392
 
2393
- fig, ax = _stackPlot(years_n, self.inames, title, range(self.N_i),
2394
- y2stack, stackNames, 'upper left', yformat)
2393
+ fig, ax = _stackPlot(
2394
+ years_n, self.inames, title, range(self.N_i), y2stack, stackNames, "upper left", yformat
2395
+ )
2395
2396
  figures.append(fig)
2396
2397
 
2397
2398
  if figure:
@@ -2400,7 +2401,7 @@ class Plan(object):
2400
2401
  plt.show()
2401
2402
  return None
2402
2403
 
2403
- def showAllocations(self, tag='', figure=False):
2404
+ def showAllocations(self, tag="", figure=False):
2404
2405
  """
2405
2406
  Plot desired allocation of savings accounts in percentage
2406
2407
  over simulation time and interpolated by the selected method
@@ -2409,39 +2410,38 @@ class Plan(object):
2409
2410
  A tag string can be set to add information to the title of the plot.
2410
2411
  """
2411
2412
  count = self.N_i
2412
- if self.ARCoord == 'spouses':
2413
+ if self.ARCoord == "spouses":
2413
2414
  acList = [self.ARCoord]
2414
2415
  count = 1
2415
- elif self.ARCoord == 'individual':
2416
+ elif self.ARCoord == "individual":
2416
2417
  acList = [self.ARCoord]
2417
- elif self.ARCoord == 'account':
2418
- acList = ['taxable', 'tax-deferred', 'tax-free']
2418
+ elif self.ARCoord == "account":
2419
+ acList = ["taxable", "tax-deferred", "tax-free"]
2419
2420
  else:
2420
- raise ValueError('Unknown coordination %s' % self.ARCoord)
2421
+ raise ValueError("Unknown coordination %s" % self.ARCoord)
2421
2422
 
2422
2423
  figures = []
2423
- assetDic = {'stocks': 0, 'C bonds': 1, 'T notes': 2, 'common': 3}
2424
+ assetDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
2424
2425
  for i in range(count):
2425
2426
  y2stack = {}
2426
2427
  for acType in acList:
2427
2428
  stackNames = []
2428
2429
  for key in assetDic:
2429
- aname = key + ' / ' + acType
2430
+ aname = key + " / " + acType
2430
2431
  stackNames.append(aname)
2431
2432
  y2stack[aname] = np.zeros((count, self.N_n))
2432
2433
  y2stack[aname][i][:] = self.alpha_ijkn[i, acList.index(acType), assetDic[key], : self.N_n]
2433
2434
 
2434
- title = self._name + '\nAsset Allocation (%) - ' + acType
2435
- if self.ARCoord == 'spouses':
2436
- title += ' spouses'
2435
+ title = self._name + "\nAsset Allocation (%) - " + acType
2436
+ if self.ARCoord == "spouses":
2437
+ title += " spouses"
2437
2438
  else:
2438
- title += ' ' + self.inames[i]
2439
+ title += " " + self.inames[i]
2439
2440
 
2440
- if tag != '':
2441
- title += ' - ' + tag
2441
+ if tag != "":
2442
+ title += " - " + tag
2442
2443
 
2443
- fig, ax = _stackPlot(self.year_n, self.inames, title, [i], y2stack,
2444
- stackNames, 'upper left', 'percent')
2444
+ fig, ax = _stackPlot(self.year_n, self.inames, title, [i], y2stack, stackNames, "upper left", "percent")
2445
2445
  figures.append(fig)
2446
2446
 
2447
2447
  if figure:
@@ -2451,7 +2451,7 @@ class Plan(object):
2451
2451
  return None
2452
2452
 
2453
2453
  @_checkCaseStatus
2454
- def showAccounts(self, tag='', value=None, figure=False):
2454
+ def showAccounts(self, tag="", value=None, figure=False):
2455
2455
  """
2456
2456
  Plot values of savings accounts over time.
2457
2457
 
@@ -2462,25 +2462,24 @@ class Plan(object):
2462
2462
  """
2463
2463
  value = self._checkValue(value)
2464
2464
 
2465
- title = self._name + '\nSavings Balance'
2466
- if tag != '':
2467
- title += ' - ' + tag
2465
+ title = self._name + "\nSavings Balance"
2466
+ if tag != "":
2467
+ title += " - " + tag
2468
2468
 
2469
2469
  stypes = self.savings_in.keys()
2470
2470
  # Add one year for estate.
2471
2471
  year_n = np.append(self.year_n, [self.year_n[-1] + 1])
2472
2472
 
2473
- if value == 'nominal':
2474
- yformat = '\\$k (nominal)'
2473
+ if value == "nominal":
2474
+ yformat = "\\$k (nominal)"
2475
2475
  savings_in = self.savings_in
2476
2476
  else:
2477
- yformat = '\\$k (' + str(self.year_n[0]) + '\\$)'
2477
+ yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2478
2478
  savings_in = {}
2479
2479
  for key in self.savings_in:
2480
2480
  savings_in[key] = self.savings_in[key] / self.gamma_n
2481
2481
 
2482
- fig, ax = _stackPlot(year_n, self.inames, title, range(self.N_i),
2483
- savings_in, stypes, 'upper left', yformat)
2482
+ fig, ax = _stackPlot(year_n, self.inames, title, range(self.N_i), savings_in, stypes, "upper left", yformat)
2484
2483
 
2485
2484
  if figure:
2486
2485
  return fig
@@ -2489,7 +2488,7 @@ class Plan(object):
2489
2488
  return None
2490
2489
 
2491
2490
  @_checkCaseStatus
2492
- def showSources(self, tag='', value=None, figure=False):
2491
+ def showSources(self, tag="", value=None, figure=False):
2493
2492
  """
2494
2493
  Plot income over time.
2495
2494
 
@@ -2500,23 +2499,24 @@ class Plan(object):
2500
2499
  """
2501
2500
  value = self._checkValue(value)
2502
2501
 
2503
- title = self._name + '\nRaw Income Sources'
2502
+ title = self._name + "\nRaw Income Sources"
2504
2503
  stypes = self.sources_in.keys()
2505
2504
 
2506
- if tag != '':
2507
- title += ' - ' + tag
2505
+ if tag != "":
2506
+ title += " - " + tag
2508
2507
 
2509
- if value == 'nominal':
2510
- yformat = '\\$k (nominal)'
2508
+ if value == "nominal":
2509
+ yformat = "\\$k (nominal)"
2511
2510
  sources_in = self.sources_in
2512
2511
  else:
2513
- yformat = '\\$k (' + str(self.year_n[0]) + '\\$)'
2512
+ yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2514
2513
  sources_in = {}
2515
2514
  for key in self.sources_in:
2516
2515
  sources_in[key] = self.sources_in[key] / self.gamma_n[:-1]
2517
2516
 
2518
- fig, ax = _stackPlot(self.year_n, self.inames, title, range(self.N_i),
2519
- sources_in, stypes, 'upper left', yformat)
2517
+ fig, ax = _stackPlot(
2518
+ self.year_n, self.inames, title, range(self.N_i), sources_in, stypes, "upper left", yformat
2519
+ )
2520
2520
 
2521
2521
  if figure:
2522
2522
  return fig
@@ -2525,34 +2525,34 @@ class Plan(object):
2525
2525
  return None
2526
2526
 
2527
2527
  @_checkCaseStatus
2528
- def _showFeff(self, tag=''):
2528
+ def _showFeff(self, tag=""):
2529
2529
  """
2530
2530
  Plot income tax paid over time.
2531
2531
 
2532
2532
  A tag string can be set to add information to the title of the plot.
2533
2533
  """
2534
- title = self._name + '\nEff f '
2535
- if tag != '':
2536
- title += ' - ' + tag
2534
+ title = self._name + "\nEff f "
2535
+ if tag != "":
2536
+ title += " - " + tag
2537
2537
 
2538
- various = ['-', '--', '-.', ':']
2538
+ various = ["-", "--", "-.", ":"]
2539
2539
  style = {}
2540
2540
  series = {}
2541
2541
  q = 0
2542
2542
  for t in range(self.N_t):
2543
- key = 'f ' + str(t)
2543
+ key = "f " + str(t)
2544
2544
  series[key] = self.F_tn[t] / self.DeltaBar_tn[t]
2545
2545
  # print(key, series[key])
2546
2546
  style[key] = various[q % len(various)]
2547
2547
  q += 1
2548
2548
 
2549
- fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat='')
2549
+ fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat="")
2550
2550
 
2551
2551
  plt.show()
2552
2552
  return None
2553
2553
 
2554
2554
  @_checkCaseStatus
2555
- def showTaxes(self, tag='', value=None, figure=False):
2555
+ def showTaxes(self, tag="", value=None, figure=False):
2556
2556
  """
2557
2557
  Plot income tax paid over time.
2558
2558
 
@@ -2563,21 +2563,21 @@ class Plan(object):
2563
2563
  """
2564
2564
  value = self._checkValue(value)
2565
2565
 
2566
- style = {'income taxes': '-', 'Medicare': '-.'}
2566
+ style = {"income taxes": "-", "Medicare": "-."}
2567
2567
 
2568
- if value == 'nominal':
2569
- series = {'income taxes': self.T_n, 'Medicare': self.M_n}
2570
- yformat = '\\$k (nominal)'
2568
+ if value == "nominal":
2569
+ series = {"income taxes": self.T_n, "Medicare": self.M_n}
2570
+ yformat = "\\$k (nominal)"
2571
2571
  else:
2572
2572
  series = {
2573
- 'income taxes': self.T_n / self.gamma_n[:-1],
2574
- 'Medicare': self.M_n / self.gamma_n[:-1],
2573
+ "income taxes": self.T_n / self.gamma_n[:-1],
2574
+ "Medicare": self.M_n / self.gamma_n[:-1],
2575
2575
  }
2576
- yformat = '\\$k (' + str(self.year_n[0]) + '\\$)'
2576
+ yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2577
2577
 
2578
- title = self._name + '\nIncome Tax'
2579
- if tag != '':
2580
- title += ' - ' + tag
2578
+ title = self._name + "\nIncome Tax"
2579
+ if tag != "":
2580
+ title += " - " + tag
2581
2581
 
2582
2582
  fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
2583
2583
 
@@ -2588,7 +2588,7 @@ class Plan(object):
2588
2588
  return None
2589
2589
 
2590
2590
  @_checkCaseStatus
2591
- def showGrossIncome(self, tag='', value=None, figure=False):
2591
+ def showGrossIncome(self, tag="", value=None, figure=False):
2592
2592
  """
2593
2593
  Plot income tax and taxable income over time horizon.
2594
2594
 
@@ -2599,30 +2599,30 @@ class Plan(object):
2599
2599
  """
2600
2600
  value = self._checkValue(value)
2601
2601
 
2602
- style = {'taxable income': '-'}
2602
+ style = {"taxable income": "-"}
2603
2603
 
2604
- if value == 'nominal':
2605
- series = {'taxable income': self.G_n}
2606
- yformat = '\\$k (nominal)'
2604
+ if value == "nominal":
2605
+ series = {"taxable income": self.G_n}
2606
+ yformat = "\\$k (nominal)"
2607
2607
  infladjust = self.gamma_n[:-1]
2608
2608
  else:
2609
- series = {'taxable income': self.G_n / self.gamma_n[:-1]}
2610
- yformat = '\\$k (' + str(self.year_n[0]) + '\\$)'
2609
+ series = {"taxable income": self.G_n / self.gamma_n[:-1]}
2610
+ yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2611
2611
  infladjust = 1
2612
2612
 
2613
- title = self._name + '\nTaxable Ordinary Income vs. Tax Brackets'
2614
- if tag != '':
2615
- title += ' - ' + tag
2613
+ title = self._name + "\nTaxable Ordinary Income vs. Tax Brackets"
2614
+ if tag != "":
2615
+ title += " - " + tag
2616
2616
 
2617
2617
  fig, ax = _lineIncomePlot(self.year_n, series, style, title, yformat)
2618
2618
 
2619
2619
  data = tx.taxBrackets(self.N_i, self.n_d, self.N_n)
2620
2620
  for key in data:
2621
2621
  data_adj = data[key] * infladjust
2622
- ax.plot(self.year_n, data_adj, label=key, ls=':')
2622
+ ax.plot(self.year_n, data_adj, label=key, ls=":")
2623
2623
 
2624
- plt.grid(visible='both')
2625
- ax.legend(loc='upper left', reverse=True, fontsize=8, framealpha=0.3)
2624
+ plt.grid(visible="both")
2625
+ ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
2626
2626
 
2627
2627
  if figure:
2628
2628
  return fig
@@ -2636,7 +2636,7 @@ class Plan(object):
2636
2636
  Save parameters in a configuration file.
2637
2637
  """
2638
2638
  if basename is None:
2639
- basename = 'case_' + self._name
2639
+ basename = "case_" + self._name
2640
2640
 
2641
2641
  config.saveConfig(self, basename, self.mylog)
2642
2642
 
@@ -2678,10 +2678,11 @@ class Plan(object):
2678
2678
 
2679
2679
  Last worksheet contains summary.
2680
2680
  """
2681
+
2681
2682
  def fillsheet(sheet, dic, datatype, op=lambda x: x):
2682
2683
  rawData = {}
2683
- rawData['year'] = self.year_n
2684
- if datatype == 'currency':
2684
+ rawData["year"] = self.year_n
2685
+ if datatype == "currency":
2685
2686
  for key in dic:
2686
2687
  rawData[key] = u.roundCents(op(dic[key]))
2687
2688
  else:
@@ -2699,71 +2700,71 @@ class Plan(object):
2699
2700
 
2700
2701
  # Income.
2701
2702
  ws = wb.active
2702
- ws.title = 'Income'
2703
+ ws.title = "Income"
2703
2704
 
2704
2705
  incomeDic = {
2705
- 'net spending': self.g_n,
2706
- 'taxable ord. income': self.G_n,
2707
- 'taxable gains/divs': self.Q_n,
2708
- 'Tax bills + Med.': self.T_n + self.U_n + self.M_n,
2706
+ "net spending": self.g_n,
2707
+ "taxable ord. income": self.G_n,
2708
+ "taxable gains/divs": self.Q_n,
2709
+ "Tax bills + Med.": self.T_n + self.U_n + self.M_n,
2709
2710
  }
2710
2711
 
2711
- fillsheet(ws, incomeDic, 'currency')
2712
+ fillsheet(ws, incomeDic, "currency")
2712
2713
 
2713
2714
  # Cash flow - sum over both individuals for some.
2714
2715
  cashFlowDic = {
2715
- 'net spending': self.g_n,
2716
- 'all wages': np.sum(self.omega_in, axis=0),
2717
- 'all pensions': np.sum(self.piBar_in, axis=0),
2718
- 'all soc sec': np.sum(self.zetaBar_in, axis=0),
2716
+ "net spending": self.g_n,
2717
+ "all wages": np.sum(self.omega_in, axis=0),
2718
+ "all pensions": np.sum(self.piBar_in, axis=0),
2719
+ "all soc sec": np.sum(self.zetaBar_in, axis=0),
2719
2720
  "all BTI's": np.sum(self.Lambda_in, axis=0),
2720
- 'all wdrwls': np.sum(self.w_ijn, axis=(0, 1)),
2721
- 'all deposits': -np.sum(self.d_in, axis=0),
2722
- 'ord taxes': -self.T_n,
2723
- 'div taxes': -self.U_n,
2724
- 'Medicare': -self.M_n,
2721
+ "all wdrwls": np.sum(self.w_ijn, axis=(0, 1)),
2722
+ "all deposits": -np.sum(self.d_in, axis=0),
2723
+ "ord taxes": -self.T_n,
2724
+ "div taxes": -self.U_n,
2725
+ "Medicare": -self.M_n,
2725
2726
  }
2726
- sname = 'Cash Flow'
2727
+ sname = "Cash Flow"
2727
2728
  ws = wb.create_sheet(sname)
2728
- fillsheet(ws, cashFlowDic, 'currency')
2729
+ fillsheet(ws, cashFlowDic, "currency")
2729
2730
 
2730
2731
  # Sources are handled separately.
2731
2732
  srcDic = {
2732
- 'wages': self.sources_in['wages'],
2733
- 'social sec': self.sources_in['ssec'],
2734
- 'pension': self.sources_in['pension'],
2735
- 'txbl acc wdrwl': self.sources_in['txbl acc wdrwl'],
2736
- 'RMDs': self.sources_in['RMD'],
2737
- '+distributions': self.sources_in['+dist'],
2738
- 'Roth conv': self.sources_in['RothX'],
2739
- 'tax-free wdrwl': self.sources_in['tax-free wdrwl'],
2740
- 'big-ticket items': self.sources_in['BTI'],
2733
+ "wages": self.sources_in["wages"],
2734
+ "social sec": self.sources_in["ssec"],
2735
+ "pension": self.sources_in["pension"],
2736
+ "txbl acc wdrwl": self.sources_in["txbl acc wdrwl"],
2737
+ "RMDs": self.sources_in["RMD"],
2738
+ "+distributions": self.sources_in["+dist"],
2739
+ "Roth conv": self.sources_in["RothX"],
2740
+ "tax-free wdrwl": self.sources_in["tax-free wdrwl"],
2741
+ "big-ticket items": self.sources_in["BTI"],
2741
2742
  }
2742
2743
 
2743
2744
  for i in range(self.N_i):
2744
2745
  sname = self.inames[i] + "'s Sources"
2745
2746
  ws = wb.create_sheet(sname)
2746
- fillsheet(ws, srcDic, 'currency', op=lambda x: x[i])
2747
+ fillsheet(ws, srcDic, "currency", op=lambda x: x[i])
2747
2748
 
2748
2749
  # Account balances except final year.
2749
2750
  accDic = {
2750
- 'taxable bal': self.b_ijn[:, 0, :-1],
2751
- 'taxable ctrb': self.kappa_ijn[:, 0, :],
2752
- 'taxable dep': self.d_in,
2753
- 'taxable wdrwl': self.w_ijn[:, 0, :],
2754
- 'tax-deferred bal': self.b_ijn[:, 1, :-1],
2755
- 'tax-deferred ctrb': self.kappa_ijn[:, 1, :],
2756
- 'tax-deferred wdrwl': self.w_ijn[:, 1, :],
2757
- '(included RMDs)': self.rmd_in[:, :],
2758
- 'Roth conv': self.x_in,
2759
- 'tax-free bal': self.b_ijn[:, 2, :-1],
2760
- 'tax-free ctrb': self.kappa_ijn[:, 2, :],
2761
- 'tax-free wdrwl': self.w_ijn[:, 2, :],
2751
+ "taxable bal": self.b_ijn[:, 0, :-1],
2752
+ "taxable ctrb": self.kappa_ijn[:, 0, :],
2753
+ "taxable dep": self.d_in,
2754
+ "taxable wdrwl": self.w_ijn[:, 0, :],
2755
+ "tax-deferred bal": self.b_ijn[:, 1, :-1],
2756
+ "tax-deferred ctrb": self.kappa_ijn[:, 1, :],
2757
+ "tax-deferred wdrwl": self.w_ijn[:, 1, :],
2758
+ "(included RMDs)": self.rmd_in[:, :],
2759
+ "Roth conv": self.x_in,
2760
+ "tax-free bal": self.b_ijn[:, 2, :-1],
2761
+ "tax-free ctrb": self.kappa_ijn[:, 2, :],
2762
+ "tax-free wdrwl": self.w_ijn[:, 2, :],
2762
2763
  }
2763
2764
  for i in range(self.N_i):
2764
2765
  sname = self.inames[i] + "'s Accounts"
2765
2766
  ws = wb.create_sheet(sname)
2766
- fillsheet(ws, accDic, 'currency', op=lambda x: x[i])
2767
+ fillsheet(ws, accDic, "currency", op=lambda x: x[i])
2767
2768
  # Add final balances.
2768
2769
  lastRow = [
2769
2770
  self.year_n[-1] + 1,
@@ -2781,11 +2782,11 @@ class Plan(object):
2781
2782
  0,
2782
2783
  ]
2783
2784
  ws.append(lastRow)
2784
- _formatSpreadsheet(ws, 'currency')
2785
+ _formatSpreadsheet(ws, "currency")
2785
2786
 
2786
2787
  # Allocations.
2787
- jDic = {'taxable': 0, 'tax-deferred': 1, 'tax-free': 2}
2788
- kDic = {'stocks': 0, 'C bonds': 1, 'T notes': 2, 'common': 3}
2788
+ jDic = {"taxable": 0, "tax-deferred": 1, "tax-free": 2}
2789
+ kDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
2789
2790
 
2790
2791
  # Add one year for estate.
2791
2792
  year_n = np.append(self.year_n, [self.year_n[-1] + 1])
@@ -2793,28 +2794,30 @@ class Plan(object):
2793
2794
  sname = self.inames[i] + "'s Allocations"
2794
2795
  ws = wb.create_sheet(sname)
2795
2796
  rawData = {}
2796
- rawData['year'] = year_n
2797
+ rawData["year"] = year_n
2797
2798
  for jkey in jDic:
2798
2799
  for kkey in kDic:
2799
- rawData[jkey + '/' + kkey] = self.alpha_ijkn[i, jDic[jkey], kDic[kkey], :]
2800
+ rawData[jkey + "/" + kkey] = self.alpha_ijkn[i, jDic[jkey], kDic[kkey], :]
2800
2801
  df = pd.DataFrame(rawData)
2801
2802
  for row in dataframe_to_rows(df, index=False, header=True):
2802
2803
  ws.append(row)
2803
2804
 
2804
- _formatSpreadsheet(ws, 'percent1')
2805
+ _formatSpreadsheet(ws, "percent1")
2805
2806
 
2806
2807
  # Rates on penultimate sheet.
2807
- ratesDic = {'S&P 500': self.tau_kn[0],
2808
- 'Corporate Baa': self.tau_kn[1],
2809
- 'T Bonds': self.tau_kn[2],
2810
- 'inflation': self.tau_kn[3]}
2811
- ws = wb.create_sheet('Rates')
2812
- fillsheet(ws, ratesDic, 'percent2')
2808
+ ratesDic = {
2809
+ "S&P 500": self.tau_kn[0],
2810
+ "Corporate Baa": self.tau_kn[1],
2811
+ "T Bonds": self.tau_kn[2],
2812
+ "inflation": self.tau_kn[3],
2813
+ }
2814
+ ws = wb.create_sheet("Rates")
2815
+ fillsheet(ws, ratesDic, "percent2")
2813
2816
 
2814
2817
  # Summary on last sheet.
2815
- ws = wb.create_sheet('Summary')
2818
+ ws = wb.create_sheet("Summary")
2816
2819
  rawData = {}
2817
- rawData['SUMMARY ==========================================================================='] = (
2820
+ rawData["SUMMARY ==========================================================================="] = (
2818
2821
  self.summaryList()
2819
2822
  )
2820
2823
 
@@ -2822,7 +2825,7 @@ class Plan(object):
2822
2825
  for row in dataframe_to_rows(df, index=False, header=True):
2823
2826
  ws.append(row)
2824
2827
 
2825
- _formatSpreadsheet(ws, 'summary')
2828
+ _formatSpreadsheet(ws, "summary")
2826
2829
 
2827
2830
  if saveToFile:
2828
2831
  if basename is None:
@@ -2841,27 +2844,27 @@ class Plan(object):
2841
2844
  """
2842
2845
 
2843
2846
  planData = {}
2844
- planData['year'] = self.year_n
2845
- planData['net spending'] = self.g_n
2846
- planData['taxable ord. income'] = self.G_n
2847
- planData['taxable gains/divs'] = self.Q_n
2848
- planData['tax bill'] = self.T_n
2847
+ planData["year"] = self.year_n
2848
+ planData["net spending"] = self.g_n
2849
+ planData["taxable ord. income"] = self.G_n
2850
+ planData["taxable gains/divs"] = self.Q_n
2851
+ planData["tax bill"] = self.T_n
2849
2852
 
2850
2853
  for i in range(self.N_i):
2851
- planData[self.inames[i] + ' txbl bal'] = self.b_ijn[i, 0, :-1]
2852
- planData[self.inames[i] + ' txbl dep'] = self.d_in[i, :]
2853
- planData[self.inames[i] + ' txbl wrdwl'] = self.w_ijn[i, 0, :]
2854
- planData[self.inames[i] + ' tx-def bal'] = self.b_ijn[i, 1, :-1]
2855
- planData[self.inames[i] + ' tx-def ctrb'] = self.kappa_ijn[i, 1, :]
2856
- planData[self.inames[i] + ' tx-def wdrl'] = self.w_ijn[i, 1, :]
2857
- planData[self.inames[i] + ' (RMD)'] = self.rmd_in[i, :]
2858
- planData[self.inames[i] + ' Roth conv'] = self.x_in[i, :]
2859
- planData[self.inames[i] + ' tx-free bal'] = self.b_ijn[i, 2, :-1]
2860
- planData[self.inames[i] + ' tx-free ctrb'] = self.kappa_ijn[i, 2, :]
2861
- planData[self.inames[i] + ' tax-free wdrwl'] = self.w_ijn[i, 2, :]
2862
- planData[self.inames[i] + ' big-ticket items'] = self.Lambda_in[i, :]
2863
-
2864
- ratesDic = {'S&P 500': 0, 'Corporate Baa': 1, 'T Bonds': 2, 'inflation': 3}
2854
+ planData[self.inames[i] + " txbl bal"] = self.b_ijn[i, 0, :-1]
2855
+ planData[self.inames[i] + " txbl dep"] = self.d_in[i, :]
2856
+ planData[self.inames[i] + " txbl wrdwl"] = self.w_ijn[i, 0, :]
2857
+ planData[self.inames[i] + " tx-def bal"] = self.b_ijn[i, 1, :-1]
2858
+ planData[self.inames[i] + " tx-def ctrb"] = self.kappa_ijn[i, 1, :]
2859
+ planData[self.inames[i] + " tx-def wdrl"] = self.w_ijn[i, 1, :]
2860
+ planData[self.inames[i] + " (RMD)"] = self.rmd_in[i, :]
2861
+ planData[self.inames[i] + " Roth conv"] = self.x_in[i, :]
2862
+ planData[self.inames[i] + " tx-free bal"] = self.b_ijn[i, 2, :-1]
2863
+ planData[self.inames[i] + " tx-free ctrb"] = self.kappa_ijn[i, 2, :]
2864
+ planData[self.inames[i] + " tax-free wdrwl"] = self.w_ijn[i, 2, :]
2865
+ planData[self.inames[i] + " big-ticket items"] = self.Lambda_in[i, :]
2866
+
2867
+ ratesDic = {"S&P 500": 0, "Corporate Baa": 1, "T Bonds": 2, "inflation": 3}
2865
2868
  for key in ratesDic:
2866
2869
  planData[key] = self.tau_kn[ratesDic[key]]
2867
2870
 
@@ -2869,39 +2872,39 @@ class Plan(object):
2869
2872
 
2870
2873
  while True:
2871
2874
  try:
2872
- fname = 'worksheet' + '_' + basename + '.csv'
2875
+ fname = "worksheet" + "_" + basename + ".csv"
2873
2876
  df.to_csv(fname)
2874
2877
  break
2875
2878
  except PermissionError:
2876
- self.mylog.print('Failed to save "%s": %s.' % (fname, 'Permission denied'))
2877
- key = input('Close file and try again? [Yn] ')
2878
- if key == 'n':
2879
+ self.mylog.print('Failed to save "%s": %s.' % (fname, "Permission denied"))
2880
+ key = input("Close file and try again? [Yn] ")
2881
+ if key == "n":
2879
2882
  break
2880
2883
  except Exception as e:
2881
- raise Exception('Unanticipated exception %r.' % e)
2884
+ raise Exception("Unanticipated exception %r." % e)
2882
2885
 
2883
2886
  return None
2884
2887
 
2885
2888
 
2886
- def _lineIncomePlot(x, series, style, title, yformat='\\$k'):
2889
+ def _lineIncomePlot(x, series, style, title, yformat="\\$k"):
2887
2890
  """
2888
2891
  Core line plotter function.
2889
2892
  """
2890
2893
  import matplotlib.ticker as tk
2891
2894
 
2892
2895
  fig, ax = plt.subplots(figsize=(6, 4))
2893
- plt.grid(visible='both')
2896
+ plt.grid(visible="both")
2894
2897
 
2895
2898
  for sname in series:
2896
2899
  ax.plot(x, series[sname], label=sname, ls=style[sname])
2897
2900
 
2898
- ax.legend(loc='upper left', reverse=True, fontsize=8, framealpha=0.3)
2901
+ ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
2899
2902
  ax.set_title(title)
2900
- ax.set_xlabel('year')
2903
+ ax.set_xlabel("year")
2901
2904
  ax.set_ylabel(yformat)
2902
2905
  ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
2903
- if 'k' in yformat:
2904
- ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ',')))
2906
+ if "k" in yformat:
2907
+ ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
2905
2908
  # Give range to y values in unindexed flat profiles.
2906
2909
  ymin, ymax = ax.get_ylim()
2907
2910
  if ymax - ymin < 5000:
@@ -2910,7 +2913,7 @@ def _lineIncomePlot(x, series, style, title, yformat='\\$k'):
2910
2913
  return fig, ax
2911
2914
 
2912
2915
 
2913
- def _stackPlot(x, inames, title, irange, series, snames, location, yformat='\\$k'):
2916
+ def _stackPlot(x, inames, title, irange, series, snames, location, yformat="\\$k"):
2914
2917
  """
2915
2918
  Core function for stacked plots.
2916
2919
  """
@@ -2921,28 +2924,28 @@ def _stackPlot(x, inames, title, irange, series, snames, location, yformat='\\$k
2921
2924
  for i in irange:
2922
2925
  tmp = series[sname][i]
2923
2926
  if sum(tmp) > 1.0:
2924
- nonzeroSeries[sname + ' ' + inames[i]] = tmp
2927
+ nonzeroSeries[sname + " " + inames[i]] = tmp
2925
2928
 
2926
2929
  if len(nonzeroSeries) == 0:
2927
2930
  # print('Nothing to plot for', title)
2928
2931
  return None
2929
2932
 
2930
2933
  fig, ax = plt.subplots(figsize=(6, 4))
2931
- plt.grid(visible='both')
2934
+ plt.grid(visible="both")
2932
2935
 
2933
2936
  ax.stackplot(x, nonzeroSeries.values(), labels=nonzeroSeries.keys(), alpha=0.6)
2934
2937
  ax.legend(loc=location, reverse=True, fontsize=8, ncol=2, framealpha=0.5)
2935
2938
  ax.set_title(title)
2936
- ax.set_xlabel('year')
2939
+ ax.set_xlabel("year")
2937
2940
  ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
2938
- if 'k' in yformat:
2941
+ if "k" in yformat:
2939
2942
  ax.set_ylabel(yformat)
2940
- ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ',')))
2941
- elif yformat == 'percent':
2942
- ax.set_ylabel('%')
2943
- ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x), ',')))
2943
+ ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
2944
+ elif yformat == "percent":
2945
+ ax.set_ylabel("%")
2946
+ ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x), ",")))
2944
2947
  else:
2945
- raise RuntimeError('Unknown yformat: %s.' % yformat)
2948
+ raise RuntimeError("Unknown yformat: %s." % yformat)
2946
2949
 
2947
2950
  return fig, ax
2948
2951
 
@@ -2955,15 +2958,15 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
2955
2958
  from pathlib import Path
2956
2959
 
2957
2960
  if Path(basename).suffixes == []:
2958
- fname = 'workbook' + '_' + basename + '.xlsx'
2961
+ fname = "workbook" + "_" + basename + ".xlsx"
2959
2962
  else:
2960
2963
  fname = basename
2961
2964
 
2962
2965
  if overwrite is False and isfile(fname):
2963
2966
  mylog.print('File "%s" already exists.' % fname)
2964
- key = input('Overwrite? [Ny] ')
2965
- if key != 'y':
2966
- mylog.vprint('Skipping save and returning.')
2967
+ key = input("Overwrite? [Ny] ")
2968
+ if key != "y":
2969
+ mylog.vprint("Skipping save and returning.")
2967
2970
  return None
2968
2971
 
2969
2972
  while True:
@@ -2972,12 +2975,12 @@ def _saveWorkbook(wb, basename, overwrite, mylog):
2972
2975
  wb.save(fname)
2973
2976
  break
2974
2977
  except PermissionError:
2975
- mylog.print('Failed to save "%s": %s.' % (fname, 'Permission denied'))
2976
- key = input('Close file and try again? [Yn] ')
2977
- if key == 'n':
2978
+ mylog.print('Failed to save "%s": %s.' % (fname, "Permission denied"))
2979
+ key = input("Close file and try again? [Yn] ")
2980
+ if key == "n":
2978
2981
  break
2979
2982
  except Exception as e:
2980
- raise Exception('Unanticipated exception %r.' % e)
2983
+ raise Exception("Unanticipated exception %r." % e)
2981
2984
 
2982
2985
  return None
2983
2986
 
@@ -2986,31 +2989,31 @@ def _formatSpreadsheet(ws, ftype):
2986
2989
  """
2987
2990
  Utility function to beautify spreadsheet.
2988
2991
  """
2989
- if ftype == 'currency':
2990
- fstring = '$#,##0_);[Red]($#,##0)'
2991
- elif ftype == 'percent2':
2992
- fstring = '#.00%'
2993
- elif ftype == 'percent1':
2994
- fstring = '#.0%'
2995
- elif ftype == 'percent0':
2996
- fstring = '#0%'
2997
- elif ftype == 'summary':
2992
+ if ftype == "currency":
2993
+ fstring = "$#,##0_);[Red]($#,##0)"
2994
+ elif ftype == "percent2":
2995
+ fstring = "#.00%"
2996
+ elif ftype == "percent1":
2997
+ fstring = "#.0%"
2998
+ elif ftype == "percent0":
2999
+ fstring = "#0%"
3000
+ elif ftype == "summary":
2998
3001
  for col in ws.columns:
2999
3002
  column = col[0].column_letter
3000
3003
  width = max(len(str(col[0].value)) + 20, 40)
3001
3004
  ws.column_dimensions[column].width = width
3002
3005
  return None
3003
3006
  else:
3004
- raise RuntimeError('Unknown format: %s.' % ftype)
3007
+ raise RuntimeError("Unknown format: %s." % ftype)
3005
3008
 
3006
- for cell in ws[1] + ws['A']:
3007
- cell.style = 'Pandas'
3009
+ for cell in ws[1] + ws["A"]:
3010
+ cell.style = "Pandas"
3008
3011
  for col in ws.columns:
3009
3012
  column = col[0].column_letter
3010
3013
  # col[0].style = 'Title'
3011
3014
  width = max(len(str(col[0].value)) + 4, 10)
3012
3015
  ws.column_dimensions[column].width = width
3013
- if column != 'A':
3016
+ if column != "A":
3014
3017
  for cell in col:
3015
3018
  cell.number_format = fstring
3016
3019