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